Add end-to-end smoke driving the real CLI against the containerized fake
This commit is contained in:
@@ -53,6 +53,20 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprint(stderr, usage)
|
fmt.Fprint(stderr, usage)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
// Hoist a leading global --config PATH onto the subcommand (the
|
||||||
|
// subcommand flag sets already accept it anywhere).
|
||||||
|
var hoisted []string
|
||||||
|
for len(args) >= 2 && (args[0] == "--config" || args[0] == "-config") {
|
||||||
|
hoisted = append(hoisted, args[0], args[1])
|
||||||
|
args = args[2:]
|
||||||
|
}
|
||||||
|
if len(hoisted) > 0 {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprint(stderr, usage)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
args = append(args, hoisted...)
|
||||||
|
}
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
fmt.Fprint(stdout, usage)
|
fmt.Fprint(stdout, usage)
|
||||||
|
|||||||
@@ -126,14 +126,32 @@ type Status struct {
|
|||||||
// New starts a fake on a random port with Redmine's default
|
// New starts a fake on a random port with Redmine's default
|
||||||
// enumerations and an empty MOPAC project.
|
// enumerations and an empty MOPAC project.
|
||||||
func New(apiKey string) *Server {
|
func New(apiKey string) *Server {
|
||||||
s := &Server{
|
s := newServer(apiKey)
|
||||||
APIKey: apiKey,
|
mux := http.NewServeMux()
|
||||||
apiKeySet: true,
|
mux.HandleFunc("/", s.handler)
|
||||||
issues: map[int]*Issue{},
|
s.srv = httptest.NewServer(mux)
|
||||||
versions: map[int]Version{},
|
s.URL = s.srv.URL
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe runs the fake on a fixed address (the smoke run boots it
|
||||||
|
// in a container). An empty apiKey disables auth checking.
|
||||||
|
func ListenAndServe(addr, apiKey string) error {
|
||||||
|
s := newServer(apiKey)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", s.handler)
|
||||||
|
return http.ListenAndServe(addr, mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(apiKey string) *Server {
|
||||||
|
return &Server{
|
||||||
|
APIKey: apiKey,
|
||||||
|
apiKeySet: true,
|
||||||
|
issues: map[int]*Issue{},
|
||||||
|
versions: map[int]Version{},
|
||||||
categories: map[int]Category{},
|
categories: map[int]Category{},
|
||||||
relations: map[int]Relation{},
|
relations: map[int]Relation{},
|
||||||
nextID: 100,
|
nextID: 100,
|
||||||
Trackers: []IDName{
|
Trackers: []IDName{
|
||||||
{1, "Bug"}, {2, "Feature"}, {3, "Support"}, {4, "Task"},
|
{1, "Bug"}, {2, "Feature"}, {3, "Support"}, {4, "Task"},
|
||||||
},
|
},
|
||||||
@@ -145,11 +163,6 @@ func New(apiKey string) *Server {
|
|||||||
{1, "Low"}, {2, "Normal"}, {3, "High"}, {4, "Urgent"}, {5, "Immediate"},
|
{1, "Low"}, {2, "Normal"}, {3, "High"}, {4, "Urgent"}, {5, "Immediate"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/", s.handler)
|
|
||||||
s.srv = httptest.NewServer(mux)
|
|
||||||
s.URL = s.srv.URL
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close shuts the fake down.
|
// Close shuts the fake down.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Command fakeredmine boots the in-memory fake Redmine on a fixed
|
||||||
|
// address for the smoke run (see smoke/smoke.sh). The real tracker is
|
||||||
|
// never contacted.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.knownelement.com/ukrrs/mopac-redmine-go/internal/fakeredmine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
addr := flag.String("addr", ":8601", "listen address")
|
||||||
|
flag.Parse()
|
||||||
|
key := os.Getenv("FAKE_KEY")
|
||||||
|
if key == "" {
|
||||||
|
key = "smoke-redmine-key-0123456789"
|
||||||
|
}
|
||||||
|
log.Printf("fakeredmine listening on %s", *addr)
|
||||||
|
log.Fatal(fakeredmine.ListenAndServe(*addr, key))
|
||||||
|
}
|
||||||
Executable
+149
@@ -0,0 +1,149 @@
|
|||||||
|
#!/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"
|
||||||
Reference in New Issue
Block a user