Files
wanderer/test/EXAMPLES.md
2025-07-09 01:47:24 -04:00

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

  1. API Endpoint Tests
  2. Authentication Tests
  3. Error Handling Tests
  4. Mock Usage Examples
  5. Contract Test Examples
  6. Performance Test Examples
  7. 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:

  1. Setup: Creating necessary test data
  2. Execution: Performing the action being tested
  3. Assertions: Verifying the expected behavior
  4. 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