"""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" IRON_ORE = "Carryable_TBO_FL_1SCU_Commodity_Metal_Ore_Iron" IRON = "Carryable_TBO_FL_1SCU_Commodity_Metal_Iron" CONSTRUCTION = "Carryable_TBO_FL_1SCU_Commodity_ProcessedGoods_ConstructionMaterials" 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 = "", quality: int | None = None, ): payload = {"class_name": class_name, "quantity": quantity, "notes": notes} if quality is not None: payload["quality"] = quality return client.post("/api/v1/me/inventory", json=payload) 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["visibility"] == "private" 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_visibility_toggles_between_private_and_available(): client = authed_client() assert add(client, CARROCK).status_code == 201 resp = client.patch( f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "available"} ) assert resp.status_code == 200 assert resp.json()["visibility"] == "available" listed = client.get("/api/v1/me/inventory").json()["items"] assert listed[0]["visibility"] == "available" back = client.patch( f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "private"} ) assert back.json()["visibility"] == "private" def test_patch_rejects_unknown_visibility(): client = authed_client() assert add(client, CARROCK).status_code == 201 resp = client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "public"}) assert resp.status_code == 422 def test_visibility_is_kept_when_quantity_changes_and_item_is_readded(): client = authed_client() add(client, CARROCK) client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"visibility": "available"}) client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"quantity": 3}) assert add(client, CARROCK, quantity=1).status_code == 200 entry = client.get("/api/v1/me/inventory").json()["items"][0] assert entry["quantity"] == 4 assert entry["visibility"] == "available" 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 def test_add_commodity_uses_catalog_name_and_quantity(): client = authed_client("refiner") resp = add(client, IRON_ORE, quantity=12, notes="from Aaron Halo") assert resp.status_code == 201 entry = client.get("/api/v1/me/inventory").json()["items"][0] assert entry["name"] == "Iron (Ore)" assert entry["category"] == "commodities" assert entry["quantity"] == 12 assert entry["available"] is True assert entry["visibility"] == "private" def test_catalog_marks_only_mineable_commodities_as_quality_applicable(): client = TestClient(app) items = client.get( "/api/v1/items", params={"category": "commodities", "page_size": 100} ).json()["items"] flags = {item["class_name"]: item["quality_applicable"] for item in items} assert flags[IRON] is True assert flags[IRON_ORE] is True assert flags[CONSTRUCTION] is False assert flags["Carryable_TBO_FL_1SCU_Commodity_Metal_RMC"] is False ships = client.get("/api/v1/items", params={"category": "ships"}).json()["items"] assert all(item["quality_applicable"] is False for item in ships) def test_quality_can_be_set_and_cleared_on_a_quality_commodity(): client = authed_client() assert add(client, IRON_ORE).status_code == 201 resp = client.patch(f"/api/v1/me/inventory/{IRON_ORE}", json={"quality": 750}) assert resp.status_code == 200 body = resp.json() assert body["quality"] == 750 assert body["quality_applicable"] is True cleared = client.patch( f"/api/v1/me/inventory/{IRON_ORE}", json={"quality": None} ) assert cleared.status_code == 200 assert cleared.json()["quality"] is None def test_add_persists_quality_and_keeps_it_when_quantity_is_incremented(): client = authed_client() first = add(client, IRON, quality=900) assert first.status_code == 201 assert first.json()["entry"]["quality"] == 900 second = add(client, IRON, quantity=2) assert second.status_code == 200 entry = second.json()["entry"] assert entry["quality"] == 900 assert entry["quantity"] == 3 replaced = add(client, IRON, quality=400) assert replaced.json()["entry"]["quality"] == 400 def test_quality_is_rejected_for_non_mineable_commodities(): client = authed_client() assert add(client, CONSTRUCTION).status_code == 201 resp = client.patch( f"/api/v1/me/inventory/{CONSTRUCTION}", json={"quality": 500} ) assert resp.status_code == 422 assert "does not apply" in resp.json()["detail"] add_resp = add(client, CONSTRUCTION, quality=500) assert add_resp.status_code == 422 def test_quality_is_rejected_for_ships(): client = authed_client() assert add(client, CARROCK).status_code == 201 resp = client.patch(f"/api/v1/me/inventory/{CARROCK}", json={"quality": 100}) assert resp.status_code == 422 def test_quality_range_is_enforced(): client = authed_client() assert add(client, IRON_ORE, quality=1).status_code == 201 assert add(client, IRON, quality=1000).status_code == 201 assert add(client, IRON_ORE, quality=0).status_code == 422 assert add(client, IRON_ORE, quality=1001).status_code == 422 assert add(client, IRON, quantity=1).status_code == 200 assert client.patch( f"/api/v1/me/inventory/{IRON}", json={"quality": 1001} ).status_code == 422