Test & Harden TaskFlow for Production
Instructions
Objective
Make TaskFlow production-ready by writing a comprehensive test suite and adding production hardening layers. By the end of this lab, you will have at least 15 passing tests and 5 production middleware/features.
This lab has two parts: Testing (60 points) and Production Hardening (40 points).
Part 1: Testing (60 points)
1.1 — Test Configuration (tests/conftest.py)
Create the test infrastructure with these fixtures:
- Test database: Create an async SQLAlchemy engine pointing to a
taskflow_testdatabase - Database setup: A session-scoped fixture that creates all tables before tests and drops them after
- Async session: A function-scoped fixture that provides a database session with rollback after each test
- Test client: An
httpx.AsyncClientusingASGITransportwith the FastAPI app and database dependency override - Auth helper: A fixture or helper function that registers a user and returns an auth header dict
{"Authorization": "Bearer <token>"}
# tests/conftest.py — Required structure
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.main import app
from app.database import Base, get_db
TEST_DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/taskflow_test"
engine_test = create_async_engine(TEST_DATABASE_URL, echo=False)
async_session_test = async_sessionmaker(engine_test, class_=AsyncSession, expire_on_commit=False)
# Implement fixtures: setup_database, db_session, client, auth_headers
Also create a pytest.ini or pyproject.toml section:
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
1.2 — Auth Tests (tests/test_auth.py) — 4+ test cases
Write tests for the authentication flow:
- Register: POST
/api/v1/auth/registerwith valid data returns 201 and user object (no password in response) - Login: POST
/api/v1/auth/loginwith valid credentials returns 200 andaccess_token - Protected route access: GET
/api/v1/projectswith valid token returns 200 - Invalid token rejection: GET
/api/v1/projectswithAuthorization: Bearer invalidtokenreturns 401
1.3 — CRUD Tests (tests/test_projects.py, tests/test_tasks.py) — 6+ test cases
Write tests for project and task CRUD operations:
- Create project: POST
/api/v1/projectsreturns 201 with project data - List projects: GET
/api/v1/projectsreturns 200 with list of projects for the authenticated user - Get project by ID: GET
/api/v1/projects/{id}returns 200 with correct project - Update project: PATCH
/api/v1/projects/{id}returns 200 with updated fields - Delete project: DELETE
/api/v1/projects/{id}returns 204 - Create task in project: POST
/api/v1/projects/{id}/tasksreturns 201 with task data
1.4 — Permission Tests (tests/test_permissions.py) — 3+ test cases
Write tests verifying authorization boundaries:
- Non-member cannot access project: Register a second user, try to GET a project owned by the first user — expect 403 or 404
- Non-owner cannot delete project: Second user tries to DELETE first user's project — expect 403
- Non-member cannot create task: Second user tries to POST a task to first user's project — expect 403 or 404
1.5 — Pagination Tests (tests/test_pagination.py) — 2+ test cases
Write tests for pagination parameters:
- Default pagination: Create 5 projects, GET
/api/v1/projectsreturns all withtotalcount - Custom page/size: GET
/api/v1/projects?page=1&size=2returns exactly 2 items and correcttotal
Test Requirements Summary
- Minimum 15 test cases total across all test files
- All tests use
@pytest.mark.anyiofor async - All tests use the
clientfixture (no manual app setup) - Each test is independent (no test depends on another test's side effects)
Part 2: Production Hardening (40 points)
2.1 — Redis Caching (app/cache.py) — Cache-Aside Pattern
Implement Redis caching for read-heavy endpoints:
- Create an async Redis client using
redis.asyncio.Redis - Implement
get_cached(key)andset_cached(key, value, ttl)helper functions - Implement
invalidate_cache(pattern)to delete keys by glob pattern - Add caching to
GET /api/v1/projectswith a 60-second TTL - Add caching to
GET /api/v1/projects/{id}/taskswith a 60-second TTL - Invalidate relevant cache on any POST, PATCH, or DELETE operation
# app/cache.py — Required interface
from redis.asyncio import Redis
redis_client = Redis(host="localhost", port=6379, db=0, decode_responses=True)
async def get_cached(key: str) -> dict | None: ...
async def set_cached(key: str, value: dict, ttl: int = 60): ...
async def invalidate_cache(pattern: str): ...
2.2 — Global Error Handler Middleware (app/middleware/error_handler.py)
Create middleware that catches all unhandled exceptions and returns a consistent JSON format:
{
"error": "internal_server_error",
"message": "An unexpected error occurred",
"detail": null
}
- Catch all
Exceptiontypes in the middleware dispatch - Log the full traceback using Python logging
- In debug mode, include error details in the
detailfield; in production, set it tonull - Return status code 500 for unhandled exceptions
2.3 — Request Logging Middleware (app/middleware/logging.py)
Create middleware that logs every HTTP request:
- Log: HTTP method, URL path, response status code, and duration in milliseconds
- Use Python's
loggingmodule with a named logger (e.g.,taskflow.access) - Format:
GET /api/v1/projects status=200 duration=12.3ms - Measure duration using
time.perf_counter()
2.4 — Rate Limiting on Auth Endpoints
Add rate limiting to prevent brute-force login attempts:
- Limit
POST /api/v1/auth/loginto 5 requests per minute per IP - Use
slowapiwithget_remote_addressas the key function - Return a 429 status code with a clear error message when rate limited
- Register the rate limiter error handler in
main.py
2.5 — CORS Middleware
Configure CORS for production:
- Allow origins from an environment variable (
ALLOWED_ORIGINS, comma-separated) - Allow methods: GET, POST, PATCH, DELETE, OPTIONS
- Allow headers: Content-Type, Authorization
- Allow credentials: true
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS.split(","),
allow_credentials=True,
allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
)
2.6 — Health Check Endpoint (GET /health)
Create an endpoint that checks the status of all dependencies:
- Check database connectivity by executing
SELECT 1 - Check Redis connectivity by calling
redis_client.ping() - Return
200with{"api": "healthy", "database": "healthy", "redis": "healthy"}when all checks pass - Return
503with unhealthy status for any failed check
What to Submit
Your submission should contain 10 file sections in the editor below. Each section begins with a # FILE N: header.
Hints
- Use
httpx.ASGITransport(app=app)to create the async test client — this is the FastAPI-recommended approach - Remember to override
get_dbin your test client fixture so tests use the test database - For permission tests, create two separate users with two separate auth tokens
- Redis
scan_iterwithmatch=patternis the async-safe way to find keys for invalidation - Use
time.perf_counter()(nottime.time()) for accurate duration measurement - The health check should return 503 (Service Unavailable) if any dependency is down, not 500