style: apply black and isort formatting across entire codebase

- Standardize quote style (single to double quotes)
- Reorder and group imports alphabetically
- Fix line breaks and indentation for consistency
- Apply PEP 8 formatting standards

Also updated Makefile to exclude both venv and .venv from code quality checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

@@ -59,7 +59,9 @@ class TestAdminAPI:
assert response.status_code == 400 # Business logic error
data = response.json()
assert data["error_code"] == "CANNOT_MODIFY_SELF"
assert "Cannot perform 'deactivate account' on your own account" in data["message"]
assert (
"Cannot perform 'deactivate account' on your own account" in data["message"]
)
def test_toggle_user_status_cannot_modify_admin(
self, client, admin_headers, test_admin, another_admin
@@ -85,7 +87,9 @@ class TestAdminAPI:
# Check that test_vendor is in the response
vendor_codes = [
vendor ["vendor_code"] for vendor in data["vendors"] if "vendor_code" in vendor
vendor["vendor_code"]
for vendor in data["vendors"]
if "vendor_code" in vendor
]
assert test_vendor.vendor_code in vendor_codes
@@ -98,7 +102,7 @@ class TestAdminAPI:
assert data["error_code"] == "ADMIN_REQUIRED"
def test_verify_vendor_admin(self, client, admin_headers, test_vendor):
"""Test admin verifying/unverifying vendor """
"""Test admin verifying/unverifying vendor"""
response = client.put(
f"/api/v1/admin/vendors/{test_vendor.id}/verify", headers=admin_headers
)
@@ -109,8 +113,10 @@ class TestAdminAPI:
assert test_vendor.vendor_code in message
def test_verify_vendor_not_found(self, client, admin_headers):
"""Test admin verifying non-existent vendor """
response = client.put("/api/v1/admin/vendors/99999/verify", headers=admin_headers)
"""Test admin verifying non-existent vendor"""
response = client.put(
"/api/v1/admin/vendors/99999/verify", headers=admin_headers
)
assert response.status_code == 404
data = response.json()
@@ -129,8 +135,10 @@ class TestAdminAPI:
assert test_vendor.vendor_code in message
def test_toggle_vendor_status_not_found(self, client, admin_headers):
"""Test admin toggling status for non-existent vendor """
response = client.put("/api/v1/admin/vendors/99999/status", headers=admin_headers)
"""Test admin toggling status for non-existent vendor"""
response = client.put(
"/api/v1/admin/vendors/99999/status", headers=admin_headers
)
assert response.status_code == 404
data = response.json()
@@ -166,7 +174,8 @@ class TestAdminAPI:
data = response.json()
assert len(data) >= 1
assert all(
job["marketplace"] == test_marketplace_import_job.marketplace for job in data
job["marketplace"] == test_marketplace_import_job.marketplace
for job in data
)
def test_get_marketplace_import_jobs_non_admin(self, client, auth_headers):

View File

@@ -1,7 +1,8 @@
# tests/integration/api/v1/test_auth_endpoints.py
from datetime import datetime, timedelta, timezone
import pytest
from jose import jwt
from datetime import datetime, timedelta, timezone
@pytest.mark.integration
@@ -178,8 +179,7 @@ class TestAuthenticationAPI:
def test_get_current_user_invalid_token(self, client):
"""Test getting current user with invalid token"""
response = client.get(
"/api/v1/auth/me",
headers={"Authorization": "Bearer invalid_token_here"}
"/api/v1/auth/me", headers={"Authorization": "Bearer invalid_token_here"}
)
assert response.status_code == 401
@@ -201,14 +201,11 @@ class TestAuthenticationAPI:
}
expired_token = jwt.encode(
expired_payload,
auth_manager.secret_key,
algorithm=auth_manager.algorithm
expired_payload, auth_manager.secret_key, algorithm=auth_manager.algorithm
)
response = client.get(
"/api/v1/auth/me",
headers={"Authorization": f"Bearer {expired_token}"}
"/api/v1/auth/me", headers={"Authorization": f"Bearer {expired_token}"}
)
assert response.status_code == 401
@@ -321,4 +318,3 @@ class TestAuthManager:
user = auth_manager.authenticate_user(db, "nonexistent", "password")
assert user is None

View File

@@ -14,19 +14,34 @@ class TestFiltering:
"""Test filtering products by brand successfully"""
# Create products with different brands using unique IDs
import uuid
unique_suffix = str(uuid.uuid4())[:8]
products = [
MarketplaceProduct(marketplace_product_id=f"BRAND1_{unique_suffix}", title="MarketplaceProduct 1", brand="BrandA"),
MarketplaceProduct(marketplace_product_id=f"BRAND2_{unique_suffix}", title="MarketplaceProduct 2", brand="BrandB"),
MarketplaceProduct(marketplace_product_id=f"BRAND3_{unique_suffix}", title="MarketplaceProduct 3", brand="BrandA"),
MarketplaceProduct(
marketplace_product_id=f"BRAND1_{unique_suffix}",
title="MarketplaceProduct 1",
brand="BrandA",
),
MarketplaceProduct(
marketplace_product_id=f"BRAND2_{unique_suffix}",
title="MarketplaceProduct 2",
brand="BrandB",
),
MarketplaceProduct(
marketplace_product_id=f"BRAND3_{unique_suffix}",
title="MarketplaceProduct 3",
brand="BrandA",
),
]
db.add_all(products)
db.commit()
# Filter by BrandA
response = client.get("/api/v1/marketplace/product?brand=BrandA", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?brand=BrandA", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 2 # At least our test products
@@ -37,7 +52,9 @@ class TestFiltering:
assert product["brand"] == "BrandA"
# Filter by BrandB
response = client.get("/api/v1/marketplace/product?brand=BrandB", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?brand=BrandB", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1 # At least our test product
@@ -45,37 +62,57 @@ class TestFiltering:
def test_product_marketplace_filter_success(self, client, auth_headers, db):
"""Test filtering products by marketplace successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
products = [
MarketplaceProduct(marketplace_product_id=f"MKT1_{unique_suffix}", title="MarketplaceProduct 1", marketplace="Amazon"),
MarketplaceProduct(marketplace_product_id=f"MKT2_{unique_suffix}", title="MarketplaceProduct 2", marketplace="eBay"),
MarketplaceProduct(marketplace_product_id=f"MKT3_{unique_suffix}", title="MarketplaceProduct 3", marketplace="Amazon"),
MarketplaceProduct(
marketplace_product_id=f"MKT1_{unique_suffix}",
title="MarketplaceProduct 1",
marketplace="Amazon",
),
MarketplaceProduct(
marketplace_product_id=f"MKT2_{unique_suffix}",
title="MarketplaceProduct 2",
marketplace="eBay",
),
MarketplaceProduct(
marketplace_product_id=f"MKT3_{unique_suffix}",
title="MarketplaceProduct 3",
marketplace="Amazon",
),
]
db.add_all(products)
db.commit()
response = client.get("/api/v1/marketplace/product?marketplace=Amazon", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?marketplace=Amazon", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 2 # At least our test products
# Verify all returned products have Amazon marketplace
amazon_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
amazon_products = [
p
for p in data["products"]
if p["marketplace_product_id"].endswith(unique_suffix)
]
for product in amazon_products:
assert product["marketplace"] == "Amazon"
def test_product_search_filter_success(self, client, auth_headers, db):
"""Test searching products by text successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
products = [
MarketplaceProduct(
marketplace_product_id=f"SEARCH1_{unique_suffix}",
title=f"Apple iPhone {unique_suffix}",
description="Smartphone"
description="Smartphone",
),
MarketplaceProduct(
marketplace_product_id=f"SEARCH2_{unique_suffix}",
@@ -85,7 +122,7 @@ class TestFiltering:
MarketplaceProduct(
marketplace_product_id=f"SEARCH3_{unique_suffix}",
title=f"iPad Tablet {unique_suffix}",
description="Apple tablet"
description="Apple tablet",
),
]
@@ -93,13 +130,17 @@ class TestFiltering:
db.commit()
# Search for "Apple"
response = client.get(f"/api/v1/marketplace/product?search=Apple", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?search=Apple", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 2 # iPhone and iPad
# Search for "phone"
response = client.get(f"/api/v1/marketplace/product?search=phone", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?search=phone", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 2 # iPhone and Galaxy
@@ -107,6 +148,7 @@ class TestFiltering:
def test_combined_filters_success(self, client, auth_headers, db):
"""Test combining multiple filters successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
products = [
@@ -135,14 +177,19 @@ class TestFiltering:
# Filter by brand AND marketplace
response = client.get(
"/api/v1/marketplace/product?brand=Apple&marketplace=Amazon", headers=auth_headers
"/api/v1/marketplace/product?brand=Apple&marketplace=Amazon",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1 # At least iPhone matches both
# Find our specific test product
matching_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
matching_products = [
p
for p in data["products"]
if p["marketplace_product_id"].endswith(unique_suffix)
]
for product in matching_products:
assert product["brand"] == "Apple"
assert product["marketplace"] == "Amazon"
@@ -150,7 +197,8 @@ class TestFiltering:
def test_filter_with_no_results(self, client, auth_headers):
"""Test filtering with criteria that returns no results"""
response = client.get(
"/api/v1/marketplace/product?brand=NonexistentBrand123456", headers=auth_headers
"/api/v1/marketplace/product?brand=NonexistentBrand123456",
headers=auth_headers,
)
assert response.status_code == 200
@@ -161,6 +209,7 @@ class TestFiltering:
def test_filter_case_insensitive(self, client, auth_headers, db):
"""Test that filters are case-insensitive"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
product = MarketplaceProduct(
@@ -174,7 +223,10 @@ class TestFiltering:
# Test different case variations
for brand_filter in ["TestBrand", "testbrand", "TESTBRAND"]:
response = client.get(f"/api/v1/marketplace/product?brand={brand_filter}", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?brand={brand_filter}",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
@@ -183,9 +235,14 @@ class TestFiltering:
"""Test behavior with invalid filter parameters"""
# Test with very long filter values
long_brand = "A" * 1000
response = client.get(f"/api/v1/marketplace/product?brand={long_brand}", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?brand={long_brand}", headers=auth_headers
)
assert response.status_code == 200 # Should handle gracefully
# Test with special characters
response = client.get("/api/v1/marketplace/product?brand=<script>alert('test')</script>", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?brand=<script>alert('test')</script>",
headers=auth_headers,
)
assert response.status_code == 200 # Should handle gracefully

View File

@@ -17,7 +17,9 @@ class TestInventoryAPI:
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
assert response.status_code == 200
data = response.json()
@@ -38,7 +40,9 @@ class TestInventoryAPI:
"quantity": 75,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
assert response.status_code == 200
data = response.json()
@@ -52,7 +56,9 @@ class TestInventoryAPI:
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
assert response.status_code == 422
data = response.json()
@@ -60,7 +66,9 @@ class TestInventoryAPI:
assert data["status_code"] == 422
assert "GTIN is required" in data["message"]
def test_set_inventory_invalid_quantity_validation_error(self, client, auth_headers):
def test_set_inventory_invalid_quantity_validation_error(
self, client, auth_headers
):
"""Test setting inventory with invalid quantity returns InvalidQuantityException"""
inventory_data = {
"gtin": "1234567890123",
@@ -68,7 +76,9 @@ class TestInventoryAPI:
"quantity": -10, # Negative quantity
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
assert response.status_code in [400, 422]
data = response.json()
@@ -138,7 +148,9 @@ class TestInventoryAPI:
data = response.json()
assert data["quantity"] == 35 # 50 - 15
def test_remove_inventory_insufficient_returns_business_logic_error(self, client, auth_headers, db):
def test_remove_inventory_insufficient_returns_business_logic_error(
self, client, auth_headers, db
):
"""Test removing more inventory than available returns InsufficientInventoryException"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=10)
@@ -184,7 +196,9 @@ class TestInventoryAPI:
assert data["error_code"] == "INVENTORY_NOT_FOUND"
assert data["status_code"] == 404
def test_negative_inventory_not_allowed_business_logic_error(self, client, auth_headers, db):
def test_negative_inventory_not_allowed_business_logic_error(
self, client, auth_headers, db
):
"""Test operations resulting in negative inventory returns NegativeInventoryException"""
# Create initial inventory
inventory = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=5)
@@ -204,14 +218,21 @@ class TestInventoryAPI:
assert response.status_code == 400
data = response.json()
# This might be caught as INSUFFICIENT_INVENTORY or NEGATIVE_INVENTORY_NOT_ALLOWED
assert data["error_code"] in ["INSUFFICIENT_INVENTORY", "NEGATIVE_INVENTORY_NOT_ALLOWED"]
assert data["error_code"] in [
"INSUFFICIENT_INVENTORY",
"NEGATIVE_INVENTORY_NOT_ALLOWED",
]
assert data["status_code"] == 400
def test_get_inventory_by_gtin_success(self, client, auth_headers, db):
"""Test getting inventory summary for GTIN successfully"""
# Create inventory in multiple locations
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
inventory1 = Inventory(
gtin="1234567890123", location="WAREHOUSE_A", quantity=50
)
inventory2 = Inventory(
gtin="1234567890123", location="WAREHOUSE_B", quantity=25
)
db.add_all([inventory1, inventory2])
db.commit()
@@ -238,12 +259,18 @@ class TestInventoryAPI:
def test_get_total_inventory_success(self, client, auth_headers, db):
"""Test getting total inventory for GTIN successfully"""
# Create inventory in multiple locations
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="1234567890123", location="WAREHOUSE_B", quantity=25)
inventory1 = Inventory(
gtin="1234567890123", location="WAREHOUSE_A", quantity=50
)
inventory2 = Inventory(
gtin="1234567890123", location="WAREHOUSE_B", quantity=25
)
db.add_all([inventory1, inventory2])
db.commit()
response = client.get("/api/v1/inventory/1234567890123/total", headers=auth_headers)
response = client.get(
"/api/v1/inventory/1234567890123/total", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
@@ -253,7 +280,9 @@ class TestInventoryAPI:
def test_get_total_inventory_not_found(self, client, auth_headers):
"""Test getting total inventory for nonexistent GTIN returns InventoryNotFoundException"""
response = client.get("/api/v1/inventory/9999999999999/total", headers=auth_headers)
response = client.get(
"/api/v1/inventory/9999999999999/total", headers=auth_headers
)
assert response.status_code == 404
data = response.json()
@@ -263,8 +292,12 @@ class TestInventoryAPI:
def test_get_all_inventory_success(self, client, auth_headers, db):
"""Test getting all inventory entries successfully"""
# Create some inventory entries
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
inventory1 = Inventory(
gtin="1234567890123", location="WAREHOUSE_A", quantity=50
)
inventory2 = Inventory(
gtin="9876543210987", location="WAREHOUSE_B", quantity=25
)
db.add_all([inventory1, inventory2])
db.commit()
@@ -277,20 +310,28 @@ class TestInventoryAPI:
def test_get_all_inventory_with_filters(self, client, auth_headers, db):
"""Test getting inventory entries with filtering"""
# Create inventory entries
inventory1 = Inventory(gtin="1234567890123", location="WAREHOUSE_A", quantity=50)
inventory2 = Inventory(gtin="9876543210987", location="WAREHOUSE_B", quantity=25)
inventory1 = Inventory(
gtin="1234567890123", location="WAREHOUSE_A", quantity=50
)
inventory2 = Inventory(
gtin="9876543210987", location="WAREHOUSE_B", quantity=25
)
db.add_all([inventory1, inventory2])
db.commit()
# Filter by location
response = client.get("/api/v1/inventory?location=WAREHOUSE_A", headers=auth_headers)
response = client.get(
"/api/v1/inventory?location=WAREHOUSE_A", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
for inventory in data:
assert inventory["location"] == "WAREHOUSE_A"
# Filter by GTIN
response = client.get("/api/v1/inventory?gtin=1234567890123", headers=auth_headers)
response = client.get(
"/api/v1/inventory?gtin=1234567890123", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
for inventory in data:
@@ -390,7 +431,9 @@ class TestInventoryAPI:
"quantity": 100,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
# This depends on whether your service validates locations
if response.status_code == 404:

View File

@@ -8,9 +8,11 @@ import pytest
@pytest.mark.api
@pytest.mark.marketplace
class TestMarketplaceImportJobAPI:
def test_import_from_marketplace(self, client, auth_headers, test_vendor, test_user):
def test_import_from_marketplace(
self, client, auth_headers, test_vendor, test_user
):
"""Test marketplace import endpoint - just test job creation"""
# Ensure user owns the vendor
# Ensure user owns the vendor
test_vendor.owner_user_id = test_user.id
import_data = {
@@ -32,7 +34,7 @@ class TestMarketplaceImportJobAPI:
assert data["vendor_id"] == test_vendor.id
def test_import_from_marketplace_invalid_vendor(self, client, auth_headers):
"""Test marketplace import with invalid vendor """
"""Test marketplace import with invalid vendor"""
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "TestMarket",
@@ -48,7 +50,9 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "VENDOR_NOT_FOUND"
assert "NONEXISTENT" in data["message"]
def test_import_from_marketplace_unauthorized_vendor(self, client, auth_headers, test_vendor, other_user):
def test_import_from_marketplace_unauthorized_vendor(
self, client, auth_headers, test_vendor, other_user
):
"""Test marketplace import with unauthorized vendor access"""
# Set vendor owner to different user
test_vendor.owner_user_id = other_user.id
@@ -85,8 +89,10 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "VALIDATION_ERROR"
assert "Request validation failed" in data["message"]
def test_import_from_marketplace_admin_access(self, client, admin_headers, test_vendor):
"""Test that admin can import for any vendor """
def test_import_from_marketplace_admin_access(
self, client, admin_headers, test_vendor
):
"""Test that admin can import for any vendor"""
import_data = {
"url": "https://example.com/products.csv",
"marketplace": "AdminMarket",
@@ -94,7 +100,9 @@ class TestMarketplaceImportJobAPI:
}
response = client.post(
"/api/v1/marketplace/import-product", headers=admin_headers, json=import_data
"/api/v1/marketplace/import-product",
headers=admin_headers,
json=import_data,
)
assert response.status_code == 200
@@ -102,11 +110,13 @@ class TestMarketplaceImportJobAPI:
assert data["marketplace"] == "AdminMarket"
assert data["vendor_code"] == test_vendor.vendor_code
def test_get_marketplace_import_status(self, client, auth_headers, test_marketplace_import_job):
def test_get_marketplace_import_status(
self, client, auth_headers, test_marketplace_import_job
):
"""Test getting marketplace import status"""
response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
@@ -118,8 +128,7 @@ class TestMarketplaceImportJobAPI:
def test_get_marketplace_import_status_not_found(self, client, auth_headers):
"""Test getting status of non-existent import job"""
response = client.get(
"/api/v1/marketplace/import-status/99999",
headers=auth_headers
"/api/v1/marketplace/import-status/99999", headers=auth_headers
)
assert response.status_code == 404
@@ -127,21 +136,25 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
assert "99999" in data["message"]
def test_get_marketplace_import_status_unauthorized(self, client, auth_headers, test_marketplace_import_job, other_user):
def test_get_marketplace_import_status_unauthorized(
self, client, auth_headers, test_marketplace_import_job, other_user
):
"""Test getting status of unauthorized import job"""
# Change job owner to other user
test_marketplace_import_job.user_id = other_user.id
response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 403
data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_OWNED"
def test_get_marketplace_import_jobs(self, client, auth_headers, test_marketplace_import_job):
def test_get_marketplace_import_jobs(
self, client, auth_headers, test_marketplace_import_job
):
"""Test getting marketplace import jobs"""
response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers)
@@ -154,11 +167,13 @@ class TestMarketplaceImportJobAPI:
job_ids = [job["job_id"] for job in data]
assert test_marketplace_import_job.id in job_ids
def test_get_marketplace_import_jobs_with_filters(self, client, auth_headers, test_marketplace_import_job):
def test_get_marketplace_import_jobs_with_filters(
self, client, auth_headers, test_marketplace_import_job
):
"""Test getting import jobs with filters"""
response = client.get(
f"/api/v1/marketplace/import-jobs?marketplace={test_marketplace_import_job.marketplace}",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
@@ -167,13 +182,15 @@ class TestMarketplaceImportJobAPI:
assert len(data) >= 1
for job in data:
assert test_marketplace_import_job.marketplace.lower() in job["marketplace"].lower()
assert (
test_marketplace_import_job.marketplace.lower()
in job["marketplace"].lower()
)
def test_get_marketplace_import_jobs_pagination(self, client, auth_headers):
"""Test import jobs pagination"""
response = client.get(
"/api/v1/marketplace/import-jobs?skip=0&limit=5",
headers=auth_headers
"/api/v1/marketplace/import-jobs?skip=0&limit=5", headers=auth_headers
)
assert response.status_code == 200
@@ -181,9 +198,13 @@ class TestMarketplaceImportJobAPI:
assert isinstance(data, list)
assert len(data) <= 5
def test_get_marketplace_import_stats(self, client, auth_headers, test_marketplace_import_job):
def test_get_marketplace_import_stats(
self, client, auth_headers, test_marketplace_import_job
):
"""Test getting marketplace import statistics"""
response = client.get("/api/v1/marketplace/marketplace-import-stats", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/marketplace-import-stats", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
@@ -195,12 +216,15 @@ class TestMarketplaceImportJobAPI:
assert isinstance(data["total_jobs"], int)
assert data["total_jobs"] >= 1
def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_vendor, db):
def test_cancel_marketplace_import_job(
self, client, auth_headers, test_user, test_vendor, db
):
"""Test cancelling a marketplace import job"""
# Create a pending job that can be cancelled
from models.database.marketplace_import_job import MarketplaceImportJob
import uuid
from models.database.marketplace_import_job import MarketplaceImportJob
unique_id = str(uuid.uuid4())[:8]
job = MarketplaceImportJob(
status="pending",
@@ -219,8 +243,7 @@ class TestMarketplaceImportJobAPI:
db.refresh(job)
response = client.put(
f"/api/v1/marketplace/import-jobs/{job.id}/cancel",
headers=auth_headers
f"/api/v1/marketplace/import-jobs/{job.id}/cancel", headers=auth_headers
)
assert response.status_code == 200
@@ -232,15 +255,16 @@ class TestMarketplaceImportJobAPI:
def test_cancel_marketplace_import_job_not_found(self, client, auth_headers):
"""Test cancelling non-existent import job"""
response = client.put(
"/api/v1/marketplace/import-jobs/99999/cancel",
headers=auth_headers
"/api/v1/marketplace/import-jobs/99999/cancel", headers=auth_headers
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
def test_cancel_marketplace_import_job_cannot_cancel(self, client, auth_headers, test_marketplace_import_job, db):
def test_cancel_marketplace_import_job_cannot_cancel(
self, client, auth_headers, test_marketplace_import_job, db
):
"""Test cancelling a job that cannot be cancelled"""
# Set job to completed status
test_marketplace_import_job.status = "completed"
@@ -248,7 +272,7 @@ class TestMarketplaceImportJobAPI:
response = client.put(
f"/api/v1/marketplace/import-jobs/{test_marketplace_import_job.id}/cancel",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 400
@@ -256,12 +280,15 @@ class TestMarketplaceImportJobAPI:
assert data["error_code"] == "IMPORT_JOB_CANNOT_BE_CANCELLED"
assert "completed" in data["message"]
def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_vendor, db):
def test_delete_marketplace_import_job(
self, client, auth_headers, test_user, test_vendor, db
):
"""Test deleting a marketplace import job"""
# Create a completed job that can be deleted
from models.database.marketplace_import_job import MarketplaceImportJob
import uuid
from models.database.marketplace_import_job import MarketplaceImportJob
unique_id = str(uuid.uuid4())[:8]
job = MarketplaceImportJob(
status="completed",
@@ -280,8 +307,7 @@ class TestMarketplaceImportJobAPI:
db.refresh(job)
response = client.delete(
f"/api/v1/marketplace/import-jobs/{job.id}",
headers=auth_headers
f"/api/v1/marketplace/import-jobs/{job.id}", headers=auth_headers
)
assert response.status_code == 200
@@ -291,20 +317,22 @@ class TestMarketplaceImportJobAPI:
def test_delete_marketplace_import_job_not_found(self, client, auth_headers):
"""Test deleting non-existent import job"""
response = client.delete(
"/api/v1/marketplace/import-jobs/99999",
headers=auth_headers
"/api/v1/marketplace/import-jobs/99999", headers=auth_headers
)
assert response.status_code == 404
data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_vendor, db):
def test_delete_marketplace_import_job_cannot_delete(
self, client, auth_headers, test_user, test_vendor, db
):
"""Test deleting a job that cannot be deleted"""
# Create a pending job that cannot be deleted
from models.database.marketplace_import_job import MarketplaceImportJob
import uuid
from models.database.marketplace_import_job import MarketplaceImportJob
unique_id = str(uuid.uuid4())[:8]
job = MarketplaceImportJob(
status="pending",
@@ -323,8 +351,7 @@ class TestMarketplaceImportJobAPI:
db.refresh(job)
response = client.delete(
f"/api/v1/marketplace/import-jobs/{job.id}",
headers=auth_headers
f"/api/v1/marketplace/import-jobs/{job.id}", headers=auth_headers
)
assert response.status_code == 400
@@ -352,7 +379,9 @@ class TestMarketplaceImportJobAPI:
data = response.json()
assert data["error_code"] == "INVALID_TOKEN"
def test_admin_can_access_all_jobs(self, client, admin_headers, test_marketplace_import_job):
def test_admin_can_access_all_jobs(
self, client, admin_headers, test_marketplace_import_job
):
"""Test that admin can access all import jobs"""
response = client.get("/api/v1/marketplace/import-jobs", headers=admin_headers)
@@ -363,23 +392,28 @@ class TestMarketplaceImportJobAPI:
job_ids = [job["job_id"] for job in data]
assert test_marketplace_import_job.id in job_ids
def test_admin_can_view_any_job_status(self, client, admin_headers, test_marketplace_import_job):
def test_admin_can_view_any_job_status(
self, client, admin_headers, test_marketplace_import_job
):
"""Test that admin can view any job status"""
response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=admin_headers
headers=admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["job_id"] == test_marketplace_import_job.id
def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_vendor, db):
def test_admin_can_cancel_any_job(
self, client, admin_headers, test_user, test_vendor, db
):
"""Test that admin can cancel any job"""
# Create a pending job owned by different user
from models.database.marketplace_import_job import MarketplaceImportJob
import uuid
from models.database.marketplace_import_job import MarketplaceImportJob
unique_id = str(uuid.uuid4())[:8]
job = MarketplaceImportJob(
status="pending",
@@ -398,8 +432,7 @@ class TestMarketplaceImportJobAPI:
db.refresh(job)
response = client.put(
f"/api/v1/marketplace/import-jobs/{job.id}/cancel",
headers=admin_headers
f"/api/v1/marketplace/import-jobs/{job.id}/cancel", headers=admin_headers
)
assert response.status_code == 200

View File

@@ -1,7 +1,7 @@
# tests/integration/api/v1/test_export.py
import csv
from io import StringIO
import uuid
from io import StringIO
import pytest
@@ -13,9 +13,13 @@ from models.database.marketplace_product import MarketplaceProduct
@pytest.mark.performance # for the performance test
class TestExportFunctionality:
def test_csv_export_basic_success(self, client, auth_headers, test_marketplace_product):
def test_csv_export_basic_success(
self, client, auth_headers, test_marketplace_product
):
"""Test basic CSV export functionality successfully"""
response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/export-csv", headers=auth_headers
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -26,13 +30,22 @@ class TestExportFunctionality:
# Check header row
header = next(csv_reader)
expected_fields = ["marketplace_product_id", "title", "description", "price", "marketplace"]
expected_fields = [
"marketplace_product_id",
"title",
"description",
"price",
"marketplace",
]
for field in expected_fields:
assert field in header
# Verify test product appears in export
csv_lines = csv_content.split('\n')
test_product_found = any(test_marketplace_product.marketplace_product_id in line for line in csv_lines)
csv_lines = csv_content.split("\n")
test_product_found = any(
test_marketplace_product.marketplace_product_id in line
for line in csv_lines
)
assert test_product_found, "Test product should appear in CSV export"
def test_csv_export_with_marketplace_filter_success(self, client, auth_headers, db):
@@ -43,12 +56,12 @@ class TestExportFunctionality:
MarketplaceProduct(
marketplace_product_id=f"EXP1_{unique_suffix}",
title=f"Amazon MarketplaceProduct {unique_suffix}",
marketplace="Amazon"
marketplace="Amazon",
),
MarketplaceProduct(
marketplace_product_id=f"EXP2_{unique_suffix}",
title=f"eBay MarketplaceProduct {unique_suffix}",
marketplace="eBay"
marketplace="eBay",
),
]
@@ -56,7 +69,8 @@ class TestExportFunctionality:
db.commit()
response = client.get(
"/api/v1/marketplace/product/export-csv?marketplace=Amazon", headers=auth_headers
"/api/v1/marketplace/product/export-csv?marketplace=Amazon",
headers=auth_headers,
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -72,12 +86,12 @@ class TestExportFunctionality:
MarketplaceProduct(
marketplace_product_id=f"VENDOR1_{unique_suffix}",
title=f"Vendor1 MarketplaceProduct {unique_suffix}",
vendor_name="TestVendor1"
vendor_name="TestVendor1",
),
MarketplaceProduct(
marketplace_product_id=f"VENDOR2_{unique_suffix}",
title=f"Vendor2 MarketplaceProduct {unique_suffix}",
vendor_name="TestVendor2"
vendor_name="TestVendor2",
),
]
@@ -101,19 +115,19 @@ class TestExportFunctionality:
marketplace_product_id=f"COMBO1_{unique_suffix}",
title=f"Combo MarketplaceProduct 1 {unique_suffix}",
marketplace="Amazon",
vendor_name="TestVendor"
vendor_name="TestVendor",
),
MarketplaceProduct(
marketplace_product_id=f"COMBO2_{unique_suffix}",
title=f"Combo MarketplaceProduct 2 {unique_suffix}",
marketplace="eBay",
vendor_name="TestVendor"
vendor_name="TestVendor",
),
MarketplaceProduct(
marketplace_product_id=f"COMBO3_{unique_suffix}",
title=f"Combo MarketplaceProduct 3 {unique_suffix}",
marketplace="Amazon",
vendor_name="OtherVendor"
vendor_name="OtherVendor",
),
]
@@ -122,27 +136,27 @@ class TestExportFunctionality:
response = client.get(
"/api/v1/marketplace/product?marketplace=Amazon&name=TestVendor",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
csv_content = response.content.decode("utf-8")
assert f"COMBO1_{unique_suffix}" in csv_content # Matches both filters
assert f"COMBO2_{unique_suffix}" not in csv_content # Wrong marketplace
assert f"COMBO3_{unique_suffix}" not in csv_content # Wrong vendor
assert f"COMBO3_{unique_suffix}" not in csv_content # Wrong vendor
def test_csv_export_no_results(self, client, auth_headers):
"""Test CSV export with filters that return no results"""
response = client.get(
"/api/v1/marketplace/product/export-csv?marketplace=NonexistentMarketplace12345",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
csv_content = response.content.decode("utf-8")
csv_lines = csv_content.strip().split('\n')
csv_lines = csv_content.strip().split("\n")
# Should have header row even with no data
assert len(csv_lines) >= 1
# First line should be headers
@@ -161,27 +175,32 @@ class TestExportFunctionality:
title=f"Performance MarketplaceProduct {i}",
marketplace="Performance",
description=f"Performance test product {i}",
price="10.99"
price="10.99",
)
products.append(product)
# Add in batches to avoid memory issues
for i in range(0, len(products), 50):
batch = products[i:i + 50]
batch = products[i : i + 50]
db.add_all(batch)
db.commit()
import time
start_time = time.time()
response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/export-csv", headers=auth_headers
)
end_time = time.time()
execution_time = end_time - start_time
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
assert execution_time < 10.0, f"Export took {execution_time:.2f} seconds, should be under 10s"
assert (
execution_time < 10.0
), f"Export took {execution_time:.2f} seconds, should be under 10s"
# Verify content contains our test data
csv_content = response.content.decode("utf-8")
@@ -197,9 +216,13 @@ class TestExportFunctionality:
assert data["error_code"] == "INVALID_TOKEN"
assert data["status_code"] == 401
def test_csv_export_streaming_response_format(self, client, auth_headers, test_marketplace_product):
def test_csv_export_streaming_response_format(
self, client, auth_headers, test_marketplace_product
):
"""Test that CSV export returns proper streaming response with correct headers"""
response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/export-csv", headers=auth_headers
)
assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -217,23 +240,27 @@ class TestExportFunctionality:
# Create product with special characters that might break CSV
product = MarketplaceProduct(
marketplace_product_id=f"SPECIAL_{unique_suffix}",
title=f'MarketplaceProduct with quotes and commas {unique_suffix}', # Simplified to avoid CSV escaping issues
title=f"MarketplaceProduct with quotes and commas {unique_suffix}", # Simplified to avoid CSV escaping issues
description=f"Description with special chars {unique_suffix}",
marketplace="Test Market",
price="19.99"
price="19.99",
)
db.add(product)
db.commit()
response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/export-csv", headers=auth_headers
)
assert response.status_code == 200
csv_content = response.content.decode("utf-8")
# Verify our test product appears in the CSV content
assert f"SPECIAL_{unique_suffix}" in csv_content
assert f"MarketplaceProduct with quotes and commas {unique_suffix}" in csv_content
assert (
f"MarketplaceProduct with quotes and commas {unique_suffix}" in csv_content
)
assert "Test Market" in csv_content
assert "19.99" in csv_content
@@ -243,7 +270,12 @@ class TestExportFunctionality:
header = next(csv_reader)
# Verify header contains expected fields
expected_fields = ["marketplace_product_id", "title", "marketplace", "price"]
expected_fields = [
"marketplace_product_id",
"title",
"marketplace",
"price",
]
for field in expected_fields:
assert field in header
@@ -264,12 +296,15 @@ class TestExportFunctionality:
assert parsed_successfully, "CSV should be parseable despite special characters"
def test_csv_export_error_handling_service_failure(self, client, auth_headers, monkeypatch):
def test_csv_export_error_handling_service_failure(
self, client, auth_headers, monkeypatch
):
"""Test CSV export handles service failures gracefully"""
# Mock the service to raise an exception
def mock_generate_csv_export(*args, **kwargs):
from app.exceptions import ValidationException
raise ValidationException("Mocked service failure")
# This would require access to your service instance to mock properly
@@ -293,7 +328,9 @@ class TestExportFunctionality:
def test_csv_export_filename_generation(self, client, auth_headers):
"""Test CSV export generates appropriate filenames based on filters"""
# Test basic export filename
response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/export-csv", headers=auth_headers
)
assert response.status_code == 200
content_disposition = response.headers.get("content-disposition", "")
@@ -302,7 +339,7 @@ class TestExportFunctionality:
# Test with marketplace filter
response = client.get(
"/api/v1/marketplace/product/export-csv?marketplace=Amazon",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200

View File

@@ -16,7 +16,9 @@ class TestMarketplaceProductsAPI:
assert data["products"] == []
assert data["total"] == 0
def test_get_products_with_data(self, client, auth_headers, test_marketplace_product):
def test_get_products_with_data(
self, client, auth_headers, test_marketplace_product
):
"""Test getting products with data"""
response = client.get("/api/v1/marketplace/product", headers=auth_headers)
@@ -25,25 +27,39 @@ class TestMarketplaceProductsAPI:
assert len(data["products"]) >= 1
assert data["total"] >= 1
# Find our test product
test_product_found = any(p["marketplace_product_id"] == test_marketplace_product.marketplace_product_id for p in data["products"])
test_product_found = any(
p["marketplace_product_id"]
== test_marketplace_product.marketplace_product_id
for p in data["products"]
)
assert test_product_found
def test_get_products_with_filters(self, client, auth_headers, test_marketplace_product):
def test_get_products_with_filters(
self, client, auth_headers, test_marketplace_product
):
"""Test filtering products"""
# Test brand filter
response = client.get(f"/api/v1/marketplace/product?brand={test_marketplace_product.brand}", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?brand={test_marketplace_product.brand}",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
# Test marketplace filter
response = client.get(f"/api/v1/marketplace/product?marketplace={test_marketplace_product.marketplace}", headers=auth_headers)
response = client.get(
f"/api/v1/marketplace/product?marketplace={test_marketplace_product.marketplace}",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
# Test search
response = client.get("/api/v1/marketplace/product?search=Test", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?search=Test", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
@@ -71,7 +87,9 @@ class TestMarketplaceProductsAPI:
assert data["title"] == "New MarketplaceProduct"
assert data["marketplace"] == "Amazon"
def test_create_product_duplicate_id_returns_conflict(self, client, auth_headers, test_marketplace_product):
def test_create_product_duplicate_id_returns_conflict(
self, client, auth_headers, test_marketplace_product
):
"""Test creating product with duplicate ID returns MarketplaceProductAlreadyExistsException"""
product_data = {
"marketplace_product_id": test_marketplace_product.marketplace_product_id,
@@ -93,7 +111,10 @@ class TestMarketplaceProductsAPI:
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409
assert test_marketplace_product.marketplace_product_id in data["message"]
assert data["details"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
assert (
data["details"]["marketplace_product_id"]
== test_marketplace_product.marketplace_product_id
)
def test_create_product_missing_title_validation_error(self, client, auth_headers):
"""Test creating product without title returns ValidationException"""
@@ -114,7 +135,9 @@ class TestMarketplaceProductsAPI:
assert "MarketplaceProduct title is required" in data["message"]
assert data["details"]["field"] == "title"
def test_create_product_missing_product_id_validation_error(self, client, auth_headers):
def test_create_product_missing_product_id_validation_error(
self, client, auth_headers
):
"""Test creating product without marketplace_product_id returns ValidationException"""
product_data = {
"marketplace_product_id": "", # Empty product ID
@@ -191,20 +214,28 @@ class TestMarketplaceProductsAPI:
assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"]
def test_get_product_by_id_success(self, client, auth_headers, test_marketplace_product):
def test_get_product_by_id_success(
self, client, auth_headers, test_marketplace_product
):
"""Test getting specific product successfully"""
response = client.get(
f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}", headers=auth_headers
f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["product"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
assert (
data["product"]["marketplace_product_id"]
== test_marketplace_product.marketplace_product_id
)
assert data["product"]["title"] == test_marketplace_product.title
def test_get_nonexistent_product_returns_not_found(self, client, auth_headers):
"""Test getting nonexistent product returns MarketplaceProductNotFoundException"""
response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers
)
assert response.status_code == 404
data = response.json()
@@ -214,7 +245,9 @@ class TestMarketplaceProductsAPI:
assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_update_product_success(self, client, auth_headers, test_marketplace_product):
def test_update_product_success(
self, client, auth_headers, test_marketplace_product
):
"""Test updating product successfully"""
update_data = {"title": "Updated MarketplaceProduct Title", "price": "25.99"}
@@ -247,7 +280,9 @@ class TestMarketplaceProductsAPI:
assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_update_product_empty_title_validation_error(self, client, auth_headers, test_marketplace_product):
def test_update_product_empty_title_validation_error(
self, client, auth_headers, test_marketplace_product
):
"""Test updating product with empty title returns MarketplaceProductValidationException"""
update_data = {"title": ""}
@@ -264,7 +299,9 @@ class TestMarketplaceProductsAPI:
assert "MarketplaceProduct title cannot be empty" in data["message"]
assert data["details"]["field"] == "title"
def test_update_product_invalid_gtin_data_error(self, client, auth_headers, test_marketplace_product):
def test_update_product_invalid_gtin_data_error(
self, client, auth_headers, test_marketplace_product
):
"""Test updating product with invalid GTIN returns InvalidMarketplaceProductDataException"""
update_data = {"gtin": "invalid_gtin"}
@@ -281,7 +318,9 @@ class TestMarketplaceProductsAPI:
assert "Invalid GTIN format" in data["message"]
assert data["details"]["field"] == "gtin"
def test_update_product_invalid_price_data_error(self, client, auth_headers, test_marketplace_product):
def test_update_product_invalid_price_data_error(
self, client, auth_headers, test_marketplace_product
):
"""Test updating product with invalid price returns InvalidMarketplaceProductDataException"""
update_data = {"price": "invalid_price"}
@@ -298,10 +337,13 @@ class TestMarketplaceProductsAPI:
assert "Invalid price format" in data["message"]
assert data["details"]["field"] == "price"
def test_delete_product_success(self, client, auth_headers, test_marketplace_product):
def test_delete_product_success(
self, client, auth_headers, test_marketplace_product
):
"""Test deleting product successfully"""
response = client.delete(
f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}", headers=auth_headers
f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers,
)
assert response.status_code == 200
@@ -309,7 +351,9 @@ class TestMarketplaceProductsAPI:
def test_delete_nonexistent_product_returns_not_found(self, client, auth_headers):
"""Test deleting nonexistent product returns MarketplaceProductNotFoundException"""
response = client.delete("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
response = client.delete(
"/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers
)
assert response.status_code == 404
data = response.json()
@@ -331,7 +375,9 @@ class TestMarketplaceProductsAPI:
def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all exceptions follow the consistent WizamartException structure"""
# Test with a known error case
response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers
)
assert response.status_code == 404
data = response.json()

View File

@@ -4,6 +4,7 @@ import pytest
from models.database.marketplace_product import MarketplaceProduct
from models.database.vendor import Vendor
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.database
@@ -13,6 +14,7 @@ class TestPagination:
def test_product_pagination_success(self, client, auth_headers, db):
"""Test pagination for product listing successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create multiple products
@@ -29,7 +31,9 @@ class TestPagination:
db.commit()
# Test first page
response = client.get("/api/v1/marketplace/product?limit=10&skip=0", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=10&skip=0", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["products"]) == 10
@@ -38,21 +42,29 @@ class TestPagination:
assert data["limit"] == 10
# Test second page
response = client.get("/api/v1/marketplace/product?limit=10&skip=10", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=10&skip=10", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["products"]) == 10
assert data["skip"] == 10
# Test last page (should have remaining products)
response = client.get("/api/v1/marketplace/product?limit=10&skip=20", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=10&skip=20", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["products"]) >= 5 # At least 5 remaining from our test set
def test_pagination_boundary_negative_skip_validation_error(self, client, auth_headers):
def test_pagination_boundary_negative_skip_validation_error(
self, client, auth_headers
):
"""Test negative skip parameter returns ValidationException"""
response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=-1", headers=auth_headers
)
assert response.status_code == 422
data = response.json()
@@ -61,9 +73,13 @@ class TestPagination:
assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"]
def test_pagination_boundary_zero_limit_validation_error(self, client, auth_headers):
def test_pagination_boundary_zero_limit_validation_error(
self, client, auth_headers
):
"""Test zero limit parameter returns ValidationException"""
response = client.get("/api/v1/marketplace/product?limit=0", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=0", headers=auth_headers
)
assert response.status_code == 422
data = response.json()
@@ -71,9 +87,13 @@ class TestPagination:
assert data["status_code"] == 422
assert "Request validation failed" in data["message"]
def test_pagination_boundary_excessive_limit_validation_error(self, client, auth_headers):
def test_pagination_boundary_excessive_limit_validation_error(
self, client, auth_headers
):
"""Test excessive limit parameter returns ValidationException"""
response = client.get("/api/v1/marketplace/product?limit=10000", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=10000", headers=auth_headers
)
assert response.status_code == 422
data = response.json()
@@ -84,7 +104,9 @@ class TestPagination:
def test_pagination_beyond_available_records(self, client, auth_headers, db):
"""Test pagination beyond available records returns empty results"""
# Test skip beyond available records
response = client.get("/api/v1/marketplace/product?skip=10000&limit=10", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=10000&limit=10", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
@@ -96,6 +118,7 @@ class TestPagination:
def test_pagination_with_filters(self, client, auth_headers, db):
"""Test pagination combined with filtering"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create products with same brand for filtering
@@ -115,7 +138,7 @@ class TestPagination:
# Test first page with filter
response = client.get(
"/api/v1/marketplace/product?brand=FilterBrand&limit=5&skip=0",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
@@ -124,14 +147,18 @@ class TestPagination:
assert data["total"] >= 15 # At least our test products
# Verify all products have the filtered brand
test_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
test_products = [
p
for p in data["products"]
if p["marketplace_product_id"].endswith(unique_suffix)
]
for product in test_products:
assert product["brand"] == "FilterBrand"
# Test second page with same filter
response = client.get(
"/api/v1/marketplace/product?brand=FilterBrand&limit=5&skip=5",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
@@ -152,6 +179,7 @@ class TestPagination:
def test_pagination_consistency(self, client, auth_headers, db):
"""Test pagination consistency across multiple requests"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create products with predictable ordering
@@ -168,14 +196,22 @@ class TestPagination:
db.commit()
# Get first page
response1 = client.get("/api/v1/marketplace/product?limit=5&skip=0", headers=auth_headers)
response1 = client.get(
"/api/v1/marketplace/product?limit=5&skip=0", headers=auth_headers
)
assert response1.status_code == 200
first_page_ids = [p["marketplace_product_id"] for p in response1.json()["products"]]
first_page_ids = [
p["marketplace_product_id"] for p in response1.json()["products"]
]
# Get second page
response2 = client.get("/api/v1/marketplace/product?limit=5&skip=5", headers=auth_headers)
response2 = client.get(
"/api/v1/marketplace/product?limit=5&skip=5", headers=auth_headers
)
assert response2.status_code == 200
second_page_ids = [p["marketplace_product_id"] for p in response2.json()["products"]]
second_page_ids = [
p["marketplace_product_id"] for p in response2.json()["products"]
]
# Verify no overlap between pages
overlap = set(first_page_ids) & set(second_page_ids)
@@ -184,11 +220,13 @@ class TestPagination:
def test_vendor_pagination_success(self, client, admin_headers, db, test_user):
"""Test pagination for vendor listing successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create multiple vendors for pagination testing
from models.database.vendor import Vendor
vendors =[]
vendors = []
for i in range(15):
vendor = Vendor(
vendor_code=f"PAGEVENDOR{i:03d}_{unique_suffix}",
@@ -202,9 +240,7 @@ class TestPagination:
db.commit()
# Test first page (assuming admin endpoint exists)
response = client.get(
"/api/v1/vendor?limit=5&skip=0", headers=admin_headers
)
response = client.get("/api/v1/vendor?limit=5&skip=0", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert len(data["vendors"]) == 5
@@ -215,10 +251,12 @@ class TestPagination:
def test_inventory_pagination_success(self, client, auth_headers, db):
"""Test pagination for inventory listing successfully"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Create multiple inventory entries
from models.database.inventory import Inventory
inventory_entries = []
for i in range(20):
inventory = Inventory(
@@ -249,7 +287,9 @@ class TestPagination:
import time
start_time = time.time()
response = client.get("/api/v1/marketplace/product?skip=1000&limit=10", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=1000&limit=10", headers=auth_headers
)
end_time = time.time()
assert response.status_code == 200
@@ -262,19 +302,25 @@ class TestPagination:
def test_pagination_with_invalid_parameters_types(self, client, auth_headers):
"""Test pagination with invalid parameter types returns ValidationException"""
# Test non-numeric skip
response = client.get("/api/v1/marketplace/product?skip=invalid", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=invalid", headers=auth_headers
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test non-numeric limit
response = client.get("/api/v1/marketplace/product?limit=invalid", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=invalid", headers=auth_headers
)
assert response.status_code == 422
data = response.json()
assert data["error_code"] == "VALIDATION_ERROR"
# Test float values (should be converted or rejected)
response = client.get("/api/v1/marketplace/product?skip=10.5&limit=5.5", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=10.5&limit=5.5", headers=auth_headers
)
assert response.status_code in [200, 422] # Depends on implementation
def test_empty_dataset_pagination(self, client, auth_headers):
@@ -282,7 +328,7 @@ class TestPagination:
# Use a filter that should return no results
response = client.get(
"/api/v1/marketplace/product?brand=NonexistentBrand999&limit=10&skip=0",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
@@ -294,7 +340,9 @@ class TestPagination:
def test_exception_structure_in_pagination_errors(self, client, auth_headers):
"""Test that pagination validation errors follow consistent exception structure"""
response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=-1", headers=auth_headers
)
assert response.status_code == 422
data = response.json()

View File

@@ -1,6 +1,7 @@
# tests/integration/api/v1/test_stats_endpoints.py
import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.stats
@@ -18,7 +19,9 @@ class TestStatsAPI:
assert "unique_vendors" in data
assert data["total_products"] >= 1
def test_get_marketplace_stats(self, client, auth_headers, test_marketplace_product):
def test_get_marketplace_stats(
self, client, auth_headers, test_marketplace_product
):
"""Test getting marketplace statistics"""
response = client.get("/api/v1/stats/marketplace", headers=auth_headers)

View File

@@ -23,7 +23,9 @@ class TestVendorsAPI:
assert data["name"] == "New Vendor"
assert data["is_active"] is True
def test_create_vendor_duplicate_code_returns_conflict(self, client, auth_headers, test_vendor):
def test_create_vendor_duplicate_code_returns_conflict(
self, client, auth_headers, test_vendor
):
"""Test creating vendor with duplicate code returns VendorAlreadyExistsException"""
vendor_data = {
"vendor_code": test_vendor.vendor_code,
@@ -40,7 +42,9 @@ class TestVendorsAPI:
assert test_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == test_vendor.vendor_code
def test_create_vendor_missing_vendor_code_validation_error(self, client, auth_headers):
def test_create_vendor_missing_vendor_code_validation_error(
self, client, auth_headers
):
"""Test creating vendor without vendor_code returns ValidationException"""
vendor_data = {
"name": "Vendor without Code",
@@ -56,7 +60,9 @@ class TestVendorsAPI:
assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"]
def test_create_vendor_empty_vendor_name_validation_error(self, client, auth_headers):
def test_create_vendor_empty_vendor_name_validation_error(
self, client, auth_headers
):
"""Test creating vendor with empty name returns VendorValidationException"""
vendor_data = {
"vendor_code": "EMPTYNAME",
@@ -73,7 +79,9 @@ class TestVendorsAPI:
assert "Vendor name is required" in data["message"]
assert data["details"]["field"] == "name"
def test_create_vendor_max_vendors_reached_business_logic_error(self, client, auth_headers, db, test_user):
def test_create_vendor_max_vendors_reached_business_logic_error(
self, client, auth_headers, db, test_user
):
"""Test creating vendor when max vendors reached returns MaxVendorsReachedException"""
# This test would require creating the maximum allowed vendors first
# The exact implementation depends on your business rules
@@ -94,8 +102,10 @@ class TestVendorsAPI:
assert data["total"] >= 1
assert len(data["vendors"]) >= 1
# Find our test vendor
test_vendor_found = any(s["vendor_code"] == test_vendor.vendor_code for s in data["vendors"])
# Find our test vendor
test_vendor_found = any(
s["vendor_code"] == test_vendor.vendor_code for s in data["vendors"]
)
assert test_vendor_found
def test_get_vendors_with_filters(self, client, auth_headers, test_vendor):
@@ -105,7 +115,7 @@ class TestVendorsAPI:
assert response.status_code == 200
data = response.json()
for vendor in data["vendors"]:
assert vendor ["is_active"] is True
assert vendor["is_active"] is True
# Test verified_only filter
response = client.get("/api/v1/vendor?verified_only=true", headers=auth_headers)
@@ -135,7 +145,9 @@ class TestVendorsAPI:
assert data["details"]["resource_type"] == "Vendor"
assert data["details"]["identifier"] == "NONEXISTENT"
def test_get_vendor_unauthorized_access(self, client, auth_headers, test_vendor, other_user, db):
def test_get_vendor_unauthorized_access(
self, client, auth_headers, test_vendor, other_user, db
):
"""Test accessing vendor owned by another user returns UnauthorizedVendorAccessException"""
# Change vendor owner to other user AND make it unverified/inactive
# so that non-owner users cannot access it
@@ -154,7 +166,9 @@ class TestVendorsAPI:
assert test_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == test_vendor.vendor_code
def test_get_vendor_unauthorized_access_with_inactive_vendor(self, client, auth_headers, inactive_vendor):
def test_get_vendor_unauthorized_access_with_inactive_vendor(
self, client, auth_headers, inactive_vendor
):
"""Test accessing inactive vendor owned by another user returns UnauthorizedVendorAccessException"""
# inactive_vendor fixture already creates an unverified, inactive vendor owned by other_user
response = client.get(
@@ -168,7 +182,9 @@ class TestVendorsAPI:
assert inactive_vendor.vendor_code in data["message"]
assert data["details"]["vendor_code"] == inactive_vendor.vendor_code
def test_get_vendor_public_access_allowed(self, client, auth_headers, verified_vendor):
def test_get_vendor_public_access_allowed(
self, client, auth_headers, verified_vendor
):
"""Test accessing verified vendor owned by another user is allowed (public access)"""
# verified_vendor fixture creates a verified, active vendor owned by other_user
# This should allow public access per your business logic
@@ -181,7 +197,9 @@ class TestVendorsAPI:
assert data["vendor_code"] == verified_vendor.vendor_code
assert data["name"] == verified_vendor.name
def test_add_product_to_vendor_success(self, client, auth_headers, test_vendor, unique_product):
def test_add_product_to_vendor_success(
self, client, auth_headers, test_vendor, unique_product
):
"""Test adding product to vendor successfully"""
product_data = {
"marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
@@ -193,7 +211,7 @@ class TestVendorsAPI:
response = client.post(
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
json=product_data,
)
assert response.status_code == 200
@@ -207,10 +225,15 @@ class TestVendorsAPI:
# MarketplaceProduct details are nested in the 'marketplace_product' field
assert "marketplace_product" in data
assert data["marketplace_product"]["marketplace_product_id"] == unique_product.marketplace_product_id
assert (
data["marketplace_product"]["marketplace_product_id"]
== unique_product.marketplace_product_id
)
assert data["marketplace_product"]["id"] == unique_product.id
def test_add_product_to_vendor_already_exists_conflict(self, client, auth_headers, test_vendor, test_product):
def test_add_product_to_vendor_already_exists_conflict(
self, client, auth_headers, test_vendor, test_product
):
"""Test adding product that already exists in vendor returns ProductAlreadyExistsException"""
# test_product fixture already creates a relationship, get the marketplace_product_id string
existing_product = test_product.marketplace_product
@@ -223,7 +246,7 @@ class TestVendorsAPI:
response = client.post(
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
json=product_data,
)
assert response.status_code == 409
@@ -233,7 +256,9 @@ class TestVendorsAPI:
assert test_vendor.vendor_code in data["message"]
assert existing_product.marketplace_product_id in data["message"]
def test_add_nonexistent_product_to_vendor_not_found(self, client, auth_headers, test_vendor):
def test_add_nonexistent_product_to_vendor_not_found(
self, client, auth_headers, test_vendor
):
"""Test adding nonexistent product to vendor returns MarketplaceProductNotFoundException"""
product_data = {
"marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
@@ -243,7 +268,7 @@ class TestVendorsAPI:
response = client.post(
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
json=product_data,
)
assert response.status_code == 404
@@ -252,11 +277,12 @@ class TestVendorsAPI:
assert data["status_code"] == 404
assert "NONEXISTENT_PRODUCT" in data["message"]
def test_get_products_success(self, client, auth_headers, test_vendor, test_product):
def test_get_products_success(
self, client, auth_headers, test_vendor, test_product
):
"""Test getting vendor products successfully"""
response = client.get(
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
headers=auth_headers
f"/api/v1/vendor/{test_vendor.vendor_code}/products", headers=auth_headers
)
assert response.status_code == 200
@@ -271,22 +297,21 @@ class TestVendorsAPI:
# Test active_only filter
response = client.get(
f"/api/v1/vendor/{test_vendor.vendor_code}/products?active_only=true",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
# Test featured_only filter
response = client.get(
f"/api/v1/vendor/{test_vendor.vendor_code}/products?featured_only=true",
headers=auth_headers
headers=auth_headers,
)
assert response.status_code == 200
def test_get_products_from_nonexistent_vendor_not_found(self, client, auth_headers):
"""Test getting products from nonexistent vendor returns VendorNotFoundException"""
response = client.get(
"/api/v1/vendor/NONEXISTENT/products",
headers=auth_headers
"/api/v1/vendor/NONEXISTENT/products", headers=auth_headers
)
assert response.status_code == 404
@@ -295,7 +320,9 @@ class TestVendorsAPI:
assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"]
def test_vendor_not_active_business_logic_error(self, client, auth_headers, test_vendor, db):
def test_vendor_not_active_business_logic_error(
self, client, auth_headers, test_vendor, db
):
"""Test accessing inactive vendor returns VendorNotActiveException (if enforced)"""
# Set vendor to inactive
test_vendor.is_active = False
@@ -313,7 +340,9 @@ class TestVendorsAPI:
assert data["status_code"] == 400
assert test_vendor.vendor_code in data["message"]
def test_vendor_not_verified_business_logic_error(self, client, auth_headers, test_vendor, db):
def test_vendor_not_verified_business_logic_error(
self, client, auth_headers, test_vendor, db
):
"""Test operations requiring verification returns VendorNotVerifiedException (if enforced)"""
# Set vendor to unverified
test_vendor.is_verified = False
@@ -328,7 +357,7 @@ class TestVendorsAPI:
response = client.post(
f"/api/v1/vendor/{test_vendor.vendor_code}/products",
headers=auth_headers,
json=product_data
json=product_data,
)
# If your service requires verification for adding products

View File

@@ -10,8 +10,9 @@ These tests verify that:
5. Vendor context middleware works correctly with API authentication
"""
import pytest
from datetime import datetime, timedelta, timezone
import pytest
from jose import jwt
@@ -26,7 +27,9 @@ class TestVendorAPIAuthentication:
# Authentication Tests - /api/v1/vendor/auth/me
# ========================================================================
def test_vendor_auth_me_success(self, client, vendor_user_headers, test_vendor_user):
def test_vendor_auth_me_success(
self, client, vendor_user_headers, test_vendor_user
):
"""Test /auth/me endpoint with valid vendor user token"""
response = client.get("/api/v1/vendor/auth/me", headers=vendor_user_headers)
@@ -50,7 +53,7 @@ class TestVendorAPIAuthentication:
"""Test /auth/me endpoint with invalid token format"""
response = client.get(
"/api/v1/vendor/auth/me",
headers={"Authorization": "Bearer invalid_token_here"}
headers={"Authorization": "Bearer invalid_token_here"},
)
assert response.status_code == 401
@@ -66,7 +69,9 @@ class TestVendorAPIAuthentication:
assert data["error_code"] == "FORBIDDEN"
assert "Admin users cannot access vendor API" in data["message"]
def test_vendor_auth_me_with_regular_user_token(self, client, auth_headers, test_user):
def test_vendor_auth_me_with_regular_user_token(
self, client, auth_headers, test_user
):
"""Test /auth/me endpoint rejects regular users"""
response = client.get("/api/v1/vendor/auth/me", headers=auth_headers)
@@ -88,14 +93,12 @@ class TestVendorAPIAuthentication:
}
expired_token = jwt.encode(
expired_payload,
auth_manager.secret_key,
algorithm=auth_manager.algorithm
expired_payload, auth_manager.secret_key, algorithm=auth_manager.algorithm
)
response = client.get(
"/api/v1/vendor/auth/me",
headers={"Authorization": f"Bearer {expired_token}"}
headers={"Authorization": f"Bearer {expired_token}"},
)
assert response.status_code == 401
@@ -111,8 +114,7 @@ class TestVendorAPIAuthentication:
):
"""Test dashboard stats with valid vendor authentication"""
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
assert response.status_code == 200
@@ -131,10 +133,7 @@ class TestVendorAPIAuthentication:
def test_vendor_dashboard_stats_with_admin(self, client, admin_headers):
"""Test dashboard stats rejects admin users"""
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=admin_headers
)
response = client.get("/api/v1/vendor/dashboard/stats", headers=admin_headers)
assert response.status_code == 403
data = response.json()
@@ -145,10 +144,7 @@ class TestVendorAPIAuthentication:
# Login to get session cookie
login_response = client.post(
"/api/v1/vendor/auth/login",
json={
"username": test_vendor_user.username,
"password": "vendorpass123"
}
json={"username": test_vendor_user.username, "password": "vendorpass123"},
)
assert login_response.status_code == 200
@@ -169,10 +165,7 @@ class TestVendorAPIAuthentication:
# Get a valid session by logging in
login_response = client.post(
"/api/v1/vendor/auth/login",
json={
"username": test_vendor_user.username,
"password": "vendorpass123"
}
json={"username": test_vendor_user.username, "password": "vendorpass123"},
)
assert login_response.status_code == 200
@@ -191,8 +184,9 @@ class TestVendorAPIAuthentication:
response = client.get(endpoint)
# All should fail with 401 (header required)
assert response.status_code == 401, \
f"Endpoint {endpoint} should reject cookie-only auth"
assert (
response.status_code == 401
), f"Endpoint {endpoint} should reject cookie-only auth"
# ========================================================================
# Role-Based Access Control Tests
@@ -211,13 +205,15 @@ class TestVendorAPIAuthentication:
for endpoint in endpoints:
# Test with regular user token
response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 403, \
f"Endpoint {endpoint} should reject regular users"
assert (
response.status_code == 403
), f"Endpoint {endpoint} should reject regular users"
# Test with admin token
response = client.get(endpoint, headers=admin_headers)
assert response.status_code == 403, \
f"Endpoint {endpoint} should reject admin users"
assert (
response.status_code == 403
), f"Endpoint {endpoint} should reject admin users"
def test_vendor_api_accepts_only_vendor_role(
self, client, vendor_user_headers, test_vendor_user
@@ -229,8 +225,10 @@ class TestVendorAPIAuthentication:
for endpoint in endpoints:
response = client.get(endpoint, headers=vendor_user_headers)
assert response.status_code in [200, 404], \
f"Endpoint {endpoint} should accept vendor users (got {response.status_code})"
assert response.status_code in [
200,
404,
], f"Endpoint {endpoint} should accept vendor users (got {response.status_code})"
# ========================================================================
# Token Validation Tests
@@ -248,8 +246,9 @@ class TestVendorAPIAuthentication:
for headers in malformed_headers:
response = client.get("/api/v1/vendor/auth/me", headers=headers)
assert response.status_code == 401, \
f"Should reject malformed header: {headers}"
assert (
response.status_code == 401
), f"Should reject malformed header: {headers}"
def test_token_with_missing_claims(self, client, auth_manager):
"""Test token missing required claims"""
@@ -261,14 +260,12 @@ class TestVendorAPIAuthentication:
}
invalid_token = jwt.encode(
invalid_payload,
auth_manager.secret_key,
algorithm=auth_manager.algorithm
invalid_payload, auth_manager.secret_key, algorithm=auth_manager.algorithm
)
response = client.get(
"/api/v1/vendor/auth/me",
headers={"Authorization": f"Bearer {invalid_token}"}
headers={"Authorization": f"Bearer {invalid_token}"},
)
assert response.status_code == 401
@@ -298,9 +295,7 @@ class TestVendorAPIAuthentication:
db.add(test_vendor_user)
db.commit()
def test_concurrent_requests_with_same_token(
self, client, vendor_user_headers
):
def test_concurrent_requests_with_same_token(self, client, vendor_user_headers):
"""Test that the same token can be used for multiple concurrent requests"""
# Make multiple requests with the same token
responses = []
@@ -314,10 +309,7 @@ class TestVendorAPIAuthentication:
def test_vendor_api_with_empty_authorization_header(self, client):
"""Test vendor API with empty Authorization header value"""
response = client.get(
"/api/v1/vendor/auth/me",
headers={"Authorization": ""}
)
response = client.get("/api/v1/vendor/auth/me", headers={"Authorization": ""})
assert response.status_code == 401
@@ -328,17 +320,12 @@ class TestVendorAPIAuthentication:
class TestVendorAPIConsistency:
"""Test that all vendor API endpoints use consistent authentication"""
def test_all_vendor_endpoints_require_header_auth(
self, client, test_vendor_user
):
def test_all_vendor_endpoints_require_header_auth(self, client, test_vendor_user):
"""Verify all vendor API endpoints require Authorization header"""
# Login to establish session
client.post(
"/api/v1/vendor/auth/login",
json={
"username": test_vendor_user.username,
"password": "vendorpass123"
}
json={"username": test_vendor_user.username, "password": "vendorpass123"},
)
# All vendor API endpoints (excluding public endpoints like /info)
@@ -361,8 +348,9 @@ class TestVendorAPIConsistency:
response = client.post(endpoint, json={})
# All should reject cookie-only auth with 401
assert response.status_code == 401, \
f"Endpoint {endpoint} should require Authorization header (got {response.status_code})"
assert (
response.status_code == 401
), f"Endpoint {endpoint} should require Authorization header (got {response.status_code})"
def test_vendor_endpoints_accept_vendor_token(
self, client, vendor_user_headers, test_vendor_with_vendor_user
@@ -380,5 +368,7 @@ class TestVendorAPIConsistency:
response = client.get(endpoint, headers=vendor_user_headers)
# Should not be authentication/authorization errors
assert response.status_code not in [401, 403], \
f"Endpoint {endpoint} should accept vendor token (got {response.status_code}: {response.text})"
assert response.status_code not in [
401,
403,
], f"Endpoint {endpoint} should accept vendor token (got {response.status_code}: {response.text})"

View File

@@ -23,8 +23,7 @@ class TestVendorDashboardAPI:
):
"""Test dashboard stats returns correct data structure"""
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
assert response.status_code == 200
@@ -66,9 +65,9 @@ class TestVendorDashboardAPI:
self, client, db, test_vendor_user, auth_manager
):
"""Test that dashboard stats only show data for the authenticated vendor"""
from models.database.vendor import Vendor, VendorUser
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.product import Product
from models.database.vendor import Vendor, VendorUser
# Create two separate vendors with different data
vendor1 = Vendor(
@@ -118,10 +117,7 @@ class TestVendorDashboardAPI:
vendor1_headers = {"Authorization": f"Bearer {token_data['access_token']}"}
# Get stats for vendor1
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor1_headers
)
response = client.get("/api/v1/vendor/dashboard/stats", headers=vendor1_headers)
assert response.status_code == 200
data = response.json()
@@ -130,9 +126,7 @@ class TestVendorDashboardAPI:
assert data["vendor"]["id"] == vendor1.id
assert data["products"]["total"] == 3
def test_dashboard_stats_without_vendor_association(
self, client, db, auth_manager
):
def test_dashboard_stats_without_vendor_association(self, client, db, auth_manager):
"""Test dashboard stats for user not associated with any vendor"""
from models.database.user import User
@@ -206,8 +200,7 @@ class TestVendorDashboardAPI:
):
"""Test dashboard stats for vendor with no data"""
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
assert response.status_code == 200
@@ -224,8 +217,8 @@ class TestVendorDashboardAPI:
self, client, db, vendor_user_headers, test_vendor_with_vendor_user
):
"""Test dashboard stats accuracy with actual products"""
from models.database.product import Product
from models.database.marketplace_product import MarketplaceProduct
from models.database.product import Product
# Create marketplace products
mp = MarketplaceProduct(
@@ -249,8 +242,7 @@ class TestVendorDashboardAPI:
# Get stats
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
assert response.status_code == 200
@@ -267,8 +259,7 @@ class TestVendorDashboardAPI:
start_time = time.time()
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
end_time = time.time()
@@ -284,8 +275,7 @@ class TestVendorDashboardAPI:
responses = []
for _ in range(3):
response = client.get(
"/api/v1/vendor/dashboard/stats",
headers=vendor_user_headers
"/api/v1/vendor/dashboard/stats", headers=vendor_user_headers
)
responses.append(response)

View File

@@ -3,6 +3,7 @@
Fixtures specific to middleware integration tests.
"""
import pytest
from models.database.vendor import Vendor
from models.database.vendor_domain import VendorDomain
from models.database.vendor_theme import VendorTheme
@@ -12,10 +13,7 @@ from models.database.vendor_theme import VendorTheme
def vendor_with_subdomain(db):
"""Create a vendor with subdomain for testing."""
vendor = Vendor(
name="Test Vendor",
code="testvendor",
subdomain="testvendor",
is_active=True
name="Test Vendor", code="testvendor", subdomain="testvendor", is_active=True
)
db.add(vendor)
db.commit()
@@ -30,7 +28,7 @@ def vendor_with_custom_domain(db):
name="Custom Domain Vendor",
code="customvendor",
subdomain="customvendor",
is_active=True
is_active=True,
)
db.add(vendor)
db.commit()
@@ -38,10 +36,7 @@ def vendor_with_custom_domain(db):
# Add custom domain
domain = VendorDomain(
vendor_id=vendor.id,
domain="customdomain.com",
is_active=True,
is_primary=True
vendor_id=vendor.id, domain="customdomain.com", is_active=True, is_primary=True
)
db.add(domain)
db.commit()
@@ -56,7 +51,7 @@ def vendor_with_theme(db):
name="Themed Vendor",
code="themedvendor",
subdomain="themedvendor",
is_active=True
is_active=True,
)
db.add(vendor)
db.commit()
@@ -69,7 +64,7 @@ def vendor_with_theme(db):
secondary_color="#33FF57",
logo_url="/static/vendors/themedvendor/logo.png",
favicon_url="/static/vendors/themedvendor/favicon.ico",
custom_css="body { background: #FF5733; }"
custom_css="body { background: #FF5733; }",
)
db.add(theme)
db.commit()
@@ -81,10 +76,7 @@ def vendor_with_theme(db):
def inactive_vendor(db):
"""Create an inactive vendor for testing."""
vendor = Vendor(
name="Inactive Vendor",
code="inactive",
subdomain="inactive",
is_active=False
name="Inactive Vendor", code="inactive", subdomain="inactive", is_active=False
)
db.add(vendor)
db.commit()

View File

@@ -5,8 +5,10 @@ Integration tests for request context detection end-to-end flow.
These tests verify that context type (API, ADMIN, VENDOR_DASHBOARD, SHOP, FALLBACK)
is correctly detected through real HTTP requests.
"""
import pytest
from unittest.mock import patch
import pytest
from middleware.context import RequestContext
@@ -23,13 +25,22 @@ class TestContextDetectionFlow:
def test_api_path_detected_as_api_context(self, client):
"""Test that /api/* paths are detected as API context."""
from fastapi import Request
from main import app
@app.get("/api/test-api-context")
async def test_api(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"context_enum": request.state.context_type.name if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"context_enum": (
request.state.context_type.name
if hasattr(request.state, "context_type")
else None
),
}
response = client.get("/api/test-api-context")
@@ -42,12 +53,17 @@ class TestContextDetectionFlow:
def test_nested_api_path_detected_as_api_context(self, client):
"""Test that nested /api/ paths are detected as API context."""
from fastapi import Request
from main import app
@app.get("/api/v1/vendor/products")
async def test_nested_api(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
response = client.get("/api/v1/vendor/products")
@@ -63,13 +79,22 @@ class TestContextDetectionFlow:
def test_admin_path_detected_as_admin_context(self, client):
"""Test that /admin/* paths are detected as ADMIN context."""
from fastapi import Request
from main import app
@app.get("/admin/test-admin-context")
async def test_admin(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"context_enum": request.state.context_type.name if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"context_enum": (
request.state.context_type.name
if hasattr(request.state, "context_type")
else None
),
}
response = client.get("/admin/test-admin-context")
@@ -82,19 +107,23 @@ class TestContextDetectionFlow:
def test_admin_subdomain_detected_as_admin_context(self, client):
"""Test that admin.* subdomain is detected as ADMIN context."""
from fastapi import Request
from main import app
@app.get("/test-admin-subdomain-context")
async def test_admin_subdomain(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-admin-subdomain-context",
headers={"host": "admin.platform.com"}
"/test-admin-subdomain-context", headers={"host": "admin.platform.com"}
)
assert response.status_code == 200
@@ -104,12 +133,17 @@ class TestContextDetectionFlow:
def test_nested_admin_path_detected_as_admin_context(self, client):
"""Test that nested /admin/ paths are detected as ADMIN context."""
from fastapi import Request
from main import app
@app.get("/admin/vendors/123/edit")
async def test_nested_admin(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
response = client.get("/admin/vendors/123/edit")
@@ -125,21 +159,31 @@ class TestContextDetectionFlow:
def test_vendor_dashboard_path_detected(self, client, vendor_with_subdomain):
"""Test that /vendor/* paths are detected as VENDOR_DASHBOARD context."""
from fastapi import Request
from main import app
@app.get("/vendor/test-vendor-dashboard")
async def test_vendor_dashboard(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"context_enum": request.state.context_type.name if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"context_enum": (
request.state.context_type.name
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/vendor/test-vendor-dashboard",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -151,19 +195,24 @@ class TestContextDetectionFlow:
def test_nested_vendor_dashboard_path_detected(self, client, vendor_with_subdomain):
"""Test that nested /vendor/ paths are detected as VENDOR_DASHBOARD context."""
from fastapi import Request
from main import app
@app.get("/vendor/products/123/edit")
async def test_nested_vendor(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/vendor/products/123/edit",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -174,24 +223,36 @@ class TestContextDetectionFlow:
# Shop Context Detection Tests
# ========================================================================
def test_shop_path_with_vendor_detected_as_shop(self, client, vendor_with_subdomain):
def test_shop_path_with_vendor_detected_as_shop(
self, client, vendor_with_subdomain
):
"""Test that /shop/* paths with vendor are detected as SHOP context."""
from fastapi import Request
from main import app
@app.get("/shop/test-shop-context")
async def test_shop(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"context_enum": request.state.context_type.name if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"context_enum": (
request.state.context_type.name
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/shop/test-shop-context",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -200,23 +261,31 @@ class TestContextDetectionFlow:
assert data["context_enum"] == "SHOP"
assert data["has_vendor"] is True
def test_root_path_with_vendor_detected_as_shop(self, client, vendor_with_subdomain):
def test_root_path_with_vendor_detected_as_shop(
self, client, vendor_with_subdomain
):
"""Test that root path with vendor is detected as SHOP context."""
from fastapi import Request
from main import app
@app.get("/test-root-shop")
async def test_root_shop(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-root-shop",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -228,21 +297,27 @@ class TestContextDetectionFlow:
def test_custom_domain_shop_detected(self, client, vendor_with_custom_domain):
"""Test that custom domain shop is detected as SHOP context."""
from fastapi import Request
from main import app
@app.get("/products")
async def test_custom_domain_shop(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/products",
headers={"host": "customdomain.com"}
)
response = client.get("/products", headers={"host": "customdomain.com"})
assert response.status_code == 200
data = response.json()
@@ -256,21 +331,30 @@ class TestContextDetectionFlow:
def test_unknown_path_without_vendor_fallback_context(self, client):
"""Test that unknown paths without vendor get FALLBACK context."""
from fastapi import Request
from main import app
@app.get("/test-fallback-context")
async def test_fallback(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"context_enum": request.state.context_type.name if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"context_enum": (
request.state.context_type.name
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-fallback-context",
headers={"host": "platform.com"}
"/test-fallback-context", headers={"host": "platform.com"}
)
assert response.status_code == 200
@@ -286,20 +370,26 @@ class TestContextDetectionFlow:
def test_api_path_overrides_vendor_context(self, client, vendor_with_subdomain):
"""Test that /api/* path sets API context even with vendor."""
from fastapi import Request
from main import app
@app.get("/api/test-api-priority")
async def test_api_priority(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/api/test-api-priority",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -312,20 +402,26 @@ class TestContextDetectionFlow:
def test_admin_path_overrides_vendor_context(self, client, vendor_with_subdomain):
"""Test that /admin/* path sets ADMIN context even with vendor."""
from fastapi import Request
from main import app
@app.get("/admin/test-admin-priority")
async def test_admin_priority(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/admin/test-admin-priority",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -333,22 +429,29 @@ class TestContextDetectionFlow:
# Admin path should override vendor context
assert data["context_type"] == "admin"
def test_vendor_dashboard_overrides_shop_context(self, client, vendor_with_subdomain):
def test_vendor_dashboard_overrides_shop_context(
self, client, vendor_with_subdomain
):
"""Test that /vendor/* path sets VENDOR_DASHBOARD, not SHOP."""
from fastapi import Request
from main import app
@app.get("/vendor/test-priority")
async def test_vendor_priority(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/vendor/test-priority",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -363,21 +466,30 @@ class TestContextDetectionFlow:
def test_context_uses_clean_path_for_detection(self, client, vendor_with_subdomain):
"""Test that context detection uses clean_path, not original path."""
from fastapi import Request
from main import app
@app.get("/vendors/{vendor_code}/shop/products")
async def test_clean_path_context(vendor_code: str, request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"clean_path": request.state.clean_path if hasattr(request.state, 'clean_path') else None,
"original_path": request.url.path
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"clean_path": (
request.state.clean_path
if hasattr(request.state, "clean_path")
else None
),
"original_path": request.url.path,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
f"/vendors/{vendor_with_subdomain.code}/shop/products",
headers={"host": "localhost:8000"}
headers={"host": "localhost:8000"},
)
assert response.status_code == 200
@@ -394,15 +506,20 @@ class TestContextDetectionFlow:
def test_context_type_is_enum_instance(self, client):
"""Test that context_type is a RequestContext enum instance."""
from fastapi import Request
from main import app
@app.get("/api/test-enum")
async def test_enum(request: Request):
context = request.state.context_type if hasattr(request.state, 'context_type') else None
context = (
request.state.context_type
if hasattr(request.state, "context_type")
else None
)
return {
"is_enum": isinstance(context, RequestContext) if context else False,
"enum_name": context.name if context else None,
"enum_value": context.value if context else None
"enum_value": context.value if context else None,
}
response = client.get("/api/test-enum")
@@ -417,23 +534,30 @@ class TestContextDetectionFlow:
# Edge Cases
# ========================================================================
def test_empty_path_with_vendor_detected_as_shop(self, client, vendor_with_subdomain):
def test_empty_path_with_vendor_detected_as_shop(
self, client, vendor_with_subdomain
):
"""Test that empty/root path with vendor is detected as SHOP."""
from fastapi import Request
from main import app
@app.get("/")
async def test_root(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
"/", headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
)
assert response.status_code in [200, 404] # Might not have root handler
@@ -445,13 +569,18 @@ class TestContextDetectionFlow:
def test_case_insensitive_context_detection(self, client):
"""Test that context detection is case insensitive for paths."""
from fastapi import Request
from main import app
@app.get("/API/test-case")
@app.get("/api/test-case")
async def test_case(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
# Test uppercase

View File

@@ -5,8 +5,10 @@ Integration tests for the complete middleware stack.
These tests verify that all middleware components work together correctly
through real HTTP requests, ensuring proper execution order and state injection.
"""
import pytest
from unittest.mock import patch
import pytest
from middleware.context import RequestContext
@@ -23,14 +25,20 @@ class TestMiddlewareStackIntegration:
"""Test that /admin/* paths set ADMIN context type."""
# Create a simple endpoint to inspect request state
from fastapi import Request
from main import app
@app.get("/admin/test-context")
async def test_admin_context(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"has_theme": hasattr(request.state, 'theme')
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"has_theme": hasattr(request.state, "theme"),
}
response = client.get("/admin/test-context")
@@ -44,20 +52,24 @@ class TestMiddlewareStackIntegration:
def test_admin_subdomain_sets_admin_context(self, client):
"""Test that admin.* subdomain sets ADMIN context type."""
from fastapi import Request
from main import app
@app.get("/test-admin-subdomain")
async def test_admin_subdomain(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
# Simulate request with admin subdomain
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-admin-subdomain",
headers={"host": "admin.platform.com"}
"/test-admin-subdomain", headers={"host": "admin.platform.com"}
)
assert response.status_code == 200
@@ -71,12 +83,17 @@ class TestMiddlewareStackIntegration:
def test_api_path_sets_api_context(self, client):
"""Test that /api/* paths set API context type."""
from fastapi import Request
from main import app
@app.get("/api/test-context")
async def test_api_context(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
)
}
response = client.get("/api/test-context")
@@ -89,25 +106,40 @@ class TestMiddlewareStackIntegration:
# Vendor Dashboard Context Tests
# ========================================================================
def test_vendor_dashboard_path_sets_vendor_context(self, client, vendor_with_subdomain):
def test_vendor_dashboard_path_sets_vendor_context(
self, client, vendor_with_subdomain
):
"""Test that /vendor/* paths with vendor set VENDOR_DASHBOARD context."""
from fastapi import Request
from main import app
@app.get("/vendor/test-context")
async def test_vendor_context(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor")
else None
),
}
# Request with vendor subdomain
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/vendor/test-context",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -120,24 +152,35 @@ class TestMiddlewareStackIntegration:
# Shop Context Tests
# ========================================================================
def test_shop_path_with_subdomain_sets_shop_context(self, client, vendor_with_subdomain):
def test_shop_path_with_subdomain_sets_shop_context(
self, client, vendor_with_subdomain
):
"""Test that /shop/* paths with vendor subdomain set SHOP context."""
from fastapi import Request
from main import app
@app.get("/shop/test-context")
async def test_shop_context(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"has_theme": hasattr(request.state, 'theme')
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"has_theme": hasattr(request.state, "theme"),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/shop/test-context",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -146,23 +189,33 @@ class TestMiddlewareStackIntegration:
assert data["vendor_id"] == vendor_with_subdomain.id
assert data["has_theme"] is True
def test_shop_path_with_custom_domain_sets_shop_context(self, client, vendor_with_custom_domain):
def test_shop_path_with_custom_domain_sets_shop_context(
self, client, vendor_with_custom_domain
):
"""Test that /shop/* paths with custom domain set SHOP context."""
from fastapi import Request
from main import app
@app.get("/shop/test-custom-domain")
async def test_shop_custom_domain(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/shop/test-custom-domain",
headers={"host": "customdomain.com"}
"/shop/test-custom-domain", headers={"host": "customdomain.com"}
)
assert response.status_code == 200
@@ -174,9 +227,12 @@ class TestMiddlewareStackIntegration:
# Middleware Execution Order Tests
# ========================================================================
def test_vendor_context_runs_before_context_detection(self, client, vendor_with_subdomain):
def test_vendor_context_runs_before_context_detection(
self, client, vendor_with_subdomain
):
"""Test that VendorContextMiddleware runs before ContextDetectionMiddleware."""
from fastapi import Request
from main import app
@app.get("/test-execution-order")
@@ -184,16 +240,16 @@ class TestMiddlewareStackIntegration:
# If vendor context runs first, clean_path should be available
# before context detection uses it
return {
"has_vendor": hasattr(request.state, 'vendor'),
"has_clean_path": hasattr(request.state, 'clean_path'),
"has_context_type": hasattr(request.state, 'context_type')
"has_vendor": hasattr(request.state, "vendor"),
"has_clean_path": hasattr(request.state, "clean_path"),
"has_context_type": hasattr(request.state, "context_type"),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-execution-order",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -206,21 +262,26 @@ class TestMiddlewareStackIntegration:
def test_theme_context_runs_after_vendor_context(self, client, vendor_with_theme):
"""Test that ThemeContextMiddleware runs after VendorContextMiddleware."""
from fastapi import Request
from main import app
@app.get("/test-theme-loading")
async def test_theme_loading(request: Request):
return {
"has_vendor": hasattr(request.state, 'vendor'),
"has_theme": hasattr(request.state, 'theme'),
"theme_primary_color": request.state.theme.get('primary_color') if hasattr(request.state, 'theme') else None
"has_vendor": hasattr(request.state, "vendor"),
"has_theme": hasattr(request.state, "theme"),
"theme_primary_color": (
request.state.theme.get("primary_color")
if hasattr(request.state, "theme")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-theme-loading",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -249,21 +310,27 @@ class TestMiddlewareStackIntegration:
def test_missing_vendor_graceful_handling(self, client):
"""Test that missing vendor is handled gracefully."""
from fastapi import Request
from main import app
@app.get("/test-missing-vendor")
async def test_missing_vendor(request: Request):
return {
"has_vendor": hasattr(request.state, 'vendor'),
"vendor": request.state.vendor if hasattr(request.state, 'vendor') else None,
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None
"has_vendor": hasattr(request.state, "vendor"),
"vendor": (
request.state.vendor if hasattr(request.state, "vendor") else None
),
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-missing-vendor",
headers={"host": "nonexistent.platform.com"}
"/test-missing-vendor", headers={"host": "nonexistent.platform.com"}
)
assert response.status_code == 200
@@ -276,20 +343,23 @@ class TestMiddlewareStackIntegration:
def test_inactive_vendor_not_loaded(self, client, inactive_vendor):
"""Test that inactive vendors are not loaded."""
from fastapi import Request
from main import app
@app.get("/test-inactive-vendor")
async def test_inactive_vendor_endpoint(request: Request):
return {
"has_vendor": hasattr(request.state, 'vendor'),
"vendor": request.state.vendor if hasattr(request.state, 'vendor') else None
"has_vendor": hasattr(request.state, "vendor"),
"vendor": (
request.state.vendor if hasattr(request.state, "vendor") else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-inactive-vendor",
headers={"host": f"{inactive_vendor.subdomain}.platform.com"}
headers={"host": f"{inactive_vendor.subdomain}.platform.com"},
)
assert response.status_code == 200

View File

@@ -5,9 +5,10 @@ Integration tests for theme loading end-to-end flow.
These tests verify that vendor themes are correctly loaded and injected
into request.state through real HTTP requests.
"""
import pytest
from unittest.mock import patch
import pytest
@pytest.mark.integration
@pytest.mark.middleware
@@ -22,21 +23,22 @@ class TestThemeLoadingFlow:
def test_theme_loaded_for_vendor_with_custom_theme(self, client, vendor_with_theme):
"""Test that custom theme is loaded for vendor with theme."""
from fastapi import Request
from main import app
@app.get("/test-theme-loading")
async def test_theme(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else None
theme = request.state.theme if hasattr(request.state, "theme") else None
return {
"has_theme": theme is not None,
"theme_data": theme if theme else None
"theme_data": theme if theme else None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-theme-loading",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -46,24 +48,27 @@ class TestThemeLoadingFlow:
assert data["theme_data"]["primary_color"] == "#FF5733"
assert data["theme_data"]["secondary_color"] == "#33FF57"
def test_default_theme_loaded_for_vendor_without_theme(self, client, vendor_with_subdomain):
def test_default_theme_loaded_for_vendor_without_theme(
self, client, vendor_with_subdomain
):
"""Test that default theme is loaded for vendor without custom theme."""
from fastapi import Request
from main import app
@app.get("/test-default-theme")
async def test_default_theme(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else None
theme = request.state.theme if hasattr(request.state, "theme") else None
return {
"has_theme": theme is not None,
"theme_data": theme if theme else None
"theme_data": theme if theme else None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-default-theme",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -76,21 +81,20 @@ class TestThemeLoadingFlow:
def test_no_theme_loaded_without_vendor(self, client):
"""Test that no theme is loaded when there's no vendor."""
from fastapi import Request
from main import app
@app.get("/test-no-theme")
async def test_no_theme(request: Request):
return {
"has_theme": hasattr(request.state, 'theme'),
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None
"has_theme": hasattr(request.state, "theme"),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-no-theme",
headers={"host": "platform.com"}
)
response = client.get("/test-no-theme", headers={"host": "platform.com"})
assert response.status_code == 200
data = response.json()
@@ -105,24 +109,25 @@ class TestThemeLoadingFlow:
def test_custom_theme_contains_all_fields(self, client, vendor_with_theme):
"""Test that custom theme contains all expected fields."""
from fastapi import Request
from main import app
@app.get("/test-theme-fields")
async def test_theme_fields(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else {}
theme = request.state.theme if hasattr(request.state, "theme") else {}
return {
"primary_color": theme.get("primary_color"),
"secondary_color": theme.get("secondary_color"),
"logo_url": theme.get("logo_url"),
"favicon_url": theme.get("favicon_url"),
"custom_css": theme.get("custom_css")
"custom_css": theme.get("custom_css"),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-theme-fields",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -136,24 +141,25 @@ class TestThemeLoadingFlow:
def test_default_theme_structure(self, client, vendor_with_subdomain):
"""Test that default theme has expected structure."""
from fastapi import Request
from main import app
@app.get("/test-default-theme-structure")
async def test_default_structure(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else {}
theme = request.state.theme if hasattr(request.state, "theme") else {}
return {
"has_primary_color": "primary_color" in theme,
"has_secondary_color": "secondary_color" in theme,
"has_logo_url": "logo_url" in theme,
"has_favicon_url": "favicon_url" in theme,
"has_custom_css": "custom_css" in theme
"has_custom_css": "custom_css" in theme,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-default-theme-structure",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -169,21 +175,30 @@ class TestThemeLoadingFlow:
def test_theme_loaded_in_shop_context(self, client, vendor_with_theme):
"""Test that theme is loaded in SHOP context."""
from fastapi import Request
from main import app
@app.get("/shop/test-shop-theme")
async def test_shop_theme(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_theme": hasattr(request.state, 'theme'),
"theme_primary": request.state.theme.get("primary_color") if hasattr(request.state, 'theme') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_theme": hasattr(request.state, "theme"),
"theme_primary": (
request.state.theme.get("primary_color")
if hasattr(request.state, "theme")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/shop/test-shop-theme",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -195,21 +210,30 @@ class TestThemeLoadingFlow:
def test_theme_loaded_in_vendor_dashboard_context(self, client, vendor_with_theme):
"""Test that theme is loaded in VENDOR_DASHBOARD context."""
from fastapi import Request
from main import app
@app.get("/vendor/test-dashboard-theme")
async def test_dashboard_theme(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_theme": hasattr(request.state, 'theme'),
"theme_secondary": request.state.theme.get("secondary_color") if hasattr(request.state, 'theme') else None
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_theme": hasattr(request.state, "theme"),
"theme_secondary": (
request.state.theme.get("secondary_color")
if hasattr(request.state, "theme")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/vendor/test-dashboard-theme",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -221,21 +245,27 @@ class TestThemeLoadingFlow:
def test_theme_loaded_in_api_context_with_vendor(self, client, vendor_with_theme):
"""Test that theme is loaded in API context when vendor is present."""
from fastapi import Request
from main import app
@app.get("/api/test-api-theme")
async def test_api_theme(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"has_theme": hasattr(request.state, 'theme')
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"has_theme": hasattr(request.state, "theme"),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/api/test-api-theme",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -248,13 +278,18 @@ class TestThemeLoadingFlow:
def test_no_theme_in_admin_context(self, client):
"""Test that theme is not loaded in ADMIN context (no vendor)."""
from fastapi import Request
from main import app
@app.get("/admin/test-admin-no-theme")
async def test_admin_no_theme(request: Request):
return {
"context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None,
"has_theme": hasattr(request.state, 'theme')
"context_type": (
request.state.context_type.value
if hasattr(request.state, "context_type")
else None
),
"has_theme": hasattr(request.state, "theme"),
}
response = client.get("/admin/test-admin-no-theme")
@@ -272,20 +307,29 @@ class TestThemeLoadingFlow:
def test_theme_loaded_with_subdomain_routing(self, client, vendor_with_theme):
"""Test theme loading with subdomain routing."""
from fastapi import Request
from main import app
@app.get("/test-subdomain-theme")
async def test_subdomain_theme(request: Request):
return {
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None,
"theme_logo": request.state.theme.get("logo_url") if hasattr(request.state, 'theme') else None
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
"theme_logo": (
request.state.theme.get("logo_url")
if hasattr(request.state, "theme")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-subdomain-theme",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -293,33 +337,40 @@ class TestThemeLoadingFlow:
assert data["vendor_code"] == vendor_with_theme.code
assert data["theme_logo"] == "/static/vendors/themedvendor/logo.png"
def test_theme_loaded_with_custom_domain_routing(self, client, vendor_with_custom_domain, db):
def test_theme_loaded_with_custom_domain_routing(
self, client, vendor_with_custom_domain, db
):
"""Test theme loading with custom domain routing."""
# Add theme to custom domain vendor
from models.database.vendor_theme import VendorTheme
theme = VendorTheme(
vendor_id=vendor_with_custom_domain.id,
primary_color="#123456",
secondary_color="#654321"
secondary_color="#654321",
)
db.add(theme)
db.commit()
from fastapi import Request
from main import app
@app.get("/test-custom-domain-theme")
async def test_custom_domain_theme(request: Request):
return {
"has_theme": hasattr(request.state, 'theme'),
"theme_primary": request.state.theme.get("primary_color") if hasattr(request.state, 'theme') else None
"has_theme": hasattr(request.state, "theme"),
"theme_primary": (
request.state.theme.get("primary_color")
if hasattr(request.state, "theme")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-custom-domain-theme",
headers={"host": "customdomain.com"}
"/test-custom-domain-theme", headers={"host": "customdomain.com"}
)
assert response.status_code == 200
@@ -331,28 +382,37 @@ class TestThemeLoadingFlow:
# Theme Dependency on Vendor Context Tests
# ========================================================================
def test_theme_middleware_depends_on_vendor_middleware(self, client, vendor_with_theme):
def test_theme_middleware_depends_on_vendor_middleware(
self, client, vendor_with_theme
):
"""Test that theme loading depends on vendor being detected first."""
from fastapi import Request
from main import app
@app.get("/test-theme-vendor-dependency")
async def test_dependency(request: Request):
return {
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"has_theme": hasattr(request.state, 'theme'),
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"has_theme": hasattr(request.state, "theme"),
"vendor_matches_theme": (
request.state.vendor_id == vendor_with_theme.id
if hasattr(request.state, 'vendor_id') else False
)
if hasattr(request.state, "vendor_id")
else False
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-theme-vendor-dependency",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -369,15 +429,20 @@ class TestThemeLoadingFlow:
def test_theme_loaded_consistently_across_requests(self, client, vendor_with_theme):
"""Test that theme is loaded consistently across multiple requests."""
from fastapi import Request
from main import app
@app.get("/test-theme-consistency")
async def test_consistency(request: Request):
return {
"theme_primary": request.state.theme.get("primary_color") if hasattr(request.state, 'theme') else None
"theme_primary": (
request.state.theme.get("primary_color")
if hasattr(request.state, "theme")
else None
)
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
# Make multiple requests
@@ -385,7 +450,7 @@ class TestThemeLoadingFlow:
for _ in range(3):
response = client.get(
"/test-theme-consistency",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
responses.append(response.json())
@@ -396,25 +461,28 @@ class TestThemeLoadingFlow:
# Edge Cases and Error Handling Tests
# ========================================================================
def test_theme_gracefully_handles_missing_theme_fields(self, client, vendor_with_subdomain):
def test_theme_gracefully_handles_missing_theme_fields(
self, client, vendor_with_subdomain
):
"""Test that missing theme fields are handled gracefully."""
from fastapi import Request
from main import app
@app.get("/test-partial-theme")
async def test_partial_theme(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else {}
theme = request.state.theme if hasattr(request.state, "theme") else {}
return {
"has_theme": bool(theme),
"primary_color": theme.get("primary_color", "default"),
"logo_url": theme.get("logo_url", "default")
"logo_url": theme.get("logo_url", "default"),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-partial-theme",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -427,23 +495,21 @@ class TestThemeLoadingFlow:
def test_theme_dict_is_mutable(self, client, vendor_with_theme):
"""Test that theme dict can be accessed and read from."""
from fastapi import Request
from main import app
@app.get("/test-theme-mutable")
async def test_mutable(request: Request):
theme = request.state.theme if hasattr(request.state, 'theme') else {}
theme = request.state.theme if hasattr(request.state, "theme") else {}
# Try to access theme values
primary = theme.get("primary_color")
return {
"can_read": primary is not None,
"value": primary
}
return {"can_read": primary is not None, "value": primary}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-theme-mutable",
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"}
headers={"host": f"{vendor_with_theme.subdomain}.platform.com"},
)
assert response.status_code == 200

View File

@@ -5,9 +5,10 @@ Integration tests for vendor context detection end-to-end flow.
These tests verify that vendor detection works correctly through real HTTP requests
for all routing modes: subdomain, custom domain, and path-based.
"""
import pytest
from unittest.mock import patch
import pytest
@pytest.mark.integration
@pytest.mark.middleware
@@ -22,23 +23,37 @@ class TestVendorContextFlow:
def test_subdomain_vendor_detection(self, client, vendor_with_subdomain):
"""Test vendor detection via subdomain routing."""
from fastapi import Request
from main import app
@app.get("/test-subdomain-detection")
async def test_subdomain(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None,
"vendor_name": request.state.vendor.name if hasattr(request.state, 'vendor') and request.state.vendor else None,
"detection_method": "subdomain"
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
"vendor_name": (
request.state.vendor.name
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
"detection_method": "subdomain",
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-subdomain-detection",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -51,20 +66,28 @@ class TestVendorContextFlow:
def test_subdomain_with_port_detection(self, client, vendor_with_subdomain):
"""Test vendor detection via subdomain with port number."""
from fastapi import Request
from main import app
@app.get("/test-subdomain-port")
async def test_subdomain_port(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-subdomain-port",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com:8000"}
headers={
"host": f"{vendor_with_subdomain.subdomain}.platform.com:8000"
},
)
assert response.status_code == 200
@@ -75,20 +98,24 @@ class TestVendorContextFlow:
def test_nonexistent_subdomain_returns_no_vendor(self, client):
"""Test that nonexistent subdomain doesn't crash and returns no vendor."""
from fastapi import Request
from main import app
@app.get("/test-nonexistent-subdomain")
async def test_nonexistent(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor": request.state.vendor if hasattr(request.state, 'vendor') else None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor": (
request.state.vendor if hasattr(request.state, "vendor") else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-nonexistent-subdomain",
headers={"host": "nonexistent.platform.com"}
headers={"host": "nonexistent.platform.com"},
)
assert response.status_code == 200
@@ -102,22 +129,31 @@ class TestVendorContextFlow:
def test_custom_domain_vendor_detection(self, client, vendor_with_custom_domain):
"""Test vendor detection via custom domain."""
from fastapi import Request
from main import app
@app.get("/test-custom-domain")
async def test_custom_domain(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None,
"detection_method": "custom_domain"
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
"detection_method": "custom_domain",
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-custom-domain",
headers={"host": "customdomain.com"}
"/test-custom-domain", headers={"host": "customdomain.com"}
)
assert response.status_code == 200
@@ -129,21 +165,26 @@ class TestVendorContextFlow:
def test_custom_domain_with_www_detection(self, client, vendor_with_custom_domain):
"""Test vendor detection via custom domain with www prefix."""
from fastapi import Request
from main import app
@app.get("/test-custom-domain-www")
async def test_custom_domain_www(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
# Test with www prefix - should still detect vendor
response = client.get(
"/test-custom-domain-www",
headers={"host": "www.customdomain.com"}
"/test-custom-domain-www", headers={"host": "www.customdomain.com"}
)
# This might fail if your implementation doesn't strip www
@@ -154,25 +195,37 @@ class TestVendorContextFlow:
# Path-Based Detection Tests (Development Mode)
# ========================================================================
def test_path_based_vendor_detection_vendors_prefix(self, client, vendor_with_subdomain):
def test_path_based_vendor_detection_vendors_prefix(
self, client, vendor_with_subdomain
):
"""Test vendor detection via path-based routing with /vendors/ prefix."""
from fastapi import Request
from main import app
@app.get("/vendors/{vendor_code}/test-path")
async def test_path_based(vendor_code: str, request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_code_param": vendor_code,
"vendor_code_state": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None,
"clean_path": request.state.clean_path if hasattr(request.state, 'clean_path') else None
"vendor_code_state": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
"clean_path": (
request.state.clean_path
if hasattr(request.state, "clean_path")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
f"/vendors/{vendor_with_subdomain.code}/test-path",
headers={"host": "localhost:8000"}
headers={"host": "localhost:8000"},
)
assert response.status_code == 200
@@ -181,23 +234,31 @@ class TestVendorContextFlow:
assert data["vendor_code_param"] == vendor_with_subdomain.code
assert data["vendor_code_state"] == vendor_with_subdomain.code
def test_path_based_vendor_detection_vendor_prefix(self, client, vendor_with_subdomain):
def test_path_based_vendor_detection_vendor_prefix(
self, client, vendor_with_subdomain
):
"""Test vendor detection via path-based routing with /vendor/ prefix."""
from fastapi import Request
from main import app
@app.get("/vendor/{vendor_code}/test")
async def test_vendor_path(vendor_code: str, request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None,
"vendor_code": request.state.vendor.code if hasattr(request.state, 'vendor') and request.state.vendor else None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None,
"vendor_code": (
request.state.vendor.code
if hasattr(request.state, "vendor") and request.state.vendor
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
f"/vendor/{vendor_with_subdomain.code}/test",
headers={"host": "localhost:8000"}
headers={"host": "localhost:8000"},
)
assert response.status_code == 200
@@ -209,48 +270,63 @@ class TestVendorContextFlow:
# Clean Path Extraction Tests
# ========================================================================
def test_clean_path_extracted_from_vendor_prefix(self, client, vendor_with_subdomain):
def test_clean_path_extracted_from_vendor_prefix(
self, client, vendor_with_subdomain
):
"""Test that clean_path is correctly extracted from path-based routing."""
from fastapi import Request
from main import app
@app.get("/vendors/{vendor_code}/shop/products")
async def test_clean_path(vendor_code: str, request: Request):
return {
"clean_path": request.state.clean_path if hasattr(request.state, 'clean_path') else None,
"original_path": request.url.path
"clean_path": (
request.state.clean_path
if hasattr(request.state, "clean_path")
else None
),
"original_path": request.url.path,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
f"/vendors/{vendor_with_subdomain.code}/shop/products",
headers={"host": "localhost:8000"}
headers={"host": "localhost:8000"},
)
assert response.status_code == 200
data = response.json()
# Clean path should have vendor prefix removed
assert data["clean_path"] == "/shop/products"
assert f"/vendors/{vendor_with_subdomain.code}/shop/products" in data["original_path"]
assert (
f"/vendors/{vendor_with_subdomain.code}/shop/products"
in data["original_path"]
)
def test_clean_path_unchanged_for_subdomain(self, client, vendor_with_subdomain):
"""Test that clean_path equals original path for subdomain routing."""
from fastapi import Request
from main import app
@app.get("/shop/test-clean-path")
async def test_subdomain_clean_path(request: Request):
return {
"clean_path": request.state.clean_path if hasattr(request.state, 'clean_path') else None,
"original_path": request.url.path
"clean_path": (
request.state.clean_path
if hasattr(request.state, "clean_path")
else None
),
"original_path": request.url.path,
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/shop/test-clean-path",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -266,21 +342,30 @@ class TestVendorContextFlow:
def test_vendor_id_injected_into_request_state(self, client, vendor_with_subdomain):
"""Test that vendor_id is correctly injected into request.state."""
from fastapi import Request
from main import app
@app.get("/test-vendor-id-injection")
async def test_vendor_id(request: Request):
return {
"has_vendor_id": hasattr(request.state, 'vendor_id'),
"vendor_id": request.state.vendor_id if hasattr(request.state, 'vendor_id') else None,
"vendor_id_type": type(request.state.vendor_id).__name__ if hasattr(request.state, 'vendor_id') else None
"has_vendor_id": hasattr(request.state, "vendor_id"),
"vendor_id": (
request.state.vendor_id
if hasattr(request.state, "vendor_id")
else None
),
"vendor_id_type": (
type(request.state.vendor_id).__name__
if hasattr(request.state, "vendor_id")
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-vendor-id-injection",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -289,30 +374,41 @@ class TestVendorContextFlow:
assert data["vendor_id"] == vendor_with_subdomain.id
assert data["vendor_id_type"] == "int"
def test_vendor_object_injected_into_request_state(self, client, vendor_with_subdomain):
def test_vendor_object_injected_into_request_state(
self, client, vendor_with_subdomain
):
"""Test that full vendor object is injected into request.state."""
from fastapi import Request
from main import app
@app.get("/test-vendor-object-injection")
async def test_vendor_object(request: Request):
vendor = request.state.vendor if hasattr(request.state, 'vendor') and request.state.vendor else None
vendor = (
request.state.vendor
if hasattr(request.state, "vendor") and request.state.vendor
else None
)
return {
"has_vendor": vendor is not None,
"vendor_attributes": {
"id": vendor.id if vendor else None,
"name": vendor.name if vendor else None,
"code": vendor.code if vendor else None,
"subdomain": vendor.subdomain if vendor else None,
"is_active": vendor.is_active if vendor else None
} if vendor else None
"vendor_attributes": (
{
"id": vendor.id if vendor else None,
"name": vendor.name if vendor else None,
"code": vendor.code if vendor else None,
"subdomain": vendor.subdomain if vendor else None,
"is_active": vendor.is_active if vendor else None,
}
if vendor
else None
),
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-vendor-object-injection",
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"}
headers={"host": f"{vendor_with_subdomain.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -330,19 +426,21 @@ class TestVendorContextFlow:
def test_inactive_vendor_not_detected(self, client, inactive_vendor):
"""Test that inactive vendors are not detected."""
from fastapi import Request
from main import app
@app.get("/test-inactive-vendor-detection")
async def test_inactive(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-inactive-vendor-detection",
headers={"host": f"{inactive_vendor.subdomain}.platform.com"}
headers={"host": f"{inactive_vendor.subdomain}.platform.com"},
)
assert response.status_code == 200
@@ -352,19 +450,20 @@ class TestVendorContextFlow:
def test_platform_domain_without_subdomain_no_vendor(self, client):
"""Test that platform domain without subdomain doesn't detect vendor."""
from fastapi import Request
from main import app
@app.get("/test-platform-domain")
async def test_platform(request: Request):
return {
"vendor_detected": hasattr(request.state, 'vendor') and request.state.vendor is not None
"vendor_detected": hasattr(request.state, "vendor")
and request.state.vendor is not None
}
with patch('app.core.config.settings') as mock_settings:
with patch("app.core.config.settings") as mock_settings:
mock_settings.platform_domain = "platform.com"
response = client.get(
"/test-platform-domain",
headers={"host": "platform.com"}
"/test-platform-domain", headers={"host": "platform.com"}
)
assert response.status_code == 200

View File

@@ -11,7 +11,8 @@ class TestInputValidation:
malicious_search = "'; DROP TABLE products; --"
response = client.get(
f"/api/v1/marketplace/product?search={malicious_search}", headers=auth_headers
f"/api/v1/marketplace/product?search={malicious_search}",
headers=auth_headers,
)
# Should not crash and should return normal response
@@ -40,17 +41,23 @@ class TestInputValidation:
def test_parameter_validation(self, client, auth_headers):
"""Test parameter validation for API endpoints"""
# Test invalid pagination parameters
response = client.get("/api/v1/marketplace/product?limit=-1", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?limit=-1", headers=auth_headers
)
assert response.status_code == 422 # Validation error
response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
response = client.get(
"/api/v1/marketplace/product?skip=-1", headers=auth_headers
)
assert response.status_code == 422 # Validation error
def test_json_validation(self, client, auth_headers):
"""Test JSON validation for POST requests"""
# Test invalid JSON structure
response = client.post(
"/api/v1/marketplace/product", headers=auth_headers, content="invalid json content"
"/api/v1/marketplace/product",
headers=auth_headers,
content="invalid json content",
)
assert response.status_code == 422 # JSON decode error
@@ -58,6 +65,8 @@ class TestInputValidation:
response = client.post(
"/api/v1/marketplace/product",
headers=auth_headers,
json={"title": "Test MarketplaceProduct"}, # Missing required marketplace_product_id
json={
"title": "Test MarketplaceProduct"
}, # Missing required marketplace_product_id
)
assert response.status_code == 422 # Validation error

View File

@@ -33,12 +33,15 @@ class TestIntegrationFlows:
"quantity": 50,
}
response = client.post("/api/v1/inventory", headers=auth_headers, json=inventory_data)
response = client.post(
"/api/v1/inventory", headers=auth_headers, json=inventory_data
)
assert response.status_code == 200
# 3. Get product with inventory info
response = client.get(
f"/api/v1/marketplace/product/{product['marketplace_product_id']}", headers=auth_headers
f"/api/v1/marketplace/product/{product['marketplace_product_id']}",
headers=auth_headers,
)
assert response.status_code == 200
product_detail = response.json()
@@ -55,14 +58,15 @@ class TestIntegrationFlows:
# 5. Search for product
response = client.get(
"/api/v1/marketplace/product?search=Updated Integration", headers=auth_headers
"/api/v1/marketplace/product?search=Updated Integration",
headers=auth_headers,
)
assert response.status_code == 200
assert response.json()["total"] == 1
def test_product_workflow(self, client, auth_headers):
"""Test vendor creation and product management workflow"""
# 1. Create a vendor
# 1. Create a vendor
vendor_data = {
"vendor_code": "FLOWVENDOR",
"name": "Integration Flow Vendor",
@@ -91,7 +95,9 @@ class TestIntegrationFlows:
# This would test the vendor -product association
# 4. Get vendor details
response = client.get(f"/api/v1/vendor/{vendor ['vendor_code']}", headers=auth_headers)
response = client.get(
f"/api/v1/vendor/{vendor ['vendor_code']}", headers=auth_headers
)
assert response.status_code == 200
def test_inventory_operations_workflow(self, client, auth_headers):