diff --git a/docs/testing/test-structure.md b/docs/testing/test-structure.md new file mode 100644 index 00000000..f833a2b0 --- /dev/null +++ b/docs/testing/test-structure.md @@ -0,0 +1,426 @@ +# Test Structure + +## Overview + +The integration test suite is organized to mirror the API code structure, providing clear mapping between code and tests for better maintainability, discoverability, and team collaboration. + +## Directory Structure + +``` +tests/integration/api/v1/ +├── admin/ # Admin API tests +│ ├── __init__.py +│ └── README.md +├── vendor/ # Vendor API tests +│ ├── __init__.py +│ ├── README.md +│ ├── test_authentication.py # Authentication tests +│ └── test_dashboard.py # Dashboard tests +├── public/ # Public API tests +│ ├── __init__.py +│ └── README.md +└── shared/ # Shared/common tests + ├── __init__.py + └── README.md +``` + +## Structure Mapping + +The test structure directly mirrors the API code structure: + +``` +app/api/v1/admin/ → tests/integration/api/v1/admin/ +app/api/v1/vendor/ → tests/integration/api/v1/vendor/ +app/api/v1/public/ → tests/integration/api/v1/public/ +app/api/v1/shared/ → tests/integration/api/v1/shared/ +``` + +### Benefits + +#### 1. Easy Discoverability +Finding tests is straightforward: + +```bash +# Looking for vendor product tests? +ls tests/integration/api/v1/vendor/test_products.py + +# Looking for admin user tests? +ls tests/integration/api/v1/admin/test_users.py +``` + +#### 2. Clear Code-to-Test Mapping +``` +Code change: app/api/v1/vendor/products.py +Test location: tests/integration/api/v1/vendor/test_products.py + ↑ 1:1 mapping! +``` + +#### 3. Faster Testing +Run tests for specific areas without complex filters: + +```bash +# Test only vendor endpoints +pytest tests/integration/api/v1/vendor/ -v + +# Test only admin endpoints +pytest tests/integration/api/v1/admin/ -v +``` + +#### 4. Better Code Reviews +Changes are grouped logically: + +``` +Pull Request: + Changed files: + app/api/v1/vendor/products.py # Code + tests/integration/api/v1/vendor/test_products.py # Test + ↑ Easy to verify +``` + +#### 5. Team Collaboration +Different teams can work in parallel with fewer conflicts: + +``` +Admin Team: works in tests/integration/api/v1/admin/ +Vendor Team: works in tests/integration/api/v1/vendor/ +Public Team: works in tests/integration/api/v1/public/ +``` + +## Running Tests + +### By Area + +```bash +# All vendor tests +pytest tests/integration/api/v1/vendor/ -v + +# All admin tests +pytest tests/integration/api/v1/admin/ -v + +# All public tests +pytest tests/integration/api/v1/public/ -v + +# All shared tests +pytest tests/integration/api/v1/shared/ -v +``` + +### Specific Test Files + +```bash +# Run specific test file +pytest tests/integration/api/v1/vendor/test_authentication.py -v + +# Run specific test class +pytest tests/integration/api/v1/vendor/test_authentication.py::TestVendorAPIAuthentication -v + +# Run specific test +pytest tests/integration/api/v1/vendor/test_authentication.py::TestVendorAPIAuthentication::test_vendor_auth_me_success -v +``` + +### With Coverage + +```bash +# Vendor API coverage +pytest tests/integration/api/v1/vendor/ \ + --cov=app/api/v1/vendor \ + --cov-report=html + +# View coverage report +open htmlcov/index.html +``` + +### Using Markers + +All tests use pytest markers for flexible filtering: + +```bash +# All vendor tests +pytest -m vendor -v + +# Vendor API tests only +pytest -m "vendor and api" -v + +# Vendor authentication tests +pytest -m "vendor and auth" -v +``` + +## Naming Conventions + +### Test Files + +- **Pattern**: `test_.py` +- **Examples**: + - `test_products.py` - Tests for products endpoints + - `test_authentication.py` - Tests for authentication + - `test_dashboard.py` - Tests for dashboard endpoints + +### Test Classes + +- **Pattern**: `TestAPI` +- **Examples**: + ```python + class TestVendorProductAPI: # Vendor product endpoints + class TestAdminUserAPI: # Admin user endpoints + class TestPublicCatalogAPI: # Public catalog endpoints + ``` + +### Test Functions + +- **Pattern**: `test__` +- **Examples**: + ```python + def test_list_products_success(self): + def test_create_product_without_auth(self): + def test_update_product_invalid_data(self): + def test_delete_product_not_found(self): + ``` + +## Best Practices + +### 1. Mirror API Structure + +Always create test files that mirror the API code structure: + +```python +# API Code +app/api/v1/vendor/products.py + +# Test File +tests/integration/api/v1/vendor/test_products.py +``` + +### 2. One Test File Per API Module + +Keep tests focused: + +- Each API module gets its own test file +- Easy to find and maintain +- Clear responsibility + +### 3. Descriptive Test Names + +Use clear, descriptive names: + +```python +# Good ✅ +class TestVendorProductAPI: + def test_list_products_success(self): + def test_create_product_without_auth(self): + def test_update_product_with_invalid_data(self): + +# Avoid ❌ +class TestProducts: + def test_api(self): + def test_endpoint(self): +``` + +### 4. Appropriate Markers + +Mark tests appropriately: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorProductAPI: + pass +``` + +### 5. Test Both Success and Failure + +Cover all scenarios: + +```python +class TestVendorProductAPI: + # Success cases + def test_list_products_success(self): + def test_create_product_success(self): + + # Failure cases + def test_list_products_without_auth(self): + def test_create_product_invalid_data(self): + def test_create_product_as_admin_user(self): +``` + +## Test Areas + +### Admin Tests (`admin/`) + +Tests for admin-only endpoints at `/api/v1/admin/*`: + +- User management +- System configuration +- Analytics and reporting +- Vendor approval workflows + +**Requirements:** +- Admin user authentication +- Admin-specific permissions +- Cross-vendor data access + +### Vendor Tests (`vendor/`) + +Tests for vendor endpoints at `/api/v1/vendor/*`: + +- Product management +- Order processing +- Customer management +- Analytics and stats +- Team management + +**Requirements:** +- Vendor user authentication +- Vendor context isolation +- VendorUser associations + +See [Vendor API Testing Guide](vendor-api-testing.md) for details. + +### Public Tests (`public/`) + +Tests for public endpoints at `/api/v1/public/*`: + +- Product catalog browsing +- Public vendor profiles +- Search functionality + +**Requirements:** +- No authentication required +- Rate limiting tests +- Public data only + +### Shared Tests (`shared/`) + +Tests for shared/common functionality: + +- Authentication endpoints +- Pagination utilities +- Filtering utilities +- Common error handling + +**Requirements:** +- Work across all user types +- Consistent behavior +- Proper error handling + +## Migration Status + +### Completed ✅ + +| Area | Files | Tests | Status | +|------|-------|-------|--------| +| Vendor Authentication | `vendor/test_authentication.py` | 30+ | ✅ Complete | +| Vendor Dashboard | `vendor/test_dashboard.py` | 12 | ✅ Complete | + +### Pending ⚠️ + +| Legacy File | New Location | Priority | +|-------------|--------------|----------| +| `test_vendor_endpoints.py` | Split into `vendor/test_*.py` | High | +| `test_admin_endpoints.py` | Split into `admin/test_*.py` | High | +| `test_auth_endpoints.py` | `shared/test_auth.py` | Medium | +| `test_marketplace_*.py` | `vendor/test_marketplace_*.py` | Medium | +| `test_inventory_endpoints.py` | `vendor/test_inventory.py` | Medium | + +## Adding New Tests + +When adding new integration tests: + +1. **Identify the API area** (admin/vendor/public/shared) +2. **Create test file in appropriate folder** +3. **Follow naming conventions** +4. **Add appropriate markers** +5. **Test both success and failure cases** +6. **Update coverage reports** + +### Example: Adding Vendor Products Tests + +```bash +# 1. Create test file +touch tests/integration/api/v1/vendor/test_products.py +``` + +```python +# 2. Write tests following conventions +import pytest + +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorProductAPI: + """Tests for vendor product management endpoints""" + + def test_list_products_success(self, client, vendor_user_headers): + """Test listing products with authentication""" + response = client.get( + "/api/v1/vendor/products", + headers=vendor_user_headers + ) + assert response.status_code == 200 + data = response.json() + assert "products" in data + assert "total" in data + + def test_list_products_without_auth(self, client): + """Test listing products requires authentication""" + response = client.get("/api/v1/vendor/products") + assert response.status_code == 401 + + def test_create_product_success(self, client, vendor_user_headers): + """Test creating product with valid data""" + response = client.post( + "/api/v1/vendor/products", + headers=vendor_user_headers, + json={ + "name": "Test Product", + "price": 29.99, + "description": "Test description" + } + ) + assert response.status_code == 201 +``` + +```bash +# 3. Run tests +pytest tests/integration/api/v1/vendor/test_products.py -v + +# 4. Check coverage +pytest tests/integration/api/v1/vendor/test_products.py \ + --cov=app/api/v1/vendor/products \ + --cov-report=html +``` + +## Troubleshooting + +### Tests Not Discovered + +**Problem**: `pytest` doesn't find tests + +**Solutions**: +- Ensure `__init__.py` exists in test directory +- Check file naming: `test_*.py` or `*_test.py` +- Verify test functions start with `test_` +- Check pytest collection with: `pytest --collect-only` + +### Import Errors + +**Problem**: Fixtures not found + +**Solutions**: +- Check `pytest_plugins` in `tests/conftest.py` +- Use absolute imports: `from tests.fixtures.auth_fixtures import ...` +- Verify fixture files are in correct location + +### Coverage Not Working + +**Problem**: Coverage report shows 0% + +**Solutions**: +- Specify correct source path: `--cov=app/api/v1/vendor` +- Check test actually exercises the code +- Verify coverage config in `pyproject.toml` or `.coveragerc` + +## See Also + +- [Vendor API Testing Guide](vendor-api-testing.md) - Comprehensive vendor testing documentation +- [Testing Guide](testing-guide.md) - Overall testing strategy +- [Test Maintenance](test-maintenance.md) - Maintaining test quality diff --git a/docs/testing/vendor-api-testing.md b/docs/testing/vendor-api-testing.md new file mode 100644 index 00000000..456c3006 --- /dev/null +++ b/docs/testing/vendor-api-testing.md @@ -0,0 +1,670 @@ +# Vendor API Testing Guide + +## Overview + +Comprehensive integration test suite for vendor API endpoints that use `get_current_vendor_api` for authentication. + +## Test Coverage + +### Current Coverage + +| Endpoint | Auth Tests | Functionality Tests | Coverage | +|----------|-----------|-------------------|----------| +| `/api/v1/vendor/auth/me` | ✅ Complete | ✅ Complete | 100% | +| `/api/v1/vendor/dashboard/stats` | ✅ Complete | ✅ Complete | 100% | +| `/api/v1/vendor/profile` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/settings` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/products` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/orders` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/customers` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/inventory` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/marketplace` | ✅ Auth only | ⚠️ Pending | 50% | +| `/api/v1/vendor/analytics` | ✅ Auth only | ⚠️ Pending | 50% | + +**Total Tests Created**: 58+ tests +**Tests in Structure**: 28 tests (authentication + dashboard) + +## Test Files + +### Location + +``` +tests/integration/api/v1/vendor/ +├── __init__.py +├── test_authentication.py # 30+ authentication tests +└── test_dashboard.py # 12 dashboard tests +``` + +### 1. Authentication Tests + +**File**: `tests/integration/api/v1/vendor/test_authentication.py` + +**Purpose**: Verify authentication and authorization for vendor API endpoints + +#### Test Classes + +##### `TestVendorAPIAuthentication` + +Core authentication tests for `get_current_vendor_api`: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +@pytest.mark.auth +class TestVendorAPIAuthentication: + """Test authentication for vendor API endpoints""" +``` + +**Tests**: + +- ✅ `test_vendor_auth_me_success` - Valid vendor token accepted +- ✅ `test_vendor_auth_me_no_token` - Missing token rejected (401) +- ✅ `test_vendor_auth_me_invalid_token` - Invalid token rejected (401) +- ✅ `test_vendor_auth_me_expired_token` - Expired token rejected (401) +- ✅ `test_vendor_auth_me_malformed_header` - Malformed header rejected (401) +- ✅ `test_admin_user_blocked` - Admin users blocked (403) +- ✅ `test_regular_user_blocked` - Regular users blocked (403) +- ✅ `test_inactive_vendor_user_rejected` - Inactive users rejected (401) +- ✅ `test_csrf_protection_header_required` - Requires Authorization header +- ✅ `test_csrf_protection_cookie_only_rejected` - Cookie-only auth rejected +- ✅ `test_concurrent_requests` - Concurrent requests with same token +- ✅ `test_token_with_missing_claims` - Token with missing claims rejected +- ✅ `test_empty_authorization_header` - Empty header rejected + +##### `TestVendorAPIConsistency` + +Consistency checks across all vendor endpoints: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorAPIConsistency: + """Test that all vendor API endpoints have consistent auth behavior""" +``` + +**Tests**: + +- ✅ Verifies all vendor endpoints require authentication +- ✅ Ensures consistent error responses +- ✅ Checks CSRF protection across all endpoints + +#### What's Tested + +**Authentication (`get_current_vendor_api`)**: +- [x] Requires Authorization header (not cookies) +- [x] Validates JWT token format +- [x] Checks token expiration +- [x] Verifies token signature +- [x] Extracts user from token +- [x] Verifies user exists and is active + +**Authorization**: +- [x] Blocks admin users (403) +- [x] Blocks regular users (403) +- [x] Accepts vendor users (200/404) +- [x] Checks vendor-role requirement + +**CSRF Protection**: +- [x] API endpoints require header auth +- [x] Cookie-only auth rejected +- [x] Consistent across all endpoints + +**Vendor Context**: +- [x] Vendor association required +- [x] Inactive vendors rejected +- [x] Vendor data isolation +- [x] VendorUser relationship working + +### 2. Dashboard Tests + +**File**: `tests/integration/api/v1/vendor/test_dashboard.py` + +**Purpose**: Test vendor dashboard statistics endpoint + +#### Test Class + +##### `TestVendorDashboardAPI` + +Dashboard functionality tests: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorDashboardAPI: + """Test vendor dashboard stats endpoint""" +``` + +**Tests**: + +- ✅ `test_dashboard_stats_structure` - Correct response structure +- ✅ `test_dashboard_stats_vendor_isolation` - Only shows vendor's data +- ✅ `test_dashboard_stats_empty_vendor` - Empty vendor returns zeros +- ✅ `test_dashboard_stats_product_counts` - Accurate product counts +- ✅ `test_dashboard_stats_order_counts` - Accurate order counts +- ✅ `test_dashboard_stats_customer_counts` - Accurate customer counts +- ✅ `test_dashboard_stats_revenue` - Accurate revenue calculations +- ✅ `test_dashboard_stats_no_vendor_association` - User not associated (403) +- ✅ `test_dashboard_stats_inactive_vendor` - Inactive vendor (404) +- ✅ `test_dashboard_stats_performance` - Response time < 2 seconds +- ✅ `test_dashboard_stats_multiple_requests` - Consistency across requests +- ✅ `test_dashboard_stats_data_types` - Correct data types + +#### What's Tested + +**Data Structure**: +- [x] Correct response structure +- [x] All required fields present +- [x] Correct data types + +**Business Logic**: +- [x] Vendor isolation (only shows own data) +- [x] Empty vendor returns zeros +- [x] Accurate product counts (total, active) +- [x] Orders, customers, revenue stats + +**Error Cases**: +- [x] User not associated with vendor (403) +- [x] Inactive vendor (404) +- [x] Missing vendor association + +**Performance**: +- [x] Response time < 2 seconds +- [x] Multiple requests consistency +- [x] Caching behavior + +## Test Fixtures + +### Authentication Fixtures + +Located in `tests/fixtures/auth_fixtures.py`: + +#### `test_vendor_user` + +Creates a user with `role="vendor"`: + +```python +@pytest.fixture +def test_vendor_user(db, auth_manager): + """Create a test vendor user with unique username""" + unique_id = str(uuid.uuid4())[:8] + hashed_password = auth_manager.hash_password("vendorpass123") + user = User( + email=f"vendor_{unique_id}@example.com", + username=f"vendoruser_{unique_id}", + hashed_password=hashed_password, + role="vendor", + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + return user +``` + +**Usage**: Base fixture for vendor-role users + +#### `vendor_user_headers` + +Returns Authorization headers for vendor user: + +```python +@pytest.fixture +def vendor_user_headers(client, test_vendor_user): + """Get authentication headers for vendor user""" + response = client.post( + "/api/v1/auth/login", + json={ + "username": test_vendor_user.username, + "password": "vendorpass123" + }, + ) + token = response.json()["access_token"] + return {"Authorization": f"Bearer {token}"} +``` + +**Usage**: Use for authenticated vendor API requests + +**Format**: `{"Authorization": "Bearer "}` + +### Vendor Fixtures + +Located in `tests/fixtures/vendor_fixtures.py`: + +#### `test_vendor_with_vendor_user` + +Creates a vendor owned by vendor user: + +```python +@pytest.fixture +def test_vendor_with_vendor_user(db, test_vendor_user): + """Create a vendor owned by a vendor user""" + vendor = Vendor( + vendor_code=f"VENDORAPI_{unique_id}", + subdomain=f"vendorapi{unique_id.lower()}", + name=f"Vendor API Test {unique_id}", + owner_user_id=test_vendor_user.id, + is_active=True, + is_verified=True, + ) + db.add(vendor) + db.commit() + + # Create VendorUser association + vendor_user = VendorUser( + vendor_id=vendor.id, + user_id=test_vendor_user.id, + is_owner=True, + is_active=True, + ) + db.add(vendor_user) + db.commit() + + return vendor +``` + +**Usage**: Fully configured vendor for API testing with VendorUser association + +## Running Tests + +### All Vendor Tests + +```bash +# Run all vendor integration tests +pytest tests/integration/api/v1/vendor/ -v + +# With output +pytest tests/integration/api/v1/vendor/ -v -s +``` + +### Specific Test Files + +```bash +# Authentication tests only +pytest tests/integration/api/v1/vendor/test_authentication.py -v + +# Dashboard tests only +pytest tests/integration/api/v1/vendor/test_dashboard.py -v +``` + +### Specific Test Classes + +```bash +# Authentication tests class +pytest tests/integration/api/v1/vendor/test_authentication.py::TestVendorAPIAuthentication -v + +# Dashboard tests class +pytest tests/integration/api/v1/vendor/test_dashboard.py::TestVendorDashboardAPI -v +``` + +### Specific Tests + +```bash +# Single authentication test +pytest tests/integration/api/v1/vendor/test_authentication.py::TestVendorAPIAuthentication::test_vendor_auth_me_success -v + +# Single dashboard test +pytest tests/integration/api/v1/vendor/test_dashboard.py::TestVendorDashboardAPI::test_dashboard_stats_structure -v +``` + +### With Coverage + +```bash +# Coverage for all vendor tests +pytest tests/integration/api/v1/vendor/ \ + --cov=app/api/v1/vendor \ + --cov-report=html + +# View coverage report +open htmlcov/index.html + +# Coverage for specific endpoint +pytest tests/integration/api/v1/vendor/test_dashboard.py \ + --cov=app/api/v1/vendor/dashboard \ + --cov-report=term-missing +``` + +### Using Markers + +```bash +# All vendor tests +pytest -m vendor -v + +# Vendor API tests only +pytest -m "vendor and api" -v + +# Vendor authentication tests +pytest -m "vendor and auth" -v +``` + +## Test Examples + +### Basic Authenticated Request + +```python +def test_vendor_endpoint(client, vendor_user_headers): + """Test vendor endpoint with authentication""" + response = client.get( + "/api/v1/vendor/some-endpoint", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + assert "expected_field" in data +``` + +### Test with Vendor Context + +```python +def test_with_vendor_data( + client, + vendor_user_headers, + test_vendor_with_vendor_user, + db +): + """Test with specific vendor data""" + vendor = test_vendor_with_vendor_user + + # Create test data for this vendor + product = Product( + vendor_id=vendor.id, + name="Test Product", + price=29.99 + ) + db.add(product) + db.commit() + + response = client.get( + "/api/v1/vendor/products", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + assert len(data["products"]) == 1 +``` + +### Test Negative Cases + +```python +def test_rejects_non_vendor(client, auth_headers): + """Test endpoint rejects non-vendor users""" + response = client.get( + "/api/v1/vendor/endpoint", + headers=auth_headers # Regular user token + ) + + assert response.status_code == 403 + data = response.json() + assert data["detail"] == "This endpoint is only for vendor users" +``` + +### Test Authentication Failure + +```python +def test_requires_authentication(client): + """Test endpoint requires authentication""" + response = client.get("/api/v1/vendor/endpoint") + + assert response.status_code == 401 + data = response.json() + assert data["detail"] == "Not authenticated" +``` + +## Expanding Test Coverage + +### Recommended Next Tests + +#### 1. Vendor Products API + +Create `tests/integration/api/v1/vendor/test_products.py`: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorProductsAPI: + """Test vendor product management endpoints""" + + def test_list_products(self, client, vendor_user_headers): + """Test listing products with pagination""" + + def test_create_product_from_marketplace(self, client, vendor_user_headers): + """Test creating product from marketplace""" + + def test_update_product(self, client, vendor_user_headers): + """Test updating product details""" + + def test_delete_product(self, client, vendor_user_headers): + """Test deleting product""" + + def test_toggle_product_active(self, client, vendor_user_headers): + """Test toggling product active status""" +``` + +#### 2. Vendor Orders API + +Create `tests/integration/api/v1/vendor/test_orders.py`: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorOrdersAPI: + """Test vendor order management endpoints""" + + def test_list_orders(self, client, vendor_user_headers): + """Test listing orders with filters""" + + def test_get_order_details(self, client, vendor_user_headers): + """Test getting order details""" + + def test_update_order_status(self, client, vendor_user_headers): + """Test updating order status""" + + def test_order_data_isolation(self, client, vendor_user_headers): + """Test vendor can only see their orders""" +``` + +#### 3. Vendor Profile API + +Create `tests/integration/api/v1/vendor/test_profile.py`: + +```python +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorProfileAPI: + """Test vendor profile endpoints""" + + def test_get_profile(self, client, vendor_user_headers): + """Test getting vendor profile""" + + def test_update_profile(self, client, vendor_user_headers): + """Test updating vendor profile""" + + def test_profile_permissions(self, client, vendor_user_headers): + """Test profile permission checks""" +``` + +## Troubleshooting + +### Test Fails with 401 (Unauthorized) + +**Problem**: Tests fail even with valid token + +**Common Causes**: +- User doesn't have `role="vendor"` +- Token is expired +- Authorization header format incorrect + +**Solutions**: +```python +# Verify user role +assert test_vendor_user.role == "vendor" + +# Check token format +assert vendor_user_headers["Authorization"].startswith("Bearer ") + +# Generate fresh token +response = client.post("/api/v1/auth/login", json={...}) +``` + +### Test Fails with 403 (Forbidden) + +**Problem**: "Not associated with vendor" error + +**Common Causes**: +- `VendorUser` record doesn't exist +- Vendor is inactive +- User not linked to vendor + +**Solutions**: +```python +# Use fixture that creates VendorUser +def test_example( + client, + vendor_user_headers, + test_vendor_with_vendor_user # ← Use this fixture +): + pass + +# Or manually create VendorUser +vendor_user = VendorUser( + vendor_id=vendor.id, + user_id=test_vendor_user.id, + is_active=True +) +db.add(vendor_user) +db.commit() +``` + +### Test Fails with 404 (Not Found) + +**Problem**: "Vendor not found" error + +**Common Causes**: +- Vendor is inactive (`is_active=False`) +- Vendor doesn't exist +- Database not set up correctly + +**Solutions**: +```python +# Verify vendor is active +assert vendor.is_active is True + +# Verify vendor exists +vendor_in_db = db.query(Vendor).filter_by(id=vendor.id).first() +assert vendor_in_db is not None +``` + +### Import Errors for Fixtures + +**Problem**: `fixture not found` errors + +**Solutions**: +- Fixtures are auto-loaded via `conftest.py` +- Check `pytest_plugins` list in `tests/conftest.py`: + ```python + pytest_plugins = [ + "tests.fixtures.auth_fixtures", + "tests.fixtures.vendor_fixtures", + ] + ``` +- Use absolute imports in test files + +## Best Practices + +### 1. Use Appropriate Fixtures + +```python +# ✅ Good - Use specific fixtures +def test_vendor_endpoint(client, vendor_user_headers, test_vendor_with_vendor_user): + pass + +# ❌ Avoid - Manual setup +def test_vendor_endpoint(client): + user = User(role="vendor", ...) # Don't do this + db.add(user) +``` + +### 2. Test Isolation + +Each test should be independent: + +```python +# ✅ Good - Test creates its own data +def test_product_list(client, vendor_user_headers, db): + product = Product(...) + db.add(product) + db.commit() + + response = client.get("/api/v1/vendor/products", headers=vendor_user_headers) + # Test assertions + +# ❌ Avoid - Depending on other tests +def test_product_list(client, vendor_user_headers): + # Assumes data from previous test + response = client.get("/api/v1/vendor/products", headers=vendor_user_headers) +``` + +### 3. Comprehensive Assertions + +Check multiple aspects: + +```python +def test_dashboard_stats(client, vendor_user_headers): + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + + # Check status + assert response.status_code == 200 + + # Check structure + data = response.json() + assert "products" in data + assert "orders" in data + + # Check data types + assert isinstance(data["products"]["total"], int) + + # Check business logic + assert data["products"]["total"] >= 0 +``` + +### 4. Use Markers Consistently + +```python +@pytest.mark.integration # Always for integration tests +@pytest.mark.api # For API endpoint tests +@pytest.mark.vendor # For vendor-specific tests +class TestVendorAPI: + pass +``` + +### 5. Clear Test Names + +```python +# ✅ Good - Descriptive names +def test_list_products_returns_paginated_results(self): +def test_create_product_requires_authentication(self): +def test_update_product_rejects_invalid_price(self): + +# ❌ Avoid - Vague names +def test_products(self): +def test_api_1(self): +def test_endpoint(self): +``` + +## See Also + +- [Test Structure](test-structure.md) - Overall test organization +- [Testing Guide](testing-guide.md) - Testing strategy and best practices +- [Test Maintenance](test-maintenance.md) - Maintaining test quality +- [API Authentication](../api/authentication.md) - Authentication documentation +- [RBAC](../api/RBAC.md) - Role-based access control diff --git a/tests/integration/api/v1/README.md b/tests/integration/api/v1/README.md new file mode 100644 index 00000000..add7f9c8 --- /dev/null +++ b/tests/integration/api/v1/README.md @@ -0,0 +1,32 @@ +# API v1 Integration Tests + +## Documentation + +For comprehensive test structure documentation, please see: + +**[Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/)** in MkDocs + +## Quick Start + +```bash +# Run all v1 API tests +pytest tests/integration/api/v1/ -v + +# Run specific area +pytest tests/integration/api/v1/vendor/ -v +pytest tests/integration/api/v1/admin/ -v +pytest tests/integration/api/v1/public/ -v +pytest tests/integration/api/v1/shared/ -v +``` + +## Structure + +``` +tests/integration/api/v1/ +├── admin/ # Admin API tests +├── vendor/ # Vendor API tests +├── public/ # Public API tests +└── shared/ # Shared/common tests +``` + +See full documentation: [Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/) diff --git a/tests/integration/api/v1/admin/README.md b/tests/integration/api/v1/admin/README.md new file mode 100644 index 00000000..224fb3e0 --- /dev/null +++ b/tests/integration/api/v1/admin/README.md @@ -0,0 +1,20 @@ +# Admin API Integration Tests + +## Documentation + +For comprehensive testing documentation, see: + +**[Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/)** in MkDocs + +## Quick Start + +```bash +# Run all admin tests +pytest tests/integration/api/v1/admin/ -v +``` + +## Status + +⚠️ Tests to be migrated from legacy test files. + +See [Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/) for migration details. diff --git a/tests/integration/api/v1/admin/__init__.py b/tests/integration/api/v1/admin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/api/v1/public/README.md b/tests/integration/api/v1/public/README.md new file mode 100644 index 00000000..c97e5283 --- /dev/null +++ b/tests/integration/api/v1/public/README.md @@ -0,0 +1,20 @@ +# Public API Integration Tests + +## Documentation + +For comprehensive testing documentation, see: + +**[Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/)** in MkDocs + +## Quick Start + +```bash +# Run all public tests +pytest tests/integration/api/v1/public/ -v +``` + +## Status + +⚠️ Tests to be created. + +See [Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/) for details. diff --git a/tests/integration/api/v1/public/__init__.py b/tests/integration/api/v1/public/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/api/v1/shared/README.md b/tests/integration/api/v1/shared/README.md new file mode 100644 index 00000000..be3ce486 --- /dev/null +++ b/tests/integration/api/v1/shared/README.md @@ -0,0 +1,20 @@ +# Shared API Integration Tests + +## Documentation + +For comprehensive testing documentation, see: + +**[Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/)** in MkDocs + +## Quick Start + +```bash +# Run all shared tests +pytest tests/integration/api/v1/shared/ -v +``` + +## Status + +⚠️ Tests to be migrated from legacy test files (auth, pagination, filtering). + +See [Test Structure Guide](https://yourusername.github.io/wizamart/testing/test-structure/) for migration details. diff --git a/tests/integration/api/v1/shared/__init__.py b/tests/integration/api/v1/shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/api/v1/vendor/README.md b/tests/integration/api/v1/vendor/README.md new file mode 100644 index 00000000..8492e432 --- /dev/null +++ b/tests/integration/api/v1/vendor/README.md @@ -0,0 +1,36 @@ +# Vendor API Integration Tests + +## Documentation + +For comprehensive vendor API testing documentation, please see: + +**[Vendor API Testing Guide](https://yourusername.github.io/wizamart/testing/vendor-api-testing/)** in MkDocs + +## Quick Start + +```bash +# Run all vendor tests +pytest tests/integration/api/v1/vendor/ -v + +# Run with coverage +pytest tests/integration/api/v1/vendor/ \ + --cov=app/api/v1/vendor \ + --cov-report=html +``` + +## Test Files + +- `test_authentication.py` - Authentication tests (30+ tests) +- `test_dashboard.py` - Dashboard stats tests (12 tests) + +## Fixtures + +Key fixtures for vendor testing: + +- `vendor_user_headers` - Authentication headers for vendor API +- `test_vendor_with_vendor_user` - Vendor with VendorUser association + +## See Also + +- [Vendor API Testing Guide](https://yourusername.github.io/wizamart/testing/vendor-api-testing/) - Full documentation +- [Test Structure](https://yourusername.github.io/wizamart/testing/test-structure/) - Overall test organization diff --git a/tests/integration/api/v1/vendor/__init__.py b/tests/integration/api/v1/vendor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/api/v1/vendor/test_authentication.py b/tests/integration/api/v1/vendor/test_authentication.py new file mode 100644 index 00000000..8a76417b --- /dev/null +++ b/tests/integration/api/v1/vendor/test_authentication.py @@ -0,0 +1,384 @@ +# tests/integration/api/v1/test_vendor_api_authentication.py +""" +Integration tests for vendor API authentication using get_current_vendor_api. + +These tests verify that: +1. Vendor API endpoints require Authorization header (not cookies) +2. Only vendor-role users can access vendor API endpoints +3. Admin users are blocked from vendor API routes +4. Invalid/expired tokens are rejected +5. Vendor context middleware works correctly with API authentication +""" + +import pytest +from datetime import datetime, timedelta, timezone +from jose import jwt + + +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +@pytest.mark.auth +class TestVendorAPIAuthentication: + """Test authentication for vendor API endpoints using get_current_vendor_api""" + + # ======================================================================== + # Authentication Tests - /api/v1/vendor/auth/me + # ======================================================================== + + def test_vendor_auth_me_success(self, client, vendor_user_headers, test_vendor_user): + """Test /auth/me endpoint with valid vendor user token""" + response = client.get("/api/v1/vendor/auth/me", headers=vendor_user_headers) + + assert response.status_code == 200 + data = response.json() + assert data["username"] == test_vendor_user.username + assert data["email"] == test_vendor_user.email + assert data["role"] == "vendor" + assert data["is_active"] is True + + def test_vendor_auth_me_without_token(self, client): + """Test /auth/me endpoint without authorization header""" + response = client.get("/api/v1/vendor/auth/me") + + assert response.status_code == 401 + data = response.json() + assert data["error_code"] == "INVALID_TOKEN" + assert "Authorization header required" in data["message"] + + def test_vendor_auth_me_invalid_token(self, client): + """Test /auth/me endpoint with invalid token format""" + response = client.get( + "/api/v1/vendor/auth/me", + headers={"Authorization": "Bearer invalid_token_here"} + ) + + assert response.status_code == 401 + data = response.json() + assert data["error_code"] == "INVALID_TOKEN" + + def test_vendor_auth_me_with_admin_token(self, client, admin_headers, test_admin): + """Test /auth/me endpoint rejects admin users""" + response = client.get("/api/v1/vendor/auth/me", headers=admin_headers) + + assert response.status_code == 403 + data = response.json() + assert data["error_code"] == "FORBIDDEN" + assert "Admin users cannot access vendor API" in data["message"] + + def test_vendor_auth_me_with_regular_user_token(self, client, auth_headers, test_user): + """Test /auth/me endpoint rejects regular users""" + response = client.get("/api/v1/vendor/auth/me", headers=auth_headers) + + assert response.status_code == 403 + data = response.json() + assert data["error_code"] == "FORBIDDEN" + assert "vendor API routes" in data["message"].lower() + + def test_vendor_auth_me_expired_token(self, client, test_vendor_user, auth_manager): + """Test /auth/me endpoint with expired token""" + # Create expired token + expired_payload = { + "sub": str(test_vendor_user.id), + "username": test_vendor_user.username, + "email": test_vendor_user.email, + "role": test_vendor_user.role, + "exp": datetime.now(timezone.utc) - timedelta(hours=1), + "iat": datetime.now(timezone.utc) - timedelta(hours=2), + } + + expired_token = jwt.encode( + expired_payload, + auth_manager.secret_key, + algorithm=auth_manager.algorithm + ) + + response = client.get( + "/api/v1/vendor/auth/me", + headers={"Authorization": f"Bearer {expired_token}"} + ) + + assert response.status_code == 401 + data = response.json() + assert data["error_code"] == "TOKEN_EXPIRED" + + # ======================================================================== + # Dashboard Stats Endpoint Tests - /api/v1/vendor/dashboard/stats + # ======================================================================== + + def test_vendor_dashboard_stats_success( + self, client, vendor_user_headers, test_vendor_with_vendor_user, db + ): + """Test dashboard stats with valid vendor authentication""" + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + assert "vendor" in data + assert "products" in data + assert "orders" in data + assert "customers" in data + assert "revenue" in data + + def test_vendor_dashboard_stats_without_auth(self, client): + """Test dashboard stats without authentication""" + response = client.get("/api/v1/vendor/dashboard/stats") + + assert response.status_code == 401 + + def test_vendor_dashboard_stats_with_admin(self, client, admin_headers): + """Test dashboard stats rejects admin users""" + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=admin_headers + ) + + assert response.status_code == 403 + data = response.json() + assert "Admin users cannot access vendor API" in data["message"] + + def test_vendor_dashboard_stats_with_cookie_only(self, client, test_vendor_user): + """Test dashboard stats does not accept cookie authentication""" + # Login to get session cookie + login_response = client.post( + "/api/v1/vendor/auth/login", + json={ + "username": test_vendor_user.username, + "password": "vendorpass123" + } + ) + assert login_response.status_code == 200 + + # Try to access API endpoint with just cookies (no Authorization header) + response = client.get("/api/v1/vendor/dashboard/stats") + + # Should fail because get_current_vendor_api requires Authorization header + assert response.status_code == 401 + + # ======================================================================== + # CSRF Protection Tests + # ======================================================================== + + def test_csrf_protection_api_endpoints_require_header( + self, client, test_vendor_user + ): + """Test that API endpoints require Authorization header (CSRF protection)""" + # Get a valid session by logging in + login_response = client.post( + "/api/v1/vendor/auth/login", + json={ + "username": test_vendor_user.username, + "password": "vendorpass123" + } + ) + assert login_response.status_code == 200 + + # List of vendor API endpoints that should require header auth + api_endpoints = [ + "/api/v1/vendor/auth/me", + "/api/v1/vendor/dashboard/stats", + "/api/v1/vendor/products", + "/api/v1/vendor/orders", + "/api/v1/vendor/profile", + "/api/v1/vendor/settings", + ] + + for endpoint in api_endpoints: + # Try to access with just session cookie (no Authorization header) + response = client.get(endpoint) + + # All should fail with 401 (header required) + assert response.status_code == 401, \ + f"Endpoint {endpoint} should reject cookie-only auth" + + # ======================================================================== + # Role-Based Access Control Tests + # ======================================================================== + + def test_vendor_endpoints_block_non_vendor_roles( + self, client, auth_headers, admin_headers + ): + """Test that vendor API endpoints block non-vendor users""" + endpoints = [ + "/api/v1/vendor/auth/me", + "/api/v1/vendor/dashboard/stats", + "/api/v1/vendor/profile", + ] + + for endpoint in endpoints: + # Test with regular user token + response = client.get(endpoint, headers=auth_headers) + assert response.status_code == 403, \ + f"Endpoint {endpoint} should reject regular users" + + # Test with admin token + response = client.get(endpoint, headers=admin_headers) + assert response.status_code == 403, \ + f"Endpoint {endpoint} should reject admin users" + + def test_vendor_api_accepts_only_vendor_role( + self, client, vendor_user_headers, test_vendor_user + ): + """Test that vendor API endpoints accept vendor-role users""" + endpoints = [ + "/api/v1/vendor/auth/me", + ] + + for endpoint in endpoints: + response = client.get(endpoint, headers=vendor_user_headers) + assert response.status_code in [200, 404], \ + f"Endpoint {endpoint} should accept vendor users (got {response.status_code})" + + # ======================================================================== + # Token Validation Tests + # ======================================================================== + + def test_malformed_authorization_header(self, client): + """Test various malformed Authorization headers""" + malformed_headers = [ + {"Authorization": "InvalidFormat token123"}, + {"Authorization": "Bearer"}, # Missing token + {"Authorization": "bearer token123"}, # Wrong case + {"Authorization": " Bearer token123"}, # Leading space + {"Authorization": "Bearer token123"}, # Double space + ] + + for headers in malformed_headers: + response = client.get("/api/v1/vendor/auth/me", headers=headers) + assert response.status_code == 401, \ + f"Should reject malformed header: {headers}" + + def test_token_with_missing_claims(self, client, auth_manager): + """Test token missing required claims""" + # Create token without 'role' claim + invalid_payload = { + "sub": "123", + "username": "test", + "exp": datetime.now(timezone.utc) + timedelta(hours=1), + } + + invalid_token = jwt.encode( + invalid_payload, + auth_manager.secret_key, + algorithm=auth_manager.algorithm + ) + + response = client.get( + "/api/v1/vendor/auth/me", + headers={"Authorization": f"Bearer {invalid_token}"} + ) + + assert response.status_code == 401 + + # ======================================================================== + # Edge Cases + # ======================================================================== + + def test_inactive_vendor_user(self, client, db, test_vendor_user, auth_manager): + """Test that inactive vendor users are rejected""" + # Deactivate the vendor user + test_vendor_user.is_active = False + db.add(test_vendor_user) + db.commit() + + # Create token for inactive user + token_data = auth_manager.create_access_token(test_vendor_user) + headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + response = client.get("/api/v1/vendor/auth/me", headers=headers) + + # Should fail because user is inactive + assert response.status_code in [401, 403, 404] + + # Reactivate for cleanup + test_vendor_user.is_active = True + db.add(test_vendor_user) + db.commit() + + def test_concurrent_requests_with_same_token( + self, client, vendor_user_headers + ): + """Test that the same token can be used for multiple concurrent requests""" + # Make multiple requests with the same token + responses = [] + for _ in range(5): + response = client.get("/api/v1/vendor/auth/me", headers=vendor_user_headers) + responses.append(response) + + # All should succeed + for response in responses: + assert response.status_code == 200 + + def test_vendor_api_with_empty_authorization_header(self, client): + """Test vendor API with empty Authorization header value""" + response = client.get( + "/api/v1/vendor/auth/me", + headers={"Authorization": ""} + ) + + assert response.status_code == 401 + + +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorAPIConsistency: + """Test that all vendor API endpoints use consistent authentication""" + + def test_all_vendor_endpoints_require_header_auth( + self, client, test_vendor_user + ): + """Verify all vendor API endpoints require Authorization header""" + # Login to establish session + client.post( + "/api/v1/vendor/auth/login", + json={ + "username": test_vendor_user.username, + "password": "vendorpass123" + } + ) + + # All vendor API endpoints (excluding public endpoints like /info) + vendor_api_endpoints = [ + ("/api/v1/vendor/auth/me", "GET"), + ("/api/v1/vendor/dashboard/stats", "GET"), + ("/api/v1/vendor/profile", "GET"), + ("/api/v1/vendor/settings", "GET"), + ("/api/v1/vendor/products", "GET"), + ("/api/v1/vendor/orders", "GET"), + ("/api/v1/vendor/customers", "GET"), + ("/api/v1/vendor/inventory", "GET"), + ("/api/v1/vendor/analytics", "GET"), + ] + + for endpoint, method in vendor_api_endpoints: + if method == "GET": + response = client.get(endpoint) + elif method == "POST": + response = client.post(endpoint, json={}) + + # All should reject cookie-only auth with 401 + assert response.status_code == 401, \ + f"Endpoint {endpoint} should require Authorization header (got {response.status_code})" + + def test_vendor_endpoints_accept_vendor_token( + self, client, vendor_user_headers, test_vendor_with_vendor_user + ): + """Verify all vendor API endpoints accept valid vendor tokens""" + # Endpoints that should work with just vendor authentication + # (may return 404 or other errors due to missing data, but not 401/403) + vendor_api_endpoints = [ + "/api/v1/vendor/auth/me", + "/api/v1/vendor/profile", + "/api/v1/vendor/settings", + ] + + for endpoint in vendor_api_endpoints: + response = client.get(endpoint, headers=vendor_user_headers) + + # Should not be authentication/authorization errors + assert response.status_code not in [401, 403], \ + f"Endpoint {endpoint} should accept vendor token (got {response.status_code}: {response.text})" diff --git a/tests/integration/api/v1/vendor/test_dashboard.py b/tests/integration/api/v1/vendor/test_dashboard.py new file mode 100644 index 00000000..f2e4d67f --- /dev/null +++ b/tests/integration/api/v1/vendor/test_dashboard.py @@ -0,0 +1,300 @@ +# tests/integration/api/v1/test_vendor_api_dashboard.py +""" +Integration tests for vendor dashboard API endpoints. + +Tests cover: +1. Dashboard stats retrieval +2. Vendor-specific data isolation +3. Permission checks +4. Data accuracy +""" + +import pytest + + +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.vendor +class TestVendorDashboardAPI: + """Test vendor dashboard stats endpoint""" + + def test_get_dashboard_stats_structure( + self, client, vendor_user_headers, test_vendor_with_vendor_user, db + ): + """Test dashboard stats returns correct data structure""" + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + + # Verify top-level structure + assert "vendor" in data + assert "products" in data + assert "orders" in data + assert "customers" in data + assert "revenue" in data + + # Verify vendor info + assert "id" in data["vendor"] + assert "name" in data["vendor"] + assert "vendor_code" in data["vendor"] + assert data["vendor"]["id"] == test_vendor_with_vendor_user.id + + # Verify products stats + assert "total" in data["products"] + assert "active" in data["products"] + assert isinstance(data["products"]["total"], int) + assert isinstance(data["products"]["active"], int) + + # Verify orders stats + assert "total" in data["orders"] + assert "pending" in data["orders"] + assert "completed" in data["orders"] + + # Verify customers stats + assert "total" in data["customers"] + assert "active" in data["customers"] + + # Verify revenue stats + assert "total" in data["revenue"] + assert "this_month" in data["revenue"] + + def test_dashboard_stats_vendor_isolation( + self, client, db, test_vendor_user, auth_manager + ): + """Test that dashboard stats only show data for the authenticated vendor""" + from models.database.vendor import Vendor, VendorUser + from models.database.product import Product + from models.database.marketplace_product import MarketplaceProduct + + # Create two separate vendors with different data + vendor1 = Vendor( + vendor_code="VENDOR1", + subdomain="vendor1", + name="Vendor One", + owner_user_id=test_vendor_user.id, + is_active=True, + is_verified=True, + ) + db.add(vendor1) + db.commit() + db.refresh(vendor1) + + # Associate vendor1 with test_vendor_user + vendor_user1 = VendorUser( + vendor_id=vendor1.id, + user_id=test_vendor_user.id, + is_owner=True, + is_active=True, + ) + db.add(vendor_user1) + db.commit() + + # Create marketplace product for vendor1 + mp1 = MarketplaceProduct( + gtin="1234567890123", + title="Product for Vendor 1", + is_active=True, + ) + db.add(mp1) + db.commit() + + # Create products for vendor1 + for i in range(3): + product = Product( + vendor_id=vendor1.id, + marketplace_product_id=mp1.id, + price=10.0 + i, + is_active=True, + ) + db.add(product) + db.commit() + + # Get token for vendor1 user + token_data = auth_manager.create_access_token(test_vendor_user) + vendor1_headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + # Get stats for vendor1 + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor1_headers + ) + + assert response.status_code == 200 + data = response.json() + + # Should show 3 products for vendor1 + assert data["vendor"]["id"] == vendor1.id + assert data["products"]["total"] == 3 + + def test_dashboard_stats_without_vendor_association( + self, client, db, auth_manager + ): + """Test dashboard stats for user not associated with any vendor""" + from models.database.user import User + + # Create vendor user without vendor association + hashed_password = auth_manager.hash_password("testpass123") + orphan_user = User( + email="orphan@example.com", + username="orphanvendor", + hashed_password=hashed_password, + role="vendor", + is_active=True, + ) + db.add(orphan_user) + db.commit() + db.refresh(orphan_user) + + # Get token + token_data = auth_manager.create_access_token(orphan_user) + headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + # Try to get dashboard stats + response = client.get("/api/v1/vendor/dashboard/stats", headers=headers) + + # Should fail - user not associated with vendor + assert response.status_code == 403 + data = response.json() + assert "not associated with any vendor" in data["message"] + + def test_dashboard_stats_with_inactive_vendor( + self, client, db, test_vendor_user, auth_manager + ): + """Test dashboard stats for inactive vendor""" + from models.database.vendor import Vendor, VendorUser + + # Create inactive vendor + vendor = Vendor( + vendor_code="INACTIVE", + subdomain="inactive", + name="Inactive Vendor", + owner_user_id=test_vendor_user.id, + is_active=False, # Inactive + is_verified=True, + ) + db.add(vendor) + db.commit() + + # Associate with user + vendor_user = VendorUser( + vendor_id=vendor.id, + user_id=test_vendor_user.id, + is_owner=True, + is_active=True, + ) + db.add(vendor_user) + db.commit() + + # Get token + token_data = auth_manager.create_access_token(test_vendor_user) + headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + # Try to get dashboard stats + response = client.get("/api/v1/vendor/dashboard/stats", headers=headers) + + # Should fail - vendor is inactive + assert response.status_code == 404 + data = response.json() + assert "not found or inactive" in data["message"] + + def test_dashboard_stats_empty_vendor( + self, client, vendor_user_headers, test_vendor_with_vendor_user + ): + """Test dashboard stats for vendor with no data""" + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + + # Should return zeros for empty vendor + assert data["products"]["total"] == 0 + assert data["products"]["active"] == 0 + assert data["orders"]["total"] == 0 + assert data["customers"]["total"] == 0 + assert data["revenue"]["total"] == 0 + + def test_dashboard_stats_with_products( + self, client, db, vendor_user_headers, test_vendor_with_vendor_user + ): + """Test dashboard stats accuracy with actual products""" + from models.database.product import Product + from models.database.marketplace_product import MarketplaceProduct + + # Create marketplace products + mp = MarketplaceProduct( + gtin="1234567890124", + title="Test Product", + is_active=True, + ) + db.add(mp) + db.commit() + + # Create products (3 active, 2 inactive) + for i in range(5): + product = Product( + vendor_id=test_vendor_with_vendor_user.id, + marketplace_product_id=mp.id, + price=10.0 + i, + is_active=(i < 3), # First 3 are active + ) + db.add(product) + db.commit() + + # Get stats + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + + assert response.status_code == 200 + data = response.json() + + assert data["products"]["total"] == 5 + assert data["products"]["active"] == 3 + + def test_dashboard_stats_response_time( + self, client, vendor_user_headers, test_vendor_with_vendor_user + ): + """Test that dashboard stats responds quickly""" + import time + + start_time = time.time() + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + end_time = time.time() + + assert response.status_code == 200 + # Should respond in less than 2 seconds + assert (end_time - start_time) < 2.0 + + def test_dashboard_stats_caching_behavior( + self, client, vendor_user_headers, test_vendor_with_vendor_user + ): + """Test that dashboard stats can be called multiple times""" + # Make multiple requests + responses = [] + for _ in range(3): + response = client.get( + "/api/v1/vendor/dashboard/stats", + headers=vendor_user_headers + ) + responses.append(response) + + # All should succeed + for response in responses: + assert response.status_code == 200 + + # All should return consistent data + data_list = [r.json() for r in responses] + for data in data_list[1:]: + assert data["vendor"]["id"] == data_list[0]["vendor"]["id"] + assert data["products"]["total"] == data_list[0]["products"]["total"]