Files
badoHair_be/tests/test_products.py
belviskhoremk d2dc43b16f Initial Commit
2026-05-12 00:34:21 +00:00

155 lines
5.9 KiB
Python

"""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