diff --git a/app/core/config.py b/app/core/config.py index ae6b9138..24876c20 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -18,7 +18,43 @@ class Settings(BaseSettings): # Project information project_name: str = "Ecommerce Backend API with Marketplace Support" - description: str = "Advanced product management system with JWT authentication" + description: str = """ + ## ๐Ÿš€ Letzshop Import API + + Complete marketplace product import and management system built with FastAPI. + + ### ๐Ÿ“š Documentation Links + + - **[Complete Documentation Site](#)** - Full project documentation + - **[Getting Started Guide](#)** - Installation and setup + - **[User Guides](#)** - How-to guides and tutorials + - **[API Authentication Guide](#)** - Security and authentication + - **[Testing Documentation](#)** - Test suite and conventions + + ### ๐Ÿ”— Quick Links + + - **[Alternative API Docs](/redoc)** - ReDoc interface + - **[Health Check](/health)** - System status + - **[OpenAPI Spec](/openapi.json)** - Machine-readable API spec + + ### ๐Ÿ“– Key Features + + - **Product Management** - Complete CRUD operations with validation + - **Multi-Shop Support** - Independent shop configurations + - **CSV Import System** - Bulk import from various marketplace formats + - **Stock Management** - Inventory tracking across locations + - **User Management** - Role-based access control + - **Marketplace Integration** - Import from multiple platforms + + ### ๐Ÿ—๏ธ Architecture + + Built with modern Python stack: + - **FastAPI** - High-performance async API framework + - **SQLAlchemy** - Powerful ORM with PostgreSQL + - **Pydantic** - Data validation and serialization + - **JWT Authentication** - Secure token-based auth + - **pytest** - Comprehensive test suite + """ version: str = "2.2.0" # Database diff --git a/tests/test_background_tasks.py b/backup/test_background_tasks.py similarity index 100% rename from tests/test_background_tasks.py rename to backup/test_background_tasks.py diff --git a/tests/test_export.py b/backup/test_export.py similarity index 100% rename from tests/test_export.py rename to backup/test_export.py diff --git a/tests/test_filtering.py b/backup/test_filtering.py similarity index 100% rename from tests/test_filtering.py rename to backup/test_filtering.py diff --git a/tests/test_integration.py b/backup/test_integration.py similarity index 100% rename from tests/test_integration.py rename to backup/test_integration.py diff --git a/tests/test_middleware.py b/backup/test_middleware.py similarity index 100% rename from tests/test_middleware.py rename to backup/test_middleware.py diff --git a/comprehensive_readme.md b/comprehensive_readme.md index 3a4531e7..2a27c680 100644 --- a/comprehensive_readme.md +++ b/comprehensive_readme.md @@ -318,9 +318,9 @@ pytest tests/ -m integration -v # Integration tests only pytest tests/ -m "not slow" -v # Fast tests only # Run specific test files -pytest tests/test_auth.py -v # Authentication tests -pytest tests/test_product.py -v # Product tests -pytest tests/test_stock.py -v # Stock management tests +pytest tests/test_authentication_endpoints.py -v # Authentication tests +pytest tests/test_product_endpoints.py -v # Product tests +pytest tests/test_stock_endpoints.py -v # Stock management tests ``` ### Test Coverage diff --git a/enhanced_pytest_config.txt b/enhanced_pytest_config.txt deleted file mode 100644 index 4baa5db2..00000000 --- a/enhanced_pytest_config.txt +++ /dev/null @@ -1,80 +0,0 @@ -# pytest.ini - Enhanced configuration for your FastAPI test suite -[tool:pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Enhanced addopts for better development experience -addopts = - -v - --tb=short - --strict-markers - --strict-config - --color=yes - --durations=10 - --showlocals - -ra - --cov=app - --cov=models - --cov=utils - --cov=middleware - --cov-report=term-missing - --cov-report=html:htmlcov - --cov-fail-under=80 - -# Test discovery and execution settings -minversion = 6.0 -testmon = true -python_paths = . - -# Markers for your specific test organization -markers = - # Test Types (for your new structure) - unit: Unit tests - fast, isolated components - integration: Integration tests - multiple components working together - system: System tests - full application behavior - e2e: End-to-end tests - complete user workflows - - # Performance and Speed - slow: Slow running tests (deselect with '-m "not slow"') - performance: Performance and load tests - - # Domain-specific markers (matching your application structure) - auth: Authentication and authorization tests - products: Product management functionality - stock: Stock and inventory management - shops: Shop management functionality - admin: Admin functionality and permissions - marketplace: Marketplace import functionality - stats: Statistics and reporting - - # Infrastructure markers - database: Tests that require database operations - external: Tests that require external services - api: API endpoint tests - security: Security-related tests - - # Test environment markers - ci: Tests that should only run in CI - dev: Development-specific tests - -# Test filtering shortcuts -filterwarnings = - ignore::UserWarning - ignore::DeprecationWarning - ignore::PendingDeprecationWarning - ignore::sqlalchemy.exc.SAWarning - -# Timeout settings -timeout = 300 -timeout_method = thread - -# Parallel execution settings (uncomment if using pytest-xdist) -# addopts = -n auto - -# Additional logging configuration -log_cli = true -log_cli_level = INFO -log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s -log_cli_date_format = %Y-%m-%d %H:%M:%S \ No newline at end of file diff --git a/init_files.py b/init_files.py deleted file mode 100644 index d5fffe2a..00000000 --- a/init_files.py +++ /dev/null @@ -1,60 +0,0 @@ -# tests/fixtures/__init__.py -"""Test fixtures for the FastAPI application test suite.""" - -# tests/unit/__init__.py -"""Unit tests - fast, isolated component tests.""" - -# tests/unit/models/__init__.py -"""Database and API model unit tests.""" - -# tests/unit/utils/__init__.py -"""Utility function unit tests.""" - -# tests/unit/services/__init__.py -"""Service layer unit tests.""" - -# tests/integration/__init__.py -"""Integration tests - multiple components working together.""" - -# tests/integration/api/__init__.py -"""API integration tests.""" - -# tests/integration/api/v1/__init__.py -"""API v1 endpoint integration tests.""" - -# tests/integration/security/__init__.py -"""Security integration tests.""" - -# tests/performance/__init__.py -"""Performance and load tests.""" - -# tests/system/__init__.py -"""System-level tests - full application behavior.""" - -# tests/integration/conftest.py -"""Integration test specific fixtures.""" -import pytest - -# Add any integration-specific fixtures here if needed - -# tests/unit/conftest.py -"""Unit test specific fixtures.""" -import pytest - -# Add any unit-specific fixtures here if needed - -# tests/performance/conftest.py -"""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 - -# tests/system/conftest.py -"""System test specific fixtures.""" -import pytest - -# Add any system-specific fixtures here if needed \ No newline at end of file diff --git a/main_conftest.py b/main_conftest.py deleted file mode 100644 index 91c1242d..00000000 --- a/main_conftest.py +++ /dev/null @@ -1,90 +0,0 @@ -# tests/conftest.py - Updated main conftest with core fixtures only -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool - -from app.core.database import Base, get_db -from main import app -# Import all models to ensure they're registered with Base metadata -from models.database_models import (MarketplaceImportJob, Product, Shop, - ShopProduct, Stock, User) - -# Use in-memory SQLite database for tests -SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///:memory:" - - -@pytest.fixture(scope="session") -def engine(): - """Create test database engine""" - return create_engine( - SQLALCHEMY_TEST_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - echo=False, # Set to True for SQL debugging - ) - - -@pytest.fixture(scope="session") -def testing_session_local(engine): - """Create session factory for tests""" - return sessionmaker(autocommit=False, autoflush=False, bind=engine) - - -@pytest.fixture(scope="function") -def db(engine, testing_session_local): - """Create a database session for direct database operations""" - # Create all tables - Base.metadata.create_all(bind=engine) - - # Create session - db_session = testing_session_local() - - try: - yield db_session - finally: - db_session.close() - # Clean up all data after each test - Base.metadata.drop_all(bind=engine) - Base.metadata.create_all(bind=engine) - - -@pytest.fixture(scope="function") -def client(db): - """Create a test client with database dependency override""" - - # Override the dependency to use our test database - def override_get_db(): - try: - yield db - finally: - pass # Don't close here, the db fixture handles it - - app.dependency_overrides[get_db] = override_get_db - - try: - client = TestClient(app) - yield client - finally: - # Clean up the dependency override - if get_db in app.dependency_overrides: - del app.dependency_overrides[get_db] - - -# Cleanup fixture to ensure clean state -@pytest.fixture(autouse=True) -def cleanup(): - """Automatically clean up after each test""" - yield - # Clear any remaining dependency overrides - app.dependency_overrides.clear() - - -# Import fixtures from fixture modules -pytest_plugins = [ - "tests.fixtures.auth_fixtures", - "tests.fixtures.product_fixtures", - "tests.fixtures.shop_fixtures", - "tests.fixtures.marketplace_fixtures", -] \ No newline at end of file diff --git a/powershell_migration_script.ps1 b/powershell_migration_script.ps1 deleted file mode 100644 index 8b57a865..00000000 --- a/powershell_migration_script.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -# migrate_tests.ps1 - PowerShell script to migrate your test structure - -Write-Host "๐Ÿš€ Starting test structure migration..." -ForegroundColor Cyan - -# Create new directory structure -Write-Host "๐Ÿ“ Creating directory structure..." -ForegroundColor Yellow -$directories = @( - "tests\fixtures", - "tests\unit", "tests\unit\models", "tests\unit\utils", "tests\unit\services", - "tests\integration", "tests\integration\api", "tests\integration\api\v1", "tests\integration\security", - "tests\performance", - "tests\system", - "tests\test_data", "tests\test_data\csv" -) - -foreach ($dir in $directories) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null - Write-Host " Created: $dir" -ForegroundColor Gray -} - -# Create __init__.py files -Write-Host "๐Ÿ“„ Creating __init__.py files..." -ForegroundColor Yellow -$initFiles = @( - "tests\fixtures\__init__.py", - "tests\unit\__init__.py", "tests\unit\models\__init__.py", "tests\unit\utils\__init__.py", "tests\unit\services\__init__.py", - "tests\integration\__init__.py", "tests\integration\api\__init__.py", "tests\integration\api\v1\__init__.py", "tests\integration\security\__init__.py", - "tests\performance\__init__.py", - "tests\system\__init__.py" -) - -foreach ($file in $initFiles) { - New-Item -Path $file -ItemType File -Force | Out-Null - Write-Host " Created: $file" -ForegroundColor Gray -} - -# Create conftest.py files for each test category -Write-Host "โš™๏ธ Creating conftest.py files..." -ForegroundColor Yellow -$conftestFiles = @( - "tests\unit\conftest.py", - "tests\integration\conftest.py", - "tests\performance\conftest.py", - "tests\system\conftest.py" -) - -foreach ($file in $conftestFiles) { - New-Item -Path $file -ItemType File -Force | Out-Null - Write-Host " Created: $file" -ForegroundColor Gray -} - -# Backup original files -Write-Host "๐Ÿ’พ Backing up original files..." -ForegroundColor Yellow -New-Item -Path "tests\backup" -ItemType Directory -Force | Out-Null - -if (Test-Path "tests\conftest.py") { - Copy-Item "tests\conftest.py" "tests\backup\" -Force - Write-Host " Backed up: conftest.py" -ForegroundColor Gray -} - -Get-ChildItem "tests\test_*.py" -ErrorAction SilentlyContinue | ForEach-Object { - Copy-Item $_.FullName "tests\backup\" -Force - Write-Host " Backed up: $($_.Name)" -ForegroundColor Gray -} - -# Create sample test data file -Write-Host "๐Ÿ“Š Creating sample test data..." -ForegroundColor Yellow -$csvContent = @" -product_id,title,price,currency,brand,marketplace -TEST001,Sample Product 1,19.99,EUR,TestBrand,TestMarket -TEST002,Sample Product 2,29.99,EUR,TestBrand,TestMarket -TEST003,Sample Product 3,39.99,USD,AnotherBrand,TestMarket -"@ - -$csvContent | Out-File -FilePath "tests\test_data\csv\sample_products.csv" -Encoding UTF8 -Write-Host " Created: sample_products.csv" -ForegroundColor Gray - -Write-Host "โœ… Directory structure created!" -ForegroundColor Green -Write-Host "" -Write-Host "๐Ÿ“‹ Next steps:" -ForegroundColor Cyan -Write-Host "1. Copy the fixture files I provided to tests\fixtures\" -ForegroundColor White -Write-Host "2. Update tests\conftest.py with the new version" -ForegroundColor White -Write-Host "3. Move test files to their new locations:" -ForegroundColor White -Write-Host " - test_database.py โ†’ tests\unit\models\test_database_models.py" -ForegroundColor Gray -Write-Host " - test_utils.py โ†’ tests\unit\utils\test_data_processing.py" -ForegroundColor Gray -Write-Host " - test_admin_service.py โ†’ tests\unit\services\" -ForegroundColor Gray -Write-Host " - test_admin.py โ†’ tests\integration\api\v1\test_admin_endpoints.py" -ForegroundColor Gray -Write-Host " - test_pagination.py โ†’ tests\integration\api\v1\" -ForegroundColor Gray -Write-Host " - test_performance.py โ†’ tests\performance\test_api_performance.py" -ForegroundColor Gray -Write-Host " - test_error_handling.py โ†’ tests\system\" -ForegroundColor Gray -Write-Host " - Split test_security.py into security subdirectory" -ForegroundColor Gray -Write-Host "" -Write-Host "4. Update imports in moved test files" -ForegroundColor White -Write-Host "5. Add pytest markers to test classes" -ForegroundColor White -Write-Host "6. Update pytest.ini with the enhanced configuration" -ForegroundColor White -Write-Host "" -Write-Host "๐Ÿงช Test the migration with:" -ForegroundColor Cyan -Write-Host "pytest tests\unit -v" -ForegroundColor Yellow -Write-Host "pytest tests\integration -v" -ForegroundColor Yellow -Write-Host "pytest -m unit" -ForegroundColor Yellow -Write-Host "" -Write-Host "๐Ÿ”ง Quick test commands after migration:" -ForegroundColor Cyan -Write-Host "pytest -m unit # Fast unit tests" -ForegroundColor White -Write-Host "pytest -m integration # Integration tests" -ForegroundColor White -Write-Host "pytest -m `"not slow`" # Skip slow tests" -ForegroundColor White -Write-Host "pytest tests\unit\models\ # Model tests only" -ForegroundColor White -Write-Host "pytest --cov=app --cov-report=html # Coverage report" -ForegroundColor White -Write-Host "" -Write-Host "โœจ Migration structure ready! Follow the steps above to complete the migration." -ForegroundColor Green -Write-Host "๐Ÿ“š All your original files are backed up in tests\backup\" -ForegroundColor Green - -# Optional: Pause to let user read the output -Write-Host "" -Write-Host "Press any key to continue..." -ForegroundColor DarkGray -$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..a3864354 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,65 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Enhanced addopts for better development experience +addopts = + -v + --tb=short + --strict-markers + --strict-config + --color=yes + --durations=10 + --showlocals + -ra + --cov=app + --cov=models + --cov=utils + --cov=middleware + --cov-report=term-missing + --cov-report=html:htmlcov + --cov-fail-under=80 + +# Test discovery and execution settings +minversion = 6.0 + +# Markers for your specific test organization +markers = + unit: marks tests as unit tests - fast, isolated components + integration: marks tests as integration tests - multiple components working together + system: marks tests as system tests - full application behavior + e2e: marks tests as end-to-end tests - complete user workflows + slow: marks tests as slow running tests (deselect with '-m "not slow"') + performance: marks tests as performance and load tests + auth: marks tests as authentication and authorization tests + products: marks tests as product management functionality + stock: marks tests as stock and inventory management + shops: marks tests as shop management functionality + admin: marks tests as admin functionality and permissions + marketplace: marks tests as marketplace import functionality + stats: marks tests as statistics and reporting + database: marks tests as tests that require database operations + external: marks tests as tests that require external services + api: marks tests as API endpoint tests + security: marks tests as security-related tests + ci: marks tests as tests that should only run in CI + dev: marks tests as development-specific tests + +# Test filtering shortcuts +filterwarnings = + ignore::UserWarning + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + ignore::sqlalchemy.exc.SAWarning + +# Timeout settings +timeout = 300 +timeout_method = thread + +# Additional logging configuration +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S diff --git a/tests/.coverage b/tests/.coverage new file mode 100644 index 00000000..7451e622 Binary files /dev/null and b/tests/.coverage differ diff --git a/tests/conftest.py b/tests/conftest.py index df3036c1..91c1242d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ -# tests/conftest.py -import uuid - +# tests/conftest.py - Updated main conftest with core fixtures only import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine @@ -9,7 +7,6 @@ from sqlalchemy.pool import StaticPool from app.core.database import Base, get_db from main import app -from middleware.auth import AuthManager # Import all models to ensure they're registered with Base metadata from models.database_models import (MarketplaceImportJob, Product, Shop, ShopProduct, Stock, User) @@ -54,7 +51,7 @@ def db(engine, testing_session_local): @pytest.fixture(scope="function") -def client(db): # Now client depends on db +def client(db): """Create a test client with database dependency override""" # Override the dependency to use our test database @@ -75,172 +72,6 @@ def client(db): # Now client depends on db del app.dependency_overrides[get_db] -@pytest.fixture(scope="session") -def auth_manager(): - """Create auth manager instance (session scope since it's stateless)""" - return AuthManager() - - -@pytest.fixture -def test_user(db, auth_manager): - """Create a test user with unique username""" - unique_id = str(uuid.uuid4())[:8] # Short unique identifier - hashed_password = auth_manager.hash_password("testpass123") - user = User( - email=f"test_{unique_id}@example.com", - username=f"testuser_{unique_id}", - hashed_password=hashed_password, - role="user", - is_active=True, - ) - db.add(user) - db.commit() - db.refresh(user) - return user - - -@pytest.fixture -def test_admin(db, auth_manager): - """Create a test admin user with unique username""" - unique_id = str(uuid.uuid4())[:8] # Short unique identifier - hashed_password = auth_manager.hash_password("adminpass123") - admin = User( - email=f"admin_{unique_id}@example.com", - username=f"admin_{unique_id}", - hashed_password=hashed_password, - role="admin", - is_active=True, - ) - db.add(admin) - db.commit() - db.refresh(admin) - return admin - - -@pytest.fixture -def auth_headers(client, test_user): - """Get authentication headers for test user""" - response = client.post( - "/api/v1/auth/login", - json={"username": test_user.username, "password": "testpass123"}, - ) - assert response.status_code == 200, f"Login failed: {response.text}" - token = response.json()["access_token"] - return {"Authorization": f"Bearer {token}"} - - -@pytest.fixture -def admin_headers(client, test_admin): - """Get authentication headers for admin user""" - response = client.post( - "/api/v1/auth/login", - json={"username": test_admin.username, "password": "adminpass123"}, - ) - assert response.status_code == 200, f"Admin login failed: {response.text}" - token = response.json()["access_token"] - return {"Authorization": f"Bearer {token}"} - - -@pytest.fixture -def test_product(db): - """Create a test product""" - product = Product( - product_id="TEST001", - title="Test Product", - description="A test product", - price="10.99", - currency="EUR", - brand="TestBrand", - gtin="1234567890123", - availability="in stock", - marketplace="Letzshop", - shop_name="TestShop", - ) - db.add(product) - db.commit() - db.refresh(product) - return product - - -@pytest.fixture -def test_shop(db, test_user): - """Create a test shop with unique shop code""" - unique_id = str(uuid.uuid4())[:8] # Short unique identifier - shop = Shop( - shop_code=f"TESTSHOP_{unique_id}", - shop_name=f"Test Shop {unique_id}", - owner_id=test_user.id, - is_active=True, - is_verified=True, - ) - db.add(shop) - db.commit() - db.refresh(shop) - return shop - - -@pytest.fixture -def test_stock(db, test_product, test_shop): - """Create test stock entry""" - unique_id = str(uuid.uuid4())[:8].upper() # Short unique identifier - stock = Stock( - gtin=test_product.gtin, # Fixed: use gtin instead of product_id - location=f"WAREHOUSE_A_{unique_id}", - quantity=10, - reserved_quantity=0, - shop_id=test_shop.id, # Add shop_id reference - ) - db.add(stock) - db.commit() - db.refresh(stock) - return stock - - -@pytest.fixture -def test_marketplace_job(db, test_shop, test_user): # Add test_shop dependency - """Create a test marketplace import job""" - job = MarketplaceImportJob( - marketplace="amazon", - shop_name="Test Import Shop", - status="completed", - source_url="https://test-marketplace.example.com/import", - shop_id=test_shop.id, # Add required shop_id - user_id=test_user.id, - imported_count=5, - updated_count=3, - total_processed=8, - error_count=0, - error_message=None, - ) - db.add(job) - db.commit() - db.refresh(job) - return job - - -def create_test_import_job(db, shop_id, **kwargs): # Add shop_id parameter - """Helper function to create MarketplaceImportJob with defaults""" - defaults = { - "marketplace": "test", - "shop_name": "Test Shop", - "status": "pending", - "source_url": "https://test.example.com/import", - "shop_id": shop_id, # Add required shop_id - "imported_count": 0, - "updated_count": 0, - "total_processed": 0, - "error_count": 0, - "error_message": None, - } - defaults.update(kwargs) - - job = MarketplaceImportJob(**defaults) - db.add(job) - db.commit() - db.refresh(job) - return job - - # Cleanup fixture to ensure clean state @pytest.fixture(autouse=True) def cleanup(): @@ -250,227 +81,10 @@ def cleanup(): app.dependency_overrides.clear() -# Add these fixtures to your existing conftest.py - - -@pytest.fixture -def unique_product(db): - """Create a unique product for tests that need isolated product data""" - unique_id = str(uuid.uuid4())[:8] - product = Product( - product_id=f"UNIQUE_{unique_id}", - title=f"Unique Product {unique_id}", - description=f"A unique test product {unique_id}", - price="19.99", - currency="EUR", - brand=f"UniqueBrand_{unique_id}", - gtin=f"123456789{unique_id[:4]}", - availability="in stock", - marketplace="Letzshop", - shop_name=f"UniqueShop_{unique_id}", - google_product_category=f"UniqueCategory_{unique_id}", - ) - db.add(product) - db.commit() - db.refresh(product) - return product - - -@pytest.fixture -def unique_shop(db, test_user): - """Create a unique shop for tests that need isolated shop data""" - unique_id = str(uuid.uuid4())[:8] - shop = Shop( - shop_code=f"UNIQUESHOP_{unique_id}", - shop_name=f"Unique Test Shop {unique_id}", - description=f"A unique test shop {unique_id}", - owner_id=test_user.id, - is_active=True, - is_verified=True, - ) - db.add(shop) - db.commit() - db.refresh(shop) - return shop - - -@pytest.fixture -def other_user(db, auth_manager): - """Create a different user for testing access controls""" - unique_id = str(uuid.uuid4())[:8] - hashed_password = auth_manager.hash_password("otherpass123") - user = User( - email=f"other_{unique_id}@example.com", - username=f"otheruser_{unique_id}", - hashed_password=hashed_password, - role="user", - is_active=True, - ) - db.add(user) - db.commit() - db.refresh(user) - return user - - -@pytest.fixture -def inactive_shop(db, other_user): - """Create an inactive shop owned by other_user""" - unique_id = str(uuid.uuid4())[:8] - shop = Shop( - shop_code=f"INACTIVE_{unique_id}", - shop_name=f"Inactive Shop {unique_id}", - owner_id=other_user.id, - is_active=False, - is_verified=False, - ) - db.add(shop) - db.commit() - db.refresh(shop) - return shop - - -@pytest.fixture -def verified_shop(db, other_user): - """Create a verified shop owned by other_user""" - unique_id = str(uuid.uuid4())[:8] - shop = Shop( - shop_code=f"VERIFIED_{unique_id}", - shop_name=f"Verified Shop {unique_id}", - owner_id=other_user.id, - is_active=True, - is_verified=True, - ) - db.add(shop) - db.commit() - db.refresh(shop) - return shop - - -@pytest.fixture -def shop_product(db, test_shop, unique_product): - """Create a shop product relationship""" - shop_product = ShopProduct( - shop_id=test_shop.id, product_id=unique_product.id, is_active=True - ) - # Add optional fields if they exist in your model - if hasattr(ShopProduct, "price"): - shop_product.price = "24.99" - if hasattr(ShopProduct, "is_featured"): - shop_product.is_featured = False - if hasattr(ShopProduct, "stock_quantity"): - shop_product.stock_quantity = 10 - - db.add(shop_product) - db.commit() - db.refresh(shop_product) - return shop_product - - -@pytest.fixture -def multiple_products(db): - """Create multiple products for testing statistics and pagination""" - unique_id = str(uuid.uuid4())[:8] - products = [] - - for i in range(5): - product = Product( - product_id=f"MULTI_{unique_id}_{i}", - title=f"Multi Product {i} {unique_id}", - description=f"Multi test product {i}", - price=f"{10 + i}.99", - currency="EUR", - brand=f"MultiBrand_{i % 3}", # Create 3 different brands - marketplace=f"MultiMarket_{i % 2}", # Create 2 different marketplaces - shop_name=f"MultiShop_{i}", - google_product_category=f"MultiCategory_{i % 2}", # Create 2 different categories - gtin=f"1234567890{i}{unique_id[:2]}", - ) - products.append(product) - - db.add_all(products) - db.commit() - for product in products: - db.refresh(product) - return products - - -@pytest.fixture -def multiple_stocks(db, multiple_products, test_shop): - """Create multiple stock entries for testing""" - stocks = [] - - for i, product in enumerate(multiple_products): - stock = Stock( - gtin=product.gtin, - location=f"LOC_{i}", - quantity=10 + (i * 5), # Different quantities - reserved_quantity=i, - shop_id=test_shop.id, - ) - stocks.append(stock) - - db.add_all(stocks) - db.commit() - for stock in stocks: - db.refresh(stock) - return stocks - - -# Helper fixture factory functions -def create_unique_product_factory(): - """Factory function to create unique products in tests""" - - def _create_product(db, **kwargs): - unique_id = str(uuid.uuid4())[:8] - defaults = { - "product_id": f"FACTORY_{unique_id}", - "title": f"Factory Product {unique_id}", - "price": "15.99", - "currency": "EUR", - "marketplace": "TestMarket", - "shop_name": "TestShop", - } - defaults.update(kwargs) - - product = Product(**defaults) - db.add(product) - db.commit() - db.refresh(product) - return product - - return _create_product - - -@pytest.fixture -def product_factory(): - """Fixture that provides a product factory function""" - return create_unique_product_factory() - - -def create_unique_shop_factory(): - """Factory function to create unique shops in tests""" - - def _create_shop(db, owner_id, **kwargs): - unique_id = str(uuid.uuid4())[:8] - defaults = { - "shop_code": f"FACTORY_{unique_id}", - "shop_name": f"Factory Shop {unique_id}", - "owner_id": owner_id, - "is_active": True, - "is_verified": False, - } - defaults.update(kwargs) - - shop = Shop(**defaults) - db.add(shop) - db.commit() - db.refresh(shop) - return shop - - return _create_shop - - -@pytest.fixture -def shop_factory(): - """Fixture that provides a shop factory function""" - return create_unique_shop_factory() +# Import fixtures from fixture modules +pytest_plugins = [ + "tests.fixtures.auth_fixtures", + "tests.fixtures.product_fixtures", + "tests.fixtures.shop_fixtures", + "tests.fixtures.marketplace_fixtures", +] \ No newline at end of file diff --git a/tests/ecommerce.db b/tests/ecommerce.db new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..6b4c7600 --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,3 @@ +# tests/fixtures/__init__.py +"""Test fixtures for the FastAPI application test suite.""" + diff --git a/auth_fixtures.py b/tests/fixtures/auth_fixtures.py similarity index 100% rename from auth_fixtures.py rename to tests/fixtures/auth_fixtures.py diff --git a/marketplace_fixtures.py b/tests/fixtures/marketplace_fixtures.py similarity index 100% rename from marketplace_fixtures.py rename to tests/fixtures/marketplace_fixtures.py diff --git a/product_fixtures.py b/tests/fixtures/product_fixtures.py similarity index 100% rename from product_fixtures.py rename to tests/fixtures/product_fixtures.py diff --git a/shop_fixtures.py b/tests/fixtures/shop_fixtures.py similarity index 100% rename from shop_fixtures.py rename to tests/fixtures/shop_fixtures.py diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..d939ba99 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,3 @@ +# tests/integration/__init__.py +"""Integration tests - multiple components working together.""" + diff --git a/tests/integration/api/__init__.py b/tests/integration/api/__init__.py new file mode 100644 index 00000000..5c1601f1 --- /dev/null +++ b/tests/integration/api/__init__.py @@ -0,0 +1,3 @@ +# tests/integration/api/__init__.py +"""API integration tests.""" + diff --git a/tests/integration/api/v1/__init__.py b/tests/integration/api/v1/__init__.py new file mode 100644 index 00000000..bcc8c7d5 --- /dev/null +++ b/tests/integration/api/v1/__init__.py @@ -0,0 +1,3 @@ +# tests/integration/api/v1/__init__.py +"""API v1 endpoint integration tests.""" + diff --git a/integration_admin_endpoints.py b/tests/integration/api/v1/test_admin_endpoints.py similarity index 92% rename from integration_admin_endpoints.py rename to tests/integration/api/v1/test_admin_endpoints.py index 5e756e26..0db2b198 100644 --- a/integration_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/test_auth.py b/tests/integration/api/v1/test_authentication_endpoints.py similarity index 99% rename from tests/test_auth.py rename to tests/integration/api/v1/test_authentication_endpoints.py index 96aef38e..b5fd8c50 100644 --- a/tests/test_auth.py +++ b/tests/integration/api/v1/test_authentication_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_auth.py +# tests/test_authentication_endpoints.py import pytest from fastapi import HTTPException diff --git a/tests/test_marketplace.py b/tests/integration/api/v1/test_marketplace_endpoints.py similarity index 98% rename from tests/test_marketplace.py rename to tests/integration/api/v1/test_marketplace_endpoints.py index 446ef3d3..fcac37e3 100644 --- a/tests/test_marketplace.py +++ b/tests/integration/api/v1/test_marketplace_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_marketplace.py +# tests/test_marketplace_endpoints.py from unittest.mock import AsyncMock, patch import pytest diff --git a/integration_pagination.py b/tests/integration/api/v1/test_pagination.py similarity index 97% rename from integration_pagination.py rename to tests/integration/api/v1/test_pagination.py index 849e3efa..a0cc4207 100644 --- a/integration_pagination.py +++ b/tests/integration/api/v1/test_pagination.py @@ -1,13 +1,14 @@ # tests/integration/api/v1/test_pagination.py import pytest -from models.database_models import Product +from models.database_models import Product, Shop @pytest.mark.integration @pytest.mark.api @pytest.mark.database @pytest.mark.products +@pytest.mark.shops class TestPagination: def test_product_pagination(self, client, auth_headers, db): """Test pagination for product listing""" diff --git a/tests/test_product.py b/tests/integration/api/v1/test_product_endpoints.py similarity index 99% rename from tests/test_product.py rename to tests/integration/api/v1/test_product_endpoints.py index ddf70aab..065a1bc5 100644 --- a/tests/test_product.py +++ b/tests/integration/api/v1/test_product_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_product.py +# tests/test_product_endpoints.py import pytest diff --git a/tests/test_shop.py b/tests/integration/api/v1/test_shop_endpoints.py similarity index 98% rename from tests/test_shop.py rename to tests/integration/api/v1/test_shop_endpoints.py index cead5ad2..8b094a7b 100644 --- a/tests/test_shop.py +++ b/tests/integration/api/v1/test_shop_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_shop.py +# tests/test_shop_endpoints.py import pytest diff --git a/tests/test_stats.py b/tests/integration/api/v1/test_stats_endpoints.py similarity index 97% rename from tests/test_stats.py rename to tests/integration/api/v1/test_stats_endpoints.py index cd9ba5be..fdf7a078 100644 --- a/tests/test_stats.py +++ b/tests/integration/api/v1/test_stats_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_stats.py +# tests/test_stats_endpoints.py import pytest diff --git a/tests/test_stock.py b/tests/integration/api/v1/test_stock_endpoints.py similarity index 99% rename from tests/test_stock.py rename to tests/integration/api/v1/test_stock_endpoints.py index 3c84dc24..5aa72de3 100644 --- a/tests/test_stock.py +++ b/tests/integration/api/v1/test_stock_endpoints.py @@ -1,4 +1,4 @@ -# tests/test_stock.py +# tests/test_stock_endpoints.py import pytest from models.database_models import Stock diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..703f8685 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,6 @@ +# tests/integration/conftest.py +"""Integration test specific fixtures.""" +import pytest + +# Add any integration-specific fixtures here if needed + diff --git a/tests/integration/security/__init__.py b/tests/integration/security/__init__.py new file mode 100644 index 00000000..dc1e90f7 --- /dev/null +++ b/tests/integration/security/__init__.py @@ -0,0 +1,3 @@ +# tests/integration/security/__init__.py +"""Security integration tests.""" + diff --git a/integration_authentication.py b/tests/integration/security/test_authentication.py similarity index 100% rename from integration_authentication.py rename to tests/integration/security/test_authentication.py diff --git a/integration_authorization.py b/tests/integration/security/test_authorization.py similarity index 100% rename from integration_authorization.py rename to tests/integration/security/test_authorization.py diff --git a/integration_input_validation.py b/tests/integration/security/test_input_validation.py similarity index 100% rename from integration_input_validation.py rename to tests/integration/security/test_input_validation.py diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py new file mode 100644 index 00000000..2f4789ba --- /dev/null +++ b/tests/performance/__init__.py @@ -0,0 +1,3 @@ +# tests/performance/__init__.py +"""Performance and load tests.""" + diff --git a/tests/performance/conftest.py b/tests/performance/conftest.py new file mode 100644 index 00000000..22259230 --- /dev/null +++ b/tests/performance/conftest.py @@ -0,0 +1,10 @@ +# tests/performance/conftest.py +"""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/performance_tests.py b/tests/performance/test_api_performance.py similarity index 100% rename from performance_tests.py rename to tests/performance/test_api_performance.py diff --git a/tests/pytest.ini b/tests/pytest.ini deleted file mode 100644 index 4bb15305..00000000 --- a/tests/pytest.ini +++ /dev/null @@ -1,21 +0,0 @@ -# tests/pytest.ini -[tool:pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* -addopts = - -v - --tb=short - --strict-markers - --disable-warnings - --color=yes -markers = - slow: marks tests as slow (deselect with '-m "not slow"') - integration: marks tests as integration tests - unit: marks tests as unit tests - auth: marks tests related to authentication - products: marks tests related to products - stock: marks tests related to stock management - shops: marks tests related to shop management - admin: marks tests related to admin functionality diff --git a/tests/requirements-test.txt b/tests/requirements-test.txt index 3e0fe59d..8bf5f468 100644 --- a/tests/requirements-test.txt +++ b/tests/requirements-test.txt @@ -7,3 +7,4 @@ pytest-mock>=3.11.0 httpx>=0.24.0 faker>=19.0.0 pytest-repeat>=0.9.4 +pytest-timeout>=2.1.0 diff --git a/tests/system/__init__.py b/tests/system/__init__.py new file mode 100644 index 00000000..ac25623d --- /dev/null +++ b/tests/system/__init__.py @@ -0,0 +1,3 @@ +# tests/system/__init__.py +"""System-level tests - full application behavior.""" + diff --git a/tests/system/conftest.py b/tests/system/conftest.py new file mode 100644 index 00000000..8c6bc72a --- /dev/null +++ b/tests/system/conftest.py @@ -0,0 +1,6 @@ +# tests/system/conftest.py +"""System test specific fixtures.""" +import pytest + +# Add any system-specific fixtures here if needed + diff --git a/system_error_handling.py b/tests/system/test_error_handling.py similarity index 100% rename from system_error_handling.py rename to tests/system/test_error_handling.py diff --git a/tests/test_admin.py b/tests/test_admin.py deleted file mode 100644 index 0bf46621..00000000 --- a/tests/test_admin.py +++ /dev/null @@ -1,192 +0,0 @@ -# tests/test_admin.py -import pytest - - -class TestAdminAPI: - def test_get_all_users_admin(self, client, admin_headers, test_user): - """Test admin getting all users""" - response = client.get("/api/v1/admin/users", headers=admin_headers) - - assert response.status_code == 200 - data = response.json() - assert len(data) >= 2 # test_user + admin user - - # Check that test_user is in the response - user_ids = [user["id"] for user in data if "id" in user] - assert test_user.id in user_ids - - def test_get_all_users_non_admin(self, client, auth_headers): - """Test non-admin trying to access admin endpoint""" - response = client.get("/api/v1/admin/users", headers=auth_headers) - - assert response.status_code == 403 - assert ( - "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): - """Test admin toggling user status""" - response = client.put( - f"/api/v1/admin/users/{test_user.id}/status", headers=admin_headers - ) - - assert response.status_code == 200 - message = response.json()["message"] - assert "deactivated" in message or "activated" in message - # Verify the username is in the message - assert test_user.username in message - - def test_toggle_user_status_user_not_found(self, client, admin_headers): - """Test admin toggling status for non-existent user""" - response = client.put("/api/v1/admin/users/99999/status", headers=admin_headers) - - assert response.status_code == 404 - assert "User not found" in response.json()["detail"] - - def test_toggle_user_status_cannot_deactivate_self( - self, client, admin_headers, test_admin - ): - """Test that admin cannot deactivate their own account""" - response = client.put( - f"/api/v1/admin/users/{test_admin.id}/status", headers=admin_headers - ) - - assert response.status_code == 400 - assert "Cannot deactivate your own account" in response.json()["detail"] - - def test_get_all_shops_admin(self, client, admin_headers, test_shop): - """Test admin getting all shops""" - response = client.get("/api/v1/admin/shops", headers=admin_headers) - - assert response.status_code == 200 - data = response.json() - assert data["total"] >= 1 - assert len(data["shops"]) >= 1 - - # Check that test_shop is in the response - shop_codes = [ - shop["shop_code"] for shop in data["shops"] if "shop_code" in shop - ] - assert test_shop.shop_code in shop_codes - - def test_get_all_shops_non_admin(self, client, auth_headers): - """Test non-admin trying to access admin shop endpoint""" - response = client.get("/api/v1/admin/shops", headers=auth_headers) - - assert response.status_code == 403 - assert ( - "Access denied" in response.json()["detail"] - or "admin" in response.json()["detail"].lower() - ) - - def test_verify_shop_admin(self, client, admin_headers, test_shop): - """Test admin verifying/unverifying shop""" - response = client.put( - f"/api/v1/admin/shops/{test_shop.id}/verify", headers=admin_headers - ) - - assert response.status_code == 200 - message = response.json()["message"] - assert "verified" in message or "unverified" in message - assert test_shop.shop_code in message - - def test_verify_shop_not_found(self, client, admin_headers): - """Test admin verifying non-existent shop""" - response = client.put("/api/v1/admin/shops/99999/verify", headers=admin_headers) - - assert response.status_code == 404 - assert "Shop not found" in response.json()["detail"] - - def test_toggle_shop_status_admin(self, client, admin_headers, test_shop): - """Test admin toggling shop status""" - response = client.put( - f"/api/v1/admin/shops/{test_shop.id}/status", headers=admin_headers - ) - - assert response.status_code == 200 - message = response.json()["message"] - assert "activated" in message or "deactivated" in message - assert test_shop.shop_code in message - - def test_toggle_shop_status_not_found(self, client, admin_headers): - """Test admin toggling status for non-existent shop""" - response = client.put("/api/v1/admin/shops/99999/status", headers=admin_headers) - - assert response.status_code == 404 - assert "Shop not found" in response.json()["detail"] - - def test_get_marketplace_import_jobs_admin( - self, client, admin_headers, test_marketplace_job - ): - """Test admin getting marketplace import jobs""" - response = client.get( - "/api/v1/admin/marketplace-import-jobs", headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - - # Check that test_marketplace_job is in the response - job_ids = [job["job_id"] for job in data if "job_id" in job] - assert test_marketplace_job.id in job_ids - - def test_get_marketplace_import_jobs_with_filters( - self, client, admin_headers, test_marketplace_job - ): - """Test admin getting marketplace import jobs with filters""" - response = client.get( - "/api/v1/admin/marketplace-import-jobs", - params={"marketplace": test_marketplace_job.marketplace}, - headers=admin_headers, - ) - - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - assert all( - job["marketplace"] == test_marketplace_job.marketplace for job in data - ) - - def test_get_marketplace_import_jobs_non_admin(self, client, auth_headers): - """Test non-admin trying to access marketplace import jobs""" - response = client.get( - "/api/v1/admin/marketplace-import-jobs", headers=auth_headers - ) - - assert response.status_code == 403 - assert ( - "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): - """Test user pagination works correctly""" - # Test first page - response = client.get( - "/api/v1/admin/users?skip=0&limit=1", headers=admin_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) == 1 - - # Test second page - response = client.get( - "/api/v1/admin/users?skip=1&limit=1", headers=admin_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) >= 0 # Could be 1 or 0 depending on total users - - def test_admin_pagination_shops(self, client, admin_headers, test_shop): - """Test shop pagination works correctly""" - response = client.get( - "/api/v1/admin/shops?skip=0&limit=1", headers=admin_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["total"] >= 1 - assert len(data["shops"]) >= 0 - assert "skip" in data - assert "limit" in data diff --git a/tests/test_admin_service.py b/tests/test_admin_service.py deleted file mode 100644 index f5574f21..00000000 --- a/tests/test_admin_service.py +++ /dev/null @@ -1,388 +0,0 @@ -# tests/test_admin_service.py -from datetime import datetime - -import pytest -from fastapi import HTTPException - -from app.services.admin_service import AdminService -from models.database_models import MarketplaceImportJob, Shop, User - - -class TestAdminService: - """Test suite for AdminService following the application's testing patterns""" - - def setup_method(self): - """Setup method following the same pattern as product service tests""" - self.service = AdminService() - - def test_get_all_users(self, db, test_user, test_admin): - """Test getting all users with pagination""" - users = self.service.get_all_users(db, skip=0, limit=10) - - assert len(users) >= 2 # test_user + test_admin - user_ids = [user.id for user in users] - assert test_user.id in user_ids - assert test_admin.id in user_ids - - def test_get_all_users_with_pagination(self, db, test_user, test_admin): - """Test user pagination works correctly""" - users = self.service.get_all_users(db, skip=0, limit=1) - - assert len(users) == 1 - - users_second_page = self.service.get_all_users(db, skip=1, limit=1) - assert len(users_second_page) == 1 - assert users[0].id != users_second_page[0].id - - def test_toggle_user_status_deactivate(self, db, test_user, test_admin): - """Test deactivating a user""" - assert test_user.is_active is True - - user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id) - - assert user.id == test_user.id - assert user.is_active is False - assert f"{user.username} has been deactivated" in message - - def test_toggle_user_status_activate(self, db, test_user, test_admin): - """Test activating a user""" - # First deactivate the user - test_user.is_active = False - db.commit() - - user, message = self.service.toggle_user_status(db, test_user.id, test_admin.id) - - assert user.id == test_user.id - assert user.is_active is True - assert f"{user.username} has been activated" in message - - def test_toggle_user_status_user_not_found(self, db, test_admin): - """Test toggle user status when user not found""" - with pytest.raises(HTTPException) as exc_info: - self.service.toggle_user_status(db, 99999, test_admin.id) - - assert exc_info.value.status_code == 404 - assert "User not found" in str(exc_info.value.detail) - - def test_toggle_user_status_cannot_deactivate_self(self, db, test_admin): - """Test that admin cannot deactivate their own account""" - with pytest.raises(HTTPException) as exc_info: - self.service.toggle_user_status(db, test_admin.id, test_admin.id) - - assert exc_info.value.status_code == 400 - assert "Cannot deactivate your own account" in str(exc_info.value.detail) - - def test_get_all_shops(self, db, test_shop): - """Test getting all shops with total count""" - shops, total = self.service.get_all_shops(db, skip=0, limit=10) - - assert total >= 1 - assert len(shops) >= 1 - shop_codes = [shop.shop_code for shop in shops] - assert test_shop.shop_code in shop_codes - - def test_get_all_shops_with_pagination(self, db, test_shop): - """Test shop pagination works correctly""" - # Create additional shop for pagination test using the helper function - # from conftest import create_test_import_job # If you added the helper function - - # Or create directly with proper fields - additional_shop = Shop( - shop_code=f"{test_shop.shop_code}_2", - shop_name="Test Shop 2", - owner_id=test_shop.owner_id, - is_active=True, - is_verified=False, - ) - db.add(additional_shop) - db.commit() - - shops_page_1 = self.service.get_all_shops(db, skip=0, limit=1) - assert len(shops_page_1[0]) == 1 - - shops_page_2 = self.service.get_all_shops(db, skip=1, limit=1) - assert len(shops_page_2[0]) == 1 - - # Ensure different shops on different pages - assert shops_page_1[0][0].id != shops_page_2[0][0].id - - def test_verify_shop_mark_verified(self, db, test_shop): - """Test marking shop as verified""" - # Ensure shop starts unverified - test_shop.is_verified = False - db.commit() - - shop, message = self.service.verify_shop(db, test_shop.id) - - assert shop.id == test_shop.id - assert shop.is_verified is True - assert f"{shop.shop_code} has been verified" in message - - def test_verify_shop_mark_unverified(self, db, test_shop): - """Test marking shop as unverified""" - # Ensure shop starts verified - test_shop.is_verified = True - db.commit() - - shop, message = self.service.verify_shop(db, test_shop.id) - - assert shop.id == test_shop.id - assert shop.is_verified is False - assert f"{shop.shop_code} has been unverified" in message - - def test_verify_shop_not_found(self, db): - """Test verify shop when shop not found""" - with pytest.raises(HTTPException) as exc_info: - self.service.verify_shop(db, 99999) - - assert exc_info.value.status_code == 404 - assert "Shop not found" in str(exc_info.value.detail) - - def test_toggle_shop_status_deactivate(self, db, test_shop): - """Test deactivating a shop""" - assert test_shop.is_active is True - - shop, message = self.service.toggle_shop_status(db, test_shop.id) - - assert shop.id == test_shop.id - assert shop.is_active is False - assert f"{shop.shop_code} has been deactivated" in message - - def test_toggle_shop_status_activate(self, db, test_shop): - """Test activating a shop""" - # First deactivate the shop - test_shop.is_active = False - db.commit() - - shop, message = self.service.toggle_shop_status(db, test_shop.id) - - assert shop.id == test_shop.id - assert shop.is_active is True - assert f"{shop.shop_code} has been activated" in message - - def test_toggle_shop_status_not_found(self, db): - """Test toggle shop status when shop not found""" - with pytest.raises(HTTPException) as exc_info: - self.service.toggle_shop_status(db, 99999) - - assert exc_info.value.status_code == 404 - assert "Shop not found" in str(exc_info.value.detail) - - def test_get_marketplace_import_jobs_no_filters(self, db, test_marketplace_job): - """Test getting marketplace import jobs without filters using fixture""" - result = self.service.get_marketplace_import_jobs(db, skip=0, limit=10) - - assert len(result) >= 1 - # Find our test job in the results - test_job = next( - (job for job in result if job.job_id == test_marketplace_job.id), None - ) - 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 - - def test_get_marketplace_import_jobs_with_marketplace_filter( - self, db, test_marketplace_job, test_user, test_shop - ): - """Test getting marketplace import jobs filtered by marketplace""" - # Create additional job with different marketplace - other_job = MarketplaceImportJob( - marketplace="ebay", - shop_name="eBay Shop", - status="completed", - source_url="https://ebay.example.com/import", - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - ) - db.add(other_job) - db.commit() - - # Filter by the test marketplace job's marketplace - result = self.service.get_marketplace_import_jobs( - db, marketplace=test_marketplace_job.marketplace - ) - - assert len(result) >= 1 - # All results should match the marketplace filter - for job in result: - assert test_marketplace_job.marketplace.lower() in job.marketplace.lower() - - def test_get_marketplace_import_jobs_with_shop_filter( - self, db, test_marketplace_job, test_user, test_shop - ): - """Test getting marketplace import jobs filtered by shop name""" - # Create additional job with different shop name - other_job = MarketplaceImportJob( - marketplace="amazon", - shop_name="Different Shop Name", - status="completed", - source_url="https://different.example.com/import", - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - ) - db.add(other_job) - db.commit() - - # Filter by the test marketplace job's shop name - result = self.service.get_marketplace_import_jobs( - db, shop_name=test_marketplace_job.shop_name - ) - - assert len(result) >= 1 - # All results should match the shop name filter - for job in result: - assert test_marketplace_job.shop_name.lower() in job.shop_name.lower() - - def test_get_marketplace_import_jobs_with_status_filter( - self, db, test_marketplace_job, test_user, test_shop - ): - """Test getting marketplace import jobs filtered by status""" - # Create additional job with different status - other_job = MarketplaceImportJob( - marketplace="amazon", - shop_name="Test Shop", - status="pending", - source_url="https://pending.example.com/import", - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - ) - db.add(other_job) - db.commit() - - # Filter by the test marketplace job's status - result = self.service.get_marketplace_import_jobs( - db, status=test_marketplace_job.status - ) - - assert len(result) >= 1 - # All results should match the status filter - for job in result: - assert job.status == test_marketplace_job.status - - def test_get_marketplace_import_jobs_with_multiple_filters( - self, db, test_marketplace_job, test_shop, test_user - ): - """Test getting marketplace import jobs with multiple filters""" - # Create jobs that don't match all filters - non_matching_job1 = MarketplaceImportJob( - marketplace="ebay", # Different marketplace - shop_name=test_marketplace_job.shop_name, - status=test_marketplace_job.status, - source_url="https://non-matching1.example.com/import", - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - ) - non_matching_job2 = MarketplaceImportJob( - marketplace=test_marketplace_job.marketplace, - shop_name="Different Shop", # Different shop name - status=test_marketplace_job.status, - source_url="https://non-matching2.example.com/import", - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - ) - db.add_all([non_matching_job1, non_matching_job2]) - db.commit() - - # Apply all three filters matching the test job - result = self.service.get_marketplace_import_jobs( - db, - marketplace=test_marketplace_job.marketplace, - shop_name=test_marketplace_job.shop_name, - status=test_marketplace_job.status, - ) - - assert len(result) >= 1 - # Find our test job in the results - test_job = next( - (job for job in result if job.job_id == test_marketplace_job.id), None - ) - 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 - - def test_get_marketplace_import_jobs_null_values(self, db, test_user, test_shop): - """Test that marketplace import jobs handle null values correctly""" - # Create job with null values but required fields - job = MarketplaceImportJob( - shop_id=test_shop.id, - user_id=test_user.id, # Fixed: Added missing user_id - marketplace="test", - shop_name="Test Shop", - status="pending", - source_url="https://test.example.com/import", - imported_count=None, - updated_count=None, - total_processed=None, - error_count=None, - error_message=None, - ) - db.add(job) - db.commit() - - result = self.service.get_marketplace_import_jobs(db) - - assert len(result) >= 1 - # Find the job with null values - null_job = next((j for j in result if j.job_id == job.id), None) - assert null_job is not None - assert null_job.imported == 0 # None converted to 0 - assert null_job.updated == 0 - assert null_job.total_processed == 0 - assert null_job.error_count == 0 - assert null_job.error_message is None - - def test_get_user_by_id(self, db, test_user): - """Test getting user by ID using fixture""" - user = self.service.get_user_by_id(db, test_user.id) - - assert user is not None - assert user.id == test_user.id - assert user.email == test_user.email - assert user.username == test_user.username - - def test_get_user_by_id_not_found(self, db): - """Test getting user by ID when user doesn't exist""" - user = self.service.get_user_by_id(db, 99999) - - assert user is None - - def test_get_shop_by_id(self, db, test_shop): - """Test getting shop by ID using fixture""" - shop = self.service.get_shop_by_id(db, test_shop.id) - - assert shop is not None - assert shop.id == test_shop.id - assert shop.shop_code == test_shop.shop_code - assert shop.shop_name == test_shop.shop_name - - def test_get_shop_by_id_not_found(self, db): - """Test getting shop by ID when shop doesn't exist""" - shop = self.service.get_shop_by_id(db, 99999) - - assert shop is None - - def test_user_exists_true(self, db, test_user): - """Test user_exists returns True when user exists""" - exists = self.service.user_exists(db, test_user.id) - - assert exists is True - - def test_user_exists_false(self, db): - """Test user_exists returns False when user doesn't exist""" - exists = self.service.user_exists(db, 99999) - - assert exists is False - - def test_shop_exists_true(self, db, test_shop): - """Test shop_exists returns True when shop exists""" - exists = self.service.shop_exists(db, test_shop.id) - - assert exists is True - - def test_shop_exists_false(self, db): - """Test shop_exists returns False when shop doesn't exist""" - exists = self.service.shop_exists(db, 99999) - - assert exists is False diff --git a/tests/test_data/csv/sample_products.csv b/tests/test_data/csv/sample_products.csv new file mode 100644 index 00000000..bd8d6733 --- /dev/null +++ b/tests/test_data/csv/sample_products.csv @@ -0,0 +1,4 @@ +product_id,title,price,currency,brand,marketplace +TEST001,Sample Product 1,19.99,EUR,TestBrand,TestMarket +TEST002,Sample Product 2,29.99,EUR,TestBrand,TestMarket +TEST003,Sample Product 3,39.99,USD,AnotherBrand,TestMarket diff --git a/tests/test_database.py b/tests/test_database.py deleted file mode 100644 index f2894a77..00000000 --- a/tests/test_database.py +++ /dev/null @@ -1,95 +0,0 @@ -# tests/test_database.py -import pytest -from sqlalchemy import text - -from models.database_models import Product, Shop, Stock, User - - -class TestDatabaseModels: - def test_user_model(self, db): - """Test User model creation and relationships""" - user = User( - email="db_test@example.com", - username="dbtest", - hashed_password="hashed_password_123", - role="user", - is_active=True, - ) - - db.add(user) - db.commit() - db.refresh(user) - - assert user.id is not None - assert user.email == "db_test@example.com" - assert user.created_at is not None - assert user.updated_at is not None - - def test_product_model(self, db): - """Test Product model creation""" - product = Product( - product_id="DB_TEST_001", - title="Database Test Product", - description="Testing product model", - price="25.99", - currency="USD", - brand="DBTest", - gtin="1234567890123", - availability="in stock", - marketplace="TestDB", - shop_name="DBTestShop", - ) - - db.add(product) - db.commit() - db.refresh(product) - - assert product.id is not None - assert product.product_id == "DB_TEST_001" - assert product.created_at is not None - - def test_stock_model(self, db): - """Test Stock model creation""" - stock = Stock(gtin="1234567890123", location="DB_WAREHOUSE", quantity=150) - - db.add(stock) - db.commit() - db.refresh(stock) - - assert stock.id is not None - assert stock.gtin == "1234567890123" - assert stock.location == "DB_WAREHOUSE" - assert stock.quantity == 150 - - def test_shop_model_with_owner(self, db, test_user): - """Test Shop model with owner relationship""" - shop = Shop( - shop_code="DBTEST", - shop_name="Database Test Shop", - description="Testing shop model", - owner_id=test_user.id, - is_active=True, - is_verified=False, - ) - - db.add(shop) - db.commit() - db.refresh(shop) - - assert shop.id is not None - assert shop.shop_code == "DBTEST" - assert shop.owner_id == test_user.id - assert shop.owner.username == test_user.username - - def test_database_constraints(self, db): - """Test database constraints and unique indexes""" - # Test unique product_id constraint - product1 = Product(product_id="UNIQUE_001", title="Product 1") - db.add(product1) - db.commit() - - # This should raise an integrity error - with pytest.raises(Exception): # Could be IntegrityError or similar - product2 = Product(product_id="UNIQUE_001", title="Product 2") - db.add(product2) - db.commit() diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py deleted file mode 100644 index 66951aa0..00000000 --- a/tests/test_error_handling.py +++ /dev/null @@ -1,48 +0,0 @@ -# tests/test_error_handling.py -import pytest - - -class TestErrorHandling: - def test_invalid_json(self, client, auth_headers): - """Test handling of invalid JSON""" - response = client.post( - "/api/v1/product", headers=auth_headers, content="invalid json" - ) - - assert response.status_code == 422 # Validation error - - def test_missing_required_fields(self, client, auth_headers): - """Test handling of missing required fields""" - response = client.post( - "/api/v1/product", headers=auth_headers, json={"title": "Test"} - ) # Missing product_id - - assert response.status_code == 422 - - def test_invalid_authentication(self, client): - """Test handling of invalid authentication""" - response = client.get( - "/api/v1/product", headers={"Authorization": "Bearer invalid_token"} - ) - - assert response.status_code == 401 # Token is not valid - - def test_nonexistent_resource(self, client, auth_headers): - """Test handling of nonexistent resource access""" - response = client.get("/api/v1/product/NONEXISTENT", headers=auth_headers) - assert response.status_code == 404 - - response = client.get("/api/v1/shop/NONEXISTENT", headers=auth_headers) - assert response.status_code == 404 - - def test_duplicate_resource_creation(self, client, auth_headers, test_product): - """Test handling of duplicate resource creation""" - product_data = { - "product_id": test_product.product_id, # Duplicate ID - "title": "Another Product", - } - - response = client.post( - "/api/v1/product", headers=auth_headers, json=product_data - ) - assert response.status_code == 400 diff --git a/tests/test_pagination.py b/tests/test_pagination.py deleted file mode 100644 index d2d28eda..00000000 --- a/tests/test_pagination.py +++ /dev/null @@ -1,57 +0,0 @@ -# tests/test_pagination.py -import pytest - -from models.database_models import Product - - -class TestPagination: - def test_product_pagination(self, client, auth_headers, db): - """Test pagination for product listing""" - # Create multiple products - products = [] - for i in range(25): - product = Product( - product_id=f"PAGE{i:03d}", - title=f"Pagination Test Product {i}", - marketplace="PaginationTest", - ) - products.append(product) - - db.add_all(products) - db.commit() - - # Test first page - response = client.get("/api/v1/product?limit=10&skip=0", headers=auth_headers) - assert response.status_code == 200 - data = response.json() - assert len(data["products"]) == 10 - assert data["total"] == 25 - assert data["skip"] == 0 - assert data["limit"] == 10 - - # Test second page - response = client.get("/api/v1/product?limit=10&skip=10", headers=auth_headers) - assert response.status_code == 200 - data = response.json() - assert len(data["products"]) == 10 - assert data["skip"] == 10 - - # Test last page - response = client.get("/api/v1/product?limit=10&skip=20", headers=auth_headers) - assert response.status_code == 200 - data = response.json() - assert len(data["products"]) == 5 # Only 5 remaining - - def test_pagination_boundaries(self, client, auth_headers): - """Test pagination boundary conditions""" - # Test negative skip - response = client.get("/api/v1/product?skip=-1", headers=auth_headers) - assert response.status_code == 422 # Validation error - - # Test zero limit - response = client.get("/api/v1/product?limit=0", headers=auth_headers) - assert response.status_code == 422 # Validation error - - # Test excessive limit - response = client.get("/api/v1/product?limit=10000", headers=auth_headers) - assert response.status_code == 422 # Should be limited diff --git a/tests/test_performance.py b/tests/test_performance.py deleted file mode 100644 index f71eade4..00000000 --- a/tests/test_performance.py +++ /dev/null @@ -1,59 +0,0 @@ -# tests/test_performance.py -import time - -import pytest - -from models.database_models import Product - - -class TestPerformance: - def test_product_list_performance(self, client, auth_headers, db): - """Test performance of product listing with many products""" - # Create multiple products - products = [] - for i in range(100): - product = Product( - product_id=f"PERF{i:03d}", - title=f"Performance Test Product {i}", - price=f"{i}.99", - marketplace="Performance", - ) - products.append(product) - - db.add_all(products) - db.commit() - - # Time the request - start_time = time.time() - response = client.get("/api/v1/product?limit=100", headers=auth_headers) - end_time = time.time() - - assert response.status_code == 200 - assert len(response.json()["products"]) == 100 - assert end_time - start_time < 2.0 # Should complete within 2 seconds - - def test_search_performance(self, client, auth_headers, db): - """Test search performance""" - # Create products with searchable content - products = [] - for i in range(50): - product = Product( - product_id=f"SEARCH{i:03d}", - title=f"Searchable Product {i}", - description=f"This is a searchable product number {i}", - brand="SearchBrand", - marketplace="SearchMarket", - ) - products.append(product) - - db.add_all(products) - db.commit() - - # Time search request - start_time = time.time() - response = client.get("/api/v1/product?search=Searchable", headers=auth_headers) - end_time = time.time() - - assert response.status_code == 200 - assert response.json()["total"] == 50 - assert end_time - start_time < 1.0 # Search should be fast diff --git a/tests/test_security.py b/tests/test_security.py deleted file mode 100644 index dc1ac162..00000000 --- a/tests/test_security.py +++ /dev/null @@ -1,119 +0,0 @@ -# tests/test_security.py -from unittest.mock import patch - -import pytest -from fastapi import HTTPException - - -class TestSecurity: - def test_debug_direct_bearer(self, client): - """Test HTTPBearer directly""" - - response = client.get("/api/v1/debug-bearer") - print(f"Direct Bearer - Status: {response.status_code}") - print( - f"Direct Bearer - Response: {response.json() if response.content else 'No content'}" - ) - - def test_debug_dependencies(self, client): - """Debug the dependency chain step by step""" - - # Test 1: Direct endpoint with no auth - response = client.get("/api/v1/admin/users") - print(f"Admin endpoint - Status: {response.status_code}") - try: - print(f"Admin endpoint - Response: {response.json()}") - except: - 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 - print(f"Regular endpoint - Status: {response2.status_code}") - try: - print(f"Regular endpoint - Response: {response2.json()}") - except: - print(f"Regular endpoint - Raw: {response2.content}") - - def test_debug_available_routes(self, client): - """Debug test to see all available routes""" - print("\n=== All Available Routes ===") - for route in client.app.routes: - if hasattr(route, "path") and hasattr(route, "methods"): - print(f"{list(route.methods)} {route.path}") - - print("\n=== Testing Product Endpoint Variations ===") - variations = [ - "/api/v1/product", # Your current attempt - "/api/v1/product/", # With trailing slash - "/api/v1/product/list", # With list endpoint - "/api/v1/product/all", # With all endpoint - ] - - for path in variations: - response = client.get(path) - print(f"{path}: Status {response.status_code}") - - def test_protected_endpoint_without_auth(self, client): - """Test that protected endpoints reject unauthenticated requests""" - protected_endpoints = [ - "/api/v1/admin/users", - "/api/v1/admin/shops", - "/api/v1/marketplace/import-jobs", - "/api/v1/product", - "/api/v1/shop", - "/api/v1/stats", - "/api/v1/stock", - ] - - for endpoint in protected_endpoints: - response = client.get(endpoint) - assert response.status_code == 401 # Authentication missing - - def test_protected_endpoint_with_invalid_token(self, client): - """Test protected endpoints with invalid token""" - headers = {"Authorization": "Bearer invalid_token_here"} - - response = client.get("/api/v1/product", headers=headers) - assert response.status_code == 401 # Token is not valid - - def test_admin_endpoint_requires_admin_role(self, client, auth_headers): - """Test that admin endpoints require admin role""" - response = client.get("/api/v1/admin/users", headers=auth_headers) - assert ( - response.status_code == 403 - ) # Token is valid but user does not have access. - # Regular user should be denied - - def test_sql_injection_prevention(self, client, auth_headers): - """Test SQL injection prevention in search parameters""" - # Try SQL injection in search parameter - malicious_search = "'; DROP TABLE products; --" - - response = client.get( - f"/api/v1/product?search={malicious_search}", headers=auth_headers - ) - - # Should not crash and should return normal response - assert response.status_code == 200 - # Database should still be intact (no products dropped) - - # def test_input_validation(self, client, auth_headers): - # # TODO: implement sanitization - # """Test input validation and sanitization""" - # # Test XSS attempt in product creation - # xss_payload = "" - # - # product_data = { - # "product_id": "XSS_TEST", - # "title": xss_payload, - # "description": xss_payload, - # } - # - # response = client.post("/api/v1/product", headers=auth_headers, json=product_data) - # - # assert response.status_code == 200 - # data = response.json() - # assert "