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: dev:
uvicorn main:app --reload --host 0.0.0.0 --port 8000 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: dev-with-docs:
@echo Starting API server and documentation server... @echo Starting API server and documentation server...
@start /B uvicorn main:app --reload --host 0.0.0.0 --port 8000 @start /B uvicorn main:app --reload --host 0.0.0.0 --port 8000
@timeout /t 3 >nul @timeout /t 3 >nul
@mkdocs serve --dev-addr=0.0.0.0:8001 @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 # Documentation commands
docs: docs-serve docs: docs-serve
@@ -77,13 +92,6 @@ docs-check:
docs-help: docs-help:
mkdocs --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 # Testing commands
test: test:
pytest tests/ -v pytest tests/ -v
@@ -138,19 +146,125 @@ check: format lint
# Combined test with coverage and linting # Combined test with coverage and linting
ci: format lint test-coverage 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 # Database migrations
migrate-create: 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: migrate-up:
@echo Running database migrations...
alembic upgrade head alembic upgrade head
@echo ✅ Migrations completed successfully
migrate-down: migrate-down:
@echo Rolling back last migration...
alembic downgrade -1 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: migrate-reset:
@echo Resetting database...
alembic downgrade base alembic downgrade base
alembic upgrade head 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 commands
docker-build: docker-build:
@@ -167,11 +281,16 @@ docker-logs:
docker-restart: docker-down docker-up 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 # Production deployment
deploy-staging: deploy-staging: migrate-up
docker-compose -f docker-compose.staging.yml up -d docker-compose -f docker-compose.staging.yml up -d
deploy-prod: deploy-prod: migrate-up
docker-compose -f docker-compose.prod.yml up -d docker-compose -f docker-compose.prod.yml up -d
# Documentation deployment workflow # Documentation deployment workflow
@@ -196,10 +315,14 @@ clean-all: clean docs-clean
@echo All build artifacts cleaned! @echo All build artifacts cleaned!
# Development workflow shortcuts # Development workflow shortcuts
setup: install-all migrate-up setup: install-all db-init
@echo Development environment setup complete! @echo Development environment setup complete!
@echo Run 'make dev-full' to start both API and documentation servers @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 setup-test: install-test
@echo Test environment setup complete! @echo Test environment setup complete!
@@ -218,6 +341,25 @@ qa: format lint test-coverage docs-check
release-check: qa docs-build release-check: qa docs-build
@echo Release readiness check completed! @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 command
help: help:
@echo Available commands: @echo Available commands:
@@ -264,10 +406,20 @@ help:
@echo ci - Full CI pipeline (format, lint, test) @echo ci - Full CI pipeline (format, lint, test)
@echo qa - Quality assurance (format, lint, test, docs check) @echo qa - Quality assurance (format, lint, test, docs check)
@echo. @echo.
@echo === DATABASE === @echo === DATABASE MIGRATIONS ===
@echo migrate-up - Run database migrations @echo migrate-create message="msg" - Create auto-generated migration
@echo migrate-down - Rollback last migration @echo migrate-create-manual message="msg" - Create empty migration template
@echo migrate-reset - Reset and rerun all migrations @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.
@echo === DOCKER === @echo === DOCKER ===
@echo docker-build - Build Docker containers @echo docker-build - Build Docker containers
@@ -286,4 +438,19 @@ help:
@echo make setup # First time setup @echo make setup # First time setup
@echo make dev-full # Start development environment @echo make dev-full # Start development environment
@echo make docs-serve # Start documentation server @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 app.core.database import get_db
from middleware.auth import AuthManager from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter from middleware.rate_limiter import RateLimiter
from models.database.user import User
from models.database.shop import Shop from models.database.shop import Shop
from models.database.user import User
# Set auto_error=False to prevent automatic 403 responses # Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False) 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.api.deps import get_current_admin_user
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from models.api.auth import UserResponse
from models.api.marketplace import MarketplaceImportJobResponse from models.api.marketplace import MarketplaceImportJobResponse
from models.api.shop import ShopListResponse from models.api.shop import ShopListResponse
from models.api.auth import UserResponse
from models.database.user import User from models.database.user import User
router = APIRouter() 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.core.database import get_db
from app.services.auth_service import auth_service from app.services.auth_service import auth_service
from models.api.auth import (LoginResponse, UserLogin, UserRegister, from models.api.auth import (LoginResponse, UserLogin, UserRegister,
UserResponse) UserResponse)
from models.database.user import User from models.database.user import User
router = APIRouter() 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 app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit from middleware.decorators import rate_limit
from models.api.marketplace import (MarketplaceImportJobResponse, from models.api.marketplace import (MarketplaceImportJobResponse,
MarketplaceImportRequest) MarketplaceImportRequest)
from models.database.user import User from models.database.user import User
router = APIRouter() 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.core.database import get_db
from app.services.product_service import product_service from app.services.product_service import product_service
from models.api.product import (ProductCreate, ProductDetailResponse, from models.api.product import (ProductCreate, ProductDetailResponse,
ProductListResponse, ProductResponse, ProductListResponse, ProductResponse,
ProductUpdate) ProductUpdate)
from models.database.user import User from models.database.user import User
router = APIRouter() 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.core.database import get_db
from app.services.shop_service import shop_service from app.services.shop_service import shop_service
from models.api.shop import (ShopCreate, ShopListResponse, ShopProductCreate, from models.api.shop import (ShopCreate, ShopListResponse, ShopProductCreate,
ShopProductResponse, ShopResponse) ShopProductResponse, ShopResponse)
from models.database.user import User from models.database.user import User
router = APIRouter() 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.core.database import get_db
from app.services.stock_service import stock_service from app.services.stock_service import stock_service
from models.api.stock import (StockAdd, StockCreate, StockResponse, from models.api.stock import (StockAdd, StockCreate, StockResponse,
StockSummaryResponse, StockUpdate) StockSummaryResponse, StockUpdate)
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()

View File

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

View File

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

43
main.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,13 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, DateTime from sqlalchemy import Column, DateTime
from app.core.database import Base from app.core.database import Base
class TimestampMixin: class TimestampMixin:
"""Mixin to add created_at and updated_at timestamps to models""" """Mixin to add created_at and updated_at timestamps to models"""
created_at = Column(DateTime, default=datetime.utcnow, nullable=False) created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column( updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False 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"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>" f"shop='{self.shop_name}')>"
) )

View File

@@ -43,6 +43,7 @@ class Shop(Base):
"MarketplaceImportJob", back_populates="shop" "MarketplaceImportJob", back_populates="shop"
) )
class ShopProduct(Base): class ShopProduct(Base):
__tablename__ = "shop_products" __tablename__ = "shop_products"
@@ -81,4 +82,3 @@ class ShopProduct(Base):
Index("idx_shop_product_active", "shop_id", "is_active"), Index("idx_shop_product_active", "shop_id", "is_active"),
Index("idx_shop_product_featured", "shop_id", "is_featured"), 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 # Import all models to ensure they're registered with Base metadata
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace import MarketplaceImportJob
from models.database.product import Product 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.stock import Stock
from models.database.user import User from models.database.user import User
@@ -87,7 +87,7 @@ def cleanup():
# Import fixtures from fixture modules # Import fixtures from fixture modules
pytest_plugins = [ pytest_plugins = [
"tests.fixtures.auth_fixtures", "tests.fixtures.auth_fixtures",
"tests.fixtures.product_fixtures", "tests.fixtures.product_fixtures",
"tests.fixtures.shop_fixtures", "tests.fixtures.shop_fixtures",
"tests.fixtures.marketplace_fixtures", "tests.fixtures.marketplace_fixtures",
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,7 +79,9 @@ class TestPagination:
db.commit() db.commit()
# Test first page # 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 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["shops"]) == 5 assert len(data["shops"]) == 5

View File

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

View File

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

View File

@@ -48,7 +48,9 @@ class TestAuthentication:
print(f"Admin endpoint - Raw: {response.content}") print(f"Admin endpoint - Raw: {response.content}")
# Test 2: Try a regular endpoint that uses get_current_user # 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}") print(f"Regular endpoint - Status: {response2.status_code}")
try: try:
print(f"Regular endpoint - Response: {response2.json()}") print(f"Regular endpoint - Response: {response2.json()}")

View File

@@ -36,11 +36,15 @@ class TestAuthorization:
response = client.get(endpoint, headers=auth_headers) response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 200 # Regular user should have access 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 that users can only access their own shops"""
# Test accessing own shop (should work) # 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 # 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 # The exact assertion depends on your shop access control implementation
assert response.status_code in [200, 403, 404] assert response.status_code in [200, 403, 404]

View File

@@ -50,9 +50,7 @@ class TestInputValidation:
"""Test JSON validation for POST requests""" """Test JSON validation for POST requests"""
# Test invalid JSON structure # Test invalid JSON structure
response = client.post( response = client.post(
"/api/v1/product", "/api/v1/product", headers=auth_headers, content="invalid json content"
headers=auth_headers,
content="invalid json content"
) )
assert response.status_code == 422 # JSON decode error assert response.status_code == 422 # JSON decode error
@@ -60,6 +58,6 @@ class TestInputValidation:
response = client.post( response = client.post(
"/api/v1/product", "/api/v1/product",
headers=auth_headers, 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 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 app.tasks.background_tasks import process_marketplace_import
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace import MarketplaceImportJob
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.database @pytest.mark.database
@pytest.mark.marketplace @pytest.mark.marketplace

View File

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

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ class TestPerformance:
products = [] products = []
brands = ["Brand1", "Brand2", "Brand3"] brands = ["Brand1", "Brand2", "Brand3"]
marketplaces = ["Market1", "Market2"] marketplaces = ["Market1", "Market2"]
for i in range(200): for i in range(200):
product = Product( product = Product(
product_id=f"COMPLEX{i:03d}", product_id=f"COMPLEX{i:03d}",
@@ -85,13 +85,15 @@ class TestPerformance:
# Test complex filtering performance # Test complex filtering performance
start_time = time.time() start_time = time.time()
response = client.get( response = client.get(
"/api/v1/product?brand=Brand1&marketplace=Market1&limit=50", "/api/v1/product?brand=Brand1&marketplace=Market1&limit=50",
headers=auth_headers headers=auth_headers,
) )
end_time = time.time() end_time = time.time()
assert response.status_code == 200 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): def test_pagination_performance_large_dataset(self, client, auth_headers, db):
"""Test pagination performance with large dataset""" """Test pagination performance with large dataset"""
@@ -113,11 +115,12 @@ class TestPerformance:
for offset in offsets: for offset in offsets:
start_time = time.time() start_time = time.time()
response = client.get( response = client.get(
f"/api/v1/product?skip={offset}&limit=20", f"/api/v1/product?skip={offset}&limit=20", headers=auth_headers
headers=auth_headers
) )
end_time = time.time() end_time = time.time()
assert response.status_code == 200 assert response.status_code == 200
assert len(response.json()["products"]) == 20 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 # tests/system/__init__.py
"""System-level tests - full application behavior.""" """System-level tests - full application behavior."""

View File

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

View File

@@ -72,13 +72,17 @@ class TestErrorHandling:
"""Test handling of various malformed requests""" """Test handling of various malformed requests"""
# Test extremely long URLs # Test extremely long URLs
long_search = "x" * 10000 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 # Should handle gracefully, either 200 with no results or 422 for too long
assert response.status_code in [200, 422] assert response.status_code in [200, 422]
# Test special characters in parameters # Test special characters in parameters
special_chars = "!@#$%^&*(){}[]|\\:;\"'<>,.?/~`" 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 # Should handle gracefully
assert response.status_code in [200, 422] assert response.status_code in [200, 422]
@@ -95,9 +99,13 @@ class TestErrorHandling:
response = client.post( response = client.post(
"/api/v1/product", "/api/v1/product",
headers={**auth_headers, "Content-Type": "application/xml"}, 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): def test_large_payload_handling(self, client, auth_headers):
"""Test handling of unusually large payloads""" """Test handling of unusually large payloads"""
@@ -105,9 +113,9 @@ class TestErrorHandling:
large_data = { large_data = {
"product_id": "LARGE_TEST", "product_id": "LARGE_TEST",
"title": "Large Test Product", "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) response = client.post("/api/v1/product", headers=auth_headers, json=large_data)
# Should either accept it or reject with 422 (too large) # 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 # tests/unit/__init__.py
"""Unit tests - fast, isolated component tests.""" """Unit tests - fast, isolated component tests."""

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,4 +114,4 @@ class TestAdminService:
assert test_job is not None assert test_job is not None
assert test_job.marketplace == test_marketplace_job.marketplace assert test_job.marketplace == test_marketplace_job.marketplace
assert test_job.shop_name == test_marketplace_job.shop_name 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.api.auth import UserLogin, UserRegister
from models.database.user import User from models.database.user import User
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.auth @pytest.mark.auth
class TestAuthService: class TestAuthService:

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ from app.services.stats_service import StatsService
from models.database.product import Product from models.database.product import Product
from models.database.stock import Stock from models.database.stock import Stock
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.stats @pytest.mark.stats
class TestStatsService: 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.product import Product
from models.database.stock import Stock from models.database.stock import Stock
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.stock @pytest.mark.stock
class TestStockService: class TestStockService:

View File

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

View File

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

View File

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