Backup. Both documented commands failed on the shipped stack, and the sentence
explaining them was wrong too:
- `pg_dump $DATABASE_URL` — `DATABASE_URL` is only ever in the compose
environment, never an operator's shell, and it points at `db:5432`, which is
compose-internal DNS. The app image has no postgres client either.
- `> /media/backups/…` — `/media` is a named volume mounted inside the app
container, not a host path, and nothing ever creates a `backups` subdirectory.
- `rsync /opt/eventsnap/media/` — that path does not exist anywhere.
- "a single path to back up" — false, and dangerously so: exports were moved to
their own `exports_data` volume precisely so a keepsake (which contains every
photo in the event) can't be served off the media tree. Backing up only
`media_data` silently loses every generated keepsake.
Rewritten as three commands — db via `docker compose exec -T db pg_dump`, and one
`docker run … tar` per volume — all verified against the running stack. The
volume mounts use `/src`, not `/media`: I hit the footgun while testing this.
Docker pre-populates an EMPTY volume from the image's own directory and chowns it
to match, so `-v media_data:/media alpine` tars alpine's cdrom/floppy/usb, writes
them into the volume, and leaves it root-owned so the non-root app can no longer
write. Mounting where the image has nothing avoids all of it. Documented inline
so the next person doesn't rediscover it.
Also correct the architecture notes: `/media/*` no longer routes to the backend
(that static tree was removed as a gating bypass), and `exports_data` was missing
from the volume list — the one volume an operator most needs to know about.
e2e stack: add the `EXPORT_PATH` + `/exports` volume it was missing. The file
says "mirrors production layout"; without these, exports landed on the container's
writable layer at the default path, so export-leak and export-video wrote real
archives into ephemeral storage and the "exports live outside media" invariant
was never actually exercised.
Pre-existing red test, unrelated to the audit: all four 02-upload/quota tests
have been failing since 4464147 "stop /me/quota leaking raw disk to guests"
(2026-07-19), which post-dates the spec's last edit. `setLimitTo` calibrated
`quota_tolerance` from `free_disk_bytes` read through the GUEST's token — a field
that commit deliberately zeroes for non-staff. Dividing by it yields a NaN
tolerance, so every test in the block died in the helper. Read the calibration
inputs through a staff token and keep reading the ceiling back through the guest,
whose limit is the thing under test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
2.8 KiB
YAML
88 lines
2.8 KiB
YAML
# Isolated EventSnap test stack. Mirrors production layout (Caddy → frontend +
|
|
# backend) but on its own ports, its own volumes, with rate-limits disabled and
|
|
# `EVENTSNAP_TEST_MODE=1` so the `/admin/__truncate` reset endpoint is live.
|
|
#
|
|
# Bring it up once before running the suite:
|
|
# npm run stack:up
|
|
# Tear it down (and wipe all volumes) after:
|
|
# npm run stack:down
|
|
#
|
|
# Port 3101 is the only externally exposed port: Caddy fronts everything.
|
|
|
|
services:
|
|
db:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: eventsnap_test
|
|
POSTGRES_PASSWORD: eventsnap_test
|
|
POSTGRES_DB: eventsnap_test
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U eventsnap_test -d eventsnap_test']
|
|
interval: 3s
|
|
timeout: 3s
|
|
retries: 30
|
|
ports:
|
|
- '55432:5432' # exposed so the e2e harness can connect via pg for fixture setup
|
|
|
|
app:
|
|
build:
|
|
context: ../backend
|
|
dockerfile: Dockerfile
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
DATABASE_URL: postgres://eventsnap_test:eventsnap_test@db:5432/eventsnap_test
|
|
JWT_SECRET: 00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff
|
|
# bcrypt hash for the literal string "admin-test-pw" (cost 4 — fast for tests).
|
|
# Generated once and committed. Verified with `bcrypt.compareSync`.
|
|
# The $ characters are doubled to escape compose interpolation.
|
|
ADMIN_PASSWORD_HASH: $$2b$$04$$XKJJkNX6BOi6y3S42DA5JOWwk4oxc8DHPL6.MrPfJI2vpnccZjP32
|
|
EVENT_SLUG: e2e-test-event
|
|
EVENT_NAME: E2E Test Event
|
|
APP_PORT: '3000'
|
|
MEDIA_PATH: /media
|
|
# Exports MUST live outside MEDIA_PATH — see the note on the volume below and
|
|
# config.rs::validate. Omitting this left exports on the container's writable
|
|
# layer at the /exports default, so the test stack diverged from the prod layout
|
|
# it claims to mirror, and export-leak/export-video wrote real archives into
|
|
# ephemeral storage.
|
|
EXPORT_PATH: /exports
|
|
SESSION_EXPIRY_DAYS: '30'
|
|
EVENTSNAP_TEST_MODE: '1' # ENABLES /admin/__truncate — never set in prod
|
|
RUST_LOG: eventsnap_backend=info,tower_http=warn
|
|
volumes:
|
|
- media_data:/media
|
|
# Separate volume, exactly as in production: a keepsake archive contains every
|
|
# photo in the event, so it is kept off the media tree.
|
|
- exports_data:/exports
|
|
expose:
|
|
- '3000'
|
|
|
|
frontend:
|
|
build:
|
|
context: ../frontend
|
|
dockerfile: Dockerfile
|
|
depends_on:
|
|
- app
|
|
environment:
|
|
PORT: '3001'
|
|
HOST: '0.0.0.0'
|
|
ORIGIN: 'http://localhost:3101'
|
|
expose:
|
|
- '3001'
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
depends_on:
|
|
- app
|
|
- frontend
|
|
volumes:
|
|
- ./Caddyfile.test:/etc/caddy/Caddyfile:ro
|
|
ports:
|
|
- '3101:3101'
|
|
|
|
volumes:
|
|
media_data:
|
|
exports_data:
|