Add E2E tests, fix bugs

Made-with: Cursor
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-04-13 05:17:48 +03:00
parent 1f98b5e958
commit 8f7deb3fca
71 changed files with 4740 additions and 29 deletions
@@ -0,0 +1,144 @@
# HTTP API
## 1. High-Level Overview
**Purpose**: FastAPI application that exposes HTTP endpoints for health monitoring, user authentication, encrypted resource loading/uploading, and a background Docker image unlock workflow.
**Architectural Pattern**: Thin controller — delegates all business logic to Resource Management (03) and binary_split.
**Upstream dependencies**: Core Models (01) — UnlockState enum; Resource Management (03) — ApiClient, binary_split functions
**Downstream consumers**: None — this is the system entry point, consumed by external HTTP clients.
## 2. Internal Interfaces
### Interface: Module-level Functions
| Function | Input | Output | Description |
|-------------------|---------------------------------|----------------|---------------------------------|
| `get_api_client` | — | ApiClient | Lazy singleton accessor |
| `_run_unlock` | `str email, str password` | — | Background task: full unlock flow |
## 3. External API Specification
| Endpoint | Method | Auth | Rate Limit | Description |
|--------------------|--------|----------|------------|------------------------------------------|
| `/health` | GET | Public | — | Liveness probe |
| `/status` | GET | Public | — | Auth status + model cache dir |
| `/login` | POST | Public | — | Set user credentials |
| `/load/{filename}` | POST | Implicit | — | Download + decrypt resource |
| `/upload/{filename}`| POST | Implicit | — | Encrypt + upload resource (big/small) |
| `/unlock` | POST | Public | — | Start background Docker unlock |
| `/unlock/status` | GET | Public | — | Poll unlock workflow progress |
"Implicit" auth = credentials must have been set via `/login` first; enforced by ApiClient's auto-login on token absence.
### Request/Response Schemas
**POST /login**
```json
// Request
{"email": "user@example.com", "password": "secret"}
// Response 200
{"status": "ok"}
// Response 401
{"detail": "error message"}
```
**POST /load/{filename}**
```json
// Request
{"filename": "model.bin", "folder": "models"}
// Response 200 — binary octet-stream
// Response 500
{"detail": "error message"}
```
**POST /upload/{filename}**
```
// Request — multipart/form-data
data: <file>
folder: "models" (form field, default "models")
// Response 200
{"status": "ok"}
```
**POST /unlock**
```json
// Request
{"email": "user@example.com", "password": "secret"}
// Response 200
{"state": "authenticating"}
// Response 404
{"detail": "Encrypted archive not found"}
```
**GET /unlock/status**
```json
// Response 200
{"state": "decrypting", "error": null}
```
## 4. Data Access Patterns
### Caching Strategy
| Data | Cache Type | TTL | Invalidation |
|---------------|---------------------|---------------|---------------------|
| ApiClient | In-memory singleton | Process life | Never |
| unlock_state | Module global | Until next unlock | State machine transition |
## 5. Implementation Details
**State Management**: Module-level globals (`api_client`, `unlock_state`, `unlock_error`) protected by `threading.Lock` for unlock state mutations.
**Key Dependencies**:
| Library | Version | Purpose |
|----------------|---------|------------------------------|
| fastapi | latest | HTTP framework |
| uvicorn | latest | ASGI server |
| pydantic | (via fastapi) | Request/response models |
| python-multipart| latest | File upload support |
**Error Handling Strategy**:
- `/login` — catches all exceptions, returns 401
- `/load`, `/upload` — catches all exceptions, returns 500
- `/unlock` — checks preconditions (archive exists, not already in progress), then delegates to background task
- Background task (`_run_unlock`) catches all exceptions, sets `unlock_state = error` with error message
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
**Known limitations**:
- No authentication middleware — endpoints rely on prior `/login` call having set credentials on the singleton
- `get_api_client()` uses a global without locking — race on first concurrent access
- `/load/{filename}` has a path parameter `filename` but also takes `req.filename` from the body — the path param is unused
- `_run_unlock` silently ignores `OSError` when removing tar file (acceptable cleanup behavior)
**Potential race conditions**:
- `unlock_state` mutations are lock-protected, but `api_client` singleton creation is not
- Concurrent `/unlock` calls: the lock check prevents duplicate starts, but there's a small TOCTOU window between the check and the `background_tasks.add_task` call
**Performance bottlenecks**:
- `/load` and `/upload` are synchronous — large files block the worker thread
- `_run_unlock` runs as a background task (single thread) — only one unlock can run at a time
## 8. Dependency Graph
**Must be implemented after**: Core Models (01), Resource Management (03)
**Can be implemented in parallel with**: —
**Blocks**: — (entry point)
## 9. Logging Strategy
No direct logging in this component — all logging is handled by downstream components via `constants.log()` / `constants.logerror()`.
**Log format**: N/A (delegates)
**Log storage**: N/A (delegates)