"""Product endpoint tests — both public and admin.""" from unittest.mock import AsyncMock, patch from tests.conftest import SAMPLE_PRODUCT, PRODUCT_ID # ── Public endpoints ────────────────────────────────────────────────────────── async def test_list_products_returns_paginated(anon_client, mock_db): mock_db.fetchval = AsyncMock(return_value=1) mock_db.fetch = AsyncMock(return_value=[SAMPLE_PRODUCT]) r = await anon_client.get("/api/v1/products") assert r.status_code == 200 body = r.json() assert body["success"] is True assert isinstance(body["data"], list) assert body["meta"]["total"] == 1 assert body["data"][0]["name"] == "Extensions Clip-In Luxe" async def test_list_products_category_filter(anon_client, mock_db): mock_db.fetchval = AsyncMock(return_value=0) mock_db.fetch = AsyncMock(return_value=[]) r = await anon_client.get("/api/v1/products?category=tape-in") assert r.status_code == 200 assert r.json()["meta"]["total"] == 0 async def test_list_products_bestseller_filter(anon_client, mock_db): mock_db.fetchval = AsyncMock(return_value=1) mock_db.fetch = AsyncMock(return_value=[SAMPLE_PRODUCT]) r = await anon_client.get("/api/v1/products?bestseller=true") assert r.status_code == 200 assert r.json()["data"][0]["is_bestseller"] is True async def test_list_products_exclude_param(anon_client, mock_db): """Used to fetch similar products on the product detail page.""" mock_db.fetchval = AsyncMock(return_value=0) mock_db.fetch = AsyncMock(return_value=[]) r = await anon_client.get(f"/api/v1/products?category=clip-in&exclude={PRODUCT_ID}") assert r.status_code == 200 async def test_get_product_found(anon_client, mock_db): mock_db.fetchrow = AsyncMock(return_value=SAMPLE_PRODUCT) r = await anon_client.get(f"/api/v1/products/{PRODUCT_ID}") assert r.status_code == 200 data = r.json()["data"] assert data["id"] == PRODUCT_ID assert data["category"] == "clip-in" assert isinstance(data["colors"], list) assert isinstance(data["images"], list) assert isinstance(data["image"], str) async def test_get_product_not_found(anon_client, mock_db): mock_db.fetchrow = AsyncMock(return_value=None) r = await anon_client.get(f"/api/v1/products/00000000-0000-0000-0000-000000000000") assert r.status_code == 404 body = r.json() assert body["success"] is False assert body["error"]["code"] == "PRODUCT_NOT_FOUND" async def test_product_response_has_required_frontend_fields(anon_client, mock_db): """Verify every field the frontend Product interface needs is present.""" mock_db.fetchrow = AsyncMock(return_value=SAMPLE_PRODUCT) r = await anon_client.get(f"/api/v1/products/{PRODUCT_ID}") data = r.json()["data"] required = ["id", "name", "category", "price", "originalPrice", "image", "images", "colors", "lengths", "description", "features", "isNew", "isBestseller", "rating", "reviewCount"] # Map snake_case API → camelCase frontend expectations api_fields = set(data.keys()) # These must exist (snake_case form in our API) for field in ["id", "name", "category", "price", "original_price", "image", "images", "colors", "lengths", "description", "features", "is_new", "is_bestseller", "rating", "review_count"]: assert field in api_fields, f"Missing field: {field}" # ── Admin product endpoints ─────────────────────────────────────────────────── async def test_admin_list_products_includes_hidden(admin_client, mock_db): mock_db.fetchval = AsyncMock(return_value=2) mock_db.fetch = AsyncMock(return_value=[SAMPLE_PRODUCT, {**SAMPLE_PRODUCT, "is_hidden": True}]) r = await admin_client.get("/api/v1/admin/products") assert r.status_code == 200 assert r.json()["meta"]["total"] == 2 async def test_admin_create_product(admin_client, mock_db): mock_db.fetchrow = AsyncMock(return_value=SAMPLE_PRODUCT) r = await admin_client.post("/api/v1/admin/products", json={ "name": "New Extensions", "price": 199.0, "category": "tape-in", "colors": ["Blond", "Brun"], "lengths": ["50cm"], "features": ["100% Remy"], "stock_quantity": 5, }) assert r.status_code == 201 assert r.json()["success"] is True async def test_admin_update_product(admin_client, mock_db): mock_db.fetchrow = AsyncMock(return_value=SAMPLE_PRODUCT) r = await admin_client.put(f"/api/v1/admin/products/{PRODUCT_ID}", json={"price": 175.0}) assert r.status_code == 200 async def test_admin_delete_product(admin_client, mock_db): mock_db.execute = AsyncMock(return_value="DELETE 1") r = await admin_client.delete(f"/api/v1/admin/products/{PRODUCT_ID}") assert r.status_code == 204 async def test_admin_delete_product_not_found(admin_client, mock_db): mock_db.execute = AsyncMock(return_value="DELETE 0") r = await admin_client.delete(f"/api/v1/admin/products/00000000-0000-0000-0000-000000000000") assert r.status_code == 404 async def test_admin_bulk_stock_update(admin_client, mock_db): r = await admin_client.post("/api/v1/admin/products/bulk-stock", json={ "updates": [{"id": PRODUCT_ID, "stock_quantity": 20}] }) assert r.status_code == 200 assert r.json()["data"]["updated"] == 1 async def test_admin_products_requires_admin_role(auth_client): """A client-role user must get 403.""" r = await auth_client.get("/api/v1/admin/products") assert r.status_code == 403 assert r.json()["error"]["code"] == "FORBIDDEN" async def test_admin_products_requires_auth(anon_client): r = await anon_client.get("/api/v1/admin/products") assert r.status_code == 401