# Test Plan: app/api/deps.py — Authentication Dependencies **Date:** 2026-02-19 **Status:** Planned **Priority:** P0 — Security-critical, zero test coverage **File under test:** `app/api/deps.py` (1,668 lines, 31 functions) ## Why This File Is Critical `deps.py` is the **single entry point for all authentication and authorization** in the application. Every protected route depends on it. It enforces: - Role isolation (admins can't access store routes, stores can't access admin routes) - Cookie path restrictions (prevent cross-context token leakage) - Token priority (Authorization header > cookie) - Platform access control for platform admins - Store permission checks (owner, team member, specific permissions) - Merchant ownership verification - Customer token validation with store-matching - Module and menu-based access control A bug here is a security vulnerability. Zero test coverage is unacceptable. ## Current State - **Existing test coverage:** None for deps.py directly - **Related tests:** `tests/integration/security/test_authentication.py` (3 tests — basic endpoint protection only) - **Related tests:** `tests/unit/models/schema/test_auth.py` (tests UserContext schema) ## Test Structure Tests will live at: `tests/unit/api/test_deps.py` This follows the project convention of matching source layout (`app/api/deps.py` → `tests/unit/api/test_deps.py`). ## Functions To Test (31 total) ### Phase 1: Helper Functions (4 functions) These are pure/near-pure functions — easiest to test, highest value as foundation. | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `_get_token_from_request()` | 76–105 | Extract token from header or cookie, header takes priority | 4 | | `_validate_user_token()` | 108–123 | Validate JWT, return User model | 3 | | `_get_user_model()` | 126–155 | Load User from DB by UserContext.id, copy token attrs | 3 | | `_validate_customer_token()` | 1026–1114 | Decode customer JWT, verify type/expiry/store match | 7 | **Tests for `_get_token_from_request`:** - Returns (token, "header") when Authorization header present - Returns (token, "cookie") when only cookie present - Header takes priority over cookie when both present - Returns (None, None) when neither present **Tests for `_validate_user_token`:** - Returns User for valid token - Raises InvalidTokenException for invalid/expired token - Raises InvalidTokenException for token with non-existent user **Tests for `_get_user_model`:** - Returns User model with token attributes copied - Raises InvalidTokenException when user not in DB - Copies token_store_id, token_store_code, token_store_role from context **Tests for `_validate_customer_token`:** - Returns CustomerContext for valid customer token - Rejects token with wrong type (not "customer") - Rejects token with missing "sub" claim - Rejects expired token - Rejects token for non-existent customer - Rejects token for inactive customer - Rejects token with store_id mismatch (cross-store attack) ### Phase 2: Admin Authentication (6 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `get_current_admin_from_cookie_or_header()` | 163–209 | Admin auth via cookie or header | 4 | | `get_current_admin_api()` | 212–242 | Admin auth via header only (CSRF-safe) | 3 | | `get_current_super_admin()` | 250–283 | Require super admin role | 3 | | `get_current_super_admin_api()` | 286–312 | Super admin, header only | 2 | | `require_platform_access()` | 315–357 | Factory: platform-specific admin access | 4 | | `get_admin_with_platform_context()` | 360–430 | Admin with platform from JWT | 4 | **Key tests:** - Valid admin token accepted (cookie and header) - Non-admin role rejected with AdminRequiredException - No token raises InvalidTokenException - Super admin check: platform admin rejected - Platform access: super admin bypasses, platform admin checked against accessible_platform_ids - Platform context: platform_id extracted from token, stored in request.state ### Phase 3: Store Authentication (2 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `get_current_store_from_cookie_or_header()` | 665–720 | Store auth via cookie or header | 5 | | `get_current_store_api()` | 723–780 | Store API auth, validates store context | 5 | **Key tests:** - Valid store token accepted - **Admin blocked from store routes** (critical security boundary) - Customer blocked from store routes - No token raises InvalidTokenException - Store API requires token_store_id claim - Store API verifies user still member of store (revocation check) ### Phase 4: Merchant Authentication (5 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `get_current_merchant_from_cookie_or_header()` | 788–848 | Merchant auth, verifies ownership | 4 | | `get_current_merchant_api()` | 851–896 | Merchant API auth | 3 | | `get_current_merchant_optional()` | 899–940 | Returns None if not authenticated | 3 | | `get_merchant_for_current_user()` | 943–979 | Load Merchant object for API user | 3 | | `get_merchant_for_current_user_page()` | 982–1018 | Load Merchant object for page user | 2 | **Key tests:** - Valid merchant owner accepted - User without active merchants rejected - Optional variant returns None on failure (no exception) - Merchant object stored in request.state ### Phase 5: Customer Authentication (2 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `get_current_customer_from_cookie_or_header()` | 1117–1156 | Customer auth via cookie or header | 3 | | `get_current_customer_api()` | 1159–1185 | Customer API auth | 2 | **Key tests:** - Valid customer token accepted - No token raises InvalidTokenException - API variant requires Authorization header ### Phase 6: Access Control (6 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `require_module_access()` | 438–538 | Factory: module enablement check | 4 | | `require_menu_access()` | 546–657 | Factory: menu visibility check | 4 | | `require_store_permission()` | 1278–1324 | Factory: specific permission check | 4 | | `require_store_owner()` | 1327–1369 | Require store owner role | 3 | | `require_any_store_permission()` | 1372–1425 | Factory: ANY of N permissions | 3 | | `require_all_store_permissions()` | 1428–1483 | Factory: ALL of N permissions | 3 | **Key tests:** - Module disabled → InsufficientPermissionsException - Super admin bypasses module check - Store owner has all permissions - Team member checked against specific permissions - Missing permission raises InsufficientStorePermissionsException ### Phase 7: Optional Auth & Utilities (4 functions) | Function | Lines | What it does | Test count | |----------|-------|-------------|------------| | `get_current_admin_optional()` | 1535–1579 | Returns None on failure | 3 | | `get_current_store_optional()` | 1582–1626 | Returns None on failure | 3 | | `get_current_customer_optional()` | 1629–1667 | Returns None on failure | 3 | | `get_user_permissions()` | 1486–1527 | List all permissions for user in store | 3 | | `get_user_store()` | 1225–1270 | Verify store ownership/membership | 3 | **Key tests:** - Optional variants return None (not raise) when token invalid - Optional variants return context when token valid - get_user_permissions returns all permissions for owner, specific for team member ## Total Test Count | Phase | Functions | Tests | |-------|----------|-------| | 1. Helpers | 4 | 17 | | 2. Admin auth | 6 | 20 | | 3. Store auth | 2 | 10 | | 4. Merchant auth | 5 | 15 | | 5. Customer auth | 2 | 5 | | 6. Access control | 6 | 21 | | 7. Optional & utils | 5 | 15 | | **Total** | **30** | **~103** | ## Test Approach ### Unit vs Integration These will be **unit tests** with mocked dependencies: - `db` → SQLAlchemy session with test data (using existing `db` fixture) - `auth_manager` → Use the real AuthManager but with test users/tokens (existing `auth_fixtures.py`) - `request` → Mock FastAPI Request with `request.state`, `request.url.path` - FastAPI `Depends()` → Call functions directly, passing dependencies explicitly This avoids the overhead of full HTTP request cycles and tests the logic in isolation. ### Fixtures Needed Most already exist in `tests/fixtures/auth_fixtures.py`: - `test_admin` — admin user (is_super_admin=True) - `test_platform_admin` — platform admin (is_super_admin=False) - `test_store_user` — store-role user - `test_user` — regular user **New fixtures to add in the test file:** - `mock_request` — Mock Request with configurable state and url.path - `test_customer` — Customer model for customer auth tests - `test_merchant` — Merchant model for merchant auth tests - `test_store` — Store with platform association - `admin_token` — JWT token for admin user - `store_token` — JWT token with store context claims - `customer_token` — Customer JWT token - `merchant_token` — JWT token for merchant owner ## Execution Order 1. **Phase 1 first** — helper functions have no dependencies on other deps.py functions 2. **Phase 2–5 next** — auth functions build on helpers 3. **Phase 6–7 last** — access control builds on auth functions Phases 2–5 can be done in parallel. Phase 6 depends on 2–3 being done. ## Verification ```bash # Run just the deps tests python -m pytest tests/unit/api/test_deps.py -v --timeout=60 # Run with coverage python -m pytest tests/unit/api/test_deps.py -v --cov=app.api.deps --cov-report=term-missing ``` ## Corrected Coverage Summary The earlier 360 analysis incorrectly reported module service coverage at 10%. The actual numbers (after finding tests in `app/modules/*/tests/`): | Module | Services | Unit Tests | Integration Tests | Coverage | |--------|----------|-----------|-------------------|----------| | billing | 7 | 8 | 4 | **100%+** | | catalog | 4 | 4 | 0 | **100%** | | checkout | 1 | 1 | 0 | **100%** | | cms | 4 | 4 | 0 | **100%** | | core | 6 | 5 | 0 | **83%** | | customers | 3 | 3 | 0 | **100%** | | dev_tools | 2 | 2 | 0 | **100%** | | inventory | 3 | 3 | 0 | **100%** | | loyalty | 8 | 8 | 1 | **100%** | | marketplace | 5 | 8 | 0 | **100%+** | | messaging | 6 | 5 | 0 | **83%** | | monitoring | 6 | 6 | 0 | **100%** | | orders | 6 | 5 | 0 | **83%** | | payments | 2 | 2 | 0 | **100%** | | tenancy | 11 | 17 | 6 | **100%+** | | cart | 1 | 1 | 0 | **100%** | | analytics | 1 | 0 | 0 | **0%** | **Remaining gaps (non-service):** - **Models:** 12 modules have no model tests - **Schemas:** 12 modules have no schema tests - **Routes:** 14 modules have no route/integration tests - **Tasks:** 0 modules have task tests (5 modules have tasks) - **Framework:** `app/api/deps.py`, `middleware/auth.py`, `app/exceptions/`, `app/handlers/stripe_webhook.py` have no tests