Adding vendor api tests
This commit is contained in:
426
docs/testing/test-structure.md
Normal file
426
docs/testing/test-structure.md
Normal file
@@ -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_<endpoint_name>.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**: `Test<Area><Feature>API`
|
||||||
|
- **Examples**:
|
||||||
|
```python
|
||||||
|
class TestVendorProductAPI: # Vendor product endpoints
|
||||||
|
class TestAdminUserAPI: # Admin user endpoints
|
||||||
|
class TestPublicCatalogAPI: # Public catalog endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Functions
|
||||||
|
|
||||||
|
- **Pattern**: `test_<action>_<scenario>`
|
||||||
|
- **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
|
||||||
670
docs/testing/vendor-api-testing.md
Normal file
670
docs/testing/vendor-api-testing.md
Normal file
@@ -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 <token>"}`
|
||||||
|
|
||||||
|
### 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
|
||||||
32
tests/integration/api/v1/README.md
Normal file
32
tests/integration/api/v1/README.md
Normal file
@@ -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/)
|
||||||
20
tests/integration/api/v1/admin/README.md
Normal file
20
tests/integration/api/v1/admin/README.md
Normal file
@@ -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.
|
||||||
0
tests/integration/api/v1/admin/__init__.py
Normal file
0
tests/integration/api/v1/admin/__init__.py
Normal file
20
tests/integration/api/v1/public/README.md
Normal file
20
tests/integration/api/v1/public/README.md
Normal file
@@ -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.
|
||||||
0
tests/integration/api/v1/public/__init__.py
Normal file
0
tests/integration/api/v1/public/__init__.py
Normal file
20
tests/integration/api/v1/shared/README.md
Normal file
20
tests/integration/api/v1/shared/README.md
Normal file
@@ -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.
|
||||||
0
tests/integration/api/v1/shared/__init__.py
Normal file
0
tests/integration/api/v1/shared/__init__.py
Normal file
36
tests/integration/api/v1/vendor/README.md
vendored
Normal file
36
tests/integration/api/v1/vendor/README.md
vendored
Normal file
@@ -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
|
||||||
0
tests/integration/api/v1/vendor/__init__.py
vendored
Normal file
0
tests/integration/api/v1/vendor/__init__.py
vendored
Normal file
384
tests/integration/api/v1/vendor/test_authentication.py
vendored
Normal file
384
tests/integration/api/v1/vendor/test_authentication.py
vendored
Normal file
@@ -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})"
|
||||||
300
tests/integration/api/v1/vendor/test_dashboard.py
vendored
Normal file
300
tests/integration/api/v1/vendor/test_dashboard.py
vendored
Normal file
@@ -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"]
|
||||||
Reference in New Issue
Block a user