diff --git a/copi.owasp.org/test/copi/cornucopia_test.exs b/copi.owasp.org/test/copi/cornucopia_test.exs index 1d526d467..347162c91 100644 --- a/copi.owasp.org/test/copi/cornucopia_test.exs +++ b/copi.owasp.org/test/copi/cornucopia_test.exs @@ -222,4 +222,25 @@ defmodule Copi.CornucopiaTest do assert %Ecto.Changeset{} = Cornucopia.change_card(card) end end + describe "schema changesets and find functions" do + alias Copi.Cornucopia.{DealtCard, Vote, Player} + + test "DealtCard.changeset/2 returns a changeset" do + changeset = DealtCard.changeset(%DealtCard{}, %{}) + assert changeset.valid? + end + + test "DealtCard.find/1 returns error when not found" do + assert {:error, :not_found} = DealtCard.find(999_999_999) + end + + test "Vote.changeset/2 returns a changeset" do + changeset = Vote.changeset(%Vote{}, %{}) + assert changeset.valid? + end + + test "Player.find/1 returns error when not found" do + assert {:error, :not_found} = Player.find("00000000000000000000000000") + end + end end diff --git a/copi.owasp.org/test/copi_web/components/core_components_test.exs b/copi.owasp.org/test/copi_web/components/core_components_test.exs index 4863a725e..8aa918421 100644 --- a/copi.owasp.org/test/copi_web/components/core_components_test.exs +++ b/copi.owasp.org/test/copi_web/components/core_components_test.exs @@ -119,4 +119,51 @@ defmodule CopiWeb.CoreComponentsTest do assert html =~ "Title" assert html =~ "Item 1" end + test "renders copy_url_button" do + assigns = %{uri: "http://example.com/game/123"} + html = rendered_to_string(~H""" + + """) + assert html =~ "copy-url-btn" + assert html =~ "http://example.com/game/123" + end + + test "renders header component" do + assigns = %{} + html = rendered_to_string(~H""" + + Page Title + <:subtitle>A subtitle + + """) + assert html =~ "Page Title" + assert html =~ "A subtitle" + end + + test "renders header2 component" do + assigns = %{} + html = rendered_to_string(~H""" + Section Title + """) + assert html =~ "Section Title" + assert html =~ "Click me + """) + assert html =~ "Click me" + assert html =~ "Submit + """) + assert html =~ "Submit" + assert html =~ "bg-indigo-600" + end end 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..98eb2ed6b 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 @@ -69,4 +69,24 @@ defmodule CopiWeb.ApiControllerTest do assert json_response(conn, 403)["error"] == "Player already played a card in this round" end + + test "play_card fails if game not found", %{conn: conn, player: player, dealt_card: dealt_card} do + conn = put(conn, "/api/games/nonexistent-game-id/players/#{player.id}/card", %{ + "game_id" => "nonexistent-game-id", + "player_id" => player.id, + "dealt_card_id" => to_string(dealt_card.id) + }) + + assert json_response(conn, 404)["error"] == "Could not find game" + end + + test "play_card fails if player not in game", %{conn: conn, game: game, dealt_card: dealt_card} do + conn = put(conn, "/api/games/#{game.id}/players/nonexistent-player-id/card", %{ + "game_id" => game.id, + "player_id" => "nonexistent-player-id", + "dealt_card_id" => to_string(dealt_card.id) + }) + + assert json_response(conn, 404)["error"] == "Could not find player and dealt card" + end end 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..1d4f60dba 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 @@ -44,4 +44,11 @@ defmodule CopiWeb.CardControllerTest do end + describe "format_capec" do + test "returns refs unchanged" do + alias CopiWeb.CardController + assert CardController.format_capec([1, 2, 3]) == [1, 2, 3] + assert CardController.format_capec([]) == [] + 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..5116b3d81 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,21 @@ defmodule CopiWeb.GameLive.ShowTest do alias CopiWeb.GameLive.Show assert Show.card_played_in_round([], 1) == nil end + + test "handle_info with non-matching topic is ignored", %{conn: conn, game: game} do + {:ok, show_live, _html} = live(conn, "/games/#{game.id}") + + {:ok, other_game} = Cornucopia.create_game(%{name: "Other Game", edition: "webapp"}) + {:ok, updated_other} = Cornucopia.Game.find(other_game.id) + + send(show_live.pid, %{ + topic: "game:#{other_game.id}", + event: "game:updated", + payload: updated_other + }) + + :timer.sleep(50) + assert render(show_live) =~ game.name + 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..202b17079 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 @@ -111,6 +111,30 @@ 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}) + + # player_first places current player first + other = %{id: "other-id"} + current = %{id: "current-id"} + current_player = %{id: "current-id"} + sorted = Show.player_first([other, current], current_player) + assert List.first(sorted).id == "current-id" + + # get_vote returns nil when no vote, returns vote when found + dealt_no_vote = %{votes: []} + fake_player = %{id: "player-id"} + assert Show.get_vote(dealt_no_vote, fake_player) == nil + + vote = %{player_id: "player-id"} + dealt_with_vote = %{votes: [vote]} + assert Show.get_vote(dealt_with_vote, fake_player) == vote + 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 +143,114 @@ 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 "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)) + ) + + {:ok, card1} = + Cornucopia.create_card(%{ + category: "C", value: "NR1", 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: [] + }) + + {:ok, card2} = + Cornucopia.create_card(%{ + category: "C", value: "NR2", 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: [] + }) + + # Card played in round 1 → round_open? = false + Copi.Repo.insert!(%Copi.Cornucopia.DealtCard{ + player_id: player.id, card_id: card1.id, played_in_round: 1 + }) + # Unplayed card → last_round? = false + 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 + 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)) + ) + + {:ok, card} = + Cornucopia.create_card(%{ + category: "C", value: "NR3", 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: [] + }) + + # Only card is played → no nil-round cards → last_round? = true + 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_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}") + + 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 + + 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..1e18a2de7 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,8 @@ defmodule CopiWeb.Plugs.RateLimiterPlugTest do assert conn.status != 429 refute conn.halted end + test "init/1 returns opts unchanged" do + assert RateLimiterPlug.init([]) == [] + assert RateLimiterPlug.init(foo: :bar) == [foo: :bar] + end end diff --git a/scripts/convert.py b/scripts/convert.py index 3f60754fc..9021d3012 100644 --- a/scripts/convert.py +++ b/scripts/convert.py @@ -812,7 +812,7 @@ def get_replacement_value_from_dict(el_text: str, replacement_values: List[Tuple for k, v in replacement_values: # Avoid expensive regex if key is not even in text - if k.strip() not in el_text: + if not isinstance(k, str) or k.strip() not in el_text: continue el_new = get_replacement_mapping_value(k, v, el_text) @@ -891,7 +891,7 @@ def get_template_for_edition(layout: str = "guide", template: str = "bridge", ed def get_valid_layout_choices() -> List[str]: layouts = [] - if convert_vars.args.layout.lower() == "all" or convert_vars.args.layout == "": + if not convert_vars.args.layout or convert_vars.args.layout.lower() == "all" or convert_vars.args.layout == "": for layout in convert_vars.LAYOUT_CHOICES: if layout not in ("all", "guide"): layouts.append(layout)