testing channels

This commit is contained in:
2026-03-03 09:46:12 -07:00
parent c86fbe9f67
commit e0f2d8c4aa
9 changed files with 285 additions and 5 deletions

View File

@@ -0,0 +1,108 @@
defmodule Backend.GameRunnerTest do
use ExUnit.Case, async: true
alias Backend.GameRunner
describe "update_player_keys" do
test "tracks keys_pressed when player sends keydown event" do
player_id = "player_1"
initial_state = %{
players: %{
player_id => %{x: 100, y: 100, keys_pressed: []}
},
tick_number: 0
}
keys_pressed = ["w"]
{:noreply, new_state} =
GameRunner.handle_cast({:update_player_keys, player_id, keys_pressed}, initial_state)
assert new_state.players[player_id].keys_pressed == keys_pressed
end
test "tracks multiple keys_pressed when player sends multiple keydown events" do
player_id = "player_2"
initial_state = %{
players: %{
player_id => %{x: 200, y: 200, keys_pressed: []}
},
tick_number: 0
}
keys_pressed = ["w", "a"]
{:noreply, new_state} =
GameRunner.handle_cast({:update_player_keys, player_id, keys_pressed}, initial_state)
assert new_state.players[player_id].keys_pressed == ["w", "a"]
end
test "updates keys_pressed to empty list when player sends keyup event" do
player_id = "player_3"
initial_state = %{
players: %{
player_id => %{x: 300, y: 300, keys_pressed: ["w", "d"]}
},
tick_number: 0
}
keys_pressed = []
{:noreply, new_state} =
GameRunner.handle_cast({:update_player_keys, player_id, keys_pressed}, initial_state)
assert new_state.players[player_id].keys_pressed == []
end
test "updates keys_pressed when player releases some keys but not all" do
player_id = "player_4"
initial_state = %{
players: %{
player_id => %{x: 400, y: 400, keys_pressed: ["w", "a"]}
},
tick_number: 0
}
keys_pressed = ["w"]
{:noreply, new_state} =
GameRunner.handle_cast({:update_player_keys, player_id, keys_pressed}, initial_state)
assert new_state.players[player_id].keys_pressed == ["w"]
end
test "preserves player position when updating keys_pressed" do
player_id = "player_6"
x = 500
y = 250
initial_state = %{
players: %{
player_id => %{x: x, y: y, keys_pressed: []}
},
tick_number: 0
}
keys_pressed = ["d"]
{:noreply, new_state} =
GameRunner.handle_cast({:update_player_keys, player_id, keys_pressed}, initial_state)
assert new_state.players[player_id].x == x
assert new_state.players[player_id].y == y
assert new_state.players[player_id].keys_pressed == ["d"]
end
end
describe "get_node_name" do
test "returns the current node name" do
node_name = GameRunner.get_node_name()
assert node_name == node()
end
end
end

View File

@@ -0,0 +1,84 @@
defmodule Backend.NodeClusterIntegrationTests do
use ExUnit.Case, async: false
alias Backend.GameRunner
describe "GameRunner singleton across a 4-node cluster" do
test "GameRunner runs on exactly one of the four nodes" do
peers = start_cluster(4)
peer_nodes = Enum.map(peers, fn {_p, n} -> n end)
game_runner_pids =
Enum.map(peers, fn {peer_pid, peer_node_name} ->
runner_pid = :peer.call(peer_pid, :global, :whereis_name, [Backend.GameRunner])
assert is_pid(runner_pid),
"#{peer_node_name} could not find GameRunner in :global registry"
runner_pid
end)
assert length(Enum.uniq(game_runner_pids)) == 1,
"Peers disagree on GameRunner PID: #{inspect(Enum.uniq(game_runner_pids))}"
runner_pid = hd(game_runner_pids)
owner_node = node(runner_pid)
assert owner_node in peer_nodes,
"Owner node #{owner_node} is not one of the cluster peers #{inspect(peer_nodes)}"
assert length(peer_nodes -- [owner_node]) == 3,
"Expected 3 non-owner peer nodes, got #{inspect(peer_nodes -- [owner_node])}"
end
end
defp start_cluster(count) do
peers =
for index <- 1..count do
peer_name = :"cluster_peer_#{System.unique_integer([:positive])}_#{index}"
{:ok, peer, peer_node} =
:peer.start_link(%{
name: peer_name,
connection: :standard_io,
wait_boot: 30_000
})
_ = :peer.call(peer, :application, :ensure_all_started, [:elixir])
Enum.each(:code.get_path(), fn path ->
_ = :peer.call(peer, :code, :add_patha, [path])
end)
# needed for authentication
:peer.call(peer, :erlang, :set_cookie, [Node.get_cookie()])
:peer.call(peer, :application, :set_env, [:opentelemetry, :traces_exporter, :none])
:peer.call(peer, :application, :set_env, [:opentelemetry, :processors, []])
on_exit(fn ->
try do
:peer.stop(peer)
catch
:exit, _ -> :ok
end
end)
{peer, peer_node}
end
peer_nodes = Enum.map(peers, fn {_p, n} -> n end)
for {peer, _} <- peers do
Enum.each(peer_nodes, fn target ->
:peer.call(peer, :net_kernel, :connect_node, [target])
end)
end
for {peer, _} <- peers do
{:ok, _} = :peer.call(peer, :application, :ensure_all_started, [:backend])
end
peers
end
end

View File

@@ -0,0 +1,64 @@
defmodule BackendWeb.ConnectedUserChannelTest do
use ExUnit.Case, async: false
import Phoenix.ChannelTest
@endpoint BackendWeb.Endpoint
setup do
user_name = "test_user_#{System.unique_integer([:positive])}"
{:ok, socket} = connect(BackendWeb.UserSocket, %{"user_name" => user_name})
{:ok, reply, socket} = subscribe_and_join(socket, "user:#{user_name}", %{})
%{socket: socket, reply: reply, user_name: user_name}
end
describe "ConnectedUserChannel" do
test "join reply includes current game state", %{reply: reply} do
assert Map.has_key?(reply, :game_state)
end
test "game_state_updated pubsub broadcast pushes game_state to socket", %{socket: _socket} do
state = %{players: %{"alice" => %{x: 0, y: 0}}}
Phoenix.PubSub.broadcast(Backend.PubSub, "game_state", {:game_state_updated, state})
assert_push "game_state", %{game_state: ^state}
end
test "new_browser_connection pubsub broadcast pushes to socket", %{
socket: _socket,
user_name: user_name
} do
Phoenix.PubSub.broadcast(
Backend.PubSub,
"user_sessions:#{user_name}",
:new_browser_connection
)
assert_push "new_browser_connection", %{}
end
test "join_game message registers player and replies :ok", %{socket: socket} do
ref = push(socket, "join_game", %{"name" => "player1"})
assert_reply ref, :ok
end
test "key_down before join_game is a no-op", %{socket: socket} do
ref = push(socket, "key_down", %{"key" => "ArrowUp"})
refute_reply ref, _
end
test "key_down after join_game updates keys and replies nothing", %{socket: socket} do
push(socket, "join_game", %{"name" => "player1"})
ref = push(socket, "key_down", %{"key" => "ArrowUp"})
refute_reply ref, _
end
test "terminate removes player from game", %{socket: socket} do
push(socket, "join_game", %{"name" => "player_leaving"})
Process.unlink(socket.channel_pid)
close(socket)
# allow terminate callback to run
:timer.sleep(50)
state = Backend.GameRunner.get_state()
refute Map.has_key?(state, "player_leaving")
end
end
end