QC check
This commit is contained in:
199
Makefile
199
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
|
||||
@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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
43
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():
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -19,4 +19,4 @@ class MarketplaceStatsResponse(BaseModel):
|
||||
marketplace: str
|
||||
total_products: int
|
||||
unique_shops: int
|
||||
unique_brands: int
|
||||
unique_brands: int
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}')>"
|
||||
)
|
||||
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
35
scripts/backup_database.py
Normal file
35
scripts/backup_database.py
Normal 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()
|
||||
@@ -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",
|
||||
]
|
||||
]
|
||||
|
||||
1
tests/fixtures/__init__.py
vendored
1
tests/fixtures/__init__.py
vendored
@@ -1,3 +1,2 @@
|
||||
# tests/fixtures/__init__.py
|
||||
"""Test fixtures for the FastAPI application test suite."""
|
||||
|
||||
|
||||
2
tests/fixtures/shop_fixtures.py
vendored
2
tests/fixtures/shop_fixtures.py
vendored
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/integration/__init__.py
|
||||
"""Integration tests - multiple components working together."""
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/integration/api/__init__.py
|
||||
"""API integration tests."""
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/integration/api/v1/__init__.py
|
||||
"""API v1 endpoint integration tests."""
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
import pytest
|
||||
|
||||
# Add any integration-specific fixtures here if needed
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/integration/security/__init__.py
|
||||
"""Security integration tests."""
|
||||
|
||||
|
||||
@@ -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()}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# tests/test_integration.py
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.api
|
||||
@pytest.mark.e2e
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/performance/__init__.py
|
||||
"""Performance and load tests."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/system/__init__.py
|
||||
"""System-level tests - full application behavior."""
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
import pytest
|
||||
|
||||
# Add any system-specific fixtures here if needed
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/unit/__init__.py
|
||||
"""Unit tests - fast, isolated component tests."""
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
import pytest
|
||||
|
||||
# Add any unit-specific fixtures here if needed
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/unit/models/__init__.py
|
||||
"""Database and API model unit tests."""
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/unit/services/__init__.py
|
||||
"""Service layer unit tests."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# tests/unit/utils/__init__.py
|
||||
"""Utility function unit tests."""
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user