Files
shelfmark/tests/e2e/platform/docker-compose.e2e.yml
CaliBrain 404e8cc5c5 path timeouts (#1103)
- 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
2026-07-06 01:51:58 -04:00

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