diff --git a/Makefile b/Makefile index 742c4b63..3729c371 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + @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 diff --git a/app/api/deps.py b/app/api/deps.py index 2fcc7ed9..66f78584 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -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) diff --git a/app/api/v1/admin.py b/app/api/v1/admin.py index a968eb0f..0a461ec7 100644 --- a/app/api/v1/admin.py +++ b/app/api/v1/admin.py @@ -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() diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py index 96c60b3f..223d9ee1 100644 --- a/app/api/v1/auth.py +++ b/app/api/v1/auth.py @@ -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() diff --git a/app/api/v1/marketplace.py b/app/api/v1/marketplace.py index 4560648a..f04ace87 100644 --- a/app/api/v1/marketplace.py +++ b/app/api/v1/marketplace.py @@ -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() diff --git a/app/api/v1/product.py b/app/api/v1/product.py index 92f8bcfe..4361f8bf 100644 --- a/app/api/v1/product.py +++ b/app/api/v1/product.py @@ -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() diff --git a/app/api/v1/shop.py b/app/api/v1/shop.py index 5051fe76..7a183e40 100644 --- a/app/api/v1/shop.py +++ b/app/api/v1/shop.py @@ -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() diff --git a/app/api/v1/stock.py b/app/api/v1/stock.py index c41b62ac..b747949e 100644 --- a/app/api/v1/stock.py +++ b/app/api/v1/stock.py @@ -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() diff --git a/app/services/marketplace_service.py b/app/services/marketplace_service.py index 1746de0e..e312bfe7 100644 --- a/app/services/marketplace_service.py +++ b/app/services/marketplace_service.py @@ -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 diff --git a/app/services/stock_service.py b/app/services/stock_service.py index b362ee78..ed04bed7 100644 --- a/app/services/stock_service.py +++ b/app/services/stock_service.py @@ -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 diff --git a/main.py b/main.py index a2cef52b..4f01d278 100644 --- a/main.py +++ b/main.py @@ -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(): """ + if __name__ == "__main__": import uvicorn diff --git a/models/api/base.py b/models/api/base.py index 0b26ddd3..122a1a11 100644 --- a/models/api/base.py +++ b/models/api/base.py @@ -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 diff --git a/models/api/product.py b/models/api/product.py index d587be77..68ddb07c 100644 --- a/models/api/product.py +++ b/models/api/product.py @@ -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 diff --git a/models/api/shop.py b/models/api/shop.py index d27fd9b7..149ebdbe 100644 --- a/models/api/shop.py +++ b/models/api/shop.py @@ -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( diff --git a/models/api/stats.py b/models/api/stats.py index 9d09a208..5aa7c478 100644 --- a/models/api/stats.py +++ b/models/api/stats.py @@ -19,4 +19,4 @@ class MarketplaceStatsResponse(BaseModel): marketplace: str total_products: int unique_shops: int - unique_brands: int \ No newline at end of file + unique_brands: int diff --git a/models/api/stock.py b/models/api/stock.py index f6d057bb..c0af5c78 100644 --- a/models/api/stock.py +++ b/models/api/stock.py @@ -43,4 +43,4 @@ class StockSummaryResponse(BaseModel): gtin: str total_quantity: int locations: List[StockLocationResponse] - product_title: Optional[str] = None \ No newline at end of file + product_title: Optional[str] = None diff --git a/models/database/base.py b/models/database/base.py index c7e48696..eebacf20 100644 --- a/models/database/base.py +++ b/models/database/base.py @@ -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 diff --git a/models/database/product.py b/models/database/product.py index 5181d95e..d070a6d0 100644 --- a/models/database/product.py +++ b/models/database/product.py @@ -85,4 +85,3 @@ class Product(Base): f"" ) - diff --git a/models/database/shop.py b/models/database/shop.py index 09ca6ffd..577f576c 100644 --- a/models/database/shop.py +++ b/models/database/shop.py @@ -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"), ) - diff --git a/scripts/backup_database.py b/scripts/backup_database.py new file mode 100644 index 00000000..19cf30c7 --- /dev/null +++ b/scripts/backup_database.py @@ -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() diff --git a/tests/conftest.py b/tests/conftest.py index 4c2dd1f0..98f3fd69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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", -] \ No newline at end of file +] diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 6b4c7600..e073d03b 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,3 +1,2 @@ # tests/fixtures/__init__.py """Test fixtures for the FastAPI application test suite.""" - diff --git a/tests/fixtures/shop_fixtures.py b/tests/fixtures/shop_fixtures.py index 1f69596a..0bb492ee 100644 --- a/tests/fixtures/shop_fixtures.py +++ b/tests/fixtures/shop_fixtures.py @@ -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 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index d939ba99..b84f12b7 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,3 +1,2 @@ # tests/integration/__init__.py """Integration tests - multiple components working together.""" - diff --git a/tests/integration/api/__init__.py b/tests/integration/api/__init__.py index 5c1601f1..6be8150c 100644 --- a/tests/integration/api/__init__.py +++ b/tests/integration/api/__init__.py @@ -1,3 +1,2 @@ # tests/integration/api/__init__.py """API integration tests.""" - diff --git a/tests/integration/api/v1/__init__.py b/tests/integration/api/v1/__init__.py index bcc8c7d5..c6d716d6 100644 --- a/tests/integration/api/v1/__init__.py +++ b/tests/integration/api/v1/__init__.py @@ -1,3 +1,2 @@ # tests/integration/api/v1/__init__.py """API v1 endpoint integration tests.""" - diff --git a/tests/integration/api/v1/test_admin_endpoints.py b/tests/integration/api/v1/test_admin_endpoints.py index 0db2b198..5e756e26 100644 --- a/tests/integration/api/v1/test_admin_endpoints.py +++ b/tests/integration/api/v1/test_admin_endpoints.py @@ -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): diff --git a/tests/integration/api/v1/test_pagination.py b/tests/integration/api/v1/test_pagination.py index 8e6eb083..463a7e4e 100644 --- a/tests/integration/api/v1/test_pagination.py +++ b/tests/integration/api/v1/test_pagination.py @@ -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 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 703f8685..41f1893b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,4 +3,3 @@ import pytest # Add any integration-specific fixtures here if needed - diff --git a/tests/integration/security/__init__.py b/tests/integration/security/__init__.py index dc1e90f7..5de2bf1f 100644 --- a/tests/integration/security/__init__.py +++ b/tests/integration/security/__init__.py @@ -1,3 +1,2 @@ # tests/integration/security/__init__.py """Security integration tests.""" - diff --git a/tests/integration/security/test_authentication.py b/tests/integration/security/test_authentication.py index 8b4d4fa0..1ad4cb33 100644 --- a/tests/integration/security/test_authentication.py +++ b/tests/integration/security/test_authentication.py @@ -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()}") diff --git a/tests/integration/security/test_authorization.py b/tests/integration/security/test_authorization.py index 26ab9e3d..6a7b392e 100644 --- a/tests/integration/security/test_authorization.py +++ b/tests/integration/security/test_authorization.py @@ -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] diff --git a/tests/integration/security/test_input_validation.py b/tests/integration/security/test_input_validation.py index 6afedfdd..dfaaec38 100644 --- a/tests/integration/security/test_input_validation.py +++ b/tests/integration/security/test_input_validation.py @@ -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 diff --git a/tests/integration/tasks/test_background_tasks.py b/tests/integration/tasks/test_background_tasks.py index 20b6a927..31478f10 100644 --- a/tests/integration/tasks/test_background_tasks.py +++ b/tests/integration/tasks/test_background_tasks.py @@ -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 diff --git a/tests/integration/workflows/test_integration.py b/tests/integration/workflows/test_integration.py index 3902a774..faab5054 100644 --- a/tests/integration/workflows/test_integration.py +++ b/tests/integration/workflows/test_integration.py @@ -1,6 +1,7 @@ # tests/test_integration.py import pytest + @pytest.mark.integration @pytest.mark.api @pytest.mark.e2e diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py index 2f4789ba..3eb1c40f 100644 --- a/tests/performance/__init__.py +++ b/tests/performance/__init__.py @@ -1,3 +1,2 @@ # tests/performance/__init__.py """Performance and load tests.""" - diff --git a/tests/performance/conftest.py b/tests/performance/conftest.py index 22259230..b514e22c 100644 --- a/tests/performance/conftest.py +++ b/tests/performance/conftest.py @@ -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 - diff --git a/tests/performance/test_api_performance.py b/tests/performance/test_api_performance.py index a8ad027e..17c9230b 100644 --- a/tests/performance/test_api_performance.py +++ b/tests/performance/test_api_performance.py @@ -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 diff --git a/tests/system/__init__.py b/tests/system/__init__.py index ac25623d..9202004b 100644 --- a/tests/system/__init__.py +++ b/tests/system/__init__.py @@ -1,3 +1,2 @@ # tests/system/__init__.py """System-level tests - full application behavior.""" - diff --git a/tests/system/conftest.py b/tests/system/conftest.py index 8c6bc72a..bc0c4f6a 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -3,4 +3,3 @@ import pytest # Add any system-specific fixtures here if needed - diff --git a/tests/system/test_error_handling.py b/tests/system/test_error_handling.py index cd57131c..c3f0ca9d 100644 --- a/tests/system/test_error_handling.py +++ b/tests/system/test_error_handling.py @@ -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="not json" + content="not json", ) - 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] \ No newline at end of file + assert response.status_code in [200, 201, 422, 413] diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 09f23950..5bf0cd9b 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,3 +1,2 @@ # tests/unit/__init__.py """Unit tests - fast, isolated component tests.""" - diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 4f9cda6c..3c833b5d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,4 +3,3 @@ import pytest # Add any unit-specific fixtures here if needed - diff --git a/tests/unit/middleware/test_middleware.py b/tests/unit/middleware/test_middleware.py index 4e4bdcb2..27c0616d 100644 --- a/tests/unit/middleware/test_middleware.py +++ b/tests/unit/middleware/test_middleware.py @@ -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: diff --git a/tests/unit/models/__init__.py b/tests/unit/models/__init__.py index f8fd345e..9720e510 100644 --- a/tests/unit/models/__init__.py +++ b/tests/unit/models/__init__.py @@ -1,3 +1,2 @@ # tests/unit/models/__init__.py """Database and API model unit tests.""" - diff --git a/tests/unit/services/__init__.py b/tests/unit/services/__init__.py index 705c4cc8..ed28fd73 100644 --- a/tests/unit/services/__init__.py +++ b/tests/unit/services/__init__.py @@ -1,3 +1,2 @@ # tests/unit/services/__init__.py """Service layer unit tests.""" - diff --git a/tests/unit/services/test_admin_service.py b/tests/unit/services/test_admin_service.py index ed650f5d..9eb64456 100644 --- a/tests/unit/services/test_admin_service.py +++ b/tests/unit/services/test_admin_service.py @@ -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 \ No newline at end of file + assert test_job.status == test_marketplace_job.status diff --git a/tests/unit/services/test_auth_service.py b/tests/unit/services/test_auth_service.py index b9a80c6c..b7a9d1cf 100644 --- a/tests/unit/services/test_auth_service.py +++ b/tests/unit/services/test_auth_service.py @@ -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: diff --git a/tests/unit/services/test_marketplace_service.py b/tests/unit/services/test_marketplace_service.py index e2ba0cde..da14c4d9 100644 --- a/tests/unit/services/test_marketplace_service.py +++ b/tests/unit/services/test_marketplace_service.py @@ -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: diff --git a/tests/unit/services/test_product_service.py b/tests/unit/services/test_product_service.py index ec974d6b..fe7000e4 100644 --- a/tests/unit/services/test_product_service.py +++ b/tests/unit/services/test_product_service.py @@ -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: diff --git a/tests/unit/services/test_shop_service.py b/tests/unit/services/test_shop_service.py index 1eb7ee73..7a89da05 100644 --- a/tests/unit/services/test_shop_service.py +++ b/tests/unit/services/test_shop_service.py @@ -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: diff --git a/tests/unit/services/test_stats_service.py b/tests/unit/services/test_stats_service.py index 6a7eb2a6..b0c8cd8e 100644 --- a/tests/unit/services/test_stats_service.py +++ b/tests/unit/services/test_stats_service.py @@ -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: diff --git a/tests/unit/services/test_stock_service.py b/tests/unit/services/test_stock_service.py index 8199f682..7699590f 100644 --- a/tests/unit/services/test_stock_service.py +++ b/tests/unit/services/test_stock_service.py @@ -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: diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py index 377bd90d..fb60a644 100644 --- a/tests/unit/utils/__init__.py +++ b/tests/unit/utils/__init__.py @@ -1,3 +1,2 @@ # tests/unit/utils/__init__.py """Utility function unit tests.""" - diff --git a/tests/unit/utils/test_csv_processor.py b/tests/unit/utils/test_csv_processor.py index 559b5652..7fdc30f5 100644 --- a/tests/unit/utils/test_csv_processor.py +++ b/tests/unit/utils/test_csv_processor.py @@ -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): diff --git a/tests/unit/utils/test_data_validation.py b/tests/unit/utils/test_data_validation.py index 7c9cc5a1..27af0cea 100644 --- a/tests/unit/utils/test_data_validation.py +++ b/tests/unit/utils/test_data_validation.py @@ -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):