Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ POST /auth/logout - Invalidate token
```
GET /api/v1/users - List users (paginated)
POST /api/v1/users - Create user
POST /api/v1/users/bulk - Create multiple users (async, uses asyncio.gather)
GET /api/v1/users/{id} - Get user details
PUT /api/v1/users/{id} - Update user
DELETE /api/v1/users/{id} - Soft delete user
Expand All @@ -51,6 +52,45 @@ GET /api/v1/admin/tenants - List all tenants
GET /api/v1/admin/stats - System statistics
```

## Project Structure

```
sample-api/
├── app/ # FastAPI application code
│ ├── main.py # API endpoints and business logic
│ ├── auth.py # JWT authentication, OAuth2 patterns
│ ├── config.py # Environment configuration
│ └── __init__.py
├── py_tests/ # Python test suite (YOUR WORK GOES HERE)
│ ├── conftest.py # Shared pytest fixtures
│ ├── test_health.py # Example starter test
│ ├── test_async_example.py # Async test patterns
│ └── (your tests here) # Create test_auth.py, test_users.py, etc.
├── rust_tests/ # Rust integration tests (OPTIONAL BONUS)
│ ├── Cargo.toml # Rust project config
│ └── tests/
│ └── integration_tests.rs # HTTP client tests from external process
├── pytest.ini # Pytest configuration (markers, coverage)
├── requirements.txt # Python dependencies
└── README.md # This file
```

**Where to add your tests:**
- Create test files in `py_tests/` directory (e.g., `test_auth.py`, `test_users.py`, `test_files.py`)
- Use provided fixtures from `conftest.py`
- Follow patterns in `test_async_example.py` for async operations
- See example fixture patterns below

**Rust tests (Optional):**
- Located in `rust_tests/tests/integration_tests.rs`
- Tests API from external HTTP client perspective
- Run with: `cd rust_tests && cargo test`
- Demonstrates polyglot testing capability
- **Completely optional** - focus on Python tests first

## Requirements (Tiered Approach)

### 🎯 Tier 1: Core Requirements (MUST COMPLETE)
Expand All @@ -69,6 +109,11 @@ GET /api/v1/admin/stats - System statistics
- Update user (authenticated) → 200 success
- Duplicate username → 409 conflict

**Async Programming (1 test)**
- Bulk user creation using async patterns → tests `POST /api/v1/users/bulk`
- Must use `@pytest.mark.asyncio` and async/await patterns
- See `py_tests/test_async_example.py` for patterns

**Basic Tenant Isolation (2 tests)**
- Tenant A cannot access Tenant B's user → 404
- List users only shows current tenant's data
Expand Down Expand Up @@ -107,6 +152,12 @@ GET /api/v1/admin/stats - System statistics
- Invalid/expired token handling
- Cross-tenant file access prevention

**Async & Concurrency:**
- Concurrent user operations using `asyncio.gather()`
- Concurrent file uploads
- Performance testing (async vs sequential)
- Race condition handling

**Performance & Limits:**
- Rate limiting enforcement (429 responses)
- File type validation (415 unsupported media)
Expand All @@ -127,13 +178,14 @@ GET /api/v1/admin/stats - System statistics
- Parametrized tests for multi-scenario coverage
- Test markers (`@pytest.mark.auth`, `@pytest.mark.tenant_isolation`, etc.)
- Proper setup/teardown for isolation
- Environment configuration support
- **Async test patterns** - At least one test using `@pytest.mark.asyncio`
- AsyncClient for testing bulk operations

**Bonus:**
- Async test patterns
- Test data factories (factory_boy, Faker)
- Custom pytest plugins
- Load/performance testing
- Performance comparison (async vs sync operations)
- Concurrent operation testing with `asyncio.gather()`
- Mock external services

### Rust Integration Tests (Optional Bonus)
Expand Down
103 changes: 0 additions & 103 deletions app/events.py

This file was deleted.

75 changes: 74 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Advanced Multi-Tenant User & File Management API with Authentication
For Senior QA Automation Assessment - Ezequiel Nams
For Senior QA Automation Assessment
"""
from fastapi import FastAPI, HTTPException, status, Depends, File, UploadFile, Request
from fastapi.responses import StreamingResponse
Expand All @@ -9,6 +9,7 @@
from datetime import datetime, UTC
import uuid
import io
import asyncio
from collections import defaultdict
from time import time

Expand Down Expand Up @@ -514,6 +515,78 @@ async def delete_user(
user["updated_at"] = datetime.now(UTC)


@app.post("/api/v1/users/bulk", response_model=List[User], status_code=status.HTTP_201_CREATED)
async def create_users_bulk(
request: Request,
users_data: List[UserCreate],
current_user: TokenData = Depends(get_current_user),
):
"""
Create multiple users in parallel (demonstrates async patterns).

This endpoint uses asyncio.gather() to simulate concurrent operations.
Tests should use @pytest.mark.asyncio to properly test async behavior.
"""
check_rate_limit(request, current_user.user_id)

if len(users_data) > 50:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot create more than 50 users at once"
)

async def create_single_user(user_data: UserCreate):
"""Async helper to simulate I/O-bound user creation"""
# Check for duplicates
for user in users_db.values():
if user["username"] == user_data.username:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Username '{user_data.username}' already exists"
)
if user["email"] == user_data.email:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Email '{user_data.email}' already exists"
)

# Simulate async operation (e.g., database write, external API call)
await asyncio.sleep(0.01) # Simulate I/O delay

user_id = str(uuid.uuid4())
now = datetime.now(UTC)

new_user = {
"id": user_id,
"tenant_id": current_user.tenant_id,
"username": user_data.username,
"email": user_data.email,
"full_name": user_data.full_name,
"password_hash": hash_password("TempPassword123!"),
"role": user_data.role,
"created_at": now,
"updated_at": now,
"is_active": True,
}

users_db[user_id] = new_user
return User(**{k: v for k, v in new_user.items() if k != "password_hash"})

# Execute all user creations concurrently
try:
created_users = await asyncio.gather(
*[create_single_user(user_data) for user_data in users_data]
)
return created_users
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Bulk user creation failed: {str(e)}"
)


# ============================================================================
# FILE MANAGEMENT ENDPOINTS (Simulating S3)
# ============================================================================
Expand Down
Loading
Loading