Tests restructuring
This commit is contained in:
@@ -18,7 +18,43 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
# Project information
|
# Project information
|
||||||
project_name: str = "Ecommerce Backend API with Marketplace Support"
|
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"
|
version: str = "2.2.0"
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
|
|||||||
@@ -318,9 +318,9 @@ pytest tests/ -m integration -v # Integration tests only
|
|||||||
pytest tests/ -m "not slow" -v # Fast tests only
|
pytest tests/ -m "not slow" -v # Fast tests only
|
||||||
|
|
||||||
# Run specific test files
|
# Run specific test files
|
||||||
pytest tests/test_auth.py -v # Authentication tests
|
pytest tests/test_authentication_endpoints.py -v # Authentication tests
|
||||||
pytest tests/test_product.py -v # Product tests
|
pytest tests/test_product_endpoints.py -v # Product tests
|
||||||
pytest tests/test_stock.py -v # Stock management tests
|
pytest tests/test_stock_endpoints.py -v # Stock management tests
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test Coverage
|
### Test Coverage
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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
65
pytest.ini
Normal 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
BIN
tests/.coverage
Normal file
Binary file not shown.
@@ -1,6 +1,4 @@
|
|||||||
# tests/conftest.py
|
# tests/conftest.py - Updated main conftest with core fixtures only
|
||||||
import uuid
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -9,7 +7,6 @@ from sqlalchemy.pool import StaticPool
|
|||||||
|
|
||||||
from app.core.database import Base, get_db
|
from app.core.database import Base, get_db
|
||||||
from main import app
|
from main import app
|
||||||
from middleware.auth import AuthManager
|
|
||||||
# Import all models to ensure they're registered with Base metadata
|
# Import all models to ensure they're registered with Base metadata
|
||||||
from models.database_models import (MarketplaceImportJob, Product, Shop,
|
from models.database_models import (MarketplaceImportJob, Product, Shop,
|
||||||
ShopProduct, Stock, User)
|
ShopProduct, Stock, User)
|
||||||
@@ -54,7 +51,7 @@ def db(engine, testing_session_local):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def client(db): # Now client depends on db
|
def client(db):
|
||||||
"""Create a test client with database dependency override"""
|
"""Create a test client with database dependency override"""
|
||||||
|
|
||||||
# Override the dependency to use our test database
|
# 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]
|
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
|
# Cleanup fixture to ensure clean state
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def cleanup():
|
def cleanup():
|
||||||
@@ -250,227 +81,10 @@ def cleanup():
|
|||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
# Add these fixtures to your existing conftest.py
|
# Import fixtures from fixture modules
|
||||||
|
pytest_plugins = [
|
||||||
|
"tests.fixtures.auth_fixtures",
|
||||||
@pytest.fixture
|
"tests.fixtures.product_fixtures",
|
||||||
def unique_product(db):
|
"tests.fixtures.shop_fixtures",
|
||||||
"""Create a unique product for tests that need isolated product data"""
|
"tests.fixtures.marketplace_fixtures",
|
||||||
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()
|
|
||||||
0
tests/ecommerce.db
Normal file
0
tests/ecommerce.db
Normal file
3
tests/fixtures/__init__.py
vendored
Normal file
3
tests/fixtures/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/fixtures/__init__.py
|
||||||
|
"""Test fixtures for the FastAPI application test suite."""
|
||||||
|
|
||||||
3
tests/integration/__init__.py
Normal file
3
tests/integration/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/integration/__init__.py
|
||||||
|
"""Integration tests - multiple components working together."""
|
||||||
|
|
||||||
3
tests/integration/api/__init__.py
Normal file
3
tests/integration/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/integration/api/__init__.py
|
||||||
|
"""API integration tests."""
|
||||||
|
|
||||||
3
tests/integration/api/v1/__init__.py
Normal file
3
tests/integration/api/v1/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/integration/api/v1/__init__.py
|
||||||
|
"""API v1 endpoint integration tests."""
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@ class TestAdminAPI:
|
|||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert (
|
assert (
|
||||||
"Access denied" in response.json()["detail"]
|
"Access denied" in response.json()["detail"]
|
||||||
or "admin" in response.json()["detail"].lower()
|
or "admin" in response.json()["detail"].lower()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_toggle_user_status_admin(self, client, admin_headers, test_user):
|
def test_toggle_user_status_admin(self, client, admin_headers, test_user):
|
||||||
@@ -48,7 +48,7 @@ class TestAdminAPI:
|
|||||||
assert "User not found" in response.json()["detail"]
|
assert "User not found" in response.json()["detail"]
|
||||||
|
|
||||||
def test_toggle_user_status_cannot_deactivate_self(
|
def test_toggle_user_status_cannot_deactivate_self(
|
||||||
self, client, admin_headers, test_admin
|
self, client, admin_headers, test_admin
|
||||||
):
|
):
|
||||||
"""Test that admin cannot deactivate their own account"""
|
"""Test that admin cannot deactivate their own account"""
|
||||||
response = client.put(
|
response = client.put(
|
||||||
@@ -79,8 +79,8 @@ class TestAdminAPI:
|
|||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert (
|
assert (
|
||||||
"Access denied" in response.json()["detail"]
|
"Access denied" in response.json()["detail"]
|
||||||
or "admin" in response.json()["detail"].lower()
|
or "admin" in response.json()["detail"].lower()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_verify_shop_admin(self, client, admin_headers, test_shop):
|
def test_verify_shop_admin(self, client, admin_headers, test_shop):
|
||||||
@@ -120,7 +120,7 @@ class TestAdminAPI:
|
|||||||
assert "Shop not found" in response.json()["detail"]
|
assert "Shop not found" in response.json()["detail"]
|
||||||
|
|
||||||
def test_get_marketplace_import_jobs_admin(
|
def test_get_marketplace_import_jobs_admin(
|
||||||
self, client, admin_headers, test_marketplace_job
|
self, client, admin_headers, test_marketplace_job
|
||||||
):
|
):
|
||||||
"""Test admin getting marketplace import jobs"""
|
"""Test admin getting marketplace import jobs"""
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -136,7 +136,7 @@ class TestAdminAPI:
|
|||||||
assert test_marketplace_job.id in job_ids
|
assert test_marketplace_job.id in job_ids
|
||||||
|
|
||||||
def test_get_marketplace_import_jobs_with_filters(
|
def test_get_marketplace_import_jobs_with_filters(
|
||||||
self, client, admin_headers, test_marketplace_job
|
self, client, admin_headers, test_marketplace_job
|
||||||
):
|
):
|
||||||
"""Test admin getting marketplace import jobs with filters"""
|
"""Test admin getting marketplace import jobs with filters"""
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -160,8 +160,8 @@ class TestAdminAPI:
|
|||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert (
|
assert (
|
||||||
"Access denied" in response.json()["detail"]
|
"Access denied" in response.json()["detail"]
|
||||||
or "admin" in response.json()["detail"].lower()
|
or "admin" in response.json()["detail"].lower()
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_admin_pagination_users(self, client, admin_headers, test_user, test_admin):
|
def test_admin_pagination_users(self, client, admin_headers, test_user, test_admin):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_auth.py
|
# tests/test_authentication_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_marketplace.py
|
# tests/test_marketplace_endpoints.py
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
# tests/integration/api/v1/test_pagination.py
|
# tests/integration/api/v1/test_pagination.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database_models import Product
|
from models.database_models import Product, Shop
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
@pytest.mark.database
|
@pytest.mark.database
|
||||||
@pytest.mark.products
|
@pytest.mark.products
|
||||||
|
@pytest.mark.shops
|
||||||
class TestPagination:
|
class TestPagination:
|
||||||
def test_product_pagination(self, client, auth_headers, db):
|
def test_product_pagination(self, client, auth_headers, db):
|
||||||
"""Test pagination for product listing"""
|
"""Test pagination for product listing"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_product.py
|
# tests/test_product_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_shop.py
|
# tests/test_shop_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_stats.py
|
# tests/test_stats_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_stock.py
|
# tests/test_stock_endpoints.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database_models import Stock
|
from models.database_models import Stock
|
||||||
6
tests/integration/conftest.py
Normal file
6
tests/integration/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# tests/integration/conftest.py
|
||||||
|
"""Integration test specific fixtures."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Add any integration-specific fixtures here if needed
|
||||||
|
|
||||||
3
tests/integration/security/__init__.py
Normal file
3
tests/integration/security/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/integration/security/__init__.py
|
||||||
|
"""Security integration tests."""
|
||||||
|
|
||||||
3
tests/performance/__init__.py
Normal file
3
tests/performance/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/performance/__init__.py
|
||||||
|
"""Performance and load tests."""
|
||||||
|
|
||||||
10
tests/performance/conftest.py
Normal file
10
tests/performance/conftest.py
Normal 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
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -7,3 +7,4 @@ pytest-mock>=3.11.0
|
|||||||
httpx>=0.24.0
|
httpx>=0.24.0
|
||||||
faker>=19.0.0
|
faker>=19.0.0
|
||||||
pytest-repeat>=0.9.4
|
pytest-repeat>=0.9.4
|
||||||
|
pytest-timeout>=2.1.0
|
||||||
|
|||||||
3
tests/system/__init__.py
Normal file
3
tests/system/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/system/__init__.py
|
||||||
|
"""System-level tests - full application behavior."""
|
||||||
|
|
||||||
6
tests/system/conftest.py
Normal file
6
tests/system/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# tests/system/conftest.py
|
||||||
|
"""System test specific fixtures."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Add any system-specific fixtures here if needed
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
4
tests/test_data/csv/sample_products.csv
Normal file
4
tests/test_data/csv/sample_products.csv
Normal 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,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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 "<script>" in data["title"]
|
|
||||||
@@ -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
3
tests/unit/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/unit/__init__.py
|
||||||
|
"""Unit tests - fast, isolated component tests."""
|
||||||
|
|
||||||
6
tests/unit/conftest.py
Normal file
6
tests/unit/conftest.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# tests/unit/conftest.py
|
||||||
|
"""Unit test specific fixtures."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Add any unit-specific fixtures here if needed
|
||||||
|
|
||||||
3
tests/unit/models/__init__.py
Normal file
3
tests/unit/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/unit/models/__init__.py
|
||||||
|
"""Database and API model unit tests."""
|
||||||
|
|
||||||
3
tests/unit/services/__init__.py
Normal file
3
tests/unit/services/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/unit/services/__init__.py
|
||||||
|
"""Service layer unit tests."""
|
||||||
|
|
||||||
@@ -6,7 +6,8 @@ from app.services.auth_service import AuthService
|
|||||||
from models.api_models import UserLogin, UserRegister
|
from models.api_models import UserLogin, UserRegister
|
||||||
from models.database_models import User
|
from models.database_models import User
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.auth
|
||||||
class TestAuthService:
|
class TestAuthService:
|
||||||
"""Test suite for AuthService following the application's testing patterns"""
|
"""Test suite for AuthService following the application's testing patterns"""
|
||||||
|
|
||||||
@@ -8,7 +8,8 @@ from app.services.marketplace_service import MarketplaceService
|
|||||||
from models.api_models import MarketplaceImportRequest
|
from models.api_models import MarketplaceImportRequest
|
||||||
from models.database_models import MarketplaceImportJob, Shop, User
|
from models.database_models import MarketplaceImportJob, Shop, User
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.marketplace
|
||||||
class TestMarketplaceService:
|
class TestMarketplaceService:
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.service = MarketplaceService()
|
self.service = MarketplaceService()
|
||||||
@@ -5,7 +5,8 @@ from app.services.product_service import ProductService
|
|||||||
from models.api_models import ProductCreate
|
from models.api_models import ProductCreate
|
||||||
from models.database_models import Product
|
from models.database_models import Product
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.products
|
||||||
class TestProductService:
|
class TestProductService:
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.service = ProductService()
|
self.service = ProductService()
|
||||||
@@ -5,7 +5,8 @@ from fastapi import HTTPException
|
|||||||
from app.services.shop_service import ShopService
|
from app.services.shop_service import ShopService
|
||||||
from models.api_models import ShopCreate, ShopProductCreate
|
from models.api_models import ShopCreate, ShopProductCreate
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.shops
|
||||||
class TestShopService:
|
class TestShopService:
|
||||||
"""Test suite for ShopService following the application's testing patterns"""
|
"""Test suite for ShopService following the application's testing patterns"""
|
||||||
|
|
||||||
@@ -4,7 +4,8 @@ import pytest
|
|||||||
from app.services.stats_service import StatsService
|
from app.services.stats_service import StatsService
|
||||||
from models.database_models import Product, Stock
|
from models.database_models import Product, Stock
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.stats
|
||||||
class TestStatsService:
|
class TestStatsService:
|
||||||
"""Test suite for StatsService following the application's testing patterns"""
|
"""Test suite for StatsService following the application's testing patterns"""
|
||||||
|
|
||||||
@@ -7,7 +7,8 @@ from app.services.stock_service import StockService
|
|||||||
from models.api_models import StockAdd, StockCreate, StockUpdate
|
from models.api_models import StockAdd, StockCreate, StockUpdate
|
||||||
from models.database_models import Product, Stock
|
from models.database_models import Product, Stock
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.stock
|
||||||
class TestStockService:
|
class TestStockService:
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.service = StockService()
|
self.service = StockService()
|
||||||
3
tests/unit/utils/__init__.py
Normal file
3
tests/unit/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# tests/unit/utils/__init__.py
|
||||||
|
"""Utility function unit tests."""
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import requests.exceptions
|
|||||||
|
|
||||||
from utils.csv_processor import CSVProcessor
|
from utils.csv_processor import CSVProcessor
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
class TestCSVProcessor:
|
class TestCSVProcessor:
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.processor = CSVProcessor()
|
self.processor = CSVProcessor()
|
||||||
@@ -73,7 +73,7 @@ class TestPriceProcessor:
|
|||||||
assert currency == "EUR"
|
assert currency == "EUR"
|
||||||
|
|
||||||
# Test with euro symbol
|
# 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 price == "8.26"
|
||||||
assert currency == "EUR"
|
assert currency == "EUR"
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import pytest
|
|||||||
|
|
||||||
from utils.data_processing import GTINProcessor, PriceProcessor
|
from utils.data_processing import GTINProcessor, PriceProcessor
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
class TestDataValidation:
|
class TestDataValidation:
|
||||||
def test_gtin_normalization_edge_cases(self):
|
def test_gtin_normalization_edge_cases(self):
|
||||||
"""Test GTIN normalization with edge cases"""
|
"""Test GTIN normalization with edge cases"""
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user