mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-06-16 00:58:45 -04:00
## Backport bug fixes from `NemesisHubris/litfinder` Forwards a curated set of bug fixes from [NemesisHubris/litfinder](https://github.com/NemesisHubris/litfinder) — a community fork of this project — that address open issues here. All commits preserve original authorship via `git cherry-pick`; this PR is a backport rather than original work. Each fix has been reviewed locally, lint/format-cleaned to match this repo's existing ruff config, and verified with the test suite. Rebrand strings, license switches, and features have been deliberately excluded. ### Upstream issues addressed - **#999** — Mirror URLs with query params no longer break search requests (strip query string/fragment in `normalize_http_url`) - **#956** — Apprise notifications now respect the configured proxy (proxy env vars injected before dispatch) - **#1025** — rTorrent: separate `RTORRENT_AUDIOBOOK_LABEL` setting, falls back to book label if unset - **#1010** — Stop button in Activity no longer makes the panel disappear (snapshot refresh on cancel) - **#1021** — Anna's Archive slow-download countdown now caps retries instead of looping forever - **#1040** — Empty destination directory cleaned up when write probe fails - **PR #1031** — Language detection from Anna's Archive distant path when listing metadata is missing ### Additional fixes (no open issue but clear bugs) - **fix: Python 2 `except` syntax across 27 files** — `except X, Y:` is a SyntaxError in Python 3 and prevents affected modules from importing at runtime. Mechanical sweep to `except (X, Y):`. - **fix(abb): info hash validation with magnet fallback** — adds SHA-1/SHA-256 hex validation on extracted info hashes; falls back to scanning the full page for a magnet link (e.g. posted in comments) when the table value is malformed. Also extends the exact-phrase fallback to manual queries and defaults the ABB listing language to `en` when missing, preventing valid results from being hidden by the language filter. Includes a small test-fixture fix (`test(abb): use valid hex info hashes in scraper test fixtures`) since the existing fixtures used non-hex placeholders that the new validation correctly rejects. - **fix: Anna's Archive title parser** — handles nested edition spans and filters `lgli` catalog descriptor entries (e.g. "Book/Online Audio") that were polluting search results. ### Deliberately not included - LitFinder rebranding (UI strings, Apprise app ID, logo). The `fix: three upstream bugs` commit (#999/#956/#1025) was cherry-picked with Apprise app-id, description, and logo-URL strings reverted from "LitFinder" back to "Shelfmark"; noted in the commit body. - Features from the LitFinder fork (multi-variant title search, multi-book flat-folder grouping, fuzzy text matching, "Leave in Place" output handler, admin display name, custom-source plugin system). These are larger behavior changes that each warrant their own focused review — happy to send any of them separately if of interest. - LitFinder-specific test environment and CI infrastructure. ### Verification - Backend: **1879 passed**, 96 skipped (1 preexisting failure on `seleniumbase`-dependent test in local venv; runs fine in the standard Docker image with the `browser` extra) - Lint, format, dead-code: all clean against this repo's existing ruff/vulture config - One follow-up cleanup commit (`style: ruff lint and format fixes for ported commits`) brings the cherry-picked code into compliance with this repo's ruff settings — no behavior changes there ### Etiquette / credit Per-commit authorship preserved by cherry-pick. The only edits to the original commits are: - `fix: three upstream bugs` — Apprise rebrand strings reverted to "Shelfmark" (noted in commit body, original author retained as `Co-Authored-By` via cherry-pick) - One follow-up `style:` commit for ruff config alignment Big thanks to [@NemesisHubris](https://github.com/NemesisHubris) for the original work in LitFinder; this PR exists to make sure these fixes reach Shelfmark's wider user base. Happy to revise scope, split into smaller PRs, or split off the Py2 cleanup separately if that's preferable. --------- Co-authored-by: NemesisHubris <155838970+NemesisHubris@users.noreply.github.com> Co-authored-by: CaliBrain <calibrain@l4n.xyz>
Test Suite
This directory contains the test suite for Shelfmark. Tests are organized by scope and component.
Quick Start
# Sync the local Python environment once
make install-python-dev
# Run all unit tests locally (fast, no external dependencies)
uv run pytest tests/ -v -m "not integration and not e2e"
# Run all Python static analysis (lint, format, typecheck, dead code)
make python-checks
# Run E2E API tests against a running app stack
uv run pytest tests/e2e/ -v -m e2e
# Run everything except integration tests locally
uv run 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.
uv run 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.
uv run 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.
# Start the test stack first
docker compose -f docker-compose.test-clients.yml up -d
# Run integration tests
docker compose -f docker-compose.test-clients.yml exec shelfmark uv run 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
# Run specific test file
uv run pytest tests/prowlarr/test_clients.py -v
# Run specific test class
uv run pytest tests/e2e/test_api.py::TestHealthEndpoint -v
# Run specific test
uv run pytest tests/e2e/test_api.py::TestHealthEndpoint::test_health_returns_ok -v
# Run with short traceback (cleaner output)
uv run pytest tests/ -v --tb=short -m "not integration"
# Run and stop on first failure
uv run pytest tests/ -v -x -m "not integration"
# Run with coverage (if pytest-cov installed)
uv run pytest tests/ --cov=shelfmark -m "not integration"
Writing New Tests
Unit Test Example
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
import pytest
from .conftest import APIClient, DownloadTracker
@pytest.mark.e2e
class TestMyEndpoint:
def test_endpoint_works(self, protected_api_client: APIClient):
resp = protected_api_client.get("/api/my-endpoint")
assert resp.status_code == 200
def test_with_cleanup(
self,
protected_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 |
function | Fresh HTTP client for general E2E calls |
protected_api_client |
function | Authenticated client for protected-route E2Es |
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
# Check the app/container is running
docker ps
# Check app logs
docker logs <your-shelfmark-container>
Import errors
# Sync the local Python environment first
uv sync --locked --extra browser
# Then run tests from the repo root
uv run pytest ...
Integration tests failing
# 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
Restart the container to reset the in-memory queue between test runs:
docker restart <your-shelfmark-container>