mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-01 12:30:20 +00:00
32 KiB
32 KiB
WandererApp Test Examples
This document provides practical examples of common test scenarios in the WandererApp project. Use these as templates when writing new tests.
Table of Contents
- API Endpoint Tests
- Authentication Tests
- Error Handling Tests
- Mock Usage Examples
- Contract Test Examples
- Performance Test Examples
- WebSocket Test Examples
API Endpoint Tests
Basic CRUD Operations
defmodule WandererAppWeb.MapSystemsAPITest do
use WandererAppWeb.ConnCase, async: true
alias WandererApp.Test.Factory
describe "systems CRUD operations" do
setup do
user = Factory.create_user()
map = Factory.create_map(%{user_id: user.id})
api_key = Factory.create_map_api_key(%{map_id: map.id})
%{
map: map,
conn: build_conn() |> put_req_header("x-api-key", api_key.key)
}
end
test "lists all systems in a map", %{conn: conn, map: map} do
# Create test data
systems = for i <- 1..3 do
Factory.create_map_system(%{
map_id: map.id,
solar_system_id: 30000142 + i,
position_x: i * 100,
position_y: i * 100
})
end
# Make request
conn = get(conn, "/api/maps/#{map.slug}/systems")
# Assert response
assert response = json_response(conn, 200)
assert length(response["data"]) == 3
# Verify each system
response_ids = Enum.map(response["data"], & &1["solar_system_id"])
expected_ids = Enum.map(systems, & &1.solar_system_id)
assert Enum.sort(response_ids) == Enum.sort(expected_ids)
# Check response structure
first_system = hd(response["data"])
assert first_system["type"] == "system"
assert first_system["position_x"]
assert first_system["position_y"]
end
test "creates a new system", %{conn: conn, map: map} do
system_params = %{
"solar_system_id" => 30000142,
"position_x" => 100,
"position_y" => 200,
"name" => "Jita",
"description" => "Trade hub"
}
conn = post(conn, "/api/maps/#{map.slug}/systems", system_params)
assert response = json_response(conn, 201)
assert response["data"]["solar_system_id"] == 30000142
assert response["data"]["name"] == "Jita"
# Verify location header
assert [location] = get_resp_header(conn, "location")
assert location =~ "/api/maps/#{map.slug}/systems/30000142"
# Verify system was actually created
conn = get(conn, "/api/maps/#{map.slug}/systems/30000142")
assert json_response(conn, 200)
end
test "updates an existing system", %{conn: conn, map: map} do
system = Factory.create_map_system(map.id, %{
solar_system_id: 30000142,
position_x: 100,
position_y: 100
})
update_params = %{
"position_x" => 200,
"position_y" => 300,
"description" => "Updated position"
}
conn = put(conn, "/api/maps/#{map.slug}/systems/#{system.solar_system_id}", update_params)
assert response = json_response(conn, 200)
assert response["data"]["position_x"] == 200
assert response["data"]["position_y"] == 300
assert response["data"]["description"] == "Updated position"
end
test "deletes a system", %{conn: conn, map: map} do
system = Factory.create_map_system(map.id)
conn = delete(conn, "/api/maps/#{map.slug}/systems/#{system.solar_system_id}")
assert conn.status == 204
assert get_resp_header(conn, "content-length") == ["0"]
# Verify deletion
conn = get(conn, "/api/maps/#{map.slug}/systems/#{system.solar_system_id}")
assert json_response(conn, 404)
end
end
end
Pagination and Filtering
defmodule WandererAppWeb.PaginationTest do
use WandererAppWeb.ConnCase
describe "pagination" do
setup do
map = Factory.create_map()
# Create 25 systems
for i <- 1..25 do
Factory.create_map_system(map.id, %{
solar_system_id: 30000100 + i,
name: "System #{i}"
})
end
api_key = Factory.create_map_api_key(%{map_id: map.id})
%{
map: map,
conn: build_conn() |> put_req_header("x-api-key", api_key.key)
}
end
test "paginates results with limit and offset", %{conn: conn, map: map} do
# First page
conn = get(conn, "/api/maps/#{map.slug}/systems?limit=10&offset=0")
page1 = json_response(conn, 200)
assert length(page1["data"]) == 10
assert page1["meta"]["total"] == 25
assert page1["meta"]["limit"] == 10
assert page1["meta"]["offset"] == 0
# Second page
conn = get(conn, "/api/maps/#{map.slug}/systems?limit=10&offset=10")
page2 = json_response(conn, 200)
assert length(page2["data"]) == 10
assert page2["meta"]["offset"] == 10
# Ensure no overlap
page1_ids = Enum.map(page1["data"], & &1["solar_system_id"])
page2_ids = Enum.map(page2["data"], & &1["solar_system_id"])
assert Enum.empty?(page1_ids -- (page1_ids -- page2_ids))
# Last page
conn = get(conn, "/api/maps/#{map.slug}/systems?limit=10&offset=20")
page3 = json_response(conn, 200)
assert length(page3["data"]) == 5
end
test "filters results by name", %{conn: conn, map: map} do
conn = get(conn, "/api/maps/#{map.slug}/systems?filter[name]=System 1")
response = json_response(conn, 200)
# Should match "System 1", "System 10-19"
assert length(response["data"]) == 11
assert Enum.all?(response["data"], &String.contains?(&1["name"], "System 1"))
end
test "sorts results", %{conn: conn, map: map} do
# Sort by name ascending
conn = get(conn, "/api/maps/#{map.slug}/systems?sort=name&limit=5")
response = json_response(conn, 200)
names = Enum.map(response["data"], & &1["name"])
assert names == Enum.sort(names)
# Sort by name descending
conn = get(conn, "/api/maps/#{map.slug}/systems?sort=-name&limit=5")
response = json_response(conn, 200)
names = Enum.map(response["data"], & &1["name"])
assert names == Enum.sort(names, :desc)
end
end
end
Authentication Tests
API Key Authentication
defmodule WandererAppWeb.APIKeyAuthTest do
use WandererAppWeb.ConnCase
describe "API key authentication" do
setup do
user = Factory.create_user()
map = Factory.create_map(%{user_id: user.id})
%{map: map, user: user}
end
test "accepts valid API key in header", %{map: map} do
api_key = Factory.create_map_api_key(%{map_id: map.id})
conn =
build_conn()
|> put_req_header("x-api-key", api_key.key)
|> get("/api/maps/#{map.slug}")
assert json_response(conn, 200)
end
test "accepts valid API key in query params", %{map: map} do
api_key = Factory.create_map_api_key(%{map_id: map.id})
conn = get(build_conn(), "/api/maps/#{map.slug}?api_key=#{api_key.key}")
assert json_response(conn, 200)
end
test "rejects invalid API key", %{map: map} do
conn =
build_conn()
|> put_req_header("x-api-key", "invalid-key-12345")
|> get("/api/maps/#{map.slug}")
assert response = json_response(conn, 401)
assert response["errors"]["status"] == "401"
assert response["errors"]["title"] == "Unauthorized"
assert response["errors"]["detail"] =~ "Invalid API key"
end
test "rejects missing API key", %{map: map} do
conn = get(build_conn(), "/api/maps/#{map.slug}")
assert response = json_response(conn, 401)
assert response["errors"]["detail"] =~ "API key required"
end
test "rejects expired API key", %{map: map} do
# Create expired key
expired_key = Factory.create_map_api_key(%{
map_id: map.id,
expires_at: DateTime.add(DateTime.utc_now(), -3600, :second)
})
conn =
build_conn()
|> put_req_header("x-api-key", expired_key.key)
|> get("/api/maps/#{map.slug}")
assert response = json_response(conn, 401)
assert response["errors"]["detail"] =~ "API key expired"
end
test "rejects revoked API key", %{map: map} do
revoked_key = Factory.create_map_api_key(%{
map_id: map.id,
revoked: true
})
conn =
build_conn()
|> put_req_header("x-api-key", revoked_key.key)
|> get("/api/maps/#{map.slug}")
assert response = json_response(conn, 401)
assert response["errors"]["detail"] =~ "API key revoked"
end
end
end
Permission Tests
defmodule WandererAppWeb.PermissionTest do
use WandererAppWeb.ConnCase
describe "ACL permissions" do
setup do
owner = Factory.create_user()
member = Factory.create_user()
map = Factory.create_map(%{user_id: owner.id})
# Create ACL with member
acl = Factory.create_access_list(%{map_id: map.id})
Factory.create_access_list_member(%{
access_list_id: acl.id,
character_id: member.character_id,
role: "viewer"
})
# Create API keys
owner_key = Factory.create_map_api_key(%{map_id: map.id, user_id: owner.id})
acl_key = Factory.create_acl_api_key(%{access_list_id: acl.id})
%{
map: map,
owner: owner,
member: member,
owner_key: owner_key,
acl_key: acl_key
}
end
test "owner can perform all operations", %{map: map, owner_key: owner_key} do
conn = build_conn() |> put_req_header("x-api-key", owner_key.key)
# Can read
conn = get(conn, "/api/maps/#{map.slug}")
assert json_response(conn, 200)
# Can create
conn = post(conn, "/api/maps/#{map.slug}/systems", %{
"solar_system_id" => 30000142,
"position_x" => 100,
"position_y" => 200
})
assert json_response(conn, 201)
# Can update
conn = put(conn, "/api/maps/#{map.slug}", %{"name" => "Updated Name"})
assert json_response(conn, 200)
# Can delete
conn = delete(conn, "/api/maps/#{map.slug}/systems/30000142")
assert conn.status == 204
end
test "viewer can only read", %{map: map, acl_key: acl_key} do
conn = build_conn() |> put_req_header("x-api-key", acl_key.key)
# Can read
conn = get(conn, "/api/maps/#{map.slug}")
assert json_response(conn, 200)
# Cannot create
conn = post(conn, "/api/maps/#{map.slug}/systems", %{
"solar_system_id" => 30000142,
"position_x" => 100,
"position_y" => 200
})
assert response = json_response(conn, 403)
assert response["errors"]["detail"] =~ "permission" or
response["errors"]["detail"] =~ "forbidden"
# Cannot update
conn = put(conn, "/api/maps/#{map.slug}", %{"name" => "Updated Name"})
assert json_response(conn, 403)
# Cannot delete
conn = delete(conn, "/api/maps/#{map.slug}")
assert json_response(conn, 403)
end
end
end
Error Handling Tests
Validation Errors
defmodule WandererAppWeb.ValidationErrorTest do
use WandererAppWeb.ConnCase
describe "input validation" do
setup [:create_authenticated_conn]
test "validates required fields", %{conn: conn, map: map} do
# Missing required fields
conn = post(conn, "/api/maps/#{map.slug}/systems", %{})
assert response = json_response(conn, 422)
assert response["errors"]["status"] == "422"
assert response["errors"]["title"] == "Unprocessable Entity"
assert response["errors"]["detail"] =~ "required"
# Check for field-specific errors
assert response["errors"]["fields"]["solar_system_id"] =~ "required"
assert response["errors"]["fields"]["position_x"] =~ "required"
assert response["errors"]["fields"]["position_y"] =~ "required"
end
test "validates field types", %{conn: conn, map: map} do
invalid_params = %{
"solar_system_id" => "not-a-number",
"position_x" => "invalid",
"position_y" => [1, 2, 3]
}
conn = post(conn, "/api/maps/#{map.slug}/systems", invalid_params)
assert response = json_response(conn, 422)
assert response["errors"]["fields"]["solar_system_id"] =~ "must be an integer"
assert response["errors"]["fields"]["position_x"] =~ "must be a number"
assert response["errors"]["fields"]["position_y"] =~ "must be a number"
end
test "validates field constraints", %{conn: conn, map: map} do
params = %{
"solar_system_id" => -1, # Invalid EVE system ID
"position_x" => 99999999, # Too large
"position_y" => -99999999, # Too small
"name" => String.duplicate("a", 500) # Too long
}
conn = post(conn, "/api/maps/#{map.slug}/systems", params)
assert response = json_response(conn, 422)
assert response["errors"]["fields"]["solar_system_id"] =~ "must be positive"
assert response["errors"]["fields"]["name"] =~ "too long"
end
test "validates business rules", %{conn: conn, map: map} do
# Create a system
Factory.create_map_system(map.id, %{solar_system_id: 30000142})
# Try to create duplicate
conn = post(conn, "/api/maps/#{map.slug}/systems", %{
"solar_system_id" => 30000142,
"position_x" => 100,
"position_y" => 200
})
assert response = json_response(conn, 422)
assert response["errors"]["detail"] =~ "already exists" or
response["errors"]["detail"] =~ "duplicate"
end
end
defp create_authenticated_conn(_) do
map = Factory.create_map()
api_key = Factory.create_map_api_key(%{map_id: map.id})
%{
map: map,
conn: build_conn() |> put_req_header("x-api-key", api_key.key)
}
end
end
Service Error Handling
defmodule WandererAppWeb.ServiceErrorTest do
use WandererAppWeb.ConnCase
import Mox
setup :verify_on_exit!
describe "external service errors" do
setup [:create_authenticated_conn]
test "handles EVE API timeout", %{conn: conn} do
Test.EVEAPIClientMock
|> expect(:get_system_info, fn _system_id ->
{:error, :timeout}
end)
conn = get(conn, "/api/common/systems/30000142")
assert response = json_response(conn, 503)
assert response["errors"]["status"] == "503"
assert response["errors"]["title"] == "Service Unavailable"
assert response["errors"]["detail"] =~ "temporarily unavailable"
# Should include retry information
assert response["errors"]["meta"]["retry_after"]
end
test "handles database connection errors", %{conn: conn, map: map} do
# This is harder to test without actually breaking the DB
# In practice, you might use a custom Repo wrapper for testing
# Simulate by mocking Ecto.Adapters.SQL
Test.RepoMock
|> expect(:all, fn _query ->
{:error, %DBConnection.ConnectionError{message: "connection timeout"}}
end)
conn = get(conn, "/api/maps/#{map.slug}/systems")
assert response = json_response(conn, 503)
assert response["errors"]["detail"] =~ "database" or
response["errors"]["detail"] =~ "connection"
end
test "handles cache failures gracefully", %{conn: conn, map: map} do
Test.CacheMock
|> expect(:get, fn _key ->
{:error, :connection_refused}
end)
|> stub(:put, fn _key, _value, _opts ->
{:error, :connection_refused}
end)
# Should still work without cache
conn = get(conn, "/api/maps/#{map.slug}")
assert json_response(conn, 200)
end
end
end
Mock Usage Examples
Complex Mock Scenarios
defmodule WandererApp.ComplexMockTest do
use WandererApp.DataCase
import Mox
setup :verify_on_exit!
describe "complex service interactions" do
test "fetches and caches character with corporation info" do
character_id = 123456789
corporation_id = 987654321
# Mock EVE API calls in sequence
Test.EVEAPIClientMock
|> expect(:get_character_info, fn ^character_id ->
{:ok, %{
"name" => "Test Character",
"corporation_id" => corporation_id,
"birthday" => "2020-01-01T00:00:00Z"
}}
end)
|> expect(:get_corporation_info, fn ^corporation_id ->
{:ok, %{
"name" => "Test Corporation",
"ticker" => "TEST",
"member_count" => 100
}}
end)
# Mock cache interactions
Test.CacheMock
|> expect(:get, fn key ->
assert key in ["character:#{character_id}", "corporation:#{corporation_id}"]
{:error, :not_found}
end)
|> expect(:put, 2, fn key, value, opts ->
assert key in ["character:#{character_id}", "corporation:#{corporation_id}"]
assert opts[:ttl] in [3600, 7200]
assert is_map(value)
:ok
end)
# Mock PubSub notification
Test.PubSubMock
|> expect(:publish, fn topic, message ->
assert topic == "character:updates"
assert message.character_id == character_id
:ok
end)
# Execute the function
{:ok, character} = WandererApp.Characters.fetch_character_with_corp(character_id)
# Verify the result
assert character.name == "Test Character"
assert character.corporation.name == "Test Corporation"
assert character.corporation.ticker == "TEST"
end
test "handles partial failures with fallbacks" do
Test.EVEAPIClientMock
|> expect(:get_character_info, fn _id ->
{:ok, %{"name" => "Test Character"}}
end)
|> expect(:get_character_location, fn _id ->
{:error, :rate_limited}
end)
Test.CacheMock
|> expect(:get, fn "character:location:123" ->
# Return cached location
{:ok, %{solar_system_id: 30000142, last_updated: DateTime.utc_now()}}
end)
{:ok, character} = WandererApp.Characters.fetch_character_full(123)
assert character.name == "Test Character"
assert character.location.solar_system_id == 30000142
assert character.location.source == :cache
end
end
end
Stub vs Expect
defmodule WandererApp.StubVsExpectTest do
use WandererApp.DataCase
import Mox
describe "when to use stub vs expect" do
test "use stub for optional background operations" do
# Logger calls are optional - use stub
Test.LoggerMock
|> stub(:info, fn _msg -> :ok end)
|> stub(:debug, fn _msg -> :ok end)
# Cache writes are optional - use stub
Test.CacheMock
|> stub(:put, fn _key, _value, _opts -> :ok end)
# Business logic doesn't fail if these don't happen
assert {:ok, _result} = WandererApp.SomeModule.do_work()
end
test "use expect for critical operations" do
# This MUST be called exactly once
Test.EVEAPIClientMock
|> expect(:verify_token, 1, fn token ->
assert token == "test-token"
{:ok, %{character_id: 123}}
end)
# This MUST be called with specific params
Test.DatabaseMock
|> expect(:insert_character, fn character ->
assert character.id == 123
{:ok, character}
end)
# Test will fail if expectations aren't met
assert {:ok, _} = WandererApp.Auth.verify_and_create("test-token")
end
test "combine stub and expect" do
# Required call
Test.EVEAPIClientMock
|> expect(:get_character_info, fn _id ->
{:ok, %{name: "Test"}}
end)
# Optional calls that might happen 0+ times
Test.CacheMock
|> stub(:get, fn _key -> {:error, :not_found} end)
|> stub(:put, fn _key, _value, _opts -> :ok end)
Test.LoggerMock
|> stub(:info, fn _msg -> :ok end)
assert {:ok, _} = WandererApp.Characters.fetch_character(123)
end
end
end
Contract Test Examples
OpenAPI Validation
defmodule WandererAppWeb.OpenAPIContractTest do
use WandererAppWeb.ConnCase
use WandererAppWeb.OpenAPICase
describe "API contract validation" do
setup [:create_test_data]
test "validates all endpoints against OpenAPI spec", %{conn: conn, map: map} do
# Test each endpoint defined in OpenAPI spec
for operation <- get_all_operations() do
test_operation_contract(conn, operation, %{
map_slug: map.slug,
system_id: "30000142"
})
end
end
test "POST /api/maps/:slug/systems matches schema", %{conn: conn, map: map} do
request_body = %{
"solar_system_id" => 30000142,
"position_x" => 100.5,
"position_y" => 200.5,
"name" => "Jita",
"description" => "Major trade hub",
"locked" => false,
"rally_point" => true
}
# Validate request matches schema
assert_valid_request_body(request_body, "CreateSystemRequest")
# Make request
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/maps/#{map.slug}/systems", request_body)
# Validate response
assert response = json_response(conn, 201)
assert_valid_response(response, 201, "CreateSystemResponse")
# Validate response headers
assert_required_headers(conn, ["content-type", "location"])
assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
# Validate response data types
assert is_integer(response["data"]["solar_system_id"])
assert is_float(response["data"]["position_x"])
assert is_boolean(response["data"]["locked"])
assert is_binary(response["data"]["created_at"])
assert DateTime.from_iso8601(response["data"]["created_at"])
end
test "error responses match error schema", %{conn: conn} do
# Test various error scenarios
error_cases = [
{"/api/maps/nonexistent", 404, "Not Found"},
{"/api/maps/test", 401, "Unauthorized"}, # No API key
]
for {path, status, title} <- error_cases do
conn = get(build_conn(), path)
assert response = json_response(conn, status)
assert_valid_response(response, status, "ErrorResponse")
assert response["errors"]["status"] == to_string(status)
assert response["errors"]["title"] == title
assert is_binary(response["errors"]["detail"])
assert is_binary(response["errors"]["id"])
end
end
end
defp test_operation_contract(conn, operation, params) do
path = build_path(operation.path, params)
case operation.method do
:get ->
conn = get(conn, path)
assert conn.status in operation.expected_statuses
:post ->
body = build_request_body(operation.request_schema)
conn = post(conn, path, body)
assert conn.status in operation.expected_statuses
_ ->
# Handle other methods
end
if conn.status < 400 do
response = json_response(conn, conn.status)
assert_valid_response(response, conn.status, operation.response_schema)
end
end
end
Performance Test Examples
Load Testing
defmodule WandererAppWeb.PerformanceTest do
use WandererAppWeb.ConnCase
@tag :performance
@tag timeout: :infinity
describe "API performance under load" do
setup do
# Create test data
map = Factory.create_map()
# Create many systems
systems = for i <- 1..1000 do
Factory.create_map_system(map.id, %{
solar_system_id: 30000000 + i
})
end
api_key = Factory.create_map_api_key(%{map_id: map.id})
%{
map: map,
systems: systems,
api_key: api_key
}
end
test "handles concurrent read requests", %{map: map, api_key: api_key} do
# Warm up
conn =
build_conn()
|> put_req_header("x-api-key", api_key.key)
|> get("/api/maps/#{map.slug}/systems")
assert json_response(conn, 200)
# Measure concurrent performance
concurrency_levels = [10, 50, 100]
for concurrency <- concurrency_levels do
{time, results} = :timer.tc(fn ->
tasks = for _ <- 1..concurrency do
Task.async(fn ->
conn =
build_conn()
|> put_req_header("x-api-key", api_key.key)
|> get("/api/maps/#{map.slug}/systems")
{conn.status, byte_size(conn.resp_body)}
end)
end
Task.await_many(tasks, 30_000)
end)
# All should succeed
assert Enum.all?(results, fn {status, _size} -> status == 200 end)
# Calculate metrics
avg_time = time / concurrency / 1000 # ms
requests_per_sec = concurrency * 1_000_000 / time
IO.puts("Concurrency: #{concurrency}")
IO.puts(" Total time: #{time / 1_000}ms")
IO.puts(" Avg time per request: #{Float.round(avg_time, 2)}ms")
IO.puts(" Requests/sec: #{Float.round(requests_per_sec, 2)}")
# Performance assertions
assert avg_time < 100, "Average response time should be under 100ms"
assert requests_per_sec > 10, "Should handle at least 10 requests/sec"
end
end
test "handles large response payloads efficiently", %{map: map, api_key: api_key} do
# Request all systems (1000 items)
{time, conn} = :timer.tc(fn ->
build_conn()
|> put_req_header("x-api-key", api_key.key)
|> get("/api/maps/#{map.slug}/systems?limit=1000")
end)
assert response = json_response(conn, 200)
assert length(response["data"]) == 1000
# Check performance
response_size = byte_size(conn.resp_body)
time_ms = time / 1000
IO.puts("Large payload performance:")
IO.puts(" Response size: #{response_size / 1024}KB")
IO.puts(" Response time: #{time_ms}ms")
IO.puts(" Throughput: #{Float.round(response_size / time * 1000, 2)}KB/s")
# Should complete in reasonable time
assert time_ms < 1000, "Large payload should return in under 1 second"
end
test "write operations maintain performance", %{map: map, api_key: api_key} do
write_times = for i <- 1..10 do
system_params = %{
"solar_system_id" => 31000000 + i,
"position_x" => i * 10,
"position_y" => i * 10
}
{time, conn} = :timer.tc(fn ->
build_conn()
|> put_req_header("x-api-key", api_key.key)
|> put_req_header("content-type", "application/json")
|> post("/api/maps/#{map.slug}/systems", system_params)
end)
assert json_response(conn, 201)
time / 1000 # Convert to ms
end
avg_write_time = Enum.sum(write_times) / length(write_times)
max_write_time = Enum.max(write_times)
IO.puts("Write operation performance:")
IO.puts(" Average time: #{Float.round(avg_write_time, 2)}ms")
IO.puts(" Max time: #{Float.round(max_write_time, 2)}ms")
assert avg_write_time < 200, "Writes should average under 200ms"
assert max_write_time < 500, "No write should take over 500ms"
end
end
end
WebSocket Test Examples
Real-time Updates
defmodule WandererAppWeb.WebSocketTest do
use WandererAppWeb.ChannelCase
alias WandererAppWeb.MapChannel
describe "map real-time updates" do
setup do
user = Factory.create_user()
map = Factory.create_map(%{user_id: user.id})
# Connect to channel
{:ok, socket} = connect(WandererAppWeb.UserSocket, %{
"token" => generate_user_token(user)
})
{:ok, _reply, socket} = subscribe_and_join(
socket,
MapChannel,
"map:#{map.slug}",
%{}
)
%{socket: socket, map: map, user: user}
end
test "broadcasts system creation", %{socket: socket, map: map} do
# Create system via API (would trigger broadcast)
system_data = %{
solar_system_id: 30000142,
position_x: 100,
position_y: 200,
name: "Jita"
}
# Simulate the broadcast that would happen
broadcast_from!(socket, "system:created", %{
"system" => system_data
})
# Client should receive the event
assert_push "system:created", %{system: pushed_system}
assert pushed_system.solar_system_id == 30000142
assert pushed_system.name == "Jita"
end
test "broadcasts system updates to all connected clients", %{map: map} do
# Connect multiple clients
clients = for i <- 1..3 do
user = Factory.create_user()
{:ok, socket} = connect(WandererAppWeb.UserSocket, %{
"token" => generate_user_token(user)
})
{:ok, _reply, socket} = subscribe_and_join(
socket,
MapChannel,
"map:#{map.slug}",
%{}
)
{user, socket}
end
# Broadcast update from first client
{_user1, socket1} = hd(clients)
broadcast_from!(socket1, "system:updated", %{
"system_id" => 30000142,
"changes" => %{"position_x" => 150}
})
# All other clients should receive it
for {_user, socket} <- tl(clients) do
assert_push "system:updated", payload, 1000
assert payload.system_id == 30000142
assert payload.changes.position_x == 150
end
end
test "handles presence tracking", %{socket: socket, map: map} do
# Track user presence
{:ok, _} = WandererAppWeb.Presence.track(
socket,
socket.assigns.user_id,
%{
character_name: "Test Character",
online_at: System.system_time(:second)
}
)
# Should receive presence state
assert_push "presence_state", state
assert map_size(state) == 1
# Another user joins
user2 = Factory.create_user()
{:ok, socket2} = connect(WandererAppWeb.UserSocket, %{
"token" => generate_user_token(user2)
})
{:ok, _reply, socket2} = subscribe_and_join(
socket2,
MapChannel,
"map:#{map.slug}",
%{}
)
# Should receive presence diff
assert_push "presence_diff", %{joins: joins, leaves: leaves}
assert map_size(joins) == 1
assert leaves == %{}
# User leaves
Process.unlink(socket2.channel_pid)
ref = leave(socket2)
assert_reply ref, :ok
# Should receive leave event
assert_push "presence_diff", %{joins: joins, leaves: leaves}
assert joins == %{}
assert map_size(leaves) == 1
end
test "authorizes actions based on permissions", %{socket: socket, map: map} do
# Try to delete system as non-owner
ref = push(socket, "system:delete", %{"system_id" => 30000142})
assert_reply ref, :error, %{reason: "unauthorized"}
# Should not broadcast to others
refute_push "system:deleted", _
end
end
defp generate_user_token(user) do
# Generate a Phoenix token for the user
Phoenix.Token.sign(WandererAppWeb.Endpoint, "user socket", user.id)
end
end
These examples demonstrate the various testing patterns used in the WandererApp project. Each example includes:
- Setup: Creating necessary test data
- Execution: Performing the action being tested
- Assertions: Verifying the expected behavior
- Cleanup: Handled automatically by ExUnit
Remember to:
- Use descriptive test names
- Keep tests focused and independent
- Mock external dependencies
- Test both success and failure cases
- Validate API contracts
- Monitor performance characteristics