mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-07-31 06:46:39 -04:00
- Add configurable completed-path wait for external clients : - Add a configurable Advanced setting, DOWNLOAD_CLIENT_COMPLETED_PATH_TIMEOUT, for how long Shelfmark waits after a torrent or usenet client reports completion before treating the completed path as missing. - Keep the default at the existing 60-second grace period, with a maximum of 3600 seconds - Add e2e testing - Add e2e testing Should fix #861
333 lines
12 KiB
YAML
333 lines
12 KiB
YAML
# Shelfmark e2e Docker testing platform
|
|
# ---------------------------------------------------------------------------
|
|
# A hermetic stack: the app under test plus *controllable* dependencies
|
|
# (fake Anna's Archive, a Cloudflare gate, a mock FlareSolverr bypasser, a DoH
|
|
# responder, a DNS server, HTTP + SOCKS5 proxies, and a Tor profile).
|
|
#
|
|
# Config profiles are env-driven: pick a profile with an env file under env/
|
|
# (it sets COMPOSE_PROFILES + the SM_* config the app boots with). The runner
|
|
# brings the stack up per profile, runs the matching cluster tests, tears down.
|
|
#
|
|
# ./run-e2e.sh env/baseline.env
|
|
# ./run-e2e.sh env/bypasser-external.env
|
|
# ./run-e2e.sh env/dns-blocked.env
|
|
#
|
|
# See README.md for the full cluster x profile matrix.
|
|
|
|
x-mock-build: &mock-build
|
|
build:
|
|
context: ./mocks
|
|
dockerfile: Dockerfile
|
|
|
|
networks:
|
|
e2e:
|
|
driver: bridge
|
|
ipam:
|
|
config:
|
|
- subnet: 172.30.0.0/24
|
|
|
|
services:
|
|
# ----- App under test --------------------------------------------------- #
|
|
shelfmark:
|
|
build:
|
|
context: ../../..
|
|
dockerfile: Dockerfile
|
|
target: ${SM_BUILD_TARGET:-shelfmark}
|
|
container_name: e2e-shelfmark
|
|
cap_add:
|
|
- NET_ADMIN # required by tor.sh iptables when USING_TOR=true
|
|
- NET_RAW
|
|
environment:
|
|
TZ: UTC
|
|
DEBUG: "true"
|
|
ONBOARDING: "false" # skip wizard; ephemeral storage
|
|
DISABLE_LOCAL_AUTH: "${SM_DISABLE_LOCAL_AUTH:-true}"
|
|
PUID: "${SM_PUID:-1000}"
|
|
PGID: "${SM_PGID:-1000}"
|
|
# --- source / mirror config (cluster 2/3) ---
|
|
DIRECT_DOWNLOAD_ENABLED: "${SM_DIRECT_DOWNLOAD_ENABLED:-true}"
|
|
AA_ADDITIONAL_URLS: "${SM_AA_URL:-http://mock-aa}"
|
|
# --- bypasser config (cluster 1) ---
|
|
USE_CF_BYPASS: "${SM_USE_CF_BYPASS:-false}"
|
|
# Selects the external (FlareSolverr) bypasser; when false the in-image
|
|
# Chrome (internal) bypasser is used. NOT derived from EXT_BYPASSER_URL.
|
|
USING_EXTERNAL_BYPASSER: "${SM_USING_EXTERNAL_BYPASSER:-false}"
|
|
EXT_BYPASSER_URL: "${SM_EXT_BYPASSER_URL:-}"
|
|
# --- prowlarr indexer + torrent client (cluster 5, `full` profile) ---
|
|
PROWLARR_ENABLED: "${SM_PROWLARR_ENABLED:-false}"
|
|
PROWLARR_URL: "${SM_PROWLARR_URL:-}"
|
|
PROWLARR_API_KEY: "${SM_PROWLARR_API_KEY:-}"
|
|
PROWLARR_TORRENT_CLIENT: "${SM_PROWLARR_TORRENT_CLIENT:-}"
|
|
QBITTORRENT_URL: "${SM_QBITTORRENT_URL:-}"
|
|
QBITTORRENT_USERNAME: "${SM_QBITTORRENT_USERNAME:-}"
|
|
QBITTORRENT_PASSWORD: "${SM_QBITTORRENT_PASSWORD:-}"
|
|
QBITTORRENT_CATEGORY: "${SM_QBITTORRENT_CATEGORY:-}"
|
|
DOWNLOAD_CLIENT_COMPLETED_PATH_TIMEOUT: "${SM_DOWNLOAD_CLIENT_COMPLETED_PATH_TIMEOUT:-60}"
|
|
# transmission / deluge / rtorrent (client-* profiles)
|
|
TRANSMISSION_URL: "${SM_TRANSMISSION_URL:-}"
|
|
TRANSMISSION_USERNAME: "${SM_TRANSMISSION_USERNAME:-}"
|
|
TRANSMISSION_PASSWORD: "${SM_TRANSMISSION_PASSWORD:-}"
|
|
DELUGE_HOST: "${SM_DELUGE_HOST:-}"
|
|
DELUGE_PORT: "${SM_DELUGE_PORT:-}"
|
|
DELUGE_PASSWORD: "${SM_DELUGE_PASSWORD:-}"
|
|
RTORRENT_URL: "${SM_RTORRENT_URL:-}"
|
|
# --- DNS / DoH (config cluster) ---
|
|
CUSTOM_DNS: "${SM_CUSTOM_DNS:-}"
|
|
CUSTOM_DNS_MANUAL: "${SM_CUSTOM_DNS_MANUAL:-}"
|
|
USE_DOH: "${SM_USE_DOH:-false}"
|
|
# Disable TLS verification so the in-stack DoH-over-HTTPS mock (self-signed)
|
|
# is accepted in the dns-doh profile. Default keeps verification ON.
|
|
CERTIFICATE_VALIDATION: "${SM_CERTIFICATE_VALIDATION:-enabled}"
|
|
# --- proxy (config cluster) ---
|
|
PROXY_MODE: "${SM_PROXY_MODE:-none}"
|
|
HTTP_PROXY_URL: "${SM_HTTP_PROXY:-}"
|
|
HTTP_PROXY: "${SM_HTTP_PROXY:-}"
|
|
SOCKS5_PROXY: "${SM_SOCKS5_PROXY:-}"
|
|
NO_PROXY: "${SM_NO_PROXY:-}"
|
|
# --- tor (cluster 1/6) ---
|
|
USING_TOR: "${SM_USING_TOR:-false}"
|
|
ports:
|
|
- "8084:8084"
|
|
volumes:
|
|
- ./.state/config:/config
|
|
- ./.state/books:/books
|
|
- ${SM_DOWNLOADS_HOST_DIR:-./.state/downloads}:/downloads
|
|
- ./.state/tmp:/tmp/shelfmark
|
|
networks:
|
|
- e2e
|
|
dns:
|
|
# When a DNS profile is active, point the container's system resolver at
|
|
# our controllable server; otherwise Docker's embedded DNS (127.0.0.11).
|
|
- ${SM_SYSTEM_DNS:-127.0.0.11}
|
|
extra_hosts:
|
|
# Redirect the DoH provider hostname to the in-stack mock-doh (dns-doh
|
|
# profile). Harmless elsewhere — only the dns-doh profile enables DoH against
|
|
# the cloudflare provider, and /etc/hosts is consulted before the resolver.
|
|
- "cloudflare-dns.com:172.30.0.21"
|
|
restart: "no"
|
|
|
|
# ----- Fake Anna's Archive origin (always on) --------------------------- #
|
|
mock-aa:
|
|
<<: *mock-build
|
|
container_name: e2e-mock-aa
|
|
environment:
|
|
MOCK_ROLE: origin-aa
|
|
# When set (the `full` profile sets it to the CF gate), AA slow-download
|
|
# links point through Cloudflare so a real download forces the internal
|
|
# Chrome bypasser to solve the challenge. Empty -> same-origin (no CF).
|
|
SLOW_DOWNLOAD_BASE: "${SM_SLOW_DOWNLOAD_BASE:-}"
|
|
AA_FILE_BASE: "http://mock-aa"
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.10
|
|
aliases:
|
|
- aa.mock.test
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://localhost/healthz')"]
|
|
interval: 3s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
# ----- Cloudflare gate (profiles: bypasser-external, full) -------------- #
|
|
# `full` puts AA's slow-download behind this gate so the real Chrome bypasser
|
|
# must solve it; `bypasser-external` puts AA search behind it for the
|
|
# FlareSolverr negative/positive controls.
|
|
mock-cf:
|
|
<<: *mock-build
|
|
container_name: e2e-mock-cf
|
|
profiles: ["bypasser-external", "full"]
|
|
environment:
|
|
MOCK_ROLE: cloudflare
|
|
ORIGIN_INTERNAL_URL: http://mock-aa
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.11
|
|
aliases:
|
|
- cf.mock.test
|
|
|
|
# ----- Mock FlareSolverr external bypasser (profile: bypasser-external) -- #
|
|
mock-flaresolverr:
|
|
<<: *mock-build
|
|
container_name: e2e-mock-flaresolverr
|
|
profiles: ["bypasser-external"]
|
|
environment:
|
|
MOCK_ROLE: flaresolverr
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.12
|
|
|
|
# NOTE: the mock `doh` role lives in mock_services.py and is exercised by
|
|
# tests/download/test_doh_resolver_mock.py (real DoHResolver over localhost HTTP).
|
|
# An in-stack DoH service is intentionally absent — see README "Known limitations".
|
|
|
|
# ----- DNS server (profile: dns-manual) --------------------------------- #
|
|
coredns:
|
|
image: coredns/coredns:1.11.1
|
|
container_name: e2e-coredns
|
|
profiles: ["dns-manual"]
|
|
command: ["-conf", "/Corefile"]
|
|
volumes:
|
|
- ./dns/Corefile:/Corefile:ro
|
|
- ./dns/mock.test.db:/zones/mock.test.db:ro
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.20
|
|
|
|
# ----- DNS server that NXDOMAINs the AA host (profile: dns-blocked) ------ #
|
|
# Simulates ISP DNS blocking (#1028); the app must fall back (DoH / direct).
|
|
coredns-blocked:
|
|
image: coredns/coredns:1.11.1
|
|
container_name: e2e-coredns-blocked
|
|
profiles: ["dns-blocked"]
|
|
command: ["-conf", "/Corefile"]
|
|
volumes:
|
|
- ./dns/Corefile.blocked:/Corefile:ro
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.22
|
|
|
|
# ----- HTTP proxy (profile: proxy-http) --------------------------------- #
|
|
tinyproxy:
|
|
image: monokal/tinyproxy:latest
|
|
container_name: e2e-tinyproxy
|
|
profiles: ["proxy-http"]
|
|
command: ANY
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.30
|
|
|
|
# ----- SOCKS5 proxy (profile: proxy-socks) ------------------------------ #
|
|
microsocks:
|
|
image: vimagick/microsocks:latest
|
|
container_name: e2e-microsocks
|
|
profiles: ["proxy-socks"]
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.31
|
|
|
|
# ----- Mock Prowlarr indexer (full + client-* profiles) ----------------- #
|
|
# Minimal Prowlarr API returning one torrent release whose .torrent is a
|
|
# webseed pointing at mock-aa. Drives the real torrent-client download in the
|
|
# `full` (qBittorrent) and `client-*` (transmission/deluge/rtorrent) profiles.
|
|
mock-prowlarr:
|
|
<<: *mock-build
|
|
container_name: e2e-mock-prowlarr
|
|
profiles: ["full", "client-transmission", "client-deluge", "client-qbittorrent-delayed"]
|
|
environment:
|
|
MOCK_ROLE: prowlarr
|
|
AA_INTERNAL_URL: http://mock-aa
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.40
|
|
aliases:
|
|
- prowlarr.mock.test
|
|
|
|
# ----- Real qBittorrent download client (profiles: full, delayed path) --- #
|
|
# Auth is bypassed for the e2e subnet (qBittorrent.conf) so shelfmark connects
|
|
# without juggling the image's random temp password.
|
|
qbittorrent:
|
|
image: lscr.io/linuxserver/qbittorrent:latest
|
|
container_name: e2e-qbittorrent
|
|
profiles: ["full", "client-qbittorrent-delayed"]
|
|
environment:
|
|
PUID: "1000"
|
|
PGID: "1000"
|
|
TZ: UTC
|
|
WEBUI_PORT: "8080"
|
|
volumes:
|
|
- ./qbittorrent/qBittorrent.conf:/config/qBittorrent/qBittorrent.conf
|
|
# Normally shared with Shelfmark. In the delayed profile this points at a
|
|
# client-only directory and the sync sidecar makes it visible later.
|
|
- ${SM_QBITTORRENT_DOWNLOADS_HOST_DIR:-./.state/downloads}:/downloads
|
|
networks:
|
|
- e2e
|
|
|
|
# ----- Delayed file visibility sidecar (profile: client-qbittorrent-delayed) #
|
|
# Simulates a seedbox/remote-sync workflow: qBittorrent writes into one host
|
|
# directory while Shelfmark sees a separate /downloads directory that is synced
|
|
# only after a delay. This exercises DOWNLOAD_CLIENT_COMPLETED_PATH_TIMEOUT.
|
|
delayed-download-sync:
|
|
image: alpine:3.20
|
|
container_name: e2e-delayed-download-sync
|
|
profiles: ["client-qbittorrent-delayed"]
|
|
environment:
|
|
SYNC_DELAY_SECONDS: "${SM_DELAYED_DOWNLOAD_SYNC_SECONDS:-20}"
|
|
command:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
mkdir -p /synced
|
|
while true; do
|
|
found=0
|
|
for path in /remote/*; do
|
|
[ -e "$${path}" ] || continue
|
|
found=1
|
|
before="$$(du -sk /remote 2>/dev/null | awk '{print $$1}')"
|
|
sleep "$${SYNC_DELAY_SECONDS}"
|
|
after="$$(du -sk /remote 2>/dev/null | awk '{print $$1}')"
|
|
if [ -n "$${before}" ] && [ "$${before}" = "$${after}" ]; then
|
|
cp -a /remote/. /synced/ 2>/dev/null || true
|
|
fi
|
|
break
|
|
done
|
|
[ "$${found}" = "1" ] || sleep 1
|
|
done
|
|
volumes:
|
|
- ${SM_QBITTORRENT_DOWNLOADS_HOST_DIR:-./.state/downloads}:/remote:ro
|
|
- ${SM_DOWNLOADS_HOST_DIR:-./.state/downloads}:/synced
|
|
networks:
|
|
- e2e
|
|
|
|
# ----- Real Transmission client (profile: client-transmission) ---------- #
|
|
# Same webseed torrent + mock Prowlarr as `full`, different real client.
|
|
transmission:
|
|
image: lscr.io/linuxserver/transmission:latest
|
|
container_name: e2e-transmission
|
|
profiles: ["client-transmission"]
|
|
environment:
|
|
PUID: "1000"
|
|
PGID: "1000"
|
|
TZ: UTC
|
|
USER: admin
|
|
PASS: admin
|
|
volumes:
|
|
- ./.state/downloads:/downloads
|
|
networks:
|
|
- e2e
|
|
|
|
# ----- Real Deluge client (profile: client-deluge) ---------------------- #
|
|
# shelfmark talks to deluge-web (default WebUI password "deluge"), which
|
|
# connects to the bundled daemon — no daemon auth-file juggling.
|
|
deluge:
|
|
image: lscr.io/linuxserver/deluge:latest
|
|
container_name: e2e-deluge
|
|
profiles: ["client-deluge"]
|
|
environment:
|
|
PUID: "1000"
|
|
PGID: "1000"
|
|
TZ: UTC
|
|
volumes:
|
|
- ./.state/downloads:/downloads
|
|
networks:
|
|
- e2e
|
|
|
|
# NOTE: rTorrent is intentionally absent — its rakshasa-libtorrent has no
|
|
# GetRight/webseed support, so the hermetic webseed torrent can't drive it.
|
|
# Supporting it would need a real tracker + seeder (see README limitations).
|
|
|
|
# ----- DoH-over-HTTPS responder (profile: dns-doh) ---------------------- #
|
|
# In-stack DoH: serves the DNS JSON API over HTTPS with a self-signed cert.
|
|
# shelfmark reaches it because the profile (a) maps the DoH provider hostname
|
|
# to this container via extra_hosts and (b) sets CERTIFICATE_VALIDATION=disabled.
|
|
mock-doh:
|
|
<<: *mock-build
|
|
container_name: e2e-mock-doh
|
|
profiles: ["dns-doh"]
|
|
environment:
|
|
MOCK_ROLE: doh
|
|
DOH_TLS: "1"
|
|
DOH_MAP: "aa.mock.test=172.30.0.10,cf.mock.test=172.30.0.11"
|
|
networks:
|
|
e2e:
|
|
ipv4_address: 172.30.0.21
|