Tests restructuring

This commit is contained in:
2025-09-19 21:23:57 +02:00
parent d0924f90c4
commit 366093bbc6
70 changed files with 212 additions and 1957 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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")

65
pytest.ini Normal file
View File

@@ -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

BIN
tests/.coverage Normal file

Binary file not shown.

View File

@@ -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",
]

0
tests/ecommerce.db Normal file
View File

3
tests/fixtures/__init__.py vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# tests/test_auth.py
# tests/test_authentication_endpoints.py
import pytest
from fastapi import HTTPException

View File

@@ -1,4 +1,4 @@
# tests/test_marketplace.py
# tests/test_marketplace_endpoints.py
from unittest.mock import AsyncMock, patch
import pytest

View File

@@ -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"""

View File

@@ -1,4 +1,4 @@
# tests/test_product.py
# tests/test_product_endpoints.py
import pytest

View File

@@ -1,4 +1,4 @@
# tests/test_shop.py
# tests/test_shop_endpoints.py
import pytest

View File

@@ -1,4 +1,4 @@
# tests/test_stats.py
# tests/test_stats_endpoints.py
import pytest

View File

@@ -1,4 +1,4 @@
# tests/test_stock.py
# tests/test_stock_endpoints.py
import pytest
from models.database_models import Stock

View File

@@ -0,0 +1,6 @@
# tests/integration/conftest.py
"""Integration test specific fixtures."""
import pytest
# Add any integration-specific fixtures here if needed

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

3
tests/system/__init__.py Normal file
View File

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

6
tests/system/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
# tests/system/conftest.py
"""System test specific fixtures."""
import pytest
# Add any system-specific fixtures here if needed

View File

@@ -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

View File

@@ -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

View File

@@ -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
1 product_id title price currency brand marketplace
2 TEST001 Sample Product 1 19.99 EUR TestBrand TestMarket
3 TEST002 Sample Product 2 29.99 EUR TestBrand TestMarket
4 TEST003 Sample Product 3 39.99 USD AnotherBrand TestMarket

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = "<script>alert('xss')</script>"
#
# 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 "<script>" not in data["title"]
# assert "&lt;script&gt;" in data["title"]

View File

@@ -1,119 +0,0 @@
# tests/test_utils.py (Enhanced version of your existing file)
import pytest
from utils.data_processing import GTINProcessor, PriceProcessor
class TestGTINProcessor:
def setup_method(self):
self.processor = GTINProcessor()
def test_normalize_valid_gtin(self):
"""Test GTIN normalization with valid inputs"""
# Test EAN-13
assert self.processor.normalize("1234567890123") == "1234567890123"
# Test UPC-A (12 digits)
assert self.processor.normalize("123456789012") == "123456789012"
# Test with decimal point
assert self.processor.normalize("123456789012.0") == "123456789012"
# Test EAN-8
assert self.processor.normalize("12345678") == "12345678"
def test_normalize_invalid_gtin(self):
"""Test GTIN normalization with invalid inputs"""
assert self.processor.normalize("") is None
assert self.processor.normalize(None) is None
assert self.processor.normalize("abc") is None
# Test short number (gets padded)
assert self.processor.normalize("123") == "0000000000123"
def test_normalize_gtin_with_formatting(self):
"""Test GTIN normalization with various formatting"""
# Test with spaces
assert self.processor.normalize("123 456 789 012") == "123456789012"
# Test with dashes
assert self.processor.normalize("123-456-789-012") == "123456789012"
# Test with mixed formatting
assert self.processor.normalize("123 456-789 012") == "123456789012"
def test_validate_gtin(self):
"""Test GTIN validation"""
assert self.processor.validate("1234567890123") is True
assert self.processor.validate("123456789012") is True
assert self.processor.validate("12345678") is True
assert self.processor.validate("123") is False
assert self.processor.validate("") is False
assert self.processor.validate(None) is False
def test_gtin_checksum_validation(self):
"""Test GTIN checksum validation if implemented"""
# This test would verify checksum calculation if your GTINProcessor implements it
# For now, we'll test the structure validation
assert self.processor.validate("1234567890123") is True
assert self.processor.validate("12345678901234") is True # 14 digits
assert self.processor.validate("123456789012345") is False # 15 digits
class TestPriceProcessor:
def setup_method(self):
self.processor = PriceProcessor()
def test_parse_price_currency_eur(self):
"""Test EUR price parsing"""
price, currency = self.processor.parse_price_currency("8.26 EUR")
assert price == "8.26"
assert currency == "EUR"
# Test with euro symbol
price, currency = self.processor.parse_price_currency("8.26 €")
assert price == "8.26"
assert currency == "EUR"
def test_parse_price_currency_usd(self):
"""Test USD price parsing"""
price, currency = self.processor.parse_price_currency("$12.50")
assert price == "12.50"
assert currency == "USD"
price, currency = self.processor.parse_price_currency("12.50 USD")
assert price == "12.50"
assert currency == "USD"
def test_parse_price_currency_comma_decimal(self):
"""Test price parsing with comma as decimal separator"""
price, currency = self.processor.parse_price_currency("8,26 EUR")
assert price == "8.26"
assert currency == "EUR"
def test_parse_invalid_price(self):
"""Test invalid price parsing"""
price, currency = self.processor.parse_price_currency("")
assert price is None
assert currency is None
price, currency = self.processor.parse_price_currency(None)
assert price is None
assert currency is None
def test_parse_price_edge_cases(self):
"""Test edge cases in price parsing"""
# Test price without currency
price, currency = self.processor.parse_price_currency("15.99")
assert price == "15.99"
# currency might be None or default value
# Test currency before price
price, currency = self.processor.parse_price_currency("EUR 25.50")
assert price == "25.50"
assert currency == "EUR"
# Test with multiple decimal places
price, currency = self.processor.parse_price_currency("12.999 USD")
assert price == "12.999"
assert currency == "USD"

3
tests/unit/__init__.py Normal file
View File

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

6
tests/unit/conftest.py Normal file
View File

@@ -0,0 +1,6 @@
# tests/unit/conftest.py
"""Unit test specific fixtures."""
import pytest
# Add any unit-specific fixtures here if needed

View File

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

View File

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

View File

@@ -6,7 +6,8 @@ from app.services.auth_service import AuthService
from models.api_models import UserLogin, UserRegister
from models.database_models import User
@pytest.mark.unit
@pytest.mark.auth
class TestAuthService:
"""Test suite for AuthService following the application's testing patterns"""

View File

@@ -8,7 +8,8 @@ from app.services.marketplace_service import MarketplaceService
from models.api_models import MarketplaceImportRequest
from models.database_models import MarketplaceImportJob, Shop, User
@pytest.mark.unit
@pytest.mark.marketplace
class TestMarketplaceService:
def setup_method(self):
self.service = MarketplaceService()

View File

@@ -5,7 +5,8 @@ from app.services.product_service import ProductService
from models.api_models import ProductCreate
from models.database_models import Product
@pytest.mark.unit
@pytest.mark.products
class TestProductService:
def setup_method(self):
self.service = ProductService()

View File

@@ -5,7 +5,8 @@ from fastapi import HTTPException
from app.services.shop_service import ShopService
from models.api_models import ShopCreate, ShopProductCreate
@pytest.mark.unit
@pytest.mark.shops
class TestShopService:
"""Test suite for ShopService following the application's testing patterns"""

View File

@@ -4,7 +4,8 @@ import pytest
from app.services.stats_service import StatsService
from models.database_models import Product, Stock
@pytest.mark.unit
@pytest.mark.stats
class TestStatsService:
"""Test suite for StatsService following the application's testing patterns"""

View File

@@ -7,7 +7,8 @@ from app.services.stock_service import StockService
from models.api_models import StockAdd, StockCreate, StockUpdate
from models.database_models import Product, Stock
@pytest.mark.unit
@pytest.mark.stock
class TestStockService:
def setup_method(self):
self.service = StockService()

View File

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

View File

@@ -8,7 +8,7 @@ import requests.exceptions
from utils.csv_processor import CSVProcessor
@pytest.mark.unit
class TestCSVProcessor:
def setup_method(self):
self.processor = CSVProcessor()

View File

@@ -73,7 +73,7 @@ class TestPriceProcessor:
assert currency == "EUR"
# Test with euro symbol
price, currency = self.processor.parse_price_currency("8.26 €")
price, currency = self.processor.parse_price_currency("8.26 ")
assert price == "8.26"
assert currency == "EUR"

View File

@@ -3,7 +3,7 @@ import pytest
from utils.data_processing import GTINProcessor, PriceProcessor
@pytest.mark.unit
class TestDataValidation:
def test_gtin_normalization_edge_cases(self):
"""Test GTIN normalization with edge cases"""

View File

@@ -1,92 +0,0 @@
@echo off
REM migrate_tests.bat - Windows script to migrate your test structure
echo Starting test structure migration...
REM Create new directory structure
echo Creating directory structure...
mkdir tests\fixtures 2>nul
mkdir tests\unit 2>nul
mkdir tests\unit\models 2>nul
mkdir tests\unit\utils 2>nul
mkdir tests\unit\services 2>nul
mkdir tests\integration 2>nul
mkdir tests\integration\api 2>nul
mkdir tests\integration\api\v1 2>nul
mkdir tests\integration\security 2>nul
mkdir tests\performance 2>nul
mkdir tests\system 2>nul
mkdir tests\test_data 2>nul
mkdir tests\test_data\csv 2>nul
REM Create __init__.py files
echo Creating __init__.py files...
echo. > tests\fixtures\__init__.py
echo. > tests\unit\__init__.py
echo. > tests\unit\models\__init__.py
echo. > tests\unit\utils\__init__.py
echo. > tests\unit\services\__init__.py
echo. > tests\integration\__init__.py
echo. > tests\integration\api\__init__.py
echo. > tests\integration\api\v1\__init__.py
echo. > tests\integration\security\__init__.py
echo. > tests\performance\__init__.py
echo. > tests\system\__init__.py
REM Create conftest.py files for each test category
echo Creating conftest.py files...
echo. > tests\unit\conftest.py
echo. > tests\integration\conftest.py
echo. > tests\performance\conftest.py
echo. > tests\system\conftest.py
REM Backup original files
echo Backing up original files...
mkdir tests\backup 2>nul
copy tests\conftest.py tests\backup\ >nul 2>&1
copy tests\test_*.py tests\backup\ >nul 2>&1
echo Directory structure created!
echo.
echo Next steps:
echo 1. Copy the fixture files I provided to tests\fixtures\
echo 2. Update tests\conftest.py with the new version
echo 3. Move test files to their new locations:
echo - test_database.py to tests\unit\models\test_database_models.py
echo - test_utils.py to tests\unit\utils\test_data_processing.py
echo - test_admin_service.py to tests\unit\services\
echo - test_admin.py to tests\integration\api\v1\test_admin_endpoints.py
echo - test_pagination.py to tests\integration\api\v1\
echo - test_performance.py to tests\performance\test_api_performance.py
echo - test_error_handling.py to tests\system\
echo - Split test_security.py into security subdirectory
echo.
echo 4. Update imports in moved test files
echo 5. Add pytest markers to test classes
echo 6. Update pytest.ini with the enhanced configuration
echo.
echo Test the migration with:
echo pytest tests\unit -v
echo pytest tests\integration -v
echo pytest -m unit
echo.
echo Quick test commands after migration:
echo pytest -m unit # Fast unit tests
echo pytest -m integration # Integration tests
echo pytest -m "not slow" # Skip slow tests
echo pytest tests\unit\models\ # Model tests only
echo pytest --cov=app --cov-report=html # Coverage report
REM Create a sample test data file
echo Creating sample test data...
(
echo product_id,title,price,currency,brand,marketplace
echo TEST001,Sample Product 1,19.99,EUR,TestBrand,TestMarket
echo TEST002,Sample Product 2,29.99,EUR,TestBrand,TestMarket
echo TEST003,Sample Product 3,39.99,USD,AnotherBrand,TestMarket
) > tests\test_data\csv\sample_products.csv
echo.
echo Migration structure ready! Follow the steps above to complete the migration.
echo All your original files are backed up in tests\backup\
pause