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
7 changes: 6 additions & 1 deletion copi.owasp.org/lib/copi_web/live/game_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ defmodule CopiWeb.GameLive.Show do
def handle_info(%{topic: message_topic, event: "game:updated", payload: updated_game}, socket) do
cond do
topic(updated_game.id) == message_topic ->
{:noreply, assign(socket, :game, updated_game) |> assign(:requested_round, updated_game.rounds_played + 1)}
current_round = if updated_game.finished_at do
updated_game.rounds_played
else
updated_game.rounds_played + 1
end
{:noreply, assign(socket, :game, updated_game) |> assign(:requested_round, current_round)}
true ->
{:noreply, socket}
end
Expand Down
54 changes: 48 additions & 6 deletions copi.owasp.org/test/copi/cornucopia_logic_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ defmodule Copi.CornucopiaLogicTest do
create_card("Wild Card", "1")
create_card("Hearts", "5")
create_card("WILD CARD", "2")

suits = Cornucopia.get_suits_from_selected_deck("webapp")

refute "Wild Card" in suits
Expand Down Expand Up @@ -200,20 +200,62 @@ defmodule Copi.CornucopiaLogicTest do
test "jokers trump all other cards", %{game: game, p1: p1, p2: p2} do
{:ok, joker} = create_card("Joker", "JokerA")
{:ok, trump} = create_card("Cornucopia", "A")

d1 = play_card(p1, trump, 1)
d2 = play_card(p2, joker, 1)

# Add votes
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d1.id, player_id: p1.id})
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d2.id, player_id: p2.id})

# Reload game
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])

winner = Cornucopia.highest_scoring_card_in_round(game, 1)

# Joker should win
assert winner.id == d2.id
end

test "highest_scoring_card_in_round returns nil when no cards have enough votes",
%{game: game, p1: p1, p2: p2} do
{:ok, c1} = create_card("Authentication", "3")
{:ok, c2} = create_card("Authentication", "7")

# Play cards but add NO votes → scoring_cards filters all out → special_lead_cards([]) → nil path
play_card(p1, c1, 1)
play_card(p2, c2, 1)

game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])

result = Cornucopia.highest_scoring_card_in_round(game, 1)
assert result == nil
end

test "lead suit wins when no trump or joker present", %{game: game, p1: p1, p2: p2} do
{:ok, c1} = create_card("Authentication", "3")
{:ok, c2} = create_card("Authentication", "8")

# p1 plays first (leads with Authentication), p2 follows
d1 = play_card(p1, c1, 1)
:timer.sleep(15)
d2 = play_card(p2, c2, 1)

# Add votes to both (2 players, need > 0.5 votes each)
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d1.id, player_id: p1.id})
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d2.id, player_id: p2.id})

game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])

winner = Cornucopia.highest_scoring_card_in_round(game, 1)

# "8" ranks higher than "3" in card_order → d2 wins
assert winner.id == d2.id
end

test "highest_scoring_card_in_round returns nil when no cards played in game",
%{game: game} do
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])
assert Cornucopia.highest_scoring_card_in_round(game, 1) == nil
end
end
33 changes: 33 additions & 0 deletions copi.owasp.org/test/copi/cornucopia_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ defmodule Copi.CornucopiaTest do
game = game_fixture()
assert %Ecto.Changeset{} = Cornucopia.change_game(game)
end

test "Game.find/1 returns OK tuple for existing game" do
game = game_fixture()
assert {:ok, found} = Copi.Cornucopia.Game.find(game.id)
assert found.id == game.id
end

test "Game.find/1 returns error for non-existent game" do
assert {:error, :not_found} =
Copi.Cornucopia.Game.find("00000000000000000000000099")
end

test "Game.continue_vote_count/1 returns count of continue votes" do
alias Copi.Cornucopia.Game
game = game_fixture()
{:ok, reloaded} = Game.find(game.id)
assert Game.continue_vote_count(reloaded) == 0
end

test "Game.majority_continue_votes_reached?/1 returns true when votes exceed half" do
alias Copi.Cornucopia.Game
alias Copi.Repo
game = game_fixture()
{:ok, created_player} = Cornucopia.create_player(%{name: "p1", game_id: game.id})
{:ok, reloaded} = Game.find(game.id)
# 0 votes, 1 player → 0 > div(1,2)=0 → false
refute Game.majority_continue_votes_reached?(reloaded)
# Add a continue vote
Repo.insert!(%Copi.Cornucopia.ContinueVote{player_id: created_player.id, game_id: game.id})
{:ok, updated} = Game.find(game.id)
# 1 vote > div(1,2)=0 → true
assert Game.majority_continue_votes_reached?(updated)
end
end

describe "players" do
Expand Down
54 changes: 53 additions & 1 deletion copi.owasp.org/test/copi/ip_helper_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,61 @@ defmodule Copi.IPHelperTest do
test "handles malformed extract_first_ip inputs" do
info = %{x_headers: [{"x-forwarded-for", "invalid"}]}
assert IPHelper.get_ip_from_connect_info(info) == nil

info2 = %{x_headers: [{"other", "10.0.0.1"}]}
assert IPHelper.get_ip_from_connect_info(info2) == nil
end

test "extracts from req_headers with atom key tuples" do
info = %{req_headers: [{:"x-forwarded-for", "10.2.3.4"}]}
assert IPHelper.get_ip_from_connect_info(info) == {10, 2, 3, 4}
end

test "handles x_headers as raw binary string" do
info = %{x_headers: "10.8.9.1"}
assert IPHelper.get_ip_from_connect_info(info) == {10, 8, 9, 1}
end
end

describe "get_ip_from_socket/1 (LiveView) - additional coverage" do
test "extracts IP from connect_info map req_headers" do
socket = %Phoenix.LiveView.Socket{
private: %{
connect_info: %{
req_headers: [{"x-forwarded-for", "10.0.5.6"}]
}
}
}

assert IPHelper.get_ip_from_socket(socket) == {10, 0, 5, 6}
end

test "handles connect_info map with x_headers as binary string" do
socket = %Phoenix.LiveView.Socket{
private: %{
connect_info: %{x_headers: "10.7.8.9"}
}
}

assert IPHelper.get_ip_from_socket(socket) == {10, 7, 8, 9}
end

test "handles connect_info map with x_headers as string-keyed map" do
socket = %Phoenix.LiveView.Socket{
private: %{
connect_info: %{x_headers: %{"x-forwarded-for" => "10.1.2.3"}}
}
}

assert IPHelper.get_ip_from_socket(socket) == {10, 1, 2, 3}
end

test "falls back to localhost when connect_info map has no usable IP info" do
socket = %Phoenix.LiveView.Socket{
private: %{connect_info: %{no_headers: "foo"}}
}

assert IPHelper.get_ip_from_socket(socket) == {127, 0, 0, 1}
end
end
end
28 changes: 28 additions & 0 deletions copi.owasp.org/test/copi/rate_limiter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,41 @@ defmodule Copi.RateLimiterTest do
# Should still work even with weird input
assert {:ok, _} = RateLimiter.check_rate("invalid-ip", :game_creation)
end

test "bypasses rate limit in production mode for localhost" do
Application.put_env(:copi, :env, :prod)

try do
result = RateLimiter.check_rate({127, 0, 0, 1}, :game_creation)
assert result == {:ok, :unlimited}
after
Application.put_env(:copi, :env, :test)
end
end

test "normalize_ip passes through non-tuple non-binary input" do
# Passing an integer (not a tuple or binary) hits the catch-all normalize_ip clause
assert {:ok, _} = RateLimiter.check_rate(12345, :game_creation)
end
end

describe "cleanup process" do
test "rate limiter process is alive" do
assert Process.whereis(Copi.RateLimiter) != nil
end

test "handles :cleanup message gracefully" do
pid = Process.whereis(Copi.RateLimiter)
# Populate some state first
RateLimiter.check_rate({10, 20, 30, 40}, :game_creation)
# Directly send the cleanup message to trigger handle_info(:cleanup, state)
send(pid, :cleanup)
Process.sleep(50)
# Should still be healthy
assert Process.alive?(pid)
assert {:ok, _} = RateLimiter.check_rate({10, 20, 30, 41}, :game_creation)
end

test "can make requests after clearing IP", %{ip: ip} do
config = RateLimiter.get_config()
limit = config.limits.connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ defmodule CopiWeb.ApiControllerTest do
assert json_response(conn, 406)["error"] == "Card already played"
end

test "play_card returns 404 when game not found", %{conn: conn} do
conn = put(conn, "/api/games/00000000000000000000000001/players/fakeplayer/card", %{
"dealt_card_id" => "999"
})

assert json_response(conn, 404)["error"] == "Could not find game"
end

test "play_card fails if player already played in round", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
# Create another card and mark it as played in this round (0 + 1 => 1)
{:ok, card2} = Cornucopia.create_card(%{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,15 @@ defmodule CopiWeb.CardControllerTest do
end
end

describe "format_capec/1" do
test "returns refs unchanged" do
refs = ["1234", "5678"]
assert CopiWeb.CardController.format_capec(refs) == refs
end

test "returns empty list unchanged" do
assert CopiWeb.CardController.format_capec([]) == []
end
end

end
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ defmodule CopiWeb.GameLive.CreateGameFormTest do

test "validation errors don't consume rate limit", %{conn: conn} do
{:ok, view, _html} = live(conn, "/games/new")

# Submit invalid form (empty name triggers validation)
html = view
|> form("#game-form", game: %{name: "", edition: "webapp"})
Expand All @@ -65,5 +65,17 @@ defmodule CopiWeb.GameLive.CreateGameFormTest do
# Successful creation redirects
assert {:ok, _view, _html} = follow_redirect(result, conn)
end

test "submit with invalid name hits changeset error path in save_game", %{conn: conn} do
{:ok, view, _html} = live(conn, "/games/new")

# Submit with empty name − passes HTML form but fails server-side validate_required
view
|> form("#game-form", game: %{name: "", edition: "webapp"})
|> render_submit()

# Form should still be present (no redirect on error)
assert has_element?(view, "#game-form")
end
end
end
62 changes: 62 additions & 0 deletions copi.owasp.org/test/copi_web/live/game_live/show_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,67 @@ defmodule CopiWeb.GameLive.ShowTest do
alias CopiWeb.GameLive.Show
assert Show.card_played_in_round([], 1) == nil
end

test "card_played_in_round/2 returns the matching card", %{conn: _conn, game: _game} do
alias CopiWeb.GameLive.Show
card = %{played_in_round: 3}
assert Show.card_played_in_round([%{played_in_round: 1}, %{played_in_round: 2}, card], 3) == card
end

test "redirects to /error when game_id is not found", %{conn: conn} do
assert {:error, {:redirect, %{to: "/error"}}} =
live(conn, "/games/00000000000000000000000001")
end

test "handle_params uses rounds_played directly for finished game", %{conn: conn, game: game} do
{:ok, finished_game} =
Cornucopia.update_game(game, %{
started_at: DateTime.truncate(DateTime.utc_now(), :second),
finished_at: DateTime.truncate(DateTime.utc_now(), :second),
rounds_played: 2
})

{:ok, _view, html} = live(conn, "/games/#{finished_game.id}")
assert html =~ finished_game.name
end

test "handle_info with non-matching topic is no-op", %{conn: conn, game: game} do
{:ok, show_live, _html} = live(conn, "/games/#{game.id}")
{:ok, updated_game} = Cornucopia.Game.find(game.id)

send(show_live.pid, %{
topic: "game:completely-different-id",
event: "game:updated",
payload: updated_game
})

:timer.sleep(50)
assert render(show_live) =~ game.name
end

test "handle_info sets requested_round to rounds_played for finished game", %{conn: conn, game: game} do
{:ok, _} =
Cornucopia.update_game(game, %{
started_at: DateTime.truncate(DateTime.utc_now(), :second),
finished_at: DateTime.truncate(DateTime.utc_now(), :second),
rounds_played: 3
})

# Use Game.find to get fully preloaded struct (same as real broadcasts)
{:ok, finished_game} = Cornucopia.Game.find(game.id)

{:ok, show_live, _html} = live(conn, "/games/#{finished_game.id}")

send(show_live.pid, %{
topic: "game:#{finished_game.id}",
event: "game:updated",
payload: finished_game
})

:timer.sleep(50)
# With the fix, requested_round = rounds_played = 3, template shows "Viewing round"
# With the bug, requested_round = rounds_played + 1 = 4, template shows "Round 4:"
assert render(show_live) =~ "Viewing round"
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,19 @@ defmodule CopiWeb.PlayerLive.FormComponentTest do

test "updates player successfully without rate limiting", %{conn: conn, game: game} do
{:ok, player} = Cornucopia.create_player(%{name: "Original", game_id: game.id})

# Go to player show page which has Edit link
{:ok, view, _html} = live(conn, "/games/#{game.id}/players/#{player.id}")

# Verify player name is displayed
assert render(view) =~ "Original"

# Update should work without triggering rate limit (skipping this complex test)
:ok
end

test "FormComponent.topic/1 returns correct topic string", %{conn: _conn, game: _game} do
assert CopiWeb.PlayerLive.FormComponent.topic("abc123") == "game:abc123"
end
end
end
Loading
Loading