"""Integration tests for the world content API: rooms + chunks. Covers docs/architecture/world_persistence_impl.md §8.1 (public router, app/routers/world_content.py) and §8.3 (internal router, app/routers/internal.py), plus the create_world -> auto "main" room behaviour (§4.1). Runs against the real ephemeral Postgres the hermetic conftest.py stands up -- these are full HTTP round trips through TestClient, not mocked. """ import base64 import pytest from fastapi.testclient import TestClient from app.main import app from app.services import chunk_codec as cc from app.services.game_servers import supervisor GRID_32 = (32, 32, 1) def _auth(client: TestClient, name: str) -> tuple[dict, int]: r = client.post("/api/auth/register", json={ "username": name, "email": f"{name}@e.com", "password": "secret123", "date_of_birth": "1990-01-01", }) assert r.status_code == 201, r.text headers = {"Authorization": f"Bearer {r.json()['access_token']}"} me = client.get("/api/users/me", headers=headers) return headers, me.json()["id"] def _make_world(client: TestClient, headers: dict, visibility: str = "public") -> int: r = client.post("/api/worlds/", headers=headers, json={"name": "W", "visibility": visibility}) assert r.status_code == 201, r.text return r.json()["id"] def _build_chunk(world_id: int, slug: str, cx: int, cy: int, cz: int, *, grid=GRID_32, revision: int = 1, extra_tag: str = "") -> bytes: """A small but structurally-real chunk payload for `world_id`/`slug`/coord, sized to match the room's default 32x32x1 grid.""" w, h, d = grid layer = cc.TileLayer( name="ground", layer_id=0, width=w, height=h, depth=d, palette=[cc.TileDef(id=0, asset_id="", solid=False, flags=0, variants=1)], tiles=[0] * (w * h * d), ) obj = cc.WorldObject( id=f"obj_{extra_tag or '1'}", type="prop", asset_id="props/rock", pos=(1.0, 1.0, 0.0), properties={"tag": extra_tag}, ) payload = cc.ChunkPayload( world_id=str(world_id), room_id=slug, coord=(cx, cy, cz), bounds=cc.ChunkBounds(min=(0.0, 0.0, 0.0), size=(float(w), float(h), float(d))), tile_layers=[layer], objects=[obj], revision=revision, author="pytest", created_at=1_700_000_000, updated_at=1_700_000_000, ) return cc.encode_binary(payload) class TestCreateWorldMainRoom: def test_create_world_auto_creates_main_room(self): client = TestClient(app) headers, _ = _auth(client, "cw_main") wid = _make_world(client, headers) r = client.get(f"/api/worlds/{wid}/rooms", headers=headers) assert r.status_code == 200, r.text rooms = r.json() assert len(rooms) == 1 assert rooms[0]["slug"] == "main" assert rooms[0]["name"] == "Main" assert rooms[0]["chunk_count"] == 0 assert rooms[0]["grid"]["size"] == [32, 32, 1] class TestRoomAuth: def test_create_room_requires_owner(self): client = TestClient(app) owner_headers, _ = _auth(client, "room_owner") wid = _make_world(client, owner_headers) other_headers, _ = _auth(client, "room_other") r = client.post(f"/api/worlds/{wid}/rooms", headers=other_headers, json={"slug": "annex", "name": "Annex"}) assert r.status_code == 403 def test_list_rooms_of_private_world_is_404_for_non_owner(self): client = TestClient(app) owner_headers, _ = _auth(client, "room_priv_owner") wid = _make_world(client, owner_headers, "private") other_headers, _ = _auth(client, "room_priv_other") r = client.get(f"/api/worlds/{wid}/rooms", headers=other_headers) assert r.status_code == 404 class TestChunkPutGetRoundTrip: def test_binary_put_then_binary_get_round_trips(self): client = TestClient(app) headers, _ = _auth(client, "chunk_rt") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 0, 0, 0) r = client.put( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) assert r.status_code == 200, r.text body = r.json() assert body["revision"] == 1 assert body["payload_len"] == len(payload) assert body["checksum"] == cc.checksum(payload[cc.HEADER_SIZE:]) assert body["live_session"] is False g = client.get( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Accept": "application/octet-stream"}, ) assert g.status_code == 200 assert g.content == payload assert g.headers["ETag"] == f'"{body["checksum"]}"' assert g.headers["X-Chunk-Revision"] == "1" def test_json_accept_returns_decoded_shape(self): client = TestClient(app) headers, _ = _auth(client, "chunk_json") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 1, 1, 0) client.put( f"/api/worlds/{wid}/rooms/main/chunks/1/1/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) g = client.get( f"/api/worlds/{wid}/rooms/main/chunks/1/1/0", headers={**headers, "Accept": "application/json"}, ) assert g.status_code == 200 data = g.json() assert data["coord"] == {"cx": 1, "cy": 1, "cz": 0} assert data["world_id"] == str(wid) assert data["room_id"] == "main" assert len(data["objects"]) == 1 def test_json_put_with_base64_payload(self): client = TestClient(app) headers, _ = _auth(client, "chunk_json_put") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 2, 2, 0) r = client.put( f"/api/worlds/{wid}/rooms/main/chunks/2/2/0", headers=headers, json={"base_revision": 0, "payload_b64": base64.b64encode(payload).decode()}, ) assert r.status_code == 200, r.text assert r.json()["revision"] == 1 def test_if_none_match_returns_304(self): client = TestClient(app) headers, _ = _auth(client, "chunk_304") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 3, 3, 0) put = client.put( f"/api/worlds/{wid}/rooms/main/chunks/3/3/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) checksum = put.json()["checksum"] g = client.get( f"/api/worlds/{wid}/rooms/main/chunks/3/3/0", headers={**headers, "If-None-Match": f'"{checksum}"'}, ) assert g.status_code == 304 def test_unwritten_coordinate_is_404(self): client = TestClient(app) headers, _ = _auth(client, "chunk_404") wid = _make_world(client, headers) g = client.get(f"/api/worlds/{wid}/rooms/main/chunks/9/9/0", headers=headers) assert g.status_code == 404 class TestRevisionConflict: def test_create_conflict_when_chunk_already_exists(self): client = TestClient(app) headers, _ = _auth(client, "conflict_create") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 4, 4, 0) first = client.put( f"/api/worlds/{wid}/rooms/main/chunks/4/4/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) assert first.status_code == 200 second = client.put( f"/api/worlds/{wid}/rooms/main/chunks/4/4/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) assert second.status_code == 409 body = second.json() assert body["detail"] == "revision conflict" assert body["current_revision"] == 1 def test_stale_base_revision_conflict_reports_current_state(self): client = TestClient(app) headers, _ = _auth(client, "conflict_stale") wid = _make_world(client, headers) payload_v1 = _build_chunk(wid, "main", 5, 5, 0, revision=1) client.put( f"/api/worlds/{wid}/rooms/main/chunks/5/5/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload_v1, ) payload_v2 = _build_chunk(wid, "main", 5, 5, 0, revision=2, extra_tag="v2") r2 = client.put( f"/api/worlds/{wid}/rooms/main/chunks/5/5/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "1"}, content=payload_v2, ) assert r2.status_code == 200 assert r2.json()["revision"] == 2 # Now retry with the now-stale base_revision=1 again. payload_v3 = _build_chunk(wid, "main", 5, 5, 0, revision=3, extra_tag="v3") stale = client.put( f"/api/worlds/{wid}/rooms/main/chunks/5/5/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "1"}, content=payload_v3, ) assert stale.status_code == 409 body = stale.json() assert body["current_revision"] == 2 assert body["current_checksum"] == r2.json()["checksum"] class TestBatchWrite: def test_batch_put_all_succeed(self): client = TestClient(app) headers, _ = _auth(client, "batch_ok") wid = _make_world(client, headers) chunks = [ {"cx": cx, "cy": 0, "cz": 0, "base_revision": 0, "payload_b64": base64.b64encode(_build_chunk(wid, "main", cx, 0, 0)).decode()} for cx in (10, 11) ] r = client.put(f"/api/worlds/{wid}/rooms/main/chunks", headers=headers, json={"chunks": chunks}) assert r.status_code == 200, r.text body = r.json() assert len(body["chunks"]) == 2 assert all(c["revision"] == 1 for c in body["chunks"]) idx = client.get(f"/api/worlds/{wid}/rooms/main/chunks", headers=headers) keys = {c["key"] for c in idx.json()["chunks"]} assert f"{wid}:main:10:0:0" in keys assert f"{wid}:main:11:0:0" in keys def test_batch_put_is_all_or_nothing_on_conflict(self): client = TestClient(app) headers, _ = _auth(client, "batch_conflict") wid = _make_world(client, headers) # Pre-create one of the two coordinates so the batch's base_revision=0 # for it will conflict. pre = _build_chunk(wid, "main", 20, 0, 0) client.put( f"/api/worlds/{wid}/rooms/main/chunks/20/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=pre, ) chunks = [ {"cx": 21, "cy": 0, "cz": 0, "base_revision": 0, "payload_b64": base64.b64encode(_build_chunk(wid, "main", 21, 0, 0)).decode()}, {"cx": 20, "cy": 0, "cz": 0, "base_revision": 0, # conflicts: already exists at revision 1 "payload_b64": base64.b64encode(_build_chunk(wid, "main", 20, 0, 0, extra_tag="v2")).decode()}, ] r = client.put(f"/api/worlds/{wid}/rooms/main/chunks", headers=headers, json={"chunks": chunks}) assert r.status_code == 409 # Nothing from this batch should have been persisted -- (21,0,0) # must NOT exist even though it was valid and came first. g = client.get(f"/api/worlds/{wid}/rooms/main/chunks/21/0/0", headers=headers) assert g.status_code == 404 class TestDelete: def test_delete_then_get_is_404(self): client = TestClient(app) headers, _ = _auth(client, "chunk_delete") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 30, 0, 0) client.put( f"/api/worlds/{wid}/rooms/main/chunks/30/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) d = client.delete(f"/api/worlds/{wid}/rooms/main/chunks/30/0/0", headers=headers) assert d.status_code == 204 g = client.get(f"/api/worlds/{wid}/rooms/main/chunks/30/0/0", headers=headers) assert g.status_code == 404 class TestRevisionsAndRollback: def test_revisions_then_rollback(self): client = TestClient(app) headers, _ = _auth(client, "chunk_rollback") wid = _make_world(client, headers) v1 = _build_chunk(wid, "main", 40, 0, 0, revision=1) r1 = client.put( f"/api/worlds/{wid}/rooms/main/chunks/40/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=v1, ) v2 = _build_chunk(wid, "main", 40, 0, 0, revision=2, extra_tag="v2") r2 = client.put( f"/api/worlds/{wid}/rooms/main/chunks/40/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "1"}, content=v2, ) assert r2.status_code == 200 revs = client.get(f"/api/worlds/{wid}/rooms/main/chunks/40/0/0/revisions", headers=headers) assert revs.status_code == 200 rev_numbers = [row["revision"] for row in revs.json()] assert 1 in rev_numbers and 2 in rev_numbers rb = client.post( f"/api/worlds/{wid}/rooms/main/chunks/40/0/0/rollback", headers=headers, json={"revision": 1}, ) assert rb.status_code == 200, rb.text assert rb.json()["revision"] == 3 # a new revision, restoring v1's content assert rb.json()["checksum"] == r1.json()["checksum"] def test_revisions_requires_owner(self): client = TestClient(app) owner_headers, _ = _auth(client, "revisions_owner") wid = _make_world(client, owner_headers) payload = _build_chunk(wid, "main", 41, 0, 0) client.put( f"/api/worlds/{wid}/rooms/main/chunks/41/0/0", headers={**owner_headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) other_headers, _ = _auth(client, "revisions_other") r = client.get(f"/api/worlds/{wid}/rooms/main/chunks/41/0/0/revisions", headers=other_headers) assert r.status_code == 403 class TestValidationErrors: def test_malformed_payload_is_422(self): client = TestClient(app) headers, _ = _auth(client, "chunk_malformed") wid = _make_world(client, headers) r = client.put( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=b"not a real chunk", ) assert r.status_code == 422 assert r.json()["reason"] == "malformed" def test_wrong_grid_size_is_422_layer_dims(self): client = TestClient(app) headers, _ = _auth(client, "chunk_layerdims") wid = _make_world(client, headers) # Room's default grid is 32x32x1; build a chunk sized for 4x4x1. payload = _build_chunk(wid, "main", 0, 0, 0, grid=(4, 4, 1)) r = client.put( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) assert r.status_code == 422 assert r.json()["reason"] == "layer_dims" def test_oversized_payload_is_413(self, monkeypatch): from app.config import settings monkeypatch.setattr(settings, "WORLD_CHUNK_MAX_PAYLOAD_BYTES", 10) client = TestClient(app) headers, _ = _auth(client, "chunk_413") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 0, 0, 0) r = client.put( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) assert r.status_code == 413 class TestInternalApi: """docs/architecture/world_persistence_impl.md §7/§8.3. No real Godot process is spawned in this test env (GAME_SERVERS_ENABLED=False); we register a fake in-memory server entry with a known token to exercise require_game_server without needing a live process.""" @pytest.fixture def fake_server_token(self): # NB: a class body can't read a same-named local from the # enclosing function via `token = token` (class bodies use LOAD_NAME, # which skips enclosing function scopes) -- use a literal instead. world_id_str = "internal-test-world" class _FakeSrv: def alive(self_inner): return True token = "test-internal-token-12345" supervisor._servers[world_id_str] = _FakeSrv() yield _FakeSrv.token supervisor._servers.pop(world_id_str, None) def test_missing_token_is_401(self): client = TestClient(app) r = client.get("/api/internal/worlds/1/manifest") assert r.status_code == 401 def test_wrong_token_is_403(self): client = TestClient(app) r = client.get( "/api/internal/worlds/1/manifest", headers={"Authorization": "Bearer not-the-real-token"}, ) assert r.status_code == 403 def test_valid_token_but_wrong_world_id_is_403(self, fake_server_token): # The fake token is registered under world_id "internal-test-world"; # a request for a different world_id must not authenticate. client = TestClient(app) r = client.get( "/api/internal/worlds/999999/manifest", headers={"Authorization": f"Bearer {fake_server_token}"}, ) assert r.status_code == 403 def test_valid_token_reaches_manifest_for_real_world(self): client = TestClient(app) headers, _ = _auth(client, "internal_manifest") wid = _make_world(client, headers) class _FakeSrv: def alive(self_inner): return True token = "another-real-token" supervisor._servers[str(wid)] = _FakeSrv() try: r = client.get( f"/api/internal/worlds/{wid}/manifest", headers={"Authorization": "Bearer another-real-token"}, ) assert r.status_code == 200, r.text body = r.json() assert body["world_id"] == wid assert len(body["rooms"]) == 1 assert body["rooms"][0]["slug"] == "main" finally: supervisor._servers.pop(str(wid), None) def test_internal_chunk_fetch_returns_binary(self): client = TestClient(app) headers, _ = _auth(client, "internal_chunk") wid = _make_world(client, headers) payload = _build_chunk(wid, "main", 0, 0, 0) client.put( f"/api/worlds/{wid}/rooms/main/chunks/0/0/0", headers={**headers, "Content-Type": "application/octet-stream", "X-Base-Revision": "0"}, content=payload, ) class _FakeSrv: def alive(self_inner): return True token = "internal-chunk-token" supervisor._servers[str(wid)] = _FakeSrv() try: r = client.get( f"/api/internal/worlds/{wid}/rooms/main/chunks/0/0/0", headers={"Authorization": "Bearer internal-chunk-token"}, ) assert r.status_code == 200 assert r.content == payload finally: supervisor._servers.pop(str(wid), None)