Skip to content

Commit 18a9847

Browse files
committed
test(copi): add COPI test coverage to reach 90% threshold
1 parent 6ed1b1d commit 18a9847

File tree

11 files changed

+420
-14
lines changed

11 files changed

+420
-14
lines changed

copi.owasp.org/test/copi/cornucopia_logic_test.exs

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ defmodule Copi.CornucopiaLogicTest do
9696
create_card("Wild Card", "1")
9797
create_card("Hearts", "5")
9898
create_card("WILD CARD", "2")
99-
99+
100100
suits = Cornucopia.get_suits_from_selected_deck("webapp")
101101

102102
refute "Wild Card" in suits
@@ -200,20 +200,62 @@ defmodule Copi.CornucopiaLogicTest do
200200
test "jokers trump all other cards", %{game: game, p1: p1, p2: p2} do
201201
{:ok, joker} = create_card("Joker", "JokerA")
202202
{:ok, trump} = create_card("Cornucopia", "A")
203-
203+
204204
d1 = play_card(p1, trump, 1)
205205
d2 = play_card(p2, joker, 1)
206-
206+
207207
# Add votes
208208
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d1.id, player_id: p1.id})
209209
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d2.id, player_id: p2.id})
210-
210+
211211
# Reload game
212212
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])
213-
213+
214214
winner = Cornucopia.highest_scoring_card_in_round(game, 1)
215-
215+
216216
# Joker should win
217217
assert winner.id == d2.id
218218
end
219+
220+
test "highest_scoring_card_in_round returns nil when no cards have enough votes",
221+
%{game: game, p1: p1, p2: p2} do
222+
{:ok, c1} = create_card("Authentication", "3")
223+
{:ok, c2} = create_card("Authentication", "7")
224+
225+
# Play cards but add NO votes → scoring_cards filters all out → special_lead_cards([]) → nil path
226+
play_card(p1, c1, 1)
227+
play_card(p2, c2, 1)
228+
229+
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])
230+
231+
result = Cornucopia.highest_scoring_card_in_round(game, 1)
232+
assert result == nil
233+
end
234+
235+
test "lead suit wins when no trump or joker present", %{game: game, p1: p1, p2: p2} do
236+
{:ok, c1} = create_card("Authentication", "3")
237+
{:ok, c2} = create_card("Authentication", "8")
238+
239+
# p1 plays first (leads with Authentication), p2 follows
240+
d1 = play_card(p1, c1, 1)
241+
:timer.sleep(15)
242+
d2 = play_card(p2, c2, 1)
243+
244+
# Add votes to both (2 players, need > 0.5 votes each)
245+
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d1.id, player_id: p1.id})
246+
Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d2.id, player_id: p2.id})
247+
248+
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])
249+
250+
winner = Cornucopia.highest_scoring_card_in_round(game, 1)
251+
252+
# "8" ranks higher than "3" in card_order → d2 wins
253+
assert winner.id == d2.id
254+
end
255+
256+
test "highest_scoring_card_in_round returns nil when no cards played in game",
257+
%{game: game} do
258+
game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])
259+
assert Cornucopia.highest_scoring_card_in_round(game, 1) == nil
260+
end
219261
end

copi.owasp.org/test/copi/cornucopia_test.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,39 @@ defmodule Copi.CornucopiaTest do
6666
game = game_fixture()
6767
assert %Ecto.Changeset{} = Cornucopia.change_game(game)
6868
end
69+
70+
test "Game.find/1 returns OK tuple for existing game" do
71+
game = game_fixture()
72+
assert {:ok, found} = Copi.Cornucopia.Game.find(game.id)
73+
assert found.id == game.id
74+
end
75+
76+
test "Game.find/1 returns error for non-existent game" do
77+
assert {:error, :not_found} =
78+
Copi.Cornucopia.Game.find("00000000000000000000000099")
79+
end
80+
81+
test "Game.continue_vote_count/1 returns count of continue votes" do
82+
alias Copi.Cornucopia.Game
83+
game = game_fixture()
84+
{:ok, reloaded} = Game.find(game.id)
85+
assert Game.continue_vote_count(reloaded) == 0
86+
end
87+
88+
test "Game.majority_continue_votes_reached?/1 returns true when votes exceed half" do
89+
alias Copi.Cornucopia.Game
90+
alias Copi.Repo
91+
game = game_fixture()
92+
{:ok, created_player} = Cornucopia.create_player(%{name: "p1", game_id: game.id})
93+
{:ok, reloaded} = Game.find(game.id)
94+
# 0 votes, 1 player → 0 > div(1,2)=0 → false
95+
refute Game.majority_continue_votes_reached?(reloaded)
96+
# Add a continue vote
97+
Repo.insert!(%Copi.Cornucopia.ContinueVote{player_id: created_player.id, game_id: game.id})
98+
{:ok, updated} = Game.find(game.id)
99+
# 1 vote > div(1,2)=0 → true
100+
assert Game.majority_continue_votes_reached?(updated)
101+
end
69102
end
70103

71104
describe "players" do

copi.owasp.org/test/copi/ip_helper_test.exs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,61 @@ defmodule Copi.IPHelperTest do
170170
test "handles malformed extract_first_ip inputs" do
171171
info = %{x_headers: [{"x-forwarded-for", "invalid"}]}
172172
assert IPHelper.get_ip_from_connect_info(info) == nil
173-
173+
174174
info2 = %{x_headers: [{"other", "10.0.0.1"}]}
175175
assert IPHelper.get_ip_from_connect_info(info2) == nil
176176
end
177+
178+
test "extracts from req_headers with atom key tuples" do
179+
info = %{req_headers: [{:"x-forwarded-for", "10.2.3.4"}]}
180+
assert IPHelper.get_ip_from_connect_info(info) == {10, 2, 3, 4}
181+
end
182+
183+
test "handles x_headers as raw binary string" do
184+
info = %{x_headers: "10.8.9.1"}
185+
assert IPHelper.get_ip_from_connect_info(info) == {10, 8, 9, 1}
186+
end
187+
end
188+
189+
describe "get_ip_from_socket/1 (LiveView) - additional coverage" do
190+
test "extracts IP from connect_info map req_headers" do
191+
socket = %Phoenix.LiveView.Socket{
192+
private: %{
193+
connect_info: %{
194+
req_headers: [{"x-forwarded-for", "10.0.5.6"}]
195+
}
196+
}
197+
}
198+
199+
assert IPHelper.get_ip_from_socket(socket) == {10, 0, 5, 6}
200+
end
201+
202+
test "handles connect_info map with x_headers as binary string" do
203+
socket = %Phoenix.LiveView.Socket{
204+
private: %{
205+
connect_info: %{x_headers: "10.7.8.9"}
206+
}
207+
}
208+
209+
assert IPHelper.get_ip_from_socket(socket) == {10, 7, 8, 9}
210+
end
211+
212+
test "handles connect_info map with x_headers as string-keyed map" do
213+
socket = %Phoenix.LiveView.Socket{
214+
private: %{
215+
connect_info: %{x_headers: %{"x-forwarded-for" => "10.1.2.3"}}
216+
}
217+
}
218+
219+
assert IPHelper.get_ip_from_socket(socket) == {10, 1, 2, 3}
220+
end
221+
222+
test "falls back to localhost when connect_info map has no usable IP info" do
223+
socket = %Phoenix.LiveView.Socket{
224+
private: %{connect_info: %{no_headers: "foo"}}
225+
}
226+
227+
assert IPHelper.get_ip_from_socket(socket) == {127, 0, 0, 1}
228+
end
177229
end
178230
end

copi.owasp.org/test/copi/rate_limiter_test.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,41 @@ defmodule Copi.RateLimiterTest do
241241
# Should still work even with weird input
242242
assert {:ok, _} = RateLimiter.check_rate("invalid-ip", :game_creation)
243243
end
244+
245+
test "bypasses rate limit in production mode for localhost" do
246+
Application.put_env(:copi, :env, :prod)
247+
248+
try do
249+
result = RateLimiter.check_rate({127, 0, 0, 1}, :game_creation)
250+
assert result == {:ok, :unlimited}
251+
after
252+
Application.put_env(:copi, :env, :test)
253+
end
254+
end
255+
256+
test "normalize_ip passes through non-tuple non-binary input" do
257+
# Passing an integer (not a tuple or binary) hits the catch-all normalize_ip clause
258+
assert {:ok, _} = RateLimiter.check_rate(12345, :game_creation)
259+
end
244260
end
245261

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

267+
test "handles :cleanup message gracefully" do
268+
pid = Process.whereis(Copi.RateLimiter)
269+
# Populate some state first
270+
RateLimiter.check_rate({10, 20, 30, 40}, :game_creation)
271+
# Directly send the cleanup message to trigger handle_info(:cleanup, state)
272+
send(pid, :cleanup)
273+
Process.sleep(50)
274+
# Should still be healthy
275+
assert Process.alive?(pid)
276+
assert {:ok, _} = RateLimiter.check_rate({10, 20, 30, 41}, :game_creation)
277+
end
278+
251279
test "can make requests after clearing IP", %{ip: ip} do
252280
config = RateLimiter.get_config()
253281
limit = config.limits.connection

copi.owasp.org/test/copi_web/controllers/api_controller_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ defmodule CopiWeb.ApiControllerTest do
5050
assert json_response(conn, 406)["error"] == "Card already played"
5151
end
5252

53+
test "play_card returns 404 when game not found", %{conn: conn} do
54+
conn = put(conn, "/api/games/00000000000000000000000001/players/fakeplayer/card", %{
55+
"dealt_card_id" => "999"
56+
})
57+
58+
assert json_response(conn, 404)["error"] == "Could not find game"
59+
end
60+
5361
test "play_card fails if player already played in round", %{conn: conn, game: game, player: player, dealt_card: dealt_card} do
5462
# Create another card and mark it as played in this round (0 + 1 => 1)
5563
{:ok, card2} = Cornucopia.create_card(%{

copi.owasp.org/test/copi_web/controllers/card_controller_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,15 @@ defmodule CopiWeb.CardControllerTest do
4343
end
4444
end
4545

46+
describe "format_capec/1" do
47+
test "returns refs unchanged" do
48+
refs = ["1234", "5678"]
49+
assert CopiWeb.CardController.format_capec(refs) == refs
50+
end
51+
52+
test "returns empty list unchanged" do
53+
assert CopiWeb.CardController.format_capec([]) == []
54+
end
55+
end
4656

4757
end

copi.owasp.org/test/copi_web/live/game_live/create_game_form_test.exs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ defmodule CopiWeb.GameLive.CreateGameFormTest do
4949

5050
test "validation errors don't consume rate limit", %{conn: conn} do
5151
{:ok, view, _html} = live(conn, "/games/new")
52-
52+
5353
# Submit invalid form (empty name triggers validation)
5454
html = view
5555
|> form("#game-form", game: %{name: "", edition: "webapp"})
@@ -65,5 +65,17 @@ defmodule CopiWeb.GameLive.CreateGameFormTest do
6565
# Successful creation redirects
6666
assert {:ok, _view, _html} = follow_redirect(result, conn)
6767
end
68+
69+
test "submit with invalid name hits changeset error path in save_game", %{conn: conn} do
70+
{:ok, view, _html} = live(conn, "/games/new")
71+
72+
# Submit with empty name − passes HTML form but fails server-side validate_required
73+
view
74+
|> form("#game-form", game: %{name: "", edition: "webapp"})
75+
|> render_submit()
76+
77+
# Form should still be present (no redirect on error)
78+
assert has_element?(view, "#game-form")
79+
end
6880
end
6981
end

copi.owasp.org/test/copi_web/live/game_live/show_test.exs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,43 @@ defmodule CopiWeb.GameLive.ShowTest do
129129
assert Show.card_played_in_round([], 1) == nil
130130
end
131131

132+
test "card_played_in_round/2 returns the matching card", %{conn: _conn, game: _game} do
133+
alias CopiWeb.GameLive.Show
134+
card = %{played_in_round: 3}
135+
assert Show.card_played_in_round([%{played_in_round: 1}, %{played_in_round: 2}, card], 3) == card
136+
end
137+
138+
test "redirects to /error when game_id is not found", %{conn: conn} do
139+
assert {:error, {:redirect, %{to: "/error"}}} =
140+
live(conn, "/games/00000000000000000000000001")
141+
end
142+
143+
test "handle_params uses rounds_played directly for finished game", %{conn: conn, game: game} do
144+
{:ok, finished_game} =
145+
Cornucopia.update_game(game, %{
146+
started_at: DateTime.truncate(DateTime.utc_now(), :second),
147+
finished_at: DateTime.truncate(DateTime.utc_now(), :second),
148+
rounds_played: 2
149+
})
150+
151+
{:ok, _view, html} = live(conn, "/games/#{finished_game.id}")
152+
assert html =~ finished_game.name
153+
end
154+
155+
test "handle_info with non-matching topic is no-op", %{conn: conn, game: game} do
156+
{:ok, show_live, _html} = live(conn, "/games/#{game.id}")
157+
{:ok, updated_game} = Cornucopia.Game.find(game.id)
158+
159+
send(show_live.pid, %{
160+
topic: "game:completely-different-id",
161+
event: "game:updated",
162+
payload: updated_game
163+
})
164+
165+
:timer.sleep(50)
166+
assert render(show_live) =~ game.name
167+
end
168+
132169
test "handle_info sets requested_round to rounds_played for finished game", %{conn: conn, game: game} do
133170
{:ok, finished_game} =
134171
Cornucopia.update_game(game, %{
@@ -146,8 +183,9 @@ defmodule CopiWeb.GameLive.ShowTest do
146183
})
147184

148185
:timer.sleep(50)
149-
# requested_round must be 3 (rounds_played), not 4 (rounds_played + 1)
150-
assert :sys.get_state(show_live.pid).socket.assigns.requested_round == 3
186+
# With the fix, requested_round = rounds_played = 3, so template shows "Viewing round"
187+
# With the bug, requested_round = rounds_played + 1 = 4, so template shows "Round 4:"
188+
assert render(show_live) =~ "Viewing round"
151189
end
152190
end
153191
end

copi.owasp.org/test/copi_web/live/player_live/form_component_test.exs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,19 @@ defmodule CopiWeb.PlayerLive.FormComponentTest do
7070

7171
test "updates player successfully without rate limiting", %{conn: conn, game: game} do
7272
{:ok, player} = Cornucopia.create_player(%{name: "Original", game_id: game.id})
73-
73+
7474
# Go to player show page which has Edit link
7575
{:ok, view, _html} = live(conn, "/games/#{game.id}/players/#{player.id}")
76-
76+
7777
# Verify player name is displayed
7878
assert render(view) =~ "Original"
79-
79+
8080
# Update should work without triggering rate limit (skipping this complex test)
8181
:ok
8282
end
83+
84+
test "FormComponent.topic/1 returns correct topic string", %{conn: _conn, game: _game} do
85+
assert CopiWeb.PlayerLive.FormComponent.topic("abc123") == "game:abc123"
86+
end
8387
end
8488
end

0 commit comments

Comments
 (0)