diff --git a/copi.owasp.org/lib/copi_web/live/game_live/show.ex b/copi.owasp.org/lib/copi_web/live/game_live/show.ex index aa84ab4ce..ef6a04f9d 100644 --- a/copi.owasp.org/lib/copi_web/live/game_live/show.ex +++ b/copi.owasp.org/lib/copi_web/live/game_live/show.ex @@ -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 diff --git a/copi.owasp.org/test/copi/cornucopia_logic_test.exs b/copi.owasp.org/test/copi/cornucopia_logic_test.exs index 0a5a11fef..3e813b111 100644 --- a/copi.owasp.org/test/copi/cornucopia_logic_test.exs +++ b/copi.owasp.org/test/copi/cornucopia_logic_test.exs @@ -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 @@ -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 diff --git a/copi.owasp.org/test/copi/cornucopia_test.exs b/copi.owasp.org/test/copi/cornucopia_test.exs index 1d526d467..3642f2a16 100644 --- a/copi.owasp.org/test/copi/cornucopia_test.exs +++ b/copi.owasp.org/test/copi/cornucopia_test.exs @@ -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 diff --git a/copi.owasp.org/test/copi/ip_helper_test.exs b/copi.owasp.org/test/copi/ip_helper_test.exs index 074a145e2..a5ead9af3 100644 --- a/copi.owasp.org/test/copi/ip_helper_test.exs +++ b/copi.owasp.org/test/copi/ip_helper_test.exs @@ -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 diff --git a/copi.owasp.org/test/copi/rate_limiter_test.exs b/copi.owasp.org/test/copi/rate_limiter_test.exs index 76f4d904f..df1c71fda 100644 --- a/copi.owasp.org/test/copi/rate_limiter_test.exs +++ b/copi.owasp.org/test/copi/rate_limiter_test.exs @@ -241,6 +241,22 @@ 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 @@ -248,6 +264,18 @@ defmodule Copi.RateLimiterTest 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 diff --git a/copi.owasp.org/test/copi_web/controllers/api_controller_test.exs b/copi.owasp.org/test/copi_web/controllers/api_controller_test.exs index d41a415b9..dccd2f3a3 100644 --- a/copi.owasp.org/test/copi_web/controllers/api_controller_test.exs +++ b/copi.owasp.org/test/copi_web/controllers/api_controller_test.exs @@ -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(%{ diff --git a/copi.owasp.org/test/copi_web/controllers/card_controller_test.exs b/copi.owasp.org/test/copi_web/controllers/card_controller_test.exs index 2d9a0ad35..245006063 100644 --- a/copi.owasp.org/test/copi_web/controllers/card_controller_test.exs +++ b/copi.owasp.org/test/copi_web/controllers/card_controller_test.exs @@ -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 diff --git a/copi.owasp.org/test/copi_web/live/game_live/create_game_form_test.exs b/copi.owasp.org/test/copi_web/live/game_live/create_game_form_test.exs index 2206db871..cf7206dbe 100644 --- a/copi.owasp.org/test/copi_web/live/game_live/create_game_form_test.exs +++ b/copi.owasp.org/test/copi_web/live/game_live/create_game_form_test.exs @@ -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"}) @@ -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 diff --git a/copi.owasp.org/test/copi_web/live/game_live/show_test.exs b/copi.owasp.org/test/copi_web/live/game_live/show_test.exs index fb19034e7..686910247 100644 --- a/copi.owasp.org/test/copi_web/live/game_live/show_test.exs +++ b/copi.owasp.org/test/copi_web/live/game_live/show_test.exs @@ -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 diff --git a/copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs b/copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs index 636169f77..50b4ed3ee 100644 --- a/copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs +++ b/copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs @@ -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 diff --git a/copi.owasp.org/test/copi_web/live/player_live/show_test.exs b/copi.owasp.org/test/copi_web/live/player_live/show_test.exs index d41fec568..30d135ee4 100644 --- a/copi.owasp.org/test/copi_web/live/player_live/show_test.exs +++ b/copi.owasp.org/test/copi_web/live/player_live/show_test.exs @@ -101,7 +101,7 @@ defmodule CopiWeb.PlayerLive.ShowTest do assert updated_game.rounds_played == 1 end - test "helper functions return expected values", %{conn: _conn, player: _player} do + test "helper functions return expected values", %{conn: _conn, player: player} do alias CopiWeb.PlayerLive.Show assert Show.ordered_cards([]) == [] @@ -111,6 +111,14 @@ defmodule CopiWeb.PlayerLive.ShowTest do # With no players, no one is still to play → round_open? is false → round_closed? is true assert Show.round_closed?(%{players: [], rounds_played: 0}) == true + # last_round? returns false when a player still has a nil-round card + player_with_unplayed = %{dealt_cards: [%{played_in_round: nil}]} + refute Show.last_round?(%{players: [player_with_unplayed], rounds_played: 0}) + + # last_round? returns true when all cards are played + player_all_played = %{dealt_cards: [%{played_in_round: 1}]} + assert Show.last_round?(%{players: [player_all_played], rounds_played: 0}) + assert Show.display_game_session("webapp") == "Cornucopia Web Session:" assert Show.display_game_session("ecommerce") == "Cornucopia Web Session:" assert Show.display_game_session("mobileapp") == "Cornucopia Mobile Session:" @@ -119,5 +127,164 @@ defmodule CopiWeb.PlayerLive.ShowTest do assert Show.display_game_session("mlsec") == "Elevation of MLSec Session:" assert Show.display_game_session("eop") == "EoP Session:" end + + test "player_first/2 places current player first in list", %{conn: _conn, player: player} do + alias CopiWeb.PlayerLive.Show + other = %{id: "other-id"} + current = %{id: player.id} + sorted = Show.player_first([other, current], player) + assert List.first(sorted).id == player.id + end + + test "get_vote/2 returns nil when no matching vote", %{conn: _conn, player: player} do + alias CopiWeb.PlayerLive.Show + dealt = %{votes: []} + assert Show.get_vote(dealt, player) == nil + end + + test "get_vote/2 returns the matching vote", %{conn: _conn, player: player} do + alias CopiWeb.PlayerLive.Show + vote = %{player_id: player.id} + dealt = %{votes: [vote]} + assert Show.get_vote(dealt, player) == vote + end + + test "next_round when round is closed and not last round advances rounds_played", + %{conn: conn, player: player} do + game_id = player.game_id + {:ok, game} = Cornucopia.Game.find(game_id) + + Copi.Repo.update!( + Ecto.Changeset.change(game, started_at: DateTime.truncate(DateTime.utc_now(), :second)) + ) + + # Card played in round 1 (current round) → round_open? = false + {:ok, card1} = + Cornucopia.create_card(%{ + category: "C", value: "V3", description: "D", edition: "webapp", + version: "2.2", external_id: "NR_CLOSED1", language: "en", misc: "m", + owasp_scp: [], owasp_devguide: [], owasp_asvs: [], owasp_appsensor: [], + capec: [], safecode: [], owasp_mastg: [], owasp_masvs: [] + }) + + # Unplayed card → last_round? = false (player still has nil-round card) + {:ok, card2} = + Cornucopia.create_card(%{ + category: "C", value: "V4", description: "D", edition: "webapp", + version: "2.2", external_id: "NR_CLOSED2", language: "en", misc: "m", + owasp_scp: [], owasp_devguide: [], owasp_asvs: [], owasp_appsensor: [], + capec: [], safecode: [], owasp_mastg: [], owasp_masvs: [] + }) + + Copi.Repo.insert!(%Copi.Cornucopia.DealtCard{ + player_id: player.id, card_id: card1.id, played_in_round: 1 + }) + + Copi.Repo.insert!(%Copi.Cornucopia.DealtCard{ + player_id: player.id, card_id: card2.id, played_in_round: nil + }) + + {:ok, show_live, _html} = live(conn, "/games/#{game_id}/players/#{player.id}") + render_click(show_live, "next_round", %{}) + :timer.sleep(100) + + {:ok, updated_game} = Cornucopia.Game.find(game_id) + assert updated_game.rounds_played == 1 + # last_round? = false because player still has unplayed card + assert updated_game.finished_at == nil + end + + test "next_round when round is closed and IS last round sets finished_at", + %{conn: conn, player: player} do + game_id = player.game_id + {:ok, game} = Cornucopia.Game.find(game_id) + + Copi.Repo.update!( + Ecto.Changeset.change(game, started_at: DateTime.truncate(DateTime.utc_now(), :second)) + ) + + # Player has exactly one card, played in round 1 → no nil-round cards remain + {:ok, card} = + Cornucopia.create_card(%{ + category: "C", value: "V5", description: "D", edition: "webapp", + version: "2.2", external_id: "NR_LAST1", language: "en", misc: "m", + owasp_scp: [], owasp_devguide: [], owasp_asvs: [], owasp_appsensor: [], + capec: [], safecode: [], owasp_mastg: [], owasp_masvs: [] + }) + + Copi.Repo.insert!(%Copi.Cornucopia.DealtCard{ + player_id: player.id, card_id: card.id, played_in_round: 1 + }) + + {:ok, show_live, _html} = live(conn, "/games/#{game_id}/players/#{player.id}") + render_click(show_live, "next_round", %{}) + :timer.sleep(100) + + {:ok, updated_game} = Cornucopia.Game.find(game_id) + assert updated_game.rounds_played == 1 + assert updated_game.finished_at != nil + end + + test "toggle_continue_vote adds then removes a continue vote", %{conn: conn, player: player} do + game_id = player.game_id + {:ok, game} = Cornucopia.Game.find(game_id) + + Copi.Repo.update!( + Ecto.Changeset.change(game, started_at: DateTime.truncate(DateTime.utc_now(), :second)) + ) + + {:ok, show_live, _html} = live(conn, "/games/#{game_id}/players/#{player.id}") + + # No vote yet → should insert a continue vote + render_click(show_live, "toggle_continue_vote", %{}) + :timer.sleep(100) + + {:ok, updated_game} = Cornucopia.Game.find(game_id) + assert length(updated_game.continue_votes) == 1 + + # Vote exists → should delete it + render_click(show_live, "toggle_continue_vote", %{}) + :timer.sleep(100) + + {:ok, updated_game2} = Cornucopia.Game.find(game_id) + assert length(updated_game2.continue_votes) == 0 + end + + test "toggle_vote adds then removes a vote for a dealt card", %{conn: conn, player: player} do + game_id = player.game_id + {:ok, game} = Cornucopia.Game.find(game_id) + + Copi.Repo.update!( + Ecto.Changeset.change(game, started_at: DateTime.truncate(DateTime.utc_now(), :second)) + ) + + {:ok, card} = + Cornucopia.create_card(%{ + category: "C", value: "TV1", description: "D", edition: "webapp", + version: "2.2", external_id: "TV_CARD1", language: "en", misc: "m", + owasp_scp: [], owasp_devguide: [], owasp_asvs: [], owasp_appsensor: [], + capec: [], safecode: [], owasp_mastg: [], owasp_masvs: [] + }) + + dealt = Copi.Repo.insert!(%Copi.Cornucopia.DealtCard{ + player_id: player.id, card_id: card.id, played_in_round: 1 + }) + + {:ok, show_live, _html} = live(conn, "/games/#{game_id}/players/#{player.id}") + + # No vote yet → should insert a vote + render_click(show_live, "toggle_vote", %{"dealt_card_id" => to_string(dealt.id)}) + :timer.sleep(100) + + {:ok, updated_dealt} = Copi.Cornucopia.DealtCard.find(to_string(dealt.id)) + assert length(updated_dealt.votes) == 1 + + # Vote exists → should delete it + render_click(show_live, "toggle_vote", %{"dealt_card_id" => to_string(dealt.id)}) + :timer.sleep(100) + + {:ok, updated_dealt2} = Copi.Cornucopia.DealtCard.find(to_string(dealt.id)) + assert length(updated_dealt2.votes) == 0 + end end end diff --git a/copi.owasp.org/test/copi_web/plugs/rate_limiter_plug_test.exs b/copi.owasp.org/test/copi_web/plugs/rate_limiter_plug_test.exs index 107d4d31a..a5a2d9308 100644 --- a/copi.owasp.org/test/copi_web/plugs/rate_limiter_plug_test.exs +++ b/copi.owasp.org/test/copi_web/plugs/rate_limiter_plug_test.exs @@ -74,4 +74,16 @@ defmodule CopiWeb.Plugs.RateLimiterPlugTest do assert conn.status != 429 refute conn.halted end + + test "skips rate limiting when remote_ip is explicitly nil" do + # Explicitly nil remote_ip + no forwarded header → {:none, nil} branch + conn = + conn(:get, "/") + |> Map.put(:remote_ip, nil) + |> init_test_session(%{}) + |> RateLimiterPlug.call([]) + + assert conn.status != 429 + refute conn.halted + end end