mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-27 22:05:56 +01:00
Closes #552 ## Summary Adds OIDC authentication and multi-user support to Shelfmark. Users can now be managed individually with per-user download settings, while maintaining full backwards compatibility with existing auth modes (no-auth, builtin, proxy, CWA). ### Authentication - **OIDC login** with PKCE, auto-discovery, group-based admin mapping - **Password fallback** when OIDC is enabled (prevents admin lockout) - **Auto-provisioning** of OIDC users (configurable on/off) - **Email-based linking** of pre-created users to OIDC accounts - **Lockout prevention** — requires a local admin before OIDC can be enabled ### User Management - **SQLite user database** (`users.db`) with admin CRUD API - **Users management tab** in settings UI (admin-only) - **Settings restricted to admins** in multi-user modes (builtin/OIDC) — non-admin users cannot access settings - Create, edit, and delete users with role assignment (admin/user) - Password management for builtin auth users - OIDC users shown with provider badge (password fields hidden) - Per-user configurable settings: - **Download destination** — custom folder path per user - **BookLore library & path** — dropdown select, each user's books go to their own library - **Email recipients** — per-user email delivery targets - **`{User}` template variable** — use in destination paths (e.g., `/books/{User}/`) - Settings override model: per-user values override globals, empty/unset falls back to global defaults ### Download Scoping - **Per-user download visibility** — non-admins only see their own downloads - **Username display** in downloads sidebar (shows who requested each download) - **WebSocket room-based filtering** — admins see all, users see only their own - **Download progress scoping** — progress events routed to correct user rooms ### BookLore Integration - **Dynamic dropdown selects** for library/path (replaces text inputs) - **Per-user library/path overrides** via user settings - **Options cache refresh** after Test Connection ### Security - SQL injection prevention (column whitelist on user updates) - Generic OIDC error messages (no internal detail leakage) - Admin self-deletion and last-local-admin deletion guards - OIDC role overwrite fix (only updates role when admin_group is configured) ## Migration **No migration script needed.** The `users.db` is created automatically on first startup. Existing builtin auth users are auto-migrated to the database on their first login. All other auth modes (no-auth, proxy, CWA) continue working unchanged. ## Test Plan - [x] All 519 tests passing, 0 failures - [ ] Test no-auth mode: settings accessible, downloads work without login - [ ] Test builtin auth: legacy credentials auto-migrate on login, new users can be created - [ ] Test OIDC auth: login flow, callback, auto-provisioning, group-based admin - [ ] Test CWA auth: unchanged behavior - [ ] Test proxy auth: unchanged behavior - [ ] Test per-user downloads: non-admin sees only own downloads - [ ] Test BookLore dropdowns: library/path selection, per-user overrides - [ ] Test Docker build: no Dockerfile changes needed --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
9.5 KiB
Markdown
264 lines
9.5 KiB
Markdown
# Test Suite
|
|
|
|
This directory contains the test suite for Shelfmark. Tests are organized by scope and component.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Run all unit tests (fast, no external dependencies)
|
|
docker exec test-cwabd python3 -m pytest tests/ -v -m "not integration and not e2e"
|
|
|
|
# Run E2E API tests
|
|
docker exec test-cwabd python3 -m pytest tests/e2e/ -v -m e2e
|
|
|
|
# Run everything except integration tests
|
|
docker exec test-cwabd python3 -m pytest tests/ -v -m "not integration"
|
|
```
|
|
|
|
## Test Structure
|
|
|
|
```
|
|
tests/
|
|
├── config/ # Settings & configuration tests
|
|
│ ├── test_docker_volumes.py # Docker volume mapping
|
|
│ ├── test_environment.py # Environment variable handling
|
|
│ ├── test_mirror_settings_live_apply.py # Mirror settings live reload
|
|
│ ├── test_mirror_settings_options.py # Mirror settings options
|
|
│ ├── test_security.py # Security settings
|
|
│ └── test_oidc_settings.py # OIDC settings fields & show_when conditions
|
|
│
|
|
├── core/ # Core application logic tests
|
|
│ ├── test_admin_users_api.py # Admin user CRUD API endpoints
|
|
│ ├── test_booklore_multiuser.py # BookLore per-user override merging
|
|
│ ├── test_builtin_multiuser.py # Builtin auth multi-user migration
|
|
│ ├── test_download_processing.py # Download file processing
|
|
│ ├── test_hardlink.py # Hardlink/copy operations
|
|
│ ├── test_library_processing.py # Library file processing
|
|
│ ├── test_manual_query.py # Manual search query handling
|
|
│ ├── test_mirrors_config.py # Mirror configuration
|
|
│ ├── test_naming.py # File naming templates
|
|
│ ├── test_oidc_auth.py # OIDC auth helpers (group claims, user provisioning)
|
|
│ ├── test_oidc_integration.py # OIDC integration into auth system (logic mirror)
|
|
│ ├── test_oidc_routes.py # OIDC Flask route handlers
|
|
│ ├── test_part_number_extraction.py # Part number extraction
|
|
│ ├── test_per_user_downloads.py # Per-user download queue filtering
|
|
│ ├── test_permission_handling.py # File permission handling
|
|
│ ├── test_processing_integration.py # Processing integration
|
|
│ ├── test_search_plan.py # Search plan logic
|
|
│ ├── test_user_db.py # UserDB CRUD operations
|
|
│ └── test_user_template_variable.py # {User} template variable in naming
|
|
│
|
|
├── e2e/ # End-to-end API tests
|
|
│ ├── conftest.py # Fixtures (APIClient, DownloadTracker)
|
|
│ ├── test_api.py # Core API endpoint tests
|
|
│ ├── test_download_flow.py # Full download journey tests
|
|
│ └── test_prowlarr_flow.py # Prowlarr-specific tests
|
|
│
|
|
├── prowlarr/ # Prowlarr plugin tests
|
|
│ ├── conftest.py # Shared fixtures
|
|
│ ├── test_clients.py # DownloadClient base, registry, DownloadStatus
|
|
│ ├── test_qbittorrent_client.py # qBittorrent client unit tests
|
|
│ ├── test_transmission_client.py # Transmission client unit tests
|
|
│ ├── test_nzbget_client.py # NZBGet client unit tests
|
|
│ ├── test_sabnzbd_client.py # SABnzbd client unit tests
|
|
│ ├── test_handler.py # ProwlarrHandler unit tests
|
|
│ ├── test_torrent_utils.py # Bencode, hash extraction, URL parsing
|
|
│ ├── test_bencode.py # Bencode encoding/decoding
|
|
│ ├── test_source.py # Release source (size parsing, format detection)
|
|
│ ├── test_cache.py # Release cache
|
|
│ ├── test_integration_clients.py # Integration tests (require Docker stack)
|
|
│ └── test_integration_handler.py # Handler integration tests
|
|
│
|
|
└── README.md # This file
|
|
```
|
|
|
|
## Test Types
|
|
|
|
### Unit Tests
|
|
Fast tests that mock external dependencies. Run these frequently during development.
|
|
|
|
```bash
|
|
docker exec test-cwabd python3 -m pytest tests/prowlarr/ -v -m "not integration"
|
|
```
|
|
|
|
**What they test:**
|
|
- Download client logic (status mapping, URL handling, error cases)
|
|
- Bencode encoding/decoding for torrent files
|
|
- Hash extraction from magnet links and .torrent files
|
|
- Protocol detection (torrent vs usenet)
|
|
- Release cache operations
|
|
- Handler download flow logic
|
|
- User database (CRUD, settings, OIDC subject linking)
|
|
- OIDC authentication (group claims, user provisioning, route handlers)
|
|
- Admin user management API (create, update, delete, password, per-user settings)
|
|
- Multi-user download queue filtering and per-user overrides
|
|
- Settings configuration (OIDC fields, show_when conditions)
|
|
|
|
### E2E Tests
|
|
Test the full application through its HTTP API. Require the app to be running.
|
|
|
|
```bash
|
|
docker exec test-cwabd python3 -m pytest tests/e2e/ -v -m e2e
|
|
```
|
|
|
|
**What they test:**
|
|
- Health check endpoint
|
|
- Configuration endpoint
|
|
- Metadata provider search (Hardcover, etc.)
|
|
- Release source listing
|
|
- Download queue operations (add, cancel, reorder, clear)
|
|
- Settings API
|
|
- Prowlarr integration
|
|
|
|
### Integration Tests
|
|
Test against real services (qBittorrent, Transmission, etc.). Require the full Docker test stack.
|
|
|
|
```bash
|
|
# Start the test stack first
|
|
docker compose -f docker-compose.test-clients.yml up -d
|
|
|
|
# Run integration tests
|
|
docker exec test-cwabd python3 -m pytest tests/prowlarr/ -v -m integration
|
|
```
|
|
|
|
**What they test:**
|
|
- Real connections to download clients
|
|
- Adding/removing actual torrents
|
|
- Status polling from real clients
|
|
|
|
## Test Markers
|
|
|
|
| Marker | Description | When to Skip |
|
|
|--------|-------------|--------------|
|
|
| `integration` | Requires running services (qBittorrent, etc.) | Default skip with `-m "not integration"` |
|
|
| `e2e` | End-to-end API tests | When app isn't running |
|
|
| `slow` | Tests that take longer (network calls, polling) | Quick feedback with `-m "not slow"` |
|
|
|
|
## Common Commands
|
|
|
|
```bash
|
|
# Run specific test file
|
|
docker exec test-cwabd python3 -m pytest tests/prowlarr/test_clients.py -v
|
|
|
|
# Run specific test class
|
|
docker exec test-cwabd python3 -m pytest tests/e2e/test_api.py::TestHealthEndpoint -v
|
|
|
|
# Run specific test
|
|
docker exec test-cwabd python3 -m pytest tests/e2e/test_api.py::TestHealthEndpoint::test_health_returns_ok -v
|
|
|
|
# Run with short traceback (cleaner output)
|
|
docker exec test-cwabd python3 -m pytest tests/ -v --tb=short -m "not integration"
|
|
|
|
# Run and stop on first failure
|
|
docker exec test-cwabd python3 -m pytest tests/ -v -x -m "not integration"
|
|
|
|
# Run with coverage (if pytest-cov installed)
|
|
docker exec test-cwabd python3 -m pytest tests/ --cov=shelfmark -m "not integration"
|
|
```
|
|
|
|
## Writing New Tests
|
|
|
|
### Unit Test Example
|
|
|
|
```python
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
class TestMyFeature:
|
|
def test_something(self, monkeypatch):
|
|
# Mock config values
|
|
monkeypatch.setattr(
|
|
"shelfmark.module.config.get",
|
|
lambda key, default="": {"KEY": "value"}.get(key, default),
|
|
)
|
|
|
|
# Test your code
|
|
result = my_function()
|
|
assert result == expected
|
|
```
|
|
|
|
### E2E Test Example
|
|
|
|
```python
|
|
import pytest
|
|
from .conftest import APIClient, DownloadTracker
|
|
|
|
@pytest.mark.e2e
|
|
class TestMyEndpoint:
|
|
def test_endpoint_works(self, api_client: APIClient):
|
|
resp = api_client.get("/api/my-endpoint")
|
|
assert resp.status_code == 200
|
|
|
|
def test_with_cleanup(self, api_client: APIClient, download_tracker: DownloadTracker):
|
|
# Track IDs for automatic cleanup after test
|
|
download_tracker.track("some-id")
|
|
# ... test code ...
|
|
```
|
|
|
|
## Test Fixtures
|
|
|
|
### E2E Fixtures (`tests/e2e/conftest.py`)
|
|
|
|
| Fixture | Scope | Description |
|
|
|---------|-------|-------------|
|
|
| `api_client` | session | HTTP client for API calls |
|
|
| `download_tracker` | function | Tracks downloads for cleanup |
|
|
| `server_config` | session | Cached server configuration |
|
|
|
|
### Prowlarr Fixtures (`tests/prowlarr/conftest.py`)
|
|
|
|
| Fixture | Scope | Description |
|
|
|---------|-------|-------------|
|
|
| `transmission_client` | module | Real Transmission client (integration) |
|
|
| `qbittorrent_client` | module | Real qBittorrent client (integration) |
|
|
| `deluge_client` | module | Real Deluge client (integration) |
|
|
| `nzbget_client` | module | Real NZBGet client (integration) |
|
|
| `sabnzbd_client` | module | Real SABnzbd client (integration) |
|
|
|
|
## Expected Skips
|
|
|
|
Some tests skip when external services aren't available. This is normal:
|
|
|
|
- **"No metadata providers available"** - Metadata provider not responding
|
|
- **"Prowlarr not configured"** - Prowlarr settings not set up
|
|
- **"No releases found"** - No indexers configured in Prowlarr
|
|
- **"Legacy search source unavailable"** - Direct download source offline
|
|
- **"Transmission/qBittorrent not available"** - Docker test stack not running
|
|
|
|
## Troubleshooting
|
|
|
|
### Tests can't connect to app
|
|
```bash
|
|
# Check the app is running
|
|
docker ps | grep test-cwabd
|
|
|
|
# Check app logs
|
|
docker logs test-cwabd
|
|
```
|
|
|
|
### Import errors
|
|
```bash
|
|
# Make sure you're running inside the container
|
|
docker exec test-cwabd python3 -m pytest ...
|
|
|
|
# Not from your local machine
|
|
pytest ... # This won't work
|
|
```
|
|
|
|
### Integration tests failing
|
|
```bash
|
|
# Make sure test stack is running
|
|
docker compose -f docker-compose.test-clients.yml up -d
|
|
|
|
# Check client containers
|
|
docker ps | grep -E "qbittorrent|transmission|deluge|nzbget|sabnzbd"
|
|
```
|
|
|
|
### Stale test data
|
|
```bash
|
|
# Clear the queue between test runs
|
|
docker exec test-cwabd python3 -c "
|
|
from shelfmark.core.queue import book_queue
|
|
book_queue.clear()
|
|
"
|
|
```
|