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
4 changes: 4 additions & 0 deletions copi.owasp.org/lib/copi/cornucopia/game.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ defmodule Copi.Cornucopia.Game do

Enum.count(players_still_to_play) > 0
end

def game_active?(game) do
game.started_at != nil and game.finished_at == nil
end
end
123 changes: 74 additions & 49 deletions copi.owasp.org/lib/copi_web/live/player_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,57 +100,86 @@ defmodule CopiWeb.PlayerLive.Show do
game = socket.assigns.game
player = socket.assigns.player

# Check if player already voted
if Copi.Cornucopia.Game.has_continue_vote?(game, player) do
# Remove their vote
continue_vote = Enum.find(game.continue_votes, fn vote -> vote.player_id == player.id end)
if continue_vote do
Copi.Repo.delete!(continue_vote)
end
# Validate game lifecycle - continue voting only allowed during active games
unless Copi.Cornucopia.Game.game_active?(game) do
Logger.warning("Continue vote attempt on inactive game: player_id: #{player.id}, game_id: #{game.id}, started_at: #{game.started_at}, finished_at: #{game.finished_at}")
{:noreply, socket}
else
# Add their vote
case Copi.Repo.insert(%Copi.Cornucopia.ContinueVote{player_id: player.id, game_id: game.id}) do
{:ok, _vote} ->
Logger.debug("Continue vote added successfully for player_id: #{player.id}, game_id: #{game.id}")
{:error, changeset} ->
Logger.warning("Continue voting failed for player_id: #{player.id}, game_id: #{game.id}, errors: #{inspect(changeset.errors)}")
# Check if player already voted
if Copi.Cornucopia.Game.has_continue_vote?(game, player) do
# Remove their vote
continue_vote = Enum.find(game.continue_votes, fn vote -> vote.player_id == player.id end)
if continue_vote do
Copi.Repo.delete!(continue_vote)
end
else
# Add their vote
case Copi.Repo.insert(%Copi.Cornucopia.ContinueVote{player_id: player.id, game_id: game.id}) do
{:ok, _vote} ->
Logger.debug("Continue vote added successfully for player_id: #{player.id}, game_id: #{game.id}")
{:error, changeset} ->
Logger.warning("Continue voting failed for player_id: #{player.id}, game_id: #{game.id}, errors: #{inspect(changeset.errors)}")
end
end
end

{:ok, updated_game} = Game.find(game.id)

CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game)

{:noreply, assign(socket, :game, updated_game)}
# Reload fresh game record after continue vote mutations
case Game.find(game.id) do
{:ok, updated_game} ->
CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game)
{:noreply, assign(socket, :game, updated_game)}
{:error, reason} ->
Logger.warning("Failed to reload game after continue vote: game_id: #{game.id}, reason: #{inspect(reason)}")
{:noreply, socket}
end
end
end

@impl true
def handle_event("toggle_vote", %{"dealt_card_id" => dealt_card_id}, socket) do
game = socket.assigns.game
player = socket.assigns.player

{:ok, dealt_card} = DealtCard.find(dealt_card_id)

vote = get_vote(dealt_card, player)

if vote do
Logger.debug("Player has voted: player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
Copi.Repo.delete!(vote)
# Validate game lifecycle - voting only allowed during active games
unless Copi.Cornucopia.Game.game_active?(game) do
Logger.warning("Voting attempt on inactive game: player_id: #{player.id}, game_id: #{game.id}, started_at: #{game.started_at}, finished_at: #{game.finished_at}")
{:noreply, socket}
else
Logger.debug("Player has not voted: player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
case Copi.Repo.insert(%Copi.Cornucopia.Vote{dealt_card_id: String.to_integer(dealt_card_id), player_id: player.id}) do
{:ok, _vote} ->
Logger.debug("Vote added successfully for player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
{:error, changeset} ->
Logger.warning("Voting failed for player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}, errors: #{inspect(changeset.errors)}")
case DealtCard.find(dealt_card_id) do
{:ok, dealt_card} ->
# Validate that dealt card belongs to current game
unless dealt_card_belongs_to_game?(dealt_card, game) do
Logger.warning("Unauthorized voting attempt: player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
{:noreply, socket}
else
vote = get_vote(dealt_card, player)

if vote do
Logger.debug("Player has voted: player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
Copi.Repo.delete!(vote)
else
Logger.debug("Player has not voted: player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
case Copi.Repo.insert(%Copi.Cornucopia.Vote{dealt_card_id: String.to_integer(dealt_card_id), player_id: player.id}) do
{:ok, _vote} ->
Logger.debug("Vote added successfully for player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}")
{:error, changeset} ->
Logger.warning("Voting failed for player_id: #{player.id}, dealt_card_id: #{dealt_card_id}, game_id: #{game.id}, errors: #{inspect(changeset.errors)}")
end
end

case Game.find(game.id) do
{:ok, updated_game} ->
CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game)
{:noreply, assign(socket, :game, updated_game)}
{:error, reason} ->
Logger.warning("Failed to reload game after vote: game_id: #{game.id}, reason: #{inspect(reason)}")
{:noreply, socket}
end
end
{:error, reason} ->
Logger.warning("Failed to find dealt card: dealt_card_id: #{dealt_card_id}, player_id: #{player.id}, game_id: #{game.id}, reason: #{inspect(reason)}")
{:noreply, socket}
end
end

{:ok, updated_game} = Game.find(game.id)

CopiWeb.Endpoint.broadcast(topic(updated_game.id), "game:updated", updated_game)

{:noreply, assign(socket, :game, updated_game)}
end

def topic(game_id) do
Expand Down Expand Up @@ -199,16 +228,12 @@ defmodule CopiWeb.PlayerLive.Show do
Enum.find(dealt_card.votes, fn vote -> vote.player_id == player.id end)
end

def display_game_session(edition) do
case edition do
"webapp" -> "Cornucopia Web Session:"
"ecommerce" -> "Cornucopia Web Session:"
"mobileapp" -> "Cornucopia Mobile Session:"
"masvs" -> "Cornucopia Mobile Session:"
"cumulus" -> "OWASP Cumulus Session:"
"mlsec" -> "Elevation of MLSec Session:"
_ -> "EoP Session:"
end
defp dealt_card_belongs_to_game?(dealt_card, game) do
# Check if the dealt card's player belongs to the current game
player_ids = Enum.map(game.players, & &1.id)
dealt_card.player_id in player_ids
end

end
def topic(game_id) do
"game:#{game_id}"
end
106 changes: 106 additions & 0 deletions copi.owasp.org/test/copi_web/live/player_live/show_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,111 @@ defmodule CopiWeb.PlayerLive.ShowTest do
{:ok, updated_dealt2} = Copi.Cornucopia.DealtCard.find(to_string(dealt.id))
assert length(updated_dealt2.votes) == 0
end

test "toggle_vote should not work before game starts", %{conn: conn, player: player} do
game_id = player.game_id
{:ok, game} = Cornucopia.Game.find(game_id)

# Ensure game has NOT started (started_at is nil)
assert game.started_at == nil

{:ok, card} =
Cornucopia.create_card(%{
category: "C", value: "TV1", description: "D", edition: "webapp",
version: "2.2", external_id: "TV_CARD_PRE_GAME", 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}")

# Attempt to vote before game starts - should be ignored
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) == 0
end

test "toggle_vote should not work after game ends", %{conn: conn, player: player} do
game_id = player.game_id
{:ok, game} = Cornucopia.Game.find(game_id)

# Start the game
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_CARD_AFTER_END", 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
})

# Now end the game
Copi.Repo.update!(
Ecto.Changeset.change(game, finished_at: DateTime.truncate(DateTime.utc_now(), :second))
)

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

# Attempt to vote after game ends - should be ignored
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) == 0
end

test "toggle_continue_vote should not work before game starts", %{conn: conn, player: player} do
game_id = player.game_id
{:ok, game} = Cornucopia.Game.find(game_id)

# Ensure game has NOT started (started_at is nil)
assert game.started_at == nil

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

# Attempt to continue vote before game starts - should be ignored
render_click(show_live, "toggle_continue_vote", %{})
:timer.sleep(100)

{:ok, updated_game} = Cornucopia.Game.find(game_id)
assert length(updated_game.continue_votes) == 0
end

test "toggle_continue_vote should not work after game ends", %{conn: conn, player: player} do
game_id = player.game_id
{:ok, game} = Cornucopia.Game.find(game_id)

# Start the game
Copi.Repo.update!(
Ecto.Changeset.change(game, started_at: DateTime.truncate(DateTime.utc_now(), :second))
)

# Now end the game
Copi.Repo.update!(
Ecto.Changeset.change(game, finished_at: DateTime.truncate(DateTime.utc_now(), :second))
)

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

# Attempt to continue vote after game ends - should be ignored
render_click(show_live, "toggle_continue_vote", %{})
:timer.sleep(100)

{:ok, updated_game} = Cornucopia.Game.find(game_id)
assert length(updated_game.continue_votes) == 0
end
end
end
Loading