"""Personal inventory tests: auth gating, CRUD, isolation, catalog immutability.""" from fastapi.testclient import TestClient from app.api import app PASSWORD = "correct horse battery staple" CARROCK = "ANVL_Carrack" RIFLE = "BEHR_Rifle_Ballistic_01" def authed_client(username: str = "alice") -> TestClient: client = TestClient(app) resp = client.post( "/api/v1/auth/register", json={"username": username, "password": PASSWORD} ) assert resp.status_code == 201, resp.text return client def add(client: TestClient, class_name: str, quantity: int = 1, notes: str = ""): return client.post( "/api/v1/me/inventory", json={"class_name": class_name, "quantity": quantity, "notes": notes}, ) def test_new_account_starts_with_empty_inventory(): client = authed_client("freshpilot") listing = client.get("/api/v1/me/inventory").json() assert listing["total"] == 0 assert listing["items"] == [] assert client.get("/api/v1/me/inventory/summary").json() == { "items": 0, "quantity": 0, "categories": {}, "unavailable": 0, } def test_inventory_requires_authentication(): client = TestClient(app) assert client.get("/api/v1/me/inventory").status_code == 401 assert add(client, CARROCK).status_code == 401 assert client.get("/api/v1/me/inventory/summary").status_code == 401 def test_add_unknown_catalog_item_is_not_found(): client = authed_client() assert add(client, "NOPE_Does_Not_Exist").status_code == 404 def test_add_creates_enriched_entry(): client = authed_client() resp = add(client, CARROCK) assert resp.status_code == 201 body = resp.json() assert body["created"] is True entry = body["entry"] assert entry["class_name"] == CARROCK assert entry["name"] == "Anvil Carrack" assert entry["category"] == "ships" assert entry["quantity"] == 1 assert entry["available"] is True assert entry["manufacturer"]["code"] == "ANVL" def test_add_same_item_increments_quantity(): client = authed_client() assert add(client, CARROCK, quantity=1).status_code == 201 resp = add(client, CARROCK, quantity=2, notes="pledged") assert resp.status_code == 200 body = resp.json() assert body["created"] is False assert body["entry"]["quantity"] == 3 assert body["entry"]["notes"] == "pledged" listing = client.get("/api/v1/me/inventory").json() assert listing["total"] == 1 def test_inventories_are_isolated_between_users(): alice = authed_client("alice") bob = authed_client("bob") assert add(alice, CARROCK).status_code == 201 assert add(bob, RIFLE).status_code == 201 alice_items = alice.get("/api/v1/me/inventory").json()["items"] bob_items = bob.get("/api/v1/me/inventory").json()["items"] assert [i["class_name"] for i in alice_items] == [CARROCK] assert [i["class_name"] for i in bob_items] == [RIFLE] def test_patch_updates_quantity_and_notes(): client = authed_client() assert add(client, CARROCK).status_code == 201 resp = client.patch( f"/api/v1/me/inventory/{CARROCK}", json={"quantity": 4, "notes": "hangar 3"} ) assert resp.status_code == 200 body = resp.json() assert body["quantity"] == 4 assert body["notes"] == "hangar 3" def test_patch_unknown_entry_is_not_found(): client = authed_client() assert client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"quantity": 2}).status_code == 404 def test_patch_rejects_zero_quantity(): client = authed_client() assert add(client, CARROCK).status_code == 201 resp = client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"quantity": 0}) assert resp.status_code == 422 def test_delete_removes_entry(): client = authed_client() assert add(client, CARROCK).status_code == 201 assert client.delete(f"/api/v1/me/inventory/{CARROCK}").status_code == 204 assert client.get("/api/v1/me/inventory").json()["total"] == 0 assert client.delete(f"/api/v1/me/inventory/{CARROCK}").status_code == 404 def test_list_filters_by_category_and_search(): client = authed_client() assert add(client, CARROCK).status_code == 201 assert add(client, RIFLE).status_code == 201 ships = client.get("/api/v1/me/inventory", params={"category": "ships"}).json() assert ships["total"] == 1 assert ships["items"][0]["class_name"] == CARROCK found = client.get("/api/v1/me/inventory", params={"search": "carrack"}).json() assert found["total"] == 1 assert found["items"][0]["class_name"] == CARROCK def test_list_pagination_envelope_and_cap(): client = authed_client() assert add(client, CARROCK).status_code == 201 assert add(client, RIFLE).status_code == 201 body = client.get( "/api/v1/me/inventory", params={"page_size": 1, "page": 2} ).json() assert body["total"] == 2 assert body["page"] == 2 assert body["page_size"] == 1 assert body["total_pages"] == 2 assert len(body["items"]) == 1 capped = client.get("/api/v1/me/inventory", params={"page_size": 500}).json() assert capped["page_size"] == 100 def test_summary_aggregates_quantity_and_categories(): client = authed_client() assert add(client, CARROCK, quantity=2).status_code == 201 assert add(client, RIFLE, quantity=3).status_code == 201 summary = client.get("/api/v1/me/inventory/summary").json() assert summary["items"] == 2 assert summary["quantity"] == 5 assert summary["unavailable"] == 0 assert summary["categories"]["ships"] == 2 def test_notes_length_is_bounded(): client = authed_client() resp = add(client, CARROCK, notes="x" * 1001) assert resp.status_code == 422 def test_catalog_is_not_mutated_by_inventory_operations(): client = authed_client() before = client.get("/api/v1/items", params={"page_size": 100}).json() assert add(client, CARROCK).status_code == 201 client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"quantity": 9}) client.delete(f"/api/v1/me/inventory/{CARROCK}") after = client.get("/api/v1/items", params={"page_size": 100}).json() assert after == before carrack = client.get(f"/api/v1/items/{CARROCK}").json() assert "quantity" not in carrack assert "notes" not in carrack def test_sort_by_category_groups_categories(): client = authed_client() add(client, CARROCK) add(client, RIFLE) items = client.get("/api/v1/me/inventory?sort=category").json()["items"] assert [i["class_name"] for i in items] == [RIFLE, CARROCK] def test_sort_by_quantity_is_descending(): client = authed_client() add(client, CARROCK, quantity=5) add(client, RIFLE, quantity=1) items = client.get("/api/v1/me/inventory?sort=quantity").json()["items"] assert [i["class_name"] for i in items] == [CARROCK, RIFLE] def test_sort_defaults_to_name_and_rejects_unknown_values(): client = authed_client() add(client, CARROCK) listing = client.get("/api/v1/me/inventory").json() assert [i["class_name"] for i in listing["items"]] == [CARROCK] assert client.get("/api/v1/me/inventory?sort=bogus").status_code == 422