254 lines
8.4 KiB
Python
254 lines
8.4 KiB
Python
"""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["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
|