cp
This commit is contained in:
parent
59fcf08696
commit
485eb5ccec
97
README.md
97
README.md
@ -11,6 +11,7 @@ SEREACT is a secure API for storing, organizing, and retrieving images with adva
|
||||
- Metadata extraction and storage
|
||||
- Image processing capabilities
|
||||
- Multi-team support
|
||||
- **Comprehensive E2E testing with real database support**
|
||||
|
||||
## Architecture
|
||||
|
||||
@ -21,7 +22,8 @@ sereact/
|
||||
│ ├── cloud-run/ # Google Cloud Run configuration
|
||||
│ └── terraform/ # Infrastructure as code
|
||||
├── docs/ # Documentation
|
||||
│ └── api/ # API documentation
|
||||
│ ├── api/ # API documentation
|
||||
│ └── TESTING.md # Comprehensive testing guide
|
||||
├── scripts/ # Utility scripts
|
||||
├── src/ # Source code
|
||||
│ ├── api/ # API endpoints and routers
|
||||
@ -42,7 +44,7 @@ sereact/
|
||||
│ ├── models/ # Model tests
|
||||
│ ├── services/ # Service tests
|
||||
│ ├── integration/ # Integration tests
|
||||
│ └── test_e2e.py # End-to-end workflow tests
|
||||
│ └── test_e2e.py # **Comprehensive E2E workflow tests**
|
||||
├── main.py # Application entry point
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README.md # This file
|
||||
@ -183,12 +185,12 @@ Refer to the Swagger UI documentation at `/docs` for detailed endpoint informati
|
||||
pytest
|
||||
```
|
||||
|
||||
### End-to-End Testing
|
||||
### **Comprehensive End-to-End Testing**
|
||||
|
||||
SEREACT includes comprehensive end-to-end tests that cover complete user workflows:
|
||||
SEREACT includes a comprehensive E2E testing suite that covers complete user workflows with **completely self-contained artificial test data**:
|
||||
|
||||
```bash
|
||||
# Run all E2E tests with mocked services (recommended for development)
|
||||
# Run all E2E tests (completely self-contained - no setup required!)
|
||||
python scripts/run_tests.py e2e
|
||||
|
||||
# Run unit tests only (fast)
|
||||
@ -204,14 +206,83 @@ python scripts/run_tests.py all
|
||||
python scripts/run_tests.py coverage
|
||||
```
|
||||
|
||||
The E2E tests cover:
|
||||
- **Team Management**: Create, update, and manage teams
|
||||
- **User Management**: User creation, roles, and permissions
|
||||
- **API Authentication**: API key generation and validation
|
||||
- **Image Workflows**: Upload, metadata management, and downloads
|
||||
- **Search Functionality**: Text and tag-based search
|
||||
- **Multi-team Isolation**: Ensuring data privacy between teams
|
||||
- **Error Handling**: Validation and error response testing
|
||||
#### **E2E Test Coverage**
|
||||
|
||||
Our comprehensive E2E tests cover:
|
||||
|
||||
**Core Functionality:**
|
||||
- ✅ **Bootstrap Setup**: Automatic creation of isolated test environment with artificial data
|
||||
- ✅ **Authentication**: API key validation and verification
|
||||
- ✅ **Team Management**: Create, read, update, delete teams
|
||||
- ✅ **User Management**: Create, read, update, delete users
|
||||
- ✅ **API Key Management**: Create, list, revoke API keys
|
||||
|
||||
**Image Operations:**
|
||||
- ✅ **Image Upload**: File upload with metadata
|
||||
- ✅ **Image Retrieval**: Get image details and download
|
||||
- ✅ **Image Updates**: Modify descriptions and tags
|
||||
- ✅ **Image Listing**: Paginated image lists with filters
|
||||
|
||||
**Advanced Search Functionality:**
|
||||
- ✅ **Text Search**: Search by description content
|
||||
- ✅ **Tag Search**: Filter by tags
|
||||
- ✅ **Advanced Search**: Combined filters and thresholds
|
||||
- ✅ **Similarity Search**: Find similar images using embeddings
|
||||
- ✅ **Search Performance**: Response time validation
|
||||
|
||||
**Security and Isolation:**
|
||||
- ✅ **User Roles**: Admin vs regular user permissions
|
||||
- ✅ **Multi-team Isolation**: Data privacy between teams
|
||||
- ✅ **Access Control**: Unauthorized access prevention
|
||||
- ✅ **Error Handling**: Graceful error responses
|
||||
|
||||
**Performance and Scalability:**
|
||||
- ✅ **Bulk Operations**: Multiple image uploads
|
||||
- ✅ **Concurrent Access**: Simultaneous user operations
|
||||
- ✅ **Database Performance**: Query response times
|
||||
- ✅ **Data Consistency**: Transaction integrity
|
||||
|
||||
#### **Test Features**
|
||||
|
||||
**🎯 Completely Self-Contained**
|
||||
- **No setup required**: Tests create their own isolated environment
|
||||
- **Artificial test data**: Each test class creates unique teams, users, and images
|
||||
- **Automatic cleanup**: All test data is deleted after tests complete
|
||||
- **No environment variables needed**: Just run the tests!
|
||||
|
||||
**🔒 Isolated and Safe**
|
||||
- **Unique identifiers**: Each test uses timestamp-based unique names
|
||||
- **No conflicts**: Tests can run in parallel without interference
|
||||
- **No database pollution**: Tests don't affect existing data
|
||||
- **Idempotent**: Can be run multiple times safely
|
||||
|
||||
**⚡ Performance-Aware**
|
||||
- **Class-scoped fixtures**: Expensive setup shared across test methods
|
||||
- **Efficient cleanup**: Resources deleted in optimal order
|
||||
- **Real database tests**: Optional performance testing with larger datasets
|
||||
- **Timing validation**: Response time assertions for critical operations
|
||||
|
||||
#### **Advanced Test Modes**
|
||||
|
||||
**Standard E2E Tests (No Setup Required)**
|
||||
```bash
|
||||
# Just run them - completely self-contained!
|
||||
python scripts/run_tests.py e2e
|
||||
```
|
||||
|
||||
**Integration Tests with Real Services**
|
||||
```bash
|
||||
# Enable integration tests with real Google Cloud services
|
||||
export E2E_INTEGRATION_TEST=1
|
||||
pytest -m integration
|
||||
```
|
||||
|
||||
**Real Database Performance Tests**
|
||||
```bash
|
||||
# Enable real database tests with larger datasets
|
||||
export E2E_REALDB_TEST=1
|
||||
pytest -m realdb
|
||||
```
|
||||
|
||||
For detailed testing information, see [docs/TESTING.md](docs/TESTING.md).
|
||||
|
||||
|
||||
653
docs/TESTING.md
653
docs/TESTING.md
@ -1,449 +1,236 @@
|
||||
# SEREACT Testing Guide
|
||||
|
||||
This document provides comprehensive information about testing the SEREACT API, including different test types, setup instructions, and best practices.
|
||||
This document provides comprehensive information about testing the SEREACT API, including unit tests, integration tests, and end-to-end tests.
|
||||
|
||||
## Test Types
|
||||
|
||||
SEREACT uses a multi-layered testing approach to ensure reliability and maintainability:
|
||||
SEREACT includes several types of tests to ensure code quality and functionality:
|
||||
|
||||
### 1. Unit Tests
|
||||
- **Purpose**: Test individual components in isolation
|
||||
### 1. Unit Tests (`unit`)
|
||||
- **Purpose**: Test individual components in isolation using mocks
|
||||
- **Speed**: Fast (< 1 second per test)
|
||||
- **Dependencies**: Use mocks and stubs
|
||||
- **Coverage**: Functions, classes, and modules
|
||||
- **Location**: `tests/` (excluding `tests/integration/`)
|
||||
- **Dependencies**: None (uses mocks)
|
||||
- **Location**: `tests/` (excluding `test_e2e.py`)
|
||||
|
||||
### 2. Integration Tests
|
||||
- **Purpose**: Test interactions with real external services
|
||||
- **Speed**: Moderate (1-10 seconds per test)
|
||||
- **Dependencies**: Real Firestore database
|
||||
- **Coverage**: Database operations, service integrations
|
||||
### 2. Integration Tests (`integration`)
|
||||
- **Purpose**: Test component interactions with real services
|
||||
- **Speed**: Medium (1-5 seconds per test)
|
||||
- **Dependencies**: Real database connections
|
||||
- **Location**: `tests/integration/`
|
||||
|
||||
### 3. End-to-End (E2E) Tests
|
||||
- **Purpose**: Test complete user workflows
|
||||
- **Speed**: Moderate to slow (5-30 seconds per test)
|
||||
- **Dependencies**: Full application stack (mocked or real)
|
||||
- **Coverage**: Complete API workflows
|
||||
### 3. End-to-End Tests (`e2e`)
|
||||
- **Purpose**: Test complete user workflows from API to database
|
||||
- **Speed**: Medium to slow (2-10 seconds per test)
|
||||
- **Dependencies**: **Self-contained with artificial test data**
|
||||
- **Location**: `tests/test_e2e.py`
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Global test configuration
|
||||
├── test_e2e.py # End-to-end workflow tests
|
||||
├── api/ # API endpoint tests
|
||||
│ ├── conftest.py # API-specific fixtures
|
||||
│ ├── test_auth.py # Authentication tests
|
||||
│ ├── test_teams.py # Team management tests
|
||||
│ ├── test_users.py # User management tests
|
||||
│ ├── test_images.py # Image management tests
|
||||
│ └── test_search.py # Search functionality tests
|
||||
├── auth/ # Authentication module tests
|
||||
├── db/ # Database layer tests
|
||||
├── integration/ # Integration tests
|
||||
│ ├── __init__.py
|
||||
│ └── test_firestore_integration.py
|
||||
├── models/ # Data model tests
|
||||
└── services/ # Business logic tests
|
||||
```
|
||||
### 4. Real Database Tests (`realdb`)
|
||||
- **Purpose**: Test performance and scalability with real database
|
||||
- **Speed**: Slow (5-30 seconds per test)
|
||||
- **Dependencies**: Real database with artificial test data
|
||||
- **Location**: `tests/test_e2e.py` (marked with `@pytest.mark.realdb`)
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Virtual Environment**: Ensure you're in the project's virtual environment:
|
||||
```bash
|
||||
# Windows (Git Bash)
|
||||
source venv/Scripts/activate
|
||||
|
||||
# Linux/macOS
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
2. **Dependencies**: Install test dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
Use the test runner script for convenient test execution:
|
||||
|
||||
```bash
|
||||
# Run unit tests only (fast, recommended for development)
|
||||
python scripts/run_tests.py unit
|
||||
|
||||
# Run end-to-end tests with mocked services
|
||||
python scripts/run_tests.py e2e
|
||||
|
||||
# Run integration tests (requires real database)
|
||||
python scripts/run_tests.py integration
|
||||
|
||||
# Run all tests
|
||||
# Run all tests (recommended for development)
|
||||
python scripts/run_tests.py all
|
||||
|
||||
# Run tests with coverage report
|
||||
# Run only unit tests (fastest)
|
||||
python scripts/run_tests.py unit
|
||||
|
||||
# Run E2E tests (completely self-contained)
|
||||
python scripts/run_tests.py e2e
|
||||
|
||||
# Run with coverage report
|
||||
python scripts/run_tests.py coverage
|
||||
```
|
||||
|
||||
### Direct pytest Commands
|
||||
|
||||
For more control, use pytest directly:
|
||||
### Using pytest directly
|
||||
|
||||
```bash
|
||||
# Unit tests only
|
||||
pytest -m "not integration and not e2e" -v
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# End-to-end tests
|
||||
pytest -m e2e -v
|
||||
# Run specific test types
|
||||
pytest -m unit # Unit tests only
|
||||
pytest -m integration # Integration tests only
|
||||
pytest -m e2e # End-to-end tests only
|
||||
pytest -m realdb # Real database tests only
|
||||
|
||||
# Integration tests
|
||||
FIRESTORE_INTEGRATION_TEST=1 pytest -m integration -v
|
||||
# Run specific test files
|
||||
pytest tests/test_e2e.py # All E2E tests
|
||||
pytest tests/api/ # All API tests
|
||||
|
||||
# Specific test file
|
||||
pytest tests/test_e2e.py -v
|
||||
|
||||
# Specific test function
|
||||
pytest tests/test_e2e.py::TestE2EWorkflows::test_complete_team_workflow -v
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=src --cov-report=html --cov-report=term
|
||||
# Run specific test methods
|
||||
pytest tests/test_e2e.py::TestE2EWorkflows::test_bootstrap_and_basic_workflow
|
||||
```
|
||||
|
||||
## End-to-End Test Coverage
|
||||
### Test Combinations
|
||||
|
||||
The E2E tests cover the following complete workflows:
|
||||
```bash
|
||||
# Run unit and integration tests (skip E2E)
|
||||
pytest -m "not e2e and not realdb"
|
||||
|
||||
### 1. Team Management Workflow
|
||||
- Create a new team
|
||||
- Retrieve team details
|
||||
- Update team information
|
||||
- List all teams
|
||||
- Verify team isolation
|
||||
# Run all tests except real database tests
|
||||
pytest -m "not realdb"
|
||||
|
||||
### 2. User Management Workflow
|
||||
- Create admin and regular users
|
||||
- Assign users to teams
|
||||
- Update user roles and permissions
|
||||
- List team members
|
||||
- Verify user access controls
|
||||
# Run only E2E tests that don't require real database
|
||||
pytest -m "e2e and not realdb"
|
||||
```
|
||||
|
||||
### 3. API Key Authentication Workflow
|
||||
- Generate API keys for users
|
||||
- Authenticate requests using API keys
|
||||
- Test protected endpoints
|
||||
- Manage API key lifecycle (create, use, deactivate)
|
||||
- Verify authentication failures
|
||||
## End-to-End Test Setup
|
||||
|
||||
### 4. Image Upload and Management Workflow
|
||||
- Upload images with metadata
|
||||
- Retrieve image details
|
||||
- Update image metadata and tags
|
||||
- List team images
|
||||
- Download images
|
||||
- Verify file handling
|
||||
**The E2E tests are now completely self-contained!** They automatically:
|
||||
|
||||
### 5. Search Workflow
|
||||
- Text-based search by description
|
||||
- Tag-based filtering
|
||||
- Combined search queries
|
||||
- Search result pagination
|
||||
- Verify search accuracy
|
||||
1. **Create artificial test data** at the start of each test class
|
||||
2. **Run all tests** against this isolated test environment
|
||||
3. **Clean up all test data** at the end automatically
|
||||
|
||||
### 6. Multi-Team Isolation
|
||||
- Create multiple teams
|
||||
- Upload images to different teams
|
||||
- Verify cross-team access restrictions
|
||||
- Test search result isolation
|
||||
- Ensure data privacy
|
||||
### No Setup Required!
|
||||
|
||||
### 7. Error Handling
|
||||
- Invalid data validation
|
||||
- Authentication failures
|
||||
- Resource not found scenarios
|
||||
- File upload errors
|
||||
- Proper error responses
|
||||
```bash
|
||||
# Just run the tests - no environment variables or API keys needed!
|
||||
python scripts/run_tests.py e2e
|
||||
|
||||
## Integration Test Setup
|
||||
# Or with pytest directly
|
||||
pytest -m e2e
|
||||
```
|
||||
|
||||
Integration tests require real external services. Follow these steps:
|
||||
### Test Environment Creation
|
||||
|
||||
### 1. Firestore Setup
|
||||
Each test class automatically creates its own isolated environment:
|
||||
|
||||
1. **Create a test database**:
|
||||
- Use a separate Firestore database for testing
|
||||
- Database name should end with `-test` (e.g., `sereact-test`)
|
||||
- **Unique team** with timestamp-based naming to avoid conflicts
|
||||
- **Admin user** with unique email addresses
|
||||
- **API keys** for authentication
|
||||
- **Test images** uploaded during tests
|
||||
- **Additional users/teams** as needed for specific tests
|
||||
|
||||
2. **Set environment variables**:
|
||||
```bash
|
||||
export FIRESTORE_INTEGRATION_TEST=1
|
||||
export FIRESTORE_PROJECT_ID=your-test-project
|
||||
export FIRESTORE_DATABASE_NAME=sereact-test
|
||||
export FIRESTORE_CREDENTIALS_FILE=path/to/test-credentials.json
|
||||
```
|
||||
### Automatic Cleanup
|
||||
|
||||
3. **Run integration tests**:
|
||||
```bash
|
||||
python scripts/run_tests.py integration
|
||||
```
|
||||
At the end of each test class, all created resources are automatically deleted:
|
||||
|
||||
### 2. Full E2E Integration Setup
|
||||
- All uploaded images are removed
|
||||
- All created users are deleted
|
||||
- All created teams are removed
|
||||
- All API keys are revoked
|
||||
|
||||
For testing with real cloud services:
|
||||
### Advanced Test Modes
|
||||
|
||||
1. **Set up all services**:
|
||||
- Google Cloud Storage bucket
|
||||
- Firestore database
|
||||
- Cloud Vision API
|
||||
- Pinecone vector database
|
||||
#### Integration Tests with Real Services
|
||||
For testing with real Google Cloud services:
|
||||
|
||||
2. **Configure environment**:
|
||||
```bash
|
||||
export E2E_INTEGRATION_TEST=1
|
||||
export GCS_BUCKET_NAME=your-test-bucket
|
||||
export VECTOR_DB_API_KEY=your-pinecone-key
|
||||
# ... other service credentials
|
||||
```
|
||||
```bash
|
||||
# Enable integration tests
|
||||
export E2E_INTEGRATION_TEST=1
|
||||
|
||||
3. **Run E2E integration tests**:
|
||||
```bash
|
||||
python scripts/run_tests.py e2e --with-integration
|
||||
```
|
||||
# Run integration tests
|
||||
pytest -m integration
|
||||
```
|
||||
|
||||
#### Real Database Performance Tests
|
||||
For testing with real database connections and larger datasets:
|
||||
|
||||
```bash
|
||||
# Enable real database tests
|
||||
export E2E_REALDB_TEST=1
|
||||
|
||||
# Run real database tests
|
||||
pytest -m realdb
|
||||
```
|
||||
|
||||
## E2E Test Coverage
|
||||
|
||||
The E2E tests cover the following workflows with artificial test data:
|
||||
|
||||
### Core Functionality
|
||||
- ✅ **Bootstrap Setup**: Automatic creation of isolated test environment
|
||||
- ✅ **Authentication**: API key validation and verification
|
||||
- ✅ **Team Management**: Create, read, update, delete teams
|
||||
- ✅ **User Management**: Create, read, update, delete users
|
||||
- ✅ **API Key Management**: Create, list, revoke API keys
|
||||
|
||||
### Image Operations
|
||||
- ✅ **Image Upload**: File upload with metadata
|
||||
- ✅ **Image Retrieval**: Get image details and download
|
||||
- ✅ **Image Updates**: Modify descriptions and tags
|
||||
- ✅ **Image Listing**: Paginated image lists with filters
|
||||
|
||||
### Advanced Search Functionality
|
||||
- ✅ **Text Search**: Search by description content
|
||||
- ✅ **Tag Search**: Filter by tags
|
||||
- ✅ **Advanced Search**: Combined filters and thresholds
|
||||
- ✅ **Similarity Search**: Find similar images using embeddings
|
||||
- ✅ **Search Performance**: Response time validation
|
||||
|
||||
### Security and Isolation
|
||||
- ✅ **User Roles**: Admin vs regular user permissions
|
||||
- ✅ **Multi-team Isolation**: Data privacy between teams
|
||||
- ✅ **Access Control**: Unauthorized access prevention
|
||||
- ✅ **Error Handling**: Graceful error responses
|
||||
|
||||
### Performance and Scalability
|
||||
- ✅ **Bulk Operations**: Multiple image uploads
|
||||
- ✅ **Concurrent Access**: Simultaneous user operations
|
||||
- ✅ **Database Performance**: Query response times
|
||||
- ✅ **Data Consistency**: Transaction integrity
|
||||
|
||||
## Test Data Management
|
||||
|
||||
### Fixtures and Test Data
|
||||
### Unique Identifiers
|
||||
All E2E tests use unique suffixes to avoid conflicts:
|
||||
```python
|
||||
unique_suffix = str(uuid.uuid4())[:8]
|
||||
team_name = f"E2E Test Team {unique_suffix}_{int(time.time())}"
|
||||
```
|
||||
|
||||
- **Shared fixtures**: Defined in `tests/conftest.py`
|
||||
- **API fixtures**: Defined in `tests/api/conftest.py`
|
||||
- **Sample images**: Generated programmatically using PIL
|
||||
- **Test data**: Isolated per test function
|
||||
### Isolation Strategy
|
||||
Tests are completely isolated:
|
||||
- Each test class creates its own environment
|
||||
- Uses timestamp-based unique identifiers
|
||||
- No dependency on existing database state
|
||||
- Can run in parallel without conflicts
|
||||
|
||||
### Automatic Resource Tracking
|
||||
The test environment tracks all created resources:
|
||||
```python
|
||||
"created_resources": {
|
||||
"teams": [team_id],
|
||||
"users": [admin_user_id],
|
||||
"api_keys": [api_key_id],
|
||||
"images": []
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup Strategy
|
||||
Comprehensive cleanup at test completion:
|
||||
- Images deleted first (to avoid orphaned files)
|
||||
- Additional users deleted (preserving admin for team deletion)
|
||||
- Additional teams deleted
|
||||
- Main team deleted last (cascades to remaining resources)
|
||||
|
||||
- **Unit tests**: Automatic cleanup through mocking
|
||||
- **Integration tests**: Manual cleanup in test teardown
|
||||
- **E2E tests**: Resource tracking and cleanup utilities
|
||||
## Environment Variables
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Writing Tests
|
||||
|
||||
1. **Test naming**: Use descriptive names that explain the scenario
|
||||
```python
|
||||
def test_user_cannot_access_other_team_images(self):
|
||||
```
|
||||
|
||||
2. **Test structure**: Follow Arrange-Act-Assert pattern
|
||||
```python
|
||||
def test_create_team(self):
|
||||
# Arrange
|
||||
team_data = {"name": "Test Team"}
|
||||
|
||||
# Act
|
||||
response = client.post("/api/v1/teams", json=team_data)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Test Team"
|
||||
```
|
||||
|
||||
3. **Test isolation**: Each test should be independent
|
||||
4. **Mock external services**: Use mocks for unit tests
|
||||
5. **Use fixtures**: Leverage pytest fixtures for common setup
|
||||
|
||||
### Running Tests in Development
|
||||
|
||||
1. **Fast feedback loop**: Run unit tests frequently
|
||||
```bash
|
||||
pytest -m "not integration and not e2e" --tb=short
|
||||
```
|
||||
|
||||
2. **Pre-commit testing**: Run E2E tests before committing
|
||||
```bash
|
||||
python scripts/run_tests.py e2e
|
||||
```
|
||||
|
||||
3. **Coverage monitoring**: Check test coverage regularly
|
||||
```bash
|
||||
python scripts/run_tests.py coverage
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
For continuous integration, use different test strategies:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions workflow
|
||||
- name: Run unit tests
|
||||
run: python scripts/run_tests.py unit
|
||||
|
||||
- name: Run E2E tests
|
||||
run: python scripts/run_tests.py e2e
|
||||
|
||||
- name: Run integration tests (if credentials available)
|
||||
run: python scripts/run_tests.py integration
|
||||
if: env.FIRESTORE_INTEGRATION_TEST == '1'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Import errors**: Ensure you're in the virtual environment
|
||||
2. **Database connection**: Check Firestore credentials for integration tests
|
||||
3. **Slow tests**: Use unit tests for development, integration tests for CI
|
||||
4. **Test isolation**: Clear test data between runs
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Run tests with additional debugging:
|
||||
### No Variables Required for Basic E2E Tests!
|
||||
The standard E2E tests now run without any environment variables.
|
||||
|
||||
### Optional for Enhanced Testing
|
||||
```bash
|
||||
# Verbose output with full tracebacks
|
||||
pytest -v --tb=long
|
||||
# Enable integration tests with real services
|
||||
E2E_INTEGRATION_TEST=1
|
||||
|
||||
# Stop on first failure
|
||||
pytest -x
|
||||
# Enable real database performance tests
|
||||
E2E_REALDB_TEST=1
|
||||
|
||||
# Run specific test with debugging
|
||||
pytest tests/test_e2e.py::TestE2EWorkflows::test_complete_team_workflow -v -s
|
||||
# Custom test database (if different from main)
|
||||
TEST_FIRESTORE_PROJECT_ID="your-test-project"
|
||||
TEST_GCS_BUCKET_NAME="your-test-bucket"
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Monitor test performance:
|
||||
|
||||
```bash
|
||||
# Show slowest tests
|
||||
pytest --durations=10
|
||||
|
||||
# Profile test execution
|
||||
pytest --profile
|
||||
```
|
||||
|
||||
## Test Metrics
|
||||
|
||||
Track these metrics to ensure test quality:
|
||||
|
||||
- **Coverage**: Aim for >80% code coverage
|
||||
- **Speed**: Unit tests <1s, E2E tests <30s
|
||||
- **Reliability**: Tests should pass consistently
|
||||
- **Maintainability**: Tests should be easy to update
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. **Write tests first**: Use TDD approach
|
||||
2. **Cover all scenarios**: Happy path, edge cases, error conditions
|
||||
3. **Update documentation**: Keep this guide current
|
||||
4. **Run full test suite**: Ensure no regressions
|
||||
|
||||
For more information about the SEREACT API architecture and features, see the main [README.md](../README.md).
|
||||
|
||||
## Running E2E Tests
|
||||
|
||||
### With Fresh Database
|
||||
If you have a fresh database, the E2E tests will automatically run the bootstrap process:
|
||||
|
||||
```bash
|
||||
pytest tests/test_e2e.py -v -m e2e
|
||||
```
|
||||
|
||||
### With Existing Database
|
||||
If your database already has teams and users (bootstrap completed), you need to provide an API key:
|
||||
|
||||
1. **Get an existing API key** from your application or create one via the API
|
||||
2. **Set the environment variable**:
|
||||
```bash
|
||||
export E2E_TEST_API_KEY="your-api-key-here"
|
||||
```
|
||||
3. **Run the tests**:
|
||||
```bash
|
||||
pytest tests/test_e2e.py -v -m e2e
|
||||
```
|
||||
|
||||
### Example with API Key
|
||||
```bash
|
||||
# Set your API key
|
||||
export E2E_TEST_API_KEY="sk_test_1234567890abcdef"
|
||||
|
||||
# Run E2E tests
|
||||
python scripts/run_tests.py e2e
|
||||
```
|
||||
|
||||
## Test Features
|
||||
|
||||
### Idempotent Tests
|
||||
The E2E tests are designed to be idempotent - they can be run multiple times against the same database without conflicts:
|
||||
|
||||
- **Unique identifiers**: Each test run uses unique suffixes for all created data
|
||||
- **Graceful handling**: Tests handle existing data gracefully
|
||||
- **Cleanup**: Tests create isolated data that doesn't interfere with existing data
|
||||
|
||||
### Test Data Isolation
|
||||
- Each test run creates unique teams, users, and images
|
||||
- Tests use UUID-based suffixes to avoid naming conflicts
|
||||
- Search tests use unique tags to find only test-created data
|
||||
|
||||
## Test Configuration
|
||||
|
||||
### Environment Variables
|
||||
- `E2E_TEST_API_KEY`: API key for E2E tests with existing database
|
||||
- `E2E_INTEGRATION_TEST`: Set to `1` to enable integration tests
|
||||
- `TEST_DATABASE_URL`: Override database for testing (optional)
|
||||
|
||||
### Pytest Configuration
|
||||
The `pytest.ini` file contains:
|
||||
- Test markers for categorizing tests
|
||||
- Async test configuration
|
||||
- Warning filters
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Writing Tests
|
||||
1. **Use descriptive names**: Test names should clearly describe what they test
|
||||
2. **Test one thing**: Each test should focus on a single workflow or feature
|
||||
3. **Use fixtures**: Leverage pytest fixtures for common setup
|
||||
4. **Handle errors**: Test both success and error scenarios
|
||||
5. **Clean up**: Ensure tests don't leave behind test data (when possible)
|
||||
|
||||
### Running Tests
|
||||
1. **Run frequently**: Run unit tests during development
|
||||
2. **CI/CD integration**: Ensure all tests pass before deployment
|
||||
3. **Test environments**: Use separate databases for testing
|
||||
4. **Monitor performance**: Track test execution time
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "Bootstrap already completed"
|
||||
- **Cause**: Database already has teams/users
|
||||
- **Solution**: Set `E2E_TEST_API_KEY` environment variable
|
||||
|
||||
#### "No existing API key found"
|
||||
- **Cause**: No valid API key provided for existing database
|
||||
- **Solution**: Create an API key via the API or bootstrap endpoint
|
||||
|
||||
#### "Failed to create test team"
|
||||
- **Cause**: Insufficient permissions or API key issues
|
||||
- **Solution**: Ensure the API key belongs to an admin user
|
||||
|
||||
#### Import errors
|
||||
- **Cause**: Python path or dependency issues
|
||||
- **Solution**: Ensure virtual environment is activated and dependencies installed
|
||||
|
||||
### Getting Help
|
||||
1. Check the test output for specific error messages
|
||||
2. Verify environment variables are set correctly
|
||||
3. Ensure the API server is running (for integration tests)
|
||||
4. Check database connectivity
|
||||
|
||||
## CI/CD Integration
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
```yaml
|
||||
@ -459,43 +246,89 @@ jobs:
|
||||
with:
|
||||
python-version: 3.10
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
run: pip install -r requirements.txt
|
||||
- name: Run unit tests
|
||||
run: python scripts/run_tests.py unit
|
||||
- name: Run integration tests
|
||||
run: python scripts/run_tests.py integration
|
||||
env:
|
||||
E2E_INTEGRATION_TEST: 1
|
||||
- name: Run E2E tests (self-contained)
|
||||
run: python scripts/run_tests.py e2e
|
||||
# No environment variables needed!
|
||||
```
|
||||
|
||||
### Docker Testing
|
||||
```dockerfile
|
||||
# Test stage
|
||||
FROM python:3.10-slim as test
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
RUN python scripts/run_tests.py unit
|
||||
```
|
||||
## Troubleshooting
|
||||
|
||||
## Coverage Reports
|
||||
### Common Issues
|
||||
|
||||
Generate coverage reports:
|
||||
#### "Cannot create isolated test environment" Error
|
||||
```bash
|
||||
python scripts/run_tests.py coverage
|
||||
# This is rare but can happen if database has conflicting constraints
|
||||
# Solution: Check database state or use a clean test database
|
||||
```
|
||||
|
||||
View HTML coverage report:
|
||||
#### Tests Skipped Due to Missing Environment Variables
|
||||
```bash
|
||||
open htmlcov/index.html
|
||||
# Only affects integration and realdb tests
|
||||
echo $E2E_INTEGRATION_TEST # Should be "1" for integration tests
|
||||
echo $E2E_REALDB_TEST # Should be "1" for real database tests
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
#### Slow Test Performance
|
||||
```bash
|
||||
# Run only fast tests
|
||||
pytest -m "not realdb and not integration"
|
||||
|
||||
For performance testing:
|
||||
1. Use `pytest-benchmark` for micro-benchmarks
|
||||
2. Test with realistic data volumes
|
||||
3. Monitor database query performance
|
||||
4. Test concurrent user scenarios
|
||||
# Run tests in parallel (requires pytest-xdist)
|
||||
pip install pytest-xdist
|
||||
pytest -n auto
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
# Run with verbose output
|
||||
pytest -v -s tests/test_e2e.py
|
||||
|
||||
# Run single test with full output
|
||||
pytest -v -s tests/test_e2e.py::TestE2EWorkflows::test_bootstrap_and_basic_workflow
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Writing New Tests
|
||||
1. **Use the test_environment fixture** for automatic setup/cleanup
|
||||
2. **Track created resources** in env["created_resources"]
|
||||
3. **Use unique identifiers** for all test data
|
||||
4. **Test both success and failure** scenarios
|
||||
5. **Use appropriate markers** (`@pytest.mark.e2e`, etc.)
|
||||
|
||||
### Test Organization
|
||||
1. **Group related tests** in classes with shared fixtures
|
||||
2. **Use descriptive test names** that explain the scenario
|
||||
3. **Keep tests independent** - no shared state between methods
|
||||
4. **Use class-scoped fixtures** for expensive setup
|
||||
5. **Document test purpose** in docstrings
|
||||
|
||||
### Performance Considerations
|
||||
1. **Use class-scoped fixtures** to share expensive setup
|
||||
2. **Minimize database operations** in individual tests
|
||||
3. **Clean up test data** automatically
|
||||
4. **Run expensive tests** only when necessary
|
||||
5. **Use artificial data** instead of real external dependencies
|
||||
|
||||
## Test Metrics
|
||||
|
||||
### Coverage Goals
|
||||
- **Unit Tests**: > 90% code coverage
|
||||
- **Integration Tests**: > 80% API endpoint coverage
|
||||
- **E2E Tests**: > 95% user workflow coverage
|
||||
|
||||
### Performance Targets
|
||||
- **Unit Tests**: < 1 second per test
|
||||
- **Integration Tests**: < 5 seconds per test
|
||||
- **E2E Tests**: < 10 seconds per test
|
||||
- **Real DB Tests**: < 30 seconds per test
|
||||
|
||||
### Quality Metrics
|
||||
- **Test Reliability**: > 99% pass rate
|
||||
- **Test Maintainability**: Clear, readable test code
|
||||
- **Test Coverage**: All critical paths tested
|
||||
- **Test Documentation**: All test purposes documented
|
||||
- **Test Isolation**: No dependencies between tests
|
||||
@ -1,4 +1,4 @@
|
||||
[tool:pytest]
|
||||
[pytest]
|
||||
asyncio_mode = auto
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
testpaths = tests
|
||||
@ -14,10 +14,12 @@ markers =
|
||||
asyncio: marks tests as async (deselect with '-m "not asyncio"')
|
||||
integration: marks tests as integration tests requiring real database (deselect with '-m "not integration"')
|
||||
e2e: marks tests as end-to-end tests covering complete workflows (deselect with '-m "not e2e"')
|
||||
realdb: marks tests as real database tests requiring actual database connections (deselect with '-m "not realdb"')
|
||||
unit: marks tests as unit tests using mocks (default)
|
||||
|
||||
# Test configuration
|
||||
# To run unit tests only: pytest -m "not integration and not e2e"
|
||||
# To run unit tests only: pytest -m "not integration and not e2e and not realdb"
|
||||
# To run integration tests: pytest -m integration
|
||||
# To run e2e tests: pytest -m e2e
|
||||
# To run real database tests: pytest -m realdb
|
||||
# To run all tests: pytest
|
||||
@ -79,7 +79,13 @@ def run_command(cmd, description):
|
||||
os.chdir(project_root)
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=False, text=True)
|
||||
# Use the same Python executable that's running this script
|
||||
# This ensures we use the virtual environment
|
||||
if cmd[0] == "python":
|
||||
cmd[0] = sys.executable
|
||||
|
||||
# Inherit the current environment (including virtual environment)
|
||||
result = subprocess.run(cmd, capture_output=False, text=True, env=os.environ.copy())
|
||||
os.chdir(original_cwd)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from bson import ObjectId
|
||||
from src.db.repositories.firestore_repository import FirestoreRepository
|
||||
from src.models.image import ImageModel
|
||||
|
||||
@ -29,6 +31,110 @@ class FirestoreImageRepository(FirestoreRepository[ImageModel]):
|
||||
logger.error(f"Error getting images by team ID: {e}")
|
||||
raise
|
||||
|
||||
async def get_by_team(
|
||||
self,
|
||||
team_id: ObjectId,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
collection_id: Optional[ObjectId] = None,
|
||||
tags: Optional[List[str]] = None
|
||||
) -> List[ImageModel]:
|
||||
"""
|
||||
Get images by team with pagination and filtering
|
||||
|
||||
Args:
|
||||
team_id: Team ID
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
collection_id: Optional collection ID filter
|
||||
tags: Optional list of tags to filter by
|
||||
|
||||
Returns:
|
||||
List of images
|
||||
"""
|
||||
try:
|
||||
# Get all images and filter in memory (for simplicity)
|
||||
images = await self.get_all()
|
||||
|
||||
# Filter by team
|
||||
filtered_images = [image for image in images if image.team_id == team_id]
|
||||
|
||||
# Filter by collection if specified
|
||||
if collection_id:
|
||||
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
||||
|
||||
# Filter by tags if specified
|
||||
if tags:
|
||||
filtered_images = [
|
||||
image for image in filtered_images
|
||||
if any(tag in image.tags for tag in tags)
|
||||
]
|
||||
|
||||
# Apply pagination
|
||||
return filtered_images[skip:skip + limit]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting images by team: {e}")
|
||||
raise
|
||||
|
||||
async def count_by_team(
|
||||
self,
|
||||
team_id: ObjectId,
|
||||
collection_id: Optional[ObjectId] = None,
|
||||
tags: Optional[List[str]] = None
|
||||
) -> int:
|
||||
"""
|
||||
Count images by team with filtering
|
||||
|
||||
Args:
|
||||
team_id: Team ID
|
||||
collection_id: Optional collection ID filter
|
||||
tags: Optional list of tags to filter by
|
||||
|
||||
Returns:
|
||||
Count of images
|
||||
"""
|
||||
try:
|
||||
# Get all images and filter in memory (for simplicity)
|
||||
images = await self.get_all()
|
||||
|
||||
# Filter by team
|
||||
filtered_images = [image for image in images if image.team_id == team_id]
|
||||
|
||||
# Filter by collection if specified
|
||||
if collection_id:
|
||||
filtered_images = [image for image in filtered_images if image.collection_id == collection_id]
|
||||
|
||||
# Filter by tags if specified
|
||||
if tags:
|
||||
filtered_images = [
|
||||
image for image in filtered_images
|
||||
if any(tag in image.tags for tag in tags)
|
||||
]
|
||||
|
||||
return len(filtered_images)
|
||||
except Exception as e:
|
||||
logger.error(f"Error counting images by team: {e}")
|
||||
raise
|
||||
|
||||
async def update_last_accessed(self, image_id: ObjectId) -> bool:
|
||||
"""
|
||||
Update the last accessed timestamp for an image
|
||||
|
||||
Args:
|
||||
image_id: Image ID
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
update_data = {"last_accessed": datetime.utcnow()}
|
||||
result = await self.update(image_id, update_data)
|
||||
return result is not None
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating last accessed: {e}")
|
||||
return False
|
||||
|
||||
async def get_by_uploader_id(self, uploader_id: str) -> list[ImageModel]:
|
||||
"""
|
||||
Get images by uploader ID
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Type, Any, Generic, TypeVar
|
||||
from typing import TypeVar, Generic, Optional, List, Dict, Any
|
||||
from bson import ObjectId
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.db.providers.firestore_provider import firestore_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T', bound=BaseModel)
|
||||
T = TypeVar('T')
|
||||
|
||||
class FirestoreRepository(Generic[T]):
|
||||
"""Generic repository for Firestore operations"""
|
||||
"""Base repository class for Firestore operations"""
|
||||
|
||||
def __init__(self, collection_name: str, model_class: Type[T]):
|
||||
def __init__(self, collection_name: str, model_class):
|
||||
self.collection_name = collection_name
|
||||
self.model_class = model_class
|
||||
self.provider = firestore_db
|
||||
|
||||
async def create(self, model: T) -> T:
|
||||
"""
|
||||
@ -30,10 +32,10 @@ class FirestoreRepository(Generic[T]):
|
||||
model_dict = model.model_dump(by_alias=True)
|
||||
|
||||
# Add document to Firestore
|
||||
doc_id = await firestore_db.add_document(self.collection_name, model_dict)
|
||||
doc_id = await self.provider.add_document(self.collection_name, model_dict)
|
||||
|
||||
# Get the created document
|
||||
doc_data = await firestore_db.get_document(self.collection_name, doc_id)
|
||||
doc_data = await self.provider.get_document(self.collection_name, doc_id)
|
||||
return self.model_class(**doc_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating {self.collection_name} document: {e}")
|
||||
@ -50,7 +52,7 @@ class FirestoreRepository(Generic[T]):
|
||||
Model if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
doc_data = await firestore_db.get_document(self.collection_name, str(doc_id))
|
||||
doc_data = await self.provider.get_document(self.collection_name, str(doc_id))
|
||||
if doc_data:
|
||||
return self.model_class(**doc_data)
|
||||
return None
|
||||
@ -60,18 +62,57 @@ class FirestoreRepository(Generic[T]):
|
||||
|
||||
async def get_all(self) -> List[T]:
|
||||
"""
|
||||
Get all documents
|
||||
Get all documents from the collection
|
||||
|
||||
Returns:
|
||||
List of models
|
||||
List of model instances
|
||||
"""
|
||||
try:
|
||||
docs = await firestore_db.list_documents(self.collection_name)
|
||||
return [self.model_class(**doc) for doc in docs]
|
||||
docs = await self.provider.list_documents(self.collection_name)
|
||||
|
||||
# Transform data to handle legacy format issues
|
||||
transformed_docs = []
|
||||
for doc in docs:
|
||||
transformed_doc = self._transform_document(doc)
|
||||
transformed_docs.append(transformed_doc)
|
||||
|
||||
return [self.model_class(**doc) for doc in transformed_docs]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all {self.collection_name} documents: {e}")
|
||||
raise
|
||||
|
||||
def _transform_document(self, doc: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Transform document data to handle legacy format issues
|
||||
|
||||
Args:
|
||||
doc: Raw document data
|
||||
|
||||
Returns:
|
||||
Transformed document data
|
||||
"""
|
||||
# Handle tags field - convert string representation to list
|
||||
if 'tags' in doc and isinstance(doc['tags'], str):
|
||||
try:
|
||||
# Try to parse as Python literal (list representation)
|
||||
import ast
|
||||
doc['tags'] = ast.literal_eval(doc['tags'])
|
||||
except (ValueError, SyntaxError):
|
||||
# If that fails, split by comma
|
||||
doc['tags'] = [tag.strip() for tag in doc['tags'].split(',') if tag.strip()]
|
||||
|
||||
# Handle metadata field - convert string representation to dict
|
||||
if 'metadata' in doc and isinstance(doc['metadata'], str):
|
||||
try:
|
||||
# Try to parse as Python literal (dict representation)
|
||||
import ast
|
||||
doc['metadata'] = ast.literal_eval(doc['metadata'])
|
||||
except (ValueError, SyntaxError):
|
||||
# If that fails, set to empty dict
|
||||
doc['metadata'] = {}
|
||||
|
||||
return doc
|
||||
|
||||
async def update(self, doc_id: str, update_data: Dict[str, Any]) -> Optional[T]:
|
||||
"""
|
||||
Update document
|
||||
@ -89,7 +130,7 @@ class FirestoreRepository(Generic[T]):
|
||||
del update_data["_id"]
|
||||
|
||||
# Update document
|
||||
success = await firestore_db.update_document(
|
||||
success = await self.provider.update_document(
|
||||
self.collection_name,
|
||||
str(doc_id),
|
||||
update_data
|
||||
@ -115,7 +156,7 @@ class FirestoreRepository(Generic[T]):
|
||||
True if document was deleted, False otherwise
|
||||
"""
|
||||
try:
|
||||
return await firestore_db.delete_document(self.collection_name, str(doc_id))
|
||||
return await self.provider.delete_document(self.collection_name, str(doc_id))
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting {self.collection_name} document: {e}")
|
||||
raise
|
||||
1178
tests/test_e2e.py
1178
tests/test_e2e.py
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user