#!/bin/sh # End-to-end smoke for mred: builds the CLI in the Docker builder, boots # the FAKE Redmine in a container on 127.0.0.1:8601, drives the real # binary from the host through a 0600 env file, and asserts the full # command surface (issue create/list/show/update with a journal note, # versions, categories, relations, -o json, exit codes) plus key # redaction. No real tracker is ever contacted. Only the exact container # ID spawned here is removed. set -e cd "$(dirname "$0")/.." IMAGE="golang@sha256:e8c859f5632dcfde7b32d2012b4351728f6437930887c2f6a91ea242459e5514" PORT=8601 KEY="smoke-redmine-key-0123456789" CID="" cleanup() { if [ -n "$CID" ]; then docker rm -f "$CID" >/dev/null 2>&1 || true fi rm -rf .smoke } trap cleanup EXIT INT TERM mkdir -p .smoke umask 077 echo "--- build CLI (docker builder)" docker run --rm -v "$PWD:/h" -w /h \ -u "$(id -u):$(id -g)" -e HOME=/tmp -e GOFLAGS=-buildvcs=false \ "$IMAGE" go build -o bin/mred ./cmd/mred echo "--- boot fake Redmine (container, port $PORT)" CID=$(docker run -d --rm \ -v "$PWD:/h" -w /h \ -u "$(id -u):$(id -g)" -e HOME=/tmp -e GOFLAGS=-buildvcs=false \ -p 127.0.0.1:$PORT:8601 \ -e FAKE_KEY="$KEY" \ "$IMAGE" go run ./smoke/fakeredmine -addr :8601) # wait for the fake to answer (any HTTP response, even 401, proves it is up) i=0 until [ -n "$CID" ] && [ "$(docker inspect -f '{{.State.Running}}' "$CID" 2>/dev/null)" = "true" ] && \ python3 -c " import urllib.request, urllib.error req = urllib.request.Request('http://127.0.0.1:$PORT/trackers.json', headers={'X-Redmine-API-Key': 'probe'}) try: urllib.request.urlopen(req, timeout=2) except urllib.error.HTTPError: raise SystemExit(0) except Exception: raise SystemExit(1) raise SystemExit(0) "; do i=$((i+1)) if [ "$i" -ge 60 ]; then echo "smoke: fake Redmine did not come up; logs:" >&2 docker logs "$CID" >&2 || true exit 1 fi sleep 1 done printf 'MRED_URL=http://127.0.0.1:%s\nMRED_KEY=%s\n' "$PORT" "$KEY" > .smoke/env chmod 600 .smoke/env mred() { ./bin/mred --config .smoke/env "$@"; } echo "--- version create + list" mred version create -p MOPAC -n Beta --due 2026-08-31 --status open > .smoke/version.out grep -q "created version #.* Beta (open)" .smoke/version.out mred version list -p MOPAC > .smoke/versions.out grep -q "Beta" .smoke/versions.out echo "--- category create + list" mred category create -p MOPAC -n "Quota & Backpressure" > .smoke/cat.out grep -q "created category #" .smoke/cat.out mred category list -p MOPAC > .smoke/cats.out grep -q "Quota & Backpressure" .smoke/cats.out echo "--- issue create (full flags, json out)" mred issue create -p MOPAC -s "Smoke: quota accounting" \ --tracker feature --priority immediate \ --category "quota & backpressure" --version beta \ --due 2026-08-31 --est 8 -o json \ --desc - <<'EOF' > .smoke/create.out ## Scope - smoke body EOF grep -q '"id":' .smoke/create.out ID=$(python3 -c 'import json; print(json.load(open(".smoke/create.out"))["issue"]["id"])') mred issue create -p MOPAC -s "Smoke: dispatcher piece" --tracker task > /dev/null ID2=$(mred issue list -p MOPAC --status all -o json | python3 -c 'import json,sys issues = json.load(sys.stdin)["issues"] print([i["id"] for i in issues if i["subject"] == "Smoke: dispatcher piece"][0])') echo "--- issue list + filters" mred issue list -p MOPAC > .smoke/list.out grep -q "Smoke: quota accounting" .smoke/list.out grep -q "@Beta" .smoke/list.out mred issue list -p MOPAC --version Beta --limit 5 > .smoke/listver.out grep -q "Smoke: quota accounting" .smoke/listver.out echo "--- issue update (status + note) then show with journals" mred issue update "$ID" --status done --done-ratio 100 --note "REPORT delivered: smoke" > .smoke/upd.out grep -q "updated issue #$ID" .smoke/upd.out mred issue show "$ID" --with journals > .smoke/show.out grep -q "REPORT delivered: smoke" .smoke/show.out grep -q "Done" .smoke/show.out grep -q "smoke body" .smoke/show.out echo "--- relation create" mred relation create "$ID" "$ID2" --type blocks > .smoke/rel.out grep -q "blocks" .smoke/rel.out echo "--- failure path: missing issue (exit 2, one-line stderr with http code)" if mred issue show 999 > .smoke/missing.out 2> .smoke/missing.err; then echo "smoke: missing issue should fail" >&2; exit 1 fi grep -q "http 404" .smoke/missing.err [ "$(wc -l < .smoke/missing.err)" -eq 1 ] || { echo "smoke: stderr not one line" >&2; exit 1; } [ ! -s .smoke/missing.out ] || { echo "smoke: stdout not empty on failure" >&2; exit 1; } echo "--- failure path: wrong key (exit 2, no key material in stderr)" printf 'MRED_URL=http://127.0.0.1:%s\nMRED_KEY=wrong-key-abcdef\n' "$PORT" > .smoke/bad.env chmod 600 .smoke/bad.env if ./bin/mred --config .smoke/bad.env issue list -p MOPAC 2> .smoke/bad.err; then echo "smoke: wrong key should fail" >&2; exit 1 fi grep -q "http 401" .smoke/bad.err echo "--- failure path: loose env file (exit 1)" printf 'MRED_URL=http://127.0.0.1:%s\nMRED_KEY=%s\n' "$PORT" "$KEY" > .smoke/loose.env chmod 644 .smoke/loose.env if ./bin/mred --config .smoke/loose.env issue list -p MOPAC 2> .smoke/loose.err; then echo "smoke: loose env file should be refused" >&2; exit 1 fi grep -q "insecure mode" .smoke/loose.err echo "--- redaction: no key material in any captured output" for f in .smoke/*.out .smoke/*.err; do grep -qF "$KEY" "$f" && { echo "smoke: API key leaked into $f" >&2; exit 1; } done grep -qF "wrong-key-abcdef" .smoke/bad.err && { echo "smoke: wrong key echoed" >&2; exit 1; } echo "smoke: OK"