Files
modbison/backend/tests/test_asset_collections.py
T

236 lines
7.6 KiB
Python

"""Integration tests for Poly Model Asset Collections and ZIP batch extraction.
Covers tasks dfe19da5a448 and 840bb736d098.
"""
import io
import zipfile
import pytest
from fastapi.testclient import TestClient
from app.main import app
def _register(client: TestClient, username: str) -> tuple[int, dict[str, str]]:
resp = client.post(
"/api/auth/register",
json={
"username": username,
"email": f"{username}@example.com",
"password": "secret123",
"date_of_birth": "1990-01-01",
},
)
assert resp.status_code == 201, resp.text
token = resp.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
me = client.get("/api/users/me", headers=headers)
assert me.status_code == 200, me.text
return me.json()["id"], headers
def test_collection_crud_and_permissions():
client = TestClient(app)
uid1, h1 = _register(client, "col_user_1")
uid2, h2 = _register(client, "col_user_2")
# 1. Create collection as user 1
resp = client.post(
"/api/assets/collections/",
headers=h1,
json={
"title": "Low Poly Fantasy Props",
"description": "Swords, shields, and potions",
"theme": "fantasy",
"is_public": True,
"tags": "fantasy,lowpoly,weapons",
},
)
assert resp.status_code == 201, resp.text
col_data = resp.json()
assert col_data["title"] == "Low Poly Fantasy Props"
assert col_data["theme"] == "fantasy"
assert col_data["owner_id"] == uid1
col_id = col_data["id"]
# 2. List collections with theme filter
resp = client.get("/api/assets/collections/", headers=h2, params={"theme": "fantasy"})
assert resp.status_code == 200
cols = resp.json()
assert any(c["id"] == col_id for c in cols)
# 3. List with non-matching theme filter
resp = client.get("/api/assets/collections/", headers=h2, params={"theme": "scifi"})
assert resp.status_code == 200
cols = resp.json()
assert not any(c["id"] == col_id for c in cols)
# 4. Search filter
resp = client.get("/api/assets/collections/", headers=h2, params={"search": "shields"})
assert resp.status_code == 200
assert any(c["id"] == col_id for c in resp.json())
# 5. User 2 views public collection
resp = client.get(f"/api/assets/collections/{col_id}", headers=h2)
assert resp.status_code == 200
assert resp.json()["id"] == col_id
# 6. User 2 cannot modify User 1's collection
resp = client.patch(
f"/api/assets/collections/{col_id}",
headers=h2,
json={"title": "Hacked Title"},
)
assert resp.status_code == 403
# 7. User 2 cannot delete User 1's collection
resp = client.delete(f"/api/assets/collections/{col_id}", headers=h2)
assert resp.status_code == 403
# 8. User 1 updates collection to private
resp = client.patch(
f"/api/assets/collections/{col_id}",
headers=h1,
json={"is_public": False, "theme": "medieval"},
)
assert resp.status_code == 200
assert resp.json()["is_public"] is False
assert resp.json()["theme"] == "medieval"
# 9. User 2 can no longer view private collection
resp = client.get(f"/api/assets/collections/{col_id}", headers=h2)
assert resp.status_code == 403
# 10. User 1 deletes collection
resp = client.delete(f"/api/assets/collections/{col_id}", headers=h1)
assert resp.status_code == 204
# 11. Verify gone
resp = client.get(f"/api/assets/collections/{col_id}", headers=h1)
assert resp.status_code == 404
def test_collection_item_association_and_ordering():
client = TestClient(app)
uid, h = _register(client, "col_items_user")
# Create 2 assets
a1_resp = client.post(
"/api/assets/",
headers=h,
json={"name": "Wooden Crate", "asset_type": "model_3d"},
)
assert a1_resp.status_code == 201, a1_resp.text
asset1_id = a1_resp.json()["id"]
a2_resp = client.post(
"/api/assets/",
headers=h,
json={"name": "Iron Barrel", "asset_type": "model_3d"},
)
assert a2_resp.status_code == 201, a2_resp.text
asset2_id = a2_resp.json()["id"]
# Create collection
col_resp = client.post(
"/api/assets/collections/",
headers=h,
json={"title": "Industrial Pack", "theme": "industrial"},
)
assert col_resp.status_code == 201
col_id = col_resp.json()["id"]
# Add items with explicit ordering
add1 = client.post(
f"/api/assets/collections/{col_id}/items",
headers=h,
json={"asset_id": asset1_id, "sort_order": 0},
)
assert add1.status_code == 200
assert add1.json()["items_count"] == 1
assert add1.json()["items"][0]["id"] == asset1_id
add2 = client.post(
f"/api/assets/collections/{col_id}/items",
headers=h,
json={"asset_id": asset2_id, "sort_order": 1},
)
assert add2.status_code == 200
assert add2.json()["items_count"] == 2
assert add2.json()["items"][1]["id"] == asset2_id
# Remove item 1
rem = client.delete(
f"/api/assets/collections/{col_id}/items/{asset1_id}",
headers=h,
)
assert rem.status_code == 200
assert rem.json()["items_count"] == 1
assert rem.json()["items"][0]["id"] == asset2_id
def test_collection_zip_upload_and_extraction():
client = TestClient(app)
uid, h = _register(client, "col_zip_user")
# 1. Build a valid in-memory zip file with mock GLB content
zip_buf = io.BytesIO()
with zipfile.ZipFile(zip_buf, "w") as zf:
zf.writestr("stone_pillar.glb", b"glTFmockheader_binary_stone_pillar")
zf.writestr("wooden_arch.glb", b"glTFmockheader_binary_wooden_arch")
zf.writestr("temple_roof.gltf", b'{"asset": {"version": "2.0"}}')
zf.writestr("readme.txt", b"Instructions for this pack")
zip_bytes = zip_buf.getvalue()
# Upload zip pack
resp = client.post(
"/api/assets/collections/upload-zip",
headers=h,
data={
"title": "Ancient Ruins Pack",
"theme": "historical",
"description": "Set of stone pillars and wooden arches",
"is_public": "true",
"tags": "ruins,ancient,stone",
},
files={"file": ("ruins_pack.zip", zip_bytes, "application/zip")},
)
assert resp.status_code == 201, resp.text
data = resp.json()
assert data["title"] == "Ancient Ruins Pack"
assert data["theme"] == "historical"
assert data["items_count"] == 3
names = [i["name"] for i in data["items"]]
assert "Stone Pillar" in names
assert "Wooden Arch" in names
assert "Temple Roof" in names
def test_collection_zip_error_handling():
client = TestClient(app)
uid, h = _register(client, "col_badzip_user")
# 1. Invalid zip payload
resp = client.post(
"/api/assets/collections/upload-zip",
headers=h,
data={"title": "Corrupt Pack", "theme": "general"},
files={"file": ("bad.zip", b"not-a-valid-zip-file", "application/zip")},
)
assert resp.status_code == 400
assert "Invalid ZIP archive" in resp.json()["detail"]
# 2. Valid zip but no 3D models
zip_buf = io.BytesIO()
with zipfile.ZipFile(zip_buf, "w") as zf:
zf.writestr("notes.txt", b"Hello world")
zf.writestr("image.png", b"fake image bytes")
resp = client.post(
"/api/assets/collections/upload-zip",
headers=h,
data={"title": "No Models Pack", "theme": "general"},
files={"file": ("empty.zip", zip_buf.getvalue(), "application/zip")},
)
assert resp.status_code == 400
assert "no valid 3D models" in resp.json()["detail"]