# 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
