This commit is contained in:
2025-09-21 13:00:10 +02:00
parent a26f8086f8
commit c2a1056db7
56 changed files with 339 additions and 104 deletions

199
Makefile
View File

@@ -32,12 +32,27 @@ install-all: install install-test install-dev install-docs
dev:
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Safe development startup (checks migrations first)
dev-safe: migrate-check
@echo 🔍 Migration check passed, starting development server...
uvicorn main:app --reload --host 0.0.0.0 --port 8000
dev-with-docs:
@echo Starting API server and documentation server...
@start /B uvicorn main:app --reload --host 0.0.0.0 --port 8000
@timeout /t 3 >nul
@mkdocs serve --dev-addr=0.0.0.0:8001
# Combined development environment
dev-full: dev-with-docs
@echo Development environment ready!
@echo API server: http://localhost:8000
@echo API docs: http://localhost:8000/docs
@echo Documentation: http://localhost:8001
# Full development setup with fresh database
dev-fresh: setup-fresh dev-full
# Documentation commands
docs: docs-serve
@@ -77,13 +92,6 @@ docs-check:
docs-help:
mkdocs --help
# Combined development environment
dev-full: dev-with-docs
@echo Development environment ready!
@echo API server: http://localhost:8000
@echo API docs: http://localhost:8000/docs
@echo Documentation: http://localhost:8001
# Testing commands
test:
pytest tests/ -v
@@ -138,19 +146,125 @@ check: format lint
# Combined test with coverage and linting
ci: format lint test-coverage
# Database-aware CI pipeline
ci-db: format lint migrate-check test-coverage
@echo ✅ CI pipeline with database checks completed
# Database migrations
migrate-create:
alembic revision --autogenerate -m "$(message)"
@if "$(message)"=="" (echo Error: Please provide a message. Usage: make migrate-create message="your_description") else (alembic revision --autogenerate -m "$(message)")
migrate-create-manual:
@if "$(message)"=="" (echo Error: Please provide a message. Usage: make migrate-create-manual message="your_description") else (alembic revision -m "$(message)")
migrate-up:
@echo Running database migrations...
alembic upgrade head
@echo ✅ Migrations completed successfully
migrate-down:
@echo Rolling back last migration...
alembic downgrade -1
@echo ✅ Rollback completed
migrate-down-to:
@if "$(revision)"=="" (echo Error: Please provide revision. Usage: make migrate-down-to revision="revision_id") else (alembic downgrade $(revision))
migrate-reset:
@echo Resetting database...
alembic downgrade base
alembic upgrade head
@echo ✅ Database reset completed
migrate-status:
@echo 📊 Current migration status:
alembic current
@echo.
@echo 📋 Migration history:
alembic history --verbose
migrate-show:
@if "$(revision)"=="" (echo Error: Please provide revision. Usage: make migrate-show revision="revision_id") else (alembic show $(revision))
migrate-heads:
@echo 📍 Current migration heads:
alembic heads
migrate-check:
@echo 🔍 Checking for pending migrations...
@python -c "from alembic import command, config; cfg = config.Config('alembic.ini'); command.check(cfg)" && echo "✅ No pending migrations" || echo "⚠️ Pending migrations found"
# Database initialization (enhanced)
db-init: migrate-up
@echo 🚀 Database initialization completed
db-fresh: migrate-reset
@echo 🔄 Fresh database setup completed
# Database backup before risky operations (if using PostgreSQL/MySQL)
backup-db:
@echo 💾 Creating database backup...
@python scripts/backup_database.py
@echo ✅ Database backup created
# === FRESH START COMMANDS (Development) ===
fresh-backup:
@echo 💾 Creating backup of current state...
@if not exist scripts mkdir scripts
@python scripts/backup_database.py
fresh-clean:
@echo 🧹 Cleaning up for fresh start...
@if exist ecommerce.db del ecommerce.db
@if exist alembic\versions\*.py (for %%f in (alembic\versions\*.py) do if not "%%~nf"=="__init__" del "%%f")
@echo ✅ Cleanup completed
fresh-setup: fresh-backup fresh-clean
@echo 🚀 Setting up fresh database with proper migrations...
@echo.
@echo Step 1: Creating initial migration from models...
alembic revision --autogenerate -m "initial_schema_and_indexes"
@echo.
@echo Step 2: Running the migration to create database...
alembic upgrade head
@echo.
@echo ✅ Fresh setup completed!
@echo Database is now managed entirely by Alembic migrations.
# Check what the fresh migration would contain
fresh-preview:
@echo 🔍 Previewing what the fresh migration would contain...
@echo This will show what tables/indexes would be created.
@echo.
@if exist ecommerce.db (echo Current database detected - showing diff) else (echo No database - showing full schema)
alembic revision --autogenerate -m "preview_only" --head-only
# Complete development environment setup with fresh database
dev-fresh-setup: install-all fresh-setup
@echo 🎉 Complete fresh development setup completed!
@echo.
@echo What was done:
@echo ✅ All dependencies installed
@echo ✅ Database created with migrations
@echo ✅ Migration tracking initialized
@echo.
@echo Next steps:
@echo 1. Review the migration file in alembic/versions/
@echo 2. Add your custom indexes to the migration
@echo 3. Run 'make dev' to start development
@echo 4. Use 'make migrate-create message="description"' for future changes
# Verify the fresh setup worked
verify-fresh:
@echo 🔍 Verifying fresh setup...
@echo.
@echo Migration status:
@alembic current
@echo.
@echo Database tables:
@python -c "from sqlalchemy import create_engine, text; engine = create_engine('sqlite:///./ecommerce.db'); print('Tables:', [r[0] for r in engine.execute(text('SELECT name FROM sqlite_master WHERE type=\"table\"')).fetchall()])"
@echo.
@echo ✅ Verification completed
# Docker commands
docker-build:
@@ -167,11 +281,16 @@ docker-logs:
docker-restart: docker-down docker-up
# Pre-deployment checks
pre-deploy: qa migrate-status
@echo 🚀 Pre-deployment checks completed!
@echo Ready for deployment.
# Production deployment
deploy-staging:
deploy-staging: migrate-up
docker-compose -f docker-compose.staging.yml up -d
deploy-prod:
deploy-prod: migrate-up
docker-compose -f docker-compose.prod.yml up -d
# Documentation deployment workflow
@@ -196,10 +315,14 @@ clean-all: clean docs-clean
@echo All build artifacts cleaned!
# Development workflow shortcuts
setup: install-all migrate-up
setup: install-all db-init
@echo Development environment setup complete!
@echo Run 'make dev-full' to start both API and documentation servers
setup-fresh: install-all db-fresh
@echo Fresh development environment setup complete!
@echo Run 'make dev-full' to start both API and documentation servers
setup-test: install-test
@echo Test environment setup complete!
@@ -218,6 +341,25 @@ qa: format lint test-coverage docs-check
release-check: qa docs-build
@echo Release readiness check completed!
# Development workflow examples
workflow-new-feature:
@echo 🚀 Starting new feature development workflow:
@echo 1. Pulling latest changes and updating dependencies...
@$(MAKE) install-all
@echo 2. Running migrations...
@$(MAKE) migrate-up
@echo 3. Running tests to ensure clean state...
@$(MAKE) test-fast
@echo 4. Starting development environment...
@$(MAKE) dev-full
workflow-deploy:
@echo 🚀 Deployment workflow:
@echo 1. Running comprehensive checks...
@$(MAKE) pre-deploy-check
@echo 2. Ready for deployment!
@echo Run 'make deploy-staging' or 'make deploy-prod' to deploy.
# Help command
help:
@echo Available commands:
@@ -264,10 +406,20 @@ help:
@echo ci - Full CI pipeline (format, lint, test)
@echo qa - Quality assurance (format, lint, test, docs check)
@echo.
@echo === DATABASE ===
@echo migrate-up - Run database migrations
@echo migrate-down - Rollback last migration
@echo migrate-reset - Reset and rerun all migrations
@echo === DATABASE MIGRATIONS ===
@echo migrate-create message="msg" - Create auto-generated migration
@echo migrate-create-manual message="msg" - Create empty migration template
@echo migrate-up - Run all pending migrations
@echo migrate-down - Rollback last migration
@echo migrate-down-to revision="id" - Rollback to specific revision
@echo migrate-reset - Reset database to base and rerun all
@echo migrate-status - Show current migration status and history
@echo migrate-show revision="id" - Show specific migration details
@echo migrate-heads - Show current migration heads
@echo migrate-check - Check for pending migrations
@echo migrate-create-indexes - Create the database indexes migration
@echo db-init - Initialize database with migrations
@echo db-fresh - Fresh database setup
@echo.
@echo === DOCKER ===
@echo docker-build - Build Docker containers
@@ -286,4 +438,19 @@ help:
@echo make setup # First time setup
@echo make dev-full # Start development environment
@echo make docs-serve # Start documentation server
@echo make qa # Run quality checks
@echo make qa # Run quality checks
# Help for fresh start
help-fresh:
@echo === FRESH START COMMANDS ===
@echo fresh-backup - Backup current database
@echo fresh-clean - Clean database and migrations
@echo fresh-setup - Complete fresh start with migrations
@echo fresh-preview - Preview what migration would create
@echo dev-fresh-setup - Complete development setup from scratch
@echo verify-fresh - Verify fresh setup worked correctly
@echo.
@echo RECOMMENDED WORKFLOW:
@echo make dev-fresh-setup # Complete setup
@echo # Edit the generated migration file to add indexes
@echo make dev # Start development

View File

@@ -14,8 +14,8 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database.user import User
from models.database.shop import Shop
from models.database.user import User
# Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False)

View File

@@ -16,9 +16,9 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_user
from app.core.database import get_db
from app.services.admin_service import admin_service
from models.api.auth import UserResponse
from models.api.marketplace import MarketplaceImportJobResponse
from models.api.shop import ShopListResponse
from models.api.auth import UserResponse
from models.database.user import User
router = APIRouter()

View File

@@ -16,7 +16,7 @@ from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.auth_service import auth_service
from models.api.auth import (LoginResponse, UserLogin, UserRegister,
UserResponse)
UserResponse)
from models.database.user import User
router = APIRouter()

View File

@@ -19,7 +19,7 @@ from app.services.marketplace_service import marketplace_service
from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit
from models.api.marketplace import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
MarketplaceImportRequest)
from models.database.user import User
router = APIRouter()

View File

@@ -18,8 +18,8 @@ from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.product_service import product_service
from models.api.product import (ProductCreate, ProductDetailResponse,
ProductListResponse, ProductResponse,
ProductUpdate)
ProductListResponse, ProductResponse,
ProductUpdate)
from models.database.user import User
router = APIRouter()

View File

@@ -16,7 +16,7 @@ from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db
from app.services.shop_service import shop_service
from models.api.shop import (ShopCreate, ShopListResponse, ShopProductCreate,
ShopProductResponse, ShopResponse)
ShopProductResponse, ShopResponse)
from models.database.user import User
router = APIRouter()

View File

@@ -17,7 +17,7 @@ from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.stock_service import stock_service
from models.api.stock import (StockAdd, StockCreate, StockResponse,
StockSummaryResponse, StockUpdate)
StockSummaryResponse, StockUpdate)
from models.database.user import User
router = APIRouter()

View File

@@ -15,7 +15,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session
from models.api.marketplace import (MarketplaceImportJobResponse,
MarketplaceImportRequest)
MarketplaceImportRequest)
from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
from models.database.user import User

View File

@@ -14,7 +14,7 @@ from typing import List, Optional
from sqlalchemy.orm import Session
from models.api.stock import (StockAdd, StockCreate, StockLocationResponse,
StockSummaryResponse, StockUpdate)
StockSummaryResponse, StockUpdate)
from models.database.product import Product
from models.database.stock import Stock
from utils.data_processing import GTINProcessor

43
main.py
View File

@@ -2,8 +2,8 @@ import logging
from datetime import datetime
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import text
from sqlalchemy.orm import Session
@@ -49,25 +49,27 @@ def health_check(db: Session = Depends(get_db)):
try:
# Test database connection
db.execute(text("SELECT 1"))
return {"status": "healthy",
"timestamp": datetime.utcnow(),
"message": f"{settings.project_name} v{settings.version}",
"docs": {
"swagger": "/docs",
"redoc": "/redoc",
"openapi": "/openapi.json",
"complete": "Documentation site URL here"
},
"features": [
"JWT Authentication",
"Marketplace-aware product import",
"Multi-shop product management",
"Stock management with location tracking",
],
"supported_marketplaces": [
"Letzshop",
],
"auth_required": "Most endpoints require Bearer token authentication", }
return {
"status": "healthy",
"timestamp": datetime.utcnow(),
"message": f"{settings.project_name} v{settings.version}",
"docs": {
"swagger": "/docs",
"redoc": "/redoc",
"openapi": "/openapi.json",
"complete": "Documentation site URL here",
},
"features": [
"JWT Authentication",
"Marketplace-aware product import",
"Multi-shop product management",
"Stock management with location tracking",
],
"supported_marketplaces": [
"Letzshop",
],
"auth_required": "Most endpoints require Bearer token authentication",
}
except Exception as e:
logger.error(f"Health check failed: {e}")
raise HTTPException(status_code=503, detail="Service unhealthy")
@@ -161,6 +163,7 @@ async def documentation_page():
</html>
"""
if __name__ == "__main__":
import uvicorn

View File

@@ -1,16 +1,21 @@
from typing import List, TypeVar, Generic
from typing import Generic, List, TypeVar
from pydantic import BaseModel
T = TypeVar('T')
T = TypeVar("T")
class ListResponse(BaseModel, Generic[T]):
"""Generic list response model"""
items: List[T]
total: int
skip: int
limit: int
class StatusResponse(BaseModel):
"""Generic status response"""
success: bool
message: str

View File

@@ -3,8 +3,10 @@ from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
from models.api.stock import StockSummaryResponse
class ProductBase(BaseModel):
product_id: Optional[str] = None
title: Optional[str] = None
@@ -71,6 +73,7 @@ class ProductResponse(ProductBase):
created_at: datetime
updated_at: datetime
class ProductListResponse(BaseModel):
products: List[ProductResponse]
total: int

View File

@@ -3,8 +3,10 @@ from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
from models.api.product import ProductResponse
class ShopCreate(BaseModel):
shop_code: str = Field(
...,
@@ -82,6 +84,7 @@ class ShopListResponse(BaseModel):
skip: int
limit: int
class ShopProductCreate(BaseModel):
product_id: str = Field(..., description="Product ID to add to shop")
shop_product_id: Optional[str] = Field(

View File

@@ -19,4 +19,4 @@ class MarketplaceStatsResponse(BaseModel):
marketplace: str
total_products: int
unique_shops: int
unique_brands: int
unique_brands: int

View File

@@ -43,4 +43,4 @@ class StockSummaryResponse(BaseModel):
gtin: str
total_quantity: int
locations: List[StockLocationResponse]
product_title: Optional[str] = None
product_title: Optional[str] = None

View File

@@ -1,9 +1,13 @@
from datetime import datetime
from sqlalchemy import Column, DateTime
from app.core.database import Base
class TimestampMixin:
"""Mixin to add created_at and updated_at timestamps to models"""
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False

View File

@@ -85,4 +85,3 @@ class Product(Base):
f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>"
)

View File

@@ -43,6 +43,7 @@ class Shop(Base):
"MarketplaceImportJob", back_populates="shop"
)
class ShopProduct(Base):
__tablename__ = "shop_products"
@@ -81,4 +82,3 @@ class ShopProduct(Base):
Index("idx_shop_product_active", "shop_id", "is_active"),
Index("idx_shop_product_featured", "shop_id", "is_featured"),
)

View File

@@ -0,0 +1,35 @@
# scripts/backup_database.py
"""Simple backup for early development."""
import os
import shutil
from datetime import datetime
from pathlib import Path
def backup_current_db():
"""Quick backup of current database."""
db_path = "ecommerce.db"
if not os.path.exists(db_path):
print(" No existing database found - nothing to backup")
return
# Create backup directory
backup_dir = Path("backups")
backup_dir.mkdir(exist_ok=True)
# Create timestamped backup
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = backup_dir / f"ecommerce_pre_reset_{timestamp}.db"
try:
shutil.copy2(db_path, backup_path)
print(f"✅ Database backed up to: {backup_path}")
except Exception as e:
print(f"⚠️ Backup failed: {e}")
if __name__ == "__main__":
backup_current_db()

View File

@@ -10,7 +10,7 @@ from main import app
# Import all models to ensure they're registered with Base metadata
from models.database.marketplace import MarketplaceImportJob
from models.database.product import Product
from models.database.shop import Shop,ShopProduct
from models.database.shop import Shop, ShopProduct
from models.database.stock import Stock
from models.database.user import User
@@ -87,7 +87,7 @@ def cleanup():
# Import fixtures from fixture modules
pytest_plugins = [
"tests.fixtures.auth_fixtures",
"tests.fixtures.product_fixtures",
"tests.fixtures.product_fixtures",
"tests.fixtures.shop_fixtures",
"tests.fixtures.marketplace_fixtures",
]
]

View File

@@ -1,3 +1,2 @@
# tests/fixtures/__init__.py
"""Test fixtures for the FastAPI application test suite."""

View File

@@ -3,7 +3,7 @@ import uuid
import pytest
from models.database.shop import Shop,ShopProduct
from models.database.shop import Shop, ShopProduct
from models.database.stock import Stock

View File

@@ -1,3 +1,2 @@
# tests/integration/__init__.py
"""Integration tests - multiple components working together."""

View File

@@ -1,3 +1,2 @@
# tests/integration/api/__init__.py
"""API integration tests."""

View File

@@ -1,3 +1,2 @@
# tests/integration/api/v1/__init__.py
"""API v1 endpoint integration tests."""

View File

@@ -24,8 +24,8 @@ class TestAdminAPI:
assert response.status_code == 403
assert (
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
)
def test_toggle_user_status_admin(self, client, admin_headers, test_user):
@@ -48,7 +48,7 @@ class TestAdminAPI:
assert "User not found" in response.json()["detail"]
def test_toggle_user_status_cannot_deactivate_self(
self, client, admin_headers, test_admin
self, client, admin_headers, test_admin
):
"""Test that admin cannot deactivate their own account"""
response = client.put(
@@ -79,8 +79,8 @@ class TestAdminAPI:
assert response.status_code == 403
assert (
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
)
def test_verify_shop_admin(self, client, admin_headers, test_shop):
@@ -120,7 +120,7 @@ class TestAdminAPI:
assert "Shop not found" in response.json()["detail"]
def test_get_marketplace_import_jobs_admin(
self, client, admin_headers, test_marketplace_job
self, client, admin_headers, test_marketplace_job
):
"""Test admin getting marketplace import jobs"""
response = client.get(
@@ -136,7 +136,7 @@ class TestAdminAPI:
assert test_marketplace_job.id in job_ids
def test_get_marketplace_import_jobs_with_filters(
self, client, admin_headers, test_marketplace_job
self, client, admin_headers, test_marketplace_job
):
"""Test admin getting marketplace import jobs with filters"""
response = client.get(
@@ -160,8 +160,8 @@ class TestAdminAPI:
assert response.status_code == 403
assert (
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
"Access denied" in response.json()["detail"]
or "admin" in response.json()["detail"].lower()
)
def test_admin_pagination_users(self, client, admin_headers, test_user, test_admin):

View File

@@ -79,7 +79,9 @@ class TestPagination:
db.commit()
# Test first page
response = client.get("/api/v1/admin/shops?limit=5&skip=0", headers=admin_headers)
response = client.get(
"/api/v1/admin/shops?limit=5&skip=0", headers=admin_headers
)
assert response.status_code == 200
data = response.json()
assert len(data["shops"]) == 5

View File

@@ -3,4 +3,3 @@
import pytest
# Add any integration-specific fixtures here if needed

View File

@@ -1,3 +1,2 @@
# tests/integration/security/__init__.py
"""Security integration tests."""

View File

@@ -48,7 +48,9 @@ class TestAuthentication:
print(f"Admin endpoint - Raw: {response.content}")
# Test 2: Try a regular endpoint that uses get_current_user
response2 = client.get("/api/v1/product") # or any endpoint with get_current_user
response2 = client.get(
"/api/v1/product"
) # or any endpoint with get_current_user
print(f"Regular endpoint - Status: {response2.status_code}")
try:
print(f"Regular endpoint - Response: {response2.json()}")

View File

@@ -36,11 +36,15 @@ class TestAuthorization:
response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 200 # Regular user should have access
def test_shop_owner_access_control(self, client, auth_headers, test_shop, other_user):
def test_shop_owner_access_control(
self, client, auth_headers, test_shop, other_user
):
"""Test that users can only access their own shops"""
# Test accessing own shop (should work)
response = client.get(f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers)
response = client.get(
f"/api/v1/shop/{test_shop.shop_code}", headers=auth_headers
)
# Response depends on your implementation - could be 200 or 404 if shop doesn't belong to user
# The exact assertion depends on your shop access control implementation
assert response.status_code in [200, 403, 404]

View File

@@ -50,9 +50,7 @@ class TestInputValidation:
"""Test JSON validation for POST requests"""
# Test invalid JSON structure
response = client.post(
"/api/v1/product",
headers=auth_headers,
content="invalid json content"
"/api/v1/product", headers=auth_headers, content="invalid json content"
)
assert response.status_code == 422 # JSON decode error
@@ -60,6 +58,6 @@ class TestInputValidation:
response = client.post(
"/api/v1/product",
headers=auth_headers,
json={"title": "Test Product"} # Missing required product_id
json={"title": "Test Product"}, # Missing required product_id
)
assert response.status_code == 422 # Validation error

View File

@@ -7,6 +7,7 @@ import pytest
from app.tasks.background_tasks import process_marketplace_import
from models.database.marketplace import MarketplaceImportJob
@pytest.mark.integration
@pytest.mark.database
@pytest.mark.marketplace

View File

@@ -1,6 +1,7 @@
# tests/test_integration.py
import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.e2e

View File

@@ -1,3 +1,2 @@
# tests/performance/__init__.py
"""Performance and load tests."""

View File

@@ -2,9 +2,9 @@
"""Performance test specific fixtures."""
import pytest
@pytest.fixture
def performance_db_session(db):
"""Database session optimized for performance testing"""
# You can add performance-specific DB configurations here
return db

View File

@@ -67,7 +67,7 @@ class TestPerformance:
products = []
brands = ["Brand1", "Brand2", "Brand3"]
marketplaces = ["Market1", "Market2"]
for i in range(200):
product = Product(
product_id=f"COMPLEX{i:03d}",
@@ -85,13 +85,15 @@ class TestPerformance:
# Test complex filtering performance
start_time = time.time()
response = client.get(
"/api/v1/product?brand=Brand1&marketplace=Market1&limit=50",
headers=auth_headers
"/api/v1/product?brand=Brand1&marketplace=Market1&limit=50",
headers=auth_headers,
)
end_time = time.time()
assert response.status_code == 200
assert end_time - start_time < 1.5 # Complex query should still be reasonably fast
assert (
end_time - start_time < 1.5
) # Complex query should still be reasonably fast
def test_pagination_performance_large_dataset(self, client, auth_headers, db):
"""Test pagination performance with large dataset"""
@@ -113,11 +115,12 @@ class TestPerformance:
for offset in offsets:
start_time = time.time()
response = client.get(
f"/api/v1/product?skip={offset}&limit=20",
headers=auth_headers
f"/api/v1/product?skip={offset}&limit=20", headers=auth_headers
)
end_time = time.time()
assert response.status_code == 200
assert len(response.json()["products"]) == 20
assert end_time - start_time < 1.0 # Pagination should be fast regardless of offset
assert (
end_time - start_time < 1.0
) # Pagination should be fast regardless of offset

View File

@@ -1,3 +1,2 @@
# tests/system/__init__.py
"""System-level tests - full application behavior."""

View File

@@ -3,4 +3,3 @@
import pytest
# Add any system-specific fixtures here if needed

View File

@@ -72,13 +72,17 @@ class TestErrorHandling:
"""Test handling of various malformed requests"""
# Test extremely long URLs
long_search = "x" * 10000
response = client.get(f"/api/v1/product?search={long_search}", headers=auth_headers)
response = client.get(
f"/api/v1/product?search={long_search}", headers=auth_headers
)
# Should handle gracefully, either 200 with no results or 422 for too long
assert response.status_code in [200, 422]
# Test special characters in parameters
special_chars = "!@#$%^&*(){}[]|\\:;\"'<>,.?/~`"
response = client.get(f"/api/v1/product?search={special_chars}", headers=auth_headers)
response = client.get(
f"/api/v1/product?search={special_chars}", headers=auth_headers
)
# Should handle gracefully
assert response.status_code in [200, 422]
@@ -95,9 +99,13 @@ class TestErrorHandling:
response = client.post(
"/api/v1/product",
headers={**auth_headers, "Content-Type": "application/xml"},
content="<xml>not json</xml>"
content="<xml>not json</xml>",
)
assert response.status_code in [400, 422, 415] # Bad request or unsupported media type
assert response.status_code in [
400,
422,
415,
] # Bad request or unsupported media type
def test_large_payload_handling(self, client, auth_headers):
"""Test handling of unusually large payloads"""
@@ -105,9 +113,9 @@ class TestErrorHandling:
large_data = {
"product_id": "LARGE_TEST",
"title": "Large Test Product",
"description": "x" * 50000 # Very long description
"description": "x" * 50000, # Very long description
}
response = client.post("/api/v1/product", headers=auth_headers, json=large_data)
# Should either accept it or reject with 422 (too large)
assert response.status_code in [200, 201, 422, 413]
assert response.status_code in [200, 201, 422, 413]

View File

@@ -1,3 +1,2 @@
# tests/unit/__init__.py
"""Unit tests - fast, isolated component tests."""

View File

@@ -3,4 +3,3 @@
import pytest
# Add any unit-specific fixtures here if needed

View File

@@ -6,6 +6,7 @@ import pytest
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
@pytest.mark.unit
@pytest.mark.auth # for auth manager tests
class TestRateLimiter:

View File

@@ -1,3 +1,2 @@
# tests/unit/models/__init__.py
"""Database and API model unit tests."""

View File

@@ -1,3 +1,2 @@
# tests/unit/services/__init__.py
"""Service layer unit tests."""

View File

@@ -114,4 +114,4 @@ class TestAdminService:
assert test_job is not None
assert test_job.marketplace == test_marketplace_job.marketplace
assert test_job.shop_name == test_marketplace_job.shop_name
assert test_job.status == test_marketplace_job.status
assert test_job.status == test_marketplace_job.status

View File

@@ -6,6 +6,7 @@ from app.services.auth_service import AuthService
from models.api.auth import UserLogin, UserRegister
from models.database.user import User
@pytest.mark.unit
@pytest.mark.auth
class TestAuthService:

View File

@@ -10,6 +10,7 @@ from models.database.marketplace import MarketplaceImportJob
from models.database.shop import Shop
from models.database.user import User
@pytest.mark.unit
@pytest.mark.marketplace
class TestMarketplaceService:

View File

@@ -5,6 +5,7 @@ from app.services.product_service import ProductService
from models.api.product import ProductCreate
from models.database.product import Product
@pytest.mark.unit
@pytest.mark.products
class TestProductService:

View File

@@ -5,6 +5,7 @@ from fastapi import HTTPException
from app.services.shop_service import ShopService
from models.api.shop import ShopCreate, ShopProductCreate
@pytest.mark.unit
@pytest.mark.shops
class TestShopService:

View File

@@ -5,6 +5,7 @@ from app.services.stats_service import StatsService
from models.database.product import Product
from models.database.stock import Stock
@pytest.mark.unit
@pytest.mark.stats
class TestStatsService:

View File

@@ -8,6 +8,7 @@ from models.api.stock import StockAdd, StockCreate, StockUpdate
from models.database.product import Product
from models.database.stock import Stock
@pytest.mark.unit
@pytest.mark.stock
class TestStockService:

View File

@@ -1,3 +1,2 @@
# tests/unit/utils/__init__.py
"""Utility function unit tests."""

View File

@@ -8,6 +8,7 @@ import requests.exceptions
from utils.csv_processor import CSVProcessor
@pytest.mark.unit
class TestCSVProcessor:
def setup_method(self):
@@ -80,7 +81,9 @@ class TestCSVProcessor:
# Mock failed HTTP response - need to make raise_for_status() raise an exception
mock_response = Mock()
mock_response.status_code = 404
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
"404 Not Found"
)
mock_get.return_value = mock_response
with pytest.raises(requests.exceptions.HTTPError):

View File

@@ -3,6 +3,7 @@ import pytest
from utils.data_processing import GTINProcessor, PriceProcessor
@pytest.mark.unit
class TestDataValidation:
def test_gtin_normalization_edge_cases(self):