testing channels
This commit is contained in:
108
backend/test/backend/game_runner_test.exs
Normal file
108
backend/test/backend/game_runner_test.exs
Normal 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
|
||||
84
backend/test/backend/node_cluster_integration_test.exs
Normal file
84
backend/test/backend/node_cluster_integration_test.exs
Normal 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
|
||||
64
backend/test/backend/user_channel_test.exs
Normal file
64
backend/test/backend/user_channel_test.exs
Normal 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
|
||||
Reference in New Issue
Block a user