#!/usr/bin/env bats # Seam: the eight cutover-era dankserver quadlet units (romm trio, jellyfin, # calibre-web, copyparty, plex, tautulli) must keep their boot-persistence and v1-generator # contracts. The podman 5.x v1 Quadlet generator silently drops or hard-fails # on several keys; these tests pin the contract that keeps the units loadable # and secret-free in the repo. load helpers @test "all eight cutover units exist under dankserver//quadlet/" { for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty plex/quadlet/plex tautulli/quadlet/tautulli; do [ -f "$REPO_ROOT/dankserver/$unit.container" ] || return 1 done } @test "every cutover unit is boot-persistent (WantedBy=default.target)" { for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty plex/quadlet/plex tautulli/quadlet/tautulli; do grep -q '^WantedBy=default.target$' "$REPO_ROOT/dankserver/$unit.container" || return 1 done } @test "copyparty uses Exec= not Command= (v1 generator hard-fails on Command=)" { unit="$REPO_ROOT/dankserver/copyparty/quadlet/copyparty.container" grep -q '^Command=' "$unit" && return 1 grep -q '^Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD}' "$unit" || return 1 } @test "copyparty and romm consume host-local env files, never in-repo secrets" { grep -q '^EnvironmentFile=/home/dank/Containers/copyparty/env$' \ "$REPO_ROOT/dankserver/copyparty/quadlet/copyparty.container" || return 1 for unit in romm/quadlet/romm romm/quadlet/romm-mariadb; do grep -q '^EnvironmentFile=/home/dank/Containers/romm/env$' \ "$REPO_ROOT/dankserver/$unit.container" || return 1 done # No literal credential may ever appear in a tracked unit file: the only # Exec= arguments must be variable expansions, never a literal account:secret. ! grep -qE '\-a [A-Za-z0-9]+:[A-Za-z0-9]' "$REPO_ROOT/dankserver/copyparty/quadlet/copyparty.container" || return 1 } @test "romm db services keep their service-discovery aliases on the appnet" { grep -q '^Network=dankserver_appnet:alias=mariadb$' \ "$REPO_ROOT/dankserver/romm/quadlet/romm-mariadb.container" || return 1 grep -q '^Network=dankserver_appnet:alias=redis$' \ "$REPO_ROOT/dankserver/romm/quadlet/romm-redis.container" || return 1 } @test "no unit uses the unsupported Device= key (v1 generator needs AddDevice=)" { # A rejected file kills its own unit silently: the deploy would then die # with "Unit jellyfin.service not found" only on the target. Device= was # the first offender (jellyfin GPU passthrough); Command= is guarded # above. Keep this list current with every generator finding. for unit in dankserver/romm/quadlet/romm dankserver/romm/quadlet/romm-mariadb \ dankserver/romm/quadlet/romm-redis dankserver/jellyfin/quadlet/jellyfin \ dankserver/calibre-web/quadlet/calibre-web dankserver/copyparty/quadlet/copyparty \ dankserver/plex/quadlet/plex dankserver/tautulli/quadlet/tautulli; do ! grep -q '^Device=' "$REPO_ROOT/$unit.container" || return 1 done grep -q '^AddDevice=/dev/dri$' "$REPO_ROOT/dankserver/jellyfin/quadlet/jellyfin.container" || return 1 } @test "every host-served port is explicitly published (no silent bridge-only)" { grep -q '^PublishPort=8085:8080$' "$REPO_ROOT/dankserver/romm/quadlet/romm.container" || return 1 grep -q '^PublishPort=8096:8096$' "$REPO_ROOT/dankserver/jellyfin/quadlet/jellyfin.container" || return 1 grep -q '^PublishPort=8083:8083$' "$REPO_ROOT/dankserver/calibre-web/quadlet/calibre-web.container" || return 1 grep -q '^PublishPort=3923:3923$' "$REPO_ROOT/dankserver/copyparty/quadlet/copyparty.container" || return 1 grep -q '^PublishPort=32400:32400$' "$REPO_ROOT/dankserver/plex/quadlet/plex.container" || return 1 grep -q '^PublishPort=8181:8181$' "$REPO_ROOT/dankserver/tautulli/quadlet/tautulli.container" || return 1 } @test "plex runs the fleet identity model (PUID=0/PGID=0, no userns remap)" { # Rootless podman cannot map the real host uid 997 (subuid range is # 100000:65536), so the fleet model applies: container root maps to the # dank user and the deployer chowns /var/lib/plexmediaserver to # 1000:1000 on first cutover; rollback chowns back to 997:984. Pin both # keys and the ABSENCE of any userns key — UsernsMode= is compose syntax # (the v1 generator hard-fails the whole unit on it) and UserNS= would # remap the container away from the fleet identity. unit="$REPO_ROOT/dankserver/plex/quadlet/plex.container" grep -q '^Environment=PUID=0$' "$unit" || return 1 grep -q '^Environment=PGID=0$' "$unit" || return 1 ! grep -qE '^(UserNS|UsernsMode)=' "$unit" || return 1 } @test "calibre-server unit exists and is boot-persistent" { unit="$REPO_ROOT/dankserver/calibre-server/quadlet/calibre-server.container" [ -f "$unit" ] || return 1 grep -q '^WantedBy=default.target$' "$unit" || return 1 grep -q '^Restart=always$' "$unit" || return 1 } @test "calibre-server serves the content server auth-gated on the published port" { # The security contract: the content server runs with --enable-auth and # --auth-mode=basic (Digest is the non-SSL default and rejects Basic # clients with 400), on the internal port the unit publishes as 8084. unit="$REPO_ROOT/dankserver/calibre-server/quadlet/calibre-server.container" grep -q '^Exec=calibre-server --port 8090 --enable-auth --auth-mode=basic --userdb /userdb.sqlite /books$' "$unit" || return 1 grep -q '^PublishPort=8084:8090$' "$unit" || return 1 } @test "calibre-server consumes host-local credential stores, never in-repo secrets" { unit="$REPO_ROOT/dankserver/calibre-server/quadlet/calibre-server.container" grep -q '^EnvironmentFile=/home/dank/Containers/calibre-server/env$' "$unit" || return 1 grep -q '^Volume=/home/dank/Containers/calibre-server/userdb.sqlite:/userdb.sqlite$' "$unit" || return 1 # The userdb's pw column is plaintext: no literal password may appear in # the tracked unit, and the desktop basic-auth credential comes from the # env file, not an Environment= line. ! grep -qE '^Environment=CUSTOM_(USER|PASSWORD)=' "$unit" || return 1 }