Files
orion/docs/proposals/test-api-deps-auth-dependencies.md
Samir Boulahtit 93731b7173 test: add 73 unit tests for app/api/deps.py auth dependencies
Cover all core authentication paths: helpers (_get_token_from_request,
_validate_user_token, _get_user_model, _validate_customer_token),
admin/store/merchant/customer auth (cookie + header + API variants),
optional auth, store permission factories, and store ownership checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:38:31 +01:00

262 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` | 76105 | Extract token from header or cookie, header takes priority | 4 |
| `_validate_user_token()` | 108123 | Validate JWT, return User model | 3 |
| `_get_user_model()` | 126155 | Load User from DB by UserContext.id, copy token attrs | 3 |
| `_validate_customer_token()` | 10261114 | 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()` | 163209 | Admin auth via cookie or header | 4 |
| `get_current_admin_api()` | 212242 | Admin auth via header only (CSRF-safe) | 3 |
| `get_current_super_admin()` | 250283 | Require super admin role | 3 |
| `get_current_super_admin_api()` | 286312 | Super admin, header only | 2 |
| `require_platform_access()` | 315357 | Factory: platform-specific admin access | 4 |
| `get_admin_with_platform_context()` | 360430 | 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()` | 665720 | Store auth via cookie or header | 5 |
| `get_current_store_api()` | 723780 | 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()` | 788848 | Merchant auth, verifies ownership | 4 |
| `get_current_merchant_api()` | 851896 | Merchant API auth | 3 |
| `get_current_merchant_optional()` | 899940 | Returns None if not authenticated | 3 |
| `get_merchant_for_current_user()` | 943979 | Load Merchant object for API user | 3 |
| `get_merchant_for_current_user_page()` | 9821018 | 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()` | 11171156 | Customer auth via cookie or header | 3 |
| `get_current_customer_api()` | 11591185 | 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()` | 438538 | Factory: module enablement check | 4 |
| `require_menu_access()` | 546657 | Factory: menu visibility check | 4 |
| `require_store_permission()` | 12781324 | Factory: specific permission check | 4 |
| `require_store_owner()` | 13271369 | Require store owner role | 3 |
| `require_any_store_permission()` | 13721425 | Factory: ANY of N permissions | 3 |
| `require_all_store_permissions()` | 14281483 | 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()` | 15351579 | Returns None on failure | 3 |
| `get_current_store_optional()` | 15821626 | Returns None on failure | 3 |
| `get_current_customer_optional()` | 16291667 | Returns None on failure | 3 |
| `get_user_permissions()` | 14861527 | List all permissions for user in store | 3 |
| `get_user_store()` | 12251270 | 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 25 next** — auth functions build on helpers
3. **Phase 67 last** — access control builds on auth functions
Phases 25 can be done in parallel. Phase 6 depends on 23 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