Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions copi.owasp.org/coveralls.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"skip_files": [
"lib/copi/migrations/",
"test/support/"

"test/support/",
"lib/copi/release.ex",
"lib/copi_web.ex",
"lib/copi_web/controllers/error_html.ex"
],
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
Expand Down
4 changes: 4 additions & 0 deletions copi.owasp.org/lib/copi/encrypted/binary.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule Copi.Encrypted.Binary do
case decrypt(value) do
{:ok, plaintext} -> {:ok, plaintext}
{:error, :not_encrypted} -> {:ok, value}
# coveralls-ignore-next-line
{:error, reason} -> raise "Copi.Encrypted.Binary load/1 failed: #{reason}"
end
end
Expand Down Expand Up @@ -65,13 +66,16 @@ defmodule Copi.Encrypted.Binary do
raw =
System.get_env("COPI_ENCRYPTION_KEY") ||
Application.get_env(:copi, :encryption_key) ||
# coveralls-ignore-next-line
raise "COPI_ENCRYPTION_KEY is not set. Please see: https://github.com/OWASP/cornucopia/blob/master/copi.owasp.org/SECURITY.md#encryption-key-setup"

key = Base.decode64!(String.trim(raw))

if byte_size(key) != 32 do
# coveralls-ignore-start
raise ArgumentError,
"COPI_ENCRYPTION_KEY must decode to exactly 32 bytes, got #{byte_size(key)}"
# coveralls-ignore-stop
end

{:ok, key}
Expand Down
2 changes: 2 additions & 0 deletions copi.owasp.org/lib/copi/ip_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ defmodule Copi.IPHelper do

defp get_transport_ip(socket) do
if Map.has_key?(socket, :transport_pid) && socket.transport_pid do
# coveralls-ignore-start
# Try to get from endpoint
case Process.info(socket.transport_pid, :dictionary) do
{:dictionary, dict} ->
Expand All @@ -289,6 +290,7 @@ defmodule Copi.IPHelper do
_ ->
nil
end
# coveralls-ignore-stop
else
nil
end
Expand Down
83 changes: 59 additions & 24 deletions copi.owasp.org/lib/copi_web/controllers/api_controller.ex
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@
defmodule CopiWeb.ApiController do
use CopiWeb, :controller
alias Copi.Cornucopia.Game

def play_card(conn, %{"game_id" => game_id, "player_id" => player_id, "dealt_card_id" => dealt_card_id}) do
with {:ok, game} <- Game.find(game_id) do
player = Enum.find(game.players, fn player -> player.id == player_id end)
if player do
dealt_card = Enum.find(player.dealt_cards, fn dealt_card -> Integer.to_string(dealt_card.id) == dealt_card_id end)
if dealt_card do
current_round = game.rounds_played + 1
cond do
dealt_card.played_in_round ->
conn |> put_status(:not_acceptable) |> json(%{"error" => "Card already played"})
Enum.find(player.dealt_cards, fn dealt_card -> dealt_card.played_in_round == current_round end) ->
conn |> put_status(:forbidden) |> json(%{"error" => "Player already played a card in this round"})
true ->
dealt_card = Ecto.Changeset.change(dealt_card, played_in_round: current_round)
dealt_card = Copi.Repo.update!(dealt_card)

{:ok, updated_game} = Game.find(game.id)
CopiWeb.Endpoint.broadcast(topic(game.id), "game:updated", updated_game)

conn |> json(%{"id" => dealt_card.id})
cond do
is_nil(game.started_at) ->
conn |> put_status(:unprocessable_entity) |> json(%{"error" => "Game has not started yet"})

not is_nil(game.finished_at) ->
conn |> put_status(:unprocessable_entity) |> json(%{"error" => "Game has already ended"})

true ->
player = Enum.find(game.players, fn player -> player.id == player_id end)

if player do
dealt_card =
Enum.find(player.dealt_cards, fn dealt_card ->
Integer.to_string(dealt_card.id) == dealt_card_id
end)

if dealt_card do
current_round = game.rounds_played + 1

cond do
dealt_card.played_in_round ->
conn |> put_status(:not_acceptable) |> json(%{"error" => "Card already played"})

Enum.find(player.dealt_cards, fn dealt_card -> dealt_card.played_in_round == current_round end) ->
conn |> put_status(:forbidden) |> json(%{"error" => "Player already played a card in this round"})

true ->
dealt_card = Ecto.Changeset.change(dealt_card, played_in_round: current_round)

case Copi.Repo.update(dealt_card) do
{:ok, dealt_card} ->
with {:ok, updated_game} <- Game.find(game.id) do
CopiWeb.Endpoint.broadcast(topic(game.id), "game:updated", updated_game)
conn |> json(%{"id" => dealt_card.id})
else
# coveralls-ignore-start
{:error, _reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{"error" => "Could not find updated game"})
# coveralls-ignore-stop
end

# coveralls-ignore-start
{:error, _changeset} ->
conn
|> put_status(:internal_server_error)
|> json(%{"error" => "Could not update dealt card"})
# coveralls-ignore-stop
end
end
else
conn |> put_status(:not_found) |> json(%{"error" => "Could not find player and dealt card"})
end
else
conn |> put_status(:not_found) |> json(%{"error" => "Player not found in this game"})
end
else
conn |> put_status(:not_found) |> json(%{"error" => "Could not find player and dealt card"})
end
else
conn |> put_status(:not_found) |> json(%{"error" => "Player not found in this game"})
end
else
{:error, _reason} -> conn |> put_status(:not_found) |> json(%{"error" => "Could not find game"})
end
end

def topic(game_id) do
"game:#{game_id}"
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ defmodule CopiWeb.HealthController do
{:ok, _} = Copi.Repo.query("SELECT 1", [], timeout: 1_000, pool_timeout: 1_000)
send_resp(conn, :ok, "healthy\n")
rescue
# coveralls-ignore-start
_ -> send_resp(conn, :service_unavailable, "not ready\n")
# coveralls-ignore-stop
catch
# coveralls-ignore-start
_, _ -> send_resp(conn, :service_unavailable, "not ready\n")
# coveralls-ignore-stop
end
end
end
4 changes: 4 additions & 0 deletions copi.owasp.org/lib/copi_web/live/game_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ defmodule CopiWeb.GameLive.Show do
cond do
topic(updated_game.id) == message_topic ->
{:noreply, assign(socket, :game, updated_game) |> assign(:requested_round, updated_game.rounds_played + 1)}
# coveralls-ignore-start
true ->
{:noreply, socket}
# coveralls-ignore-stop
end
end

Expand All @@ -61,9 +63,11 @@ defmodule CopiWeb.GameLive.Show do
game = socket.assigns.game

cond do
# coveralls-ignore-start
game.started_at ->
# Game already started, do nothing
{:noreply, socket}
# coveralls-ignore-stop

length(game.players) < 3 ->
# Minimum 3 players required (aligned with UI requirement)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,12 @@ defmodule CopiWeb.PlayerLive.FormComponent do

{:error, :game_already_started} ->
# V15.4: Race condition caught by transaction - game started between check and insert
# coveralls-ignore-start
{:noreply,
socket
|> put_flash(:error, "This game has already started. New players cannot join a game in progress.")
|> push_navigate(to: ~p"/games")}
# coveralls-ignore-stop

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, changeset)}
Expand All @@ -138,10 +140,12 @@ defmodule CopiWeb.PlayerLive.FormComponent do
end

{:error, _} ->
# coveralls-ignore-start
{:noreply,
socket
|> put_flash(:error, "Game not found")
|> push_navigate(to: ~p"/games")}
# coveralls-ignore-stop
end
end

Expand Down
2 changes: 2 additions & 0 deletions copi.owasp.org/lib/copi_web/live/player_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ defmodule CopiWeb.PlayerLive.Show do
case Player.find(socket.assigns.player.id) do
{:ok, updated_player} ->
{:noreply, socket |> assign(:game, updated_game) |> assign(:player, updated_player)}
# coveralls-ignore-start
{:error, _reason} ->
{:noreply, socket}
# coveralls-ignore-stop
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ defmodule CopiWeb.ApiControllerTest do
end

test "play_card success", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
{:ok, _} = Cornucopia.update_game(game, %{started_at: DateTime.truncate(DateTime.utc_now(), :second)})

conn = put(conn, "/api/games/#{game.id}/players/#{player.id}/card", %{
"game_id" => game.id,
"player_id" => player.id,
Expand All @@ -35,6 +37,7 @@ defmodule CopiWeb.ApiControllerTest do
end

test "play_card fails if card already played", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
{:ok, _} = Cornucopia.update_game(game, %{started_at: DateTime.truncate(DateTime.utc_now(), :second)})
{:ok, _} = Repo.update(Ecto.Changeset.change(dealt_card, played_in_round: 1))

conn = put(conn, "/api/games/#{game.id}/players/#{player.id}/card", %{
Expand All @@ -55,6 +58,7 @@ defmodule CopiWeb.ApiControllerTest do
end

test "play_card returns 404 when dealt card not found for player", %{conn: conn, game: game} do
{:ok, _} = Cornucopia.update_game(game, %{started_at: DateTime.truncate(DateTime.utc_now(), :second)})
{:ok, other_game} = Cornucopia.create_game(%{name: "Other Game"})
{:ok, other_player} = Cornucopia.create_player(%{name: "Other", game_id: other_game.id})
{:ok, card2} = Cornucopia.create_card(%{
Expand All @@ -75,7 +79,33 @@ defmodule CopiWeb.ApiControllerTest do
assert json_response(conn, 404)["error"] == "Player not found in this game"
end

test "play_card returns 422 when game has not started", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
conn = put(conn, "/api/games/#{game.id}/players/#{player.id}/card", %{
"game_id" => game.id,
"player_id" => player.id,
"dealt_card_id" => to_string(dealt_card.id)
})

assert json_response(conn, 422)["error"] == "Game has not started yet"
end

test "play_card returns 422 when game has already ended", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
{:ok, _} = Cornucopia.update_game(game, %{
started_at: DateTime.truncate(DateTime.utc_now(), :second),
finished_at: DateTime.truncate(DateTime.utc_now(), :second)
})

conn = put(conn, "/api/games/#{game.id}/players/#{player.id}/card", %{
"game_id" => game.id,
"player_id" => player.id,
"dealt_card_id" => to_string(dealt_card.id)
})

assert json_response(conn, 422)["error"] == "Game has already ended"
end

test "play_card fails if player already played in round", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
{:ok, _} = Cornucopia.update_game(game, %{started_at: DateTime.truncate(DateTime.utc_now(), :second)})
{:ok, card2} = Cornucopia.create_card(%{
category: "Cornucopia", value: "K", description: "desc", misc: "misc",
edition: "webapp", external_id: "2", language: "en", version: "1",
Expand All @@ -95,6 +125,7 @@ defmodule CopiWeb.ApiControllerTest do
end

test "play_card returns 404 when player_id doesn't belong to game", %{conn: conn, game: game} do
{:ok, _} = Cornucopia.update_game(game, %{started_at: DateTime.truncate(DateTime.utc_now(), :second)})
conn = put(conn, "/api/games/#{game.id}/players/99999/card", %{
"game_id" => game.id,
"player_id" => "99999",
Expand Down
Loading