#!/usr/bin/env bash # Disposable ImageApi instance for end-to-end / agent-driven UI testing. # # Runs a second, fully isolated server next to any real one: its own build # dir (target/e2e — a running target/debug exe is file-locked on Windows), # its own process name (image-api-e2e.exe, so `stop` can never touch the # real server), its own port, database, thumbnails and HLS output. The # media library is a *copy* of the pristine fixtures, so tests may upload, # move and delete freely; `reset` restores the known baseline. # # Written for WSL driving the Windows toolchain (cargo.exe, curl.exe, # ffmpeg.exe). WSL2's default NAT networking can't reach Windows' # 127.0.0.1, hence curl.exe for every HTTP call. # # Usage: dev/e2e/e2e.sh (see `help`) set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "$HERE/../.." && pwd)" WORK="$HERE/.work" FIXTURES="$WORK/fixtures" STATE="$WORK/state" BIN_DIR="$WORK/bin" EXE_NAME="image-api-e2e.exe" LOG="$WORK/server.log" E2E_PORT="${E2E_PORT:-8090}" # 127.0.0.1 is reachable from the Windows browser (Expo web), the Android # emulator (as 10.0.2.2) and USB devices via `adb reverse`. Set # E2E_BIND=0.0.0.0 for a physical device over Wi-Fi. E2E_BIND="${E2E_BIND:-127.0.0.1}" E2E_USER="${E2E_USER:-e2e}" E2E_PASS="${E2E_PASS:-E2eTest#2026pass}" URL="http://127.0.0.1:$E2E_PORT" win() { wslpath -w "$1"; } log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[e2e]\033[0m %s\n' "$*" >&2; exit 1; } is_running() { tasklist.exe /FI "IMAGENAME eq $EXE_NAME" /NH 2>/dev/null | grep -qi "$EXE_NAME" } cmd_build() { log "building bins into target/e2e" (cd "$REPO" && cargo.exe build --target-dir target/e2e --bin image-api --bin create_user) mkdir -p "$BIN_DIR" # Run from a copy so rebuilding never fights a running (locked) exe. local was_running=0 if is_running; then was_running=1; cmd_stop; fi cp "$REPO/target/e2e/debug/image-api.exe" "$BIN_DIR/$EXE_NAME" cp "$REPO/target/e2e/debug/create_user.exe" "$BIN_DIR/create_user.exe" if [ "$was_running" = 1 ]; then cmd_start; fi } cmd_fixtures() { local force=0 import=() while [ $# -gt 0 ]; do case "$1" in --force) force=1 ;; --import) import=(--import "$2"); force=1; shift ;; *) die "unknown fixtures option: $1" ;; esac shift done if [ -f "$FIXTURES/manifest.json" ] && [ "$force" = 0 ]; then log "fixtures exist (generated $(grep -o '"generated_on": "[^"]*"' "$FIXTURES/manifest.json" | cut -d'"' -f4)); --force to regenerate" return fi log "generating fixtures" python3 "$HERE/gen_fixtures.py" "$FIXTURES" "${import[@]}" } write_env() { cat > "$STATE/.env" <> "$STATE/.env" fi } cmd_reset() { [ -f "$FIXTURES/manifest.json" ] || cmd_fixtures is_running && cmd_stop log "resetting state (db, thumbs, hls, media copy)" rm -rf "$STATE" mkdir -p "$STATE"/{thumbs,hls,gifs,previews} cp -a "$FIXTURES/media" "$STATE/media" cp "$FIXTURES/manifest.json" "$STATE/manifest.json" write_env [ -x "$BIN_DIR/create_user.exe" ] || cmd_build (cd "$STATE" && "$BIN_DIR/create_user.exe" "$E2E_USER" "$E2E_PASS") } login() { curl.exe -sf -X POST "$URL/login" -H "Content-Type: application/json" \ -d "{\"username\":\"$E2E_USER\",\"password\":\"$E2E_PASS\"}" 2>/dev/null \ | grep -o '"token":"[^"]*"' | cut -d'"' -f4 } api() { # api METHOD PATH [JSON]; reuses $E2E_TOKEN when set (login is a bcrypt verify) local tok="${E2E_TOKEN:-$(login)}" # /dev/null || echo 0 } seed_tags() { local E2E_TOKEN; E2E_TOKEN="$(login)" python3 - "$STATE/manifest.json" <<'EOF' | while IFS=$'\t' read -r path tag; do import json, sys for tag, paths in json.load(open(sys.argv[1]))["tags"].items(): for p in paths: print(f"{p}\t{tag}") EOF api POST "image/tags" "{\"file_name\":\"$path\",\"tag_name\":\"$tag\"}" >/dev/null \ || log "warn: failed to tag $path" done touch "$STATE/.tags_seeded" } cmd_start() { is_running && { log "already running at $URL"; return; } [ -f "$STATE/.env" ] || cmd_reset [ -x "$BIN_DIR/$EXE_NAME" ] || cmd_build log "starting $EXE_NAME on $E2E_BIND:$E2E_PORT" # cwd = state dir: the server reads .env from its working directory. # Redirect the whole subshell, not just the exe: an inherited stdout # would keep a caller's pipe (`e2e.sh up | tail`) open until shutdown. (cd "$STATE" && exec setsid nohup "$BIN_DIR/$EXE_NAME") >"$LOG" 2>&1 & disown for _ in $(seq 1 60); do [ -n "$(login)" ] && break is_running || die "server exited; see $LOG" sleep 1 done [ -n "$(login)" ] || die "server did not accept logins within 60s; see $LOG" [ -f "$STATE/.tags_seeded" ] || { log "seeding tags"; seed_tags; } local want have=0 want="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["counts"]["indexed"])' "$STATE/manifest.json")" log "waiting for ingest ($want indexable fixtures)" for _ in $(seq 1 90); do have="$(indexed_count)" [ "$have" -ge "$want" ] && break sleep 2 done if [ "$have" -ge "$want" ]; then log "ready: $have files indexed" else log "warn: only $have/$want indexed after 180s (still ingesting?); see $LOG" fi cmd_info } cmd_stop() { if is_running; then log "stopping $EXE_NAME" taskkill.exe /IM "$EXE_NAME" /F >/dev/null for _ in $(seq 1 20); do is_running || break; sleep 0.5; done else log "not running" fi } cmd_status() { if is_running; then echo "running $URL (indexed: $(indexed_count))" else echo "stopped" fi } cmd_info() { cat < $URL Login: $E2E_USER / $E2E_PASS Manifest (expected content): $STATE/manifest.json Log: $LOG EOF } cmd_help() { cat < up build (if needed) + fixtures (if needed) + reset + start build cargo build into target/e2e and install the e2e exe copies fixtures [--force] [--import DIR] generate the pristine fixture library (--import copies a real folder in as media/imported/; the source is read-only) reset stop, wipe db/thumbs/hls, re-copy media, recreate user start | stop | restart | status token print a fresh JWT for the e2e user api METHOD PATH [JSON] authenticated request, e.g. api GET 'photos?path=&recursive=true' logs tail -f the server log info connection details Env: E2E_PORT (8090), E2E_BIND (127.0.0.1), E2E_USER, E2E_PASS. Extra server env: dev/e2e/env.local (appended to the generated .env on reset). EOF } case "${1:-help}" in up) shift; [ -x "$BIN_DIR/$EXE_NAME" ] || cmd_build; cmd_fixtures; cmd_reset; cmd_start ;; build) cmd_build ;; fixtures) shift; cmd_fixtures "$@" ;; reset) cmd_reset ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_stop; cmd_start ;; status) cmd_status ;; token) login ;; api) shift; api "$@" ;; logs) tail -f "$LOG" ;; info) cmd_info ;; help|-h|--help) cmd_help ;; *) die "unknown command: $1 (try help)" ;; esac