diff --git a/env.example b/env.example new file mode 100644 index 0000000..27be9a7 --- /dev/null +++ b/env.example @@ -0,0 +1,5 @@ +# mgit connection env file (copy to ~/.config/mgit/env and chmod 600). +# Only GITEA_URL and GITEA_KEY are used; both are required. The file must +# be 0600 or stricter — mgit refuses looser files before reading them. +GITEA_URL=https://git.knownelement.com +GITEA_KEY=your-gitea-api-token diff --git a/internal/fakegitea/fakegitea.go b/internal/fakegitea/fakegitea.go index d305582..2bff314 100644 --- a/internal/fakegitea/fakegitea.go +++ b/internal/fakegitea/fakegitea.go @@ -133,20 +133,29 @@ type FailSpec struct { // New starts a fake on a random port authenticating as login. func New(token, login string) *Server { s := newServer(token, login) - mux := http.NewServeMux() - mux.HandleFunc("/", s.handler) - s.srv = httptest.NewServer(mux) + s.srv = httptest.NewServer(s.Handler()) s.URL = s.srv.URL return s } +// NewState builds an unstarted fake: seed it with the Add* methods, then +// serve Handler() however the process needs (the smoke run does). +func NewState(token, login string) *Server { + return newServer(token, login) +} + +// Handler returns the fake's HTTP handler. +func (s *Server) Handler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/", s.handler) + return mux +} + // ListenAndServe runs the fake on a fixed address (the smoke run boots it // in a container). An empty token disables auth checking. func ListenAndServe(addr, token string) error { s := newServer(token, "ukrrs") - mux := http.NewServeMux() - mux.HandleFunc("/", s.handler) - return http.ListenAndServe(addr, mux) + return http.ListenAndServe(addr, s.Handler()) } func newServer(token, login string) *Server { diff --git a/smoke/fakegitea/main.go b/smoke/fakegitea/main.go new file mode 100644 index 0000000..d30429c --- /dev/null +++ b/smoke/fakegitea/main.go @@ -0,0 +1,26 @@ +// Command fakegitea boots the in-memory fake Gitea on a fixed address +// for the smoke run (see smoke/smoke.sh), seeded with the MOPAC repo the +// script drives. The real forge is never contacted. +package main + +import ( + "flag" + "log" + "net/http" + "os" + + "git.knownelement.com/ukrrs/mopac-gitea-go/internal/fakegitea" +) + +func main() { + addr := flag.String("addr", ":8602", "listen address") + flag.Parse() + key := os.Getenv("FAKE_KEY") + if key == "" { + key = "smoke-gitea-token-0123456789" + } + s := fakegitea.NewState(key, "ukrrs") + s.AddRepo("ukrrs", "MOPAC", "main", "main", "feat/quota") + log.Printf("fakegitea listening on %s", *addr) + log.Fatal(http.ListenAndServe(*addr, s.Handler())) +} diff --git a/smoke/smoke.sh b/smoke/smoke.sh new file mode 100755 index 0000000..e06fd9d --- /dev/null +++ b/smoke/smoke.sh @@ -0,0 +1,151 @@ +#!/bin/sh +# End-to-end smoke for mgit: builds the CLI in the Docker builder, boots +# the FAKE Gitea in a container on 127.0.0.1:8602, drives the real +# binary from the host through a 0600 env file, and asserts the full +# command surface (repo create/list/show, branch list, commit status, +# PR create/list/show/merge, -o json, exit codes) plus token redaction. +# No real forge is ever contacted. Only the exact container ID spawned +# here is removed. +set -e + +cd "$(dirname "$0")/.." + +IMAGE="golang@sha256:e8c859f5632dcfde7b32d2012b4351728f6437930887c2f6a91ea242459e5514" +PORT=8602 +KEY="smoke-gitea-token-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 + fake (docker builder)" +docker run --rm -v "$PWD:/h" -w /h \ + -u "$(id -u):$(id -g)" -e HOME=/tmp -e GOFLAGS=-buildvcs=false \ + "$IMAGE" sh -c 'go build -o bin/mgit ./cmd/mgit && go build -o bin/fakegitea ./smoke/fakegitea' + +echo "--- boot fake Gitea (container, port $PORT)" +CID=$(docker run -d --rm \ + -v "$PWD:/h" -w /h \ + -u "$(id -u):$(id -g)" -e HOME=/tmp \ + -p 127.0.0.1:$PORT:8602 \ + -e FAKE_KEY="$KEY" \ + "$IMAGE" /h/bin/fakegitea -addr :8602) + +# 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/api/v1/user/repos', + headers={'Authorization': 'token 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 90 ]; then + echo "smoke: fake Gitea did not come up; logs:" >&2 + docker logs "$CID" >&2 || true + exit 1 + fi + sleep 1 +done + +printf 'GITEA_URL=http://127.0.0.1:%s\nGITEA_KEY=%s\n' "$PORT" "$KEY" > .smoke/env +chmod 600 .smoke/env + +mgit() { ./bin/mgit --config .smoke/env "$@"; } + +echo "--- repo create + list + show" +mgit repo create -n vertical-x --desc "one vertical" --private --auto-init > .smoke/repo.out +grep -q "created repo ukrrs/vertical-x (private)" .smoke/repo.out +mgit repo list > .smoke/repos.out +grep -q "ukrrs/MOPAC" .smoke/repos.out +grep -q "ukrrs/vertical-x" .smoke/repos.out +mgit repo list -o json | python3 -c 'import json,sys +repos = json.load(sys.stdin) +assert any(r["full_name"] == "ukrrs/vertical-x" for r in repos), repos' +mgit repo show ukrrs/MOPAC > .smoke/reposhow.out +grep -q "default branch: main" .smoke/reposhow.out + +echo "--- branch list" +mgit branch list ukrrs/MOPAC > .smoke/branches.out +grep -q "main" .smoke/branches.out +grep -q "feat/quota" .smoke/branches.out + +echo "--- commit status (report + combined)" +mgit status create ukrrs/MOPAC feat/quota --state pending --context ci/lint \ + --description "lint queued" > .smoke/st1.out +grep -q "pending" .smoke/st1.out +mgit status create ukrrs/MOPAC feat/quota --state success --context ci/lint \ + --description "lint clean" > .smoke/st2.out +grep -q "success" .smoke/st2.out +mgit status list ukrrs/MOPAC feat/quota > .smoke/statuses.out +grep -q "combined success (1 checks)" .smoke/statuses.out +grep -q "ci/lint" .smoke/statuses.out + +echo "--- pr create (body from file) + list + show + merge" +cat > .smoke/body.md <<'EOF' +## Scope +- smoke body +EOF +mgit pr create ukrrs/MOPAC --title "Smoke: quota accounting" \ + --body .smoke/body.md --base main --head feat/quota -o json > .smoke/pr.out +grep -q '"number":' .smoke/pr.out +mgit pr list ukrrs/MOPAC > .smoke/prs.out +grep -q "Smoke: quota accounting" .smoke/prs.out +grep -q "main..feat/quota" .smoke/prs.out +mgit pr show ukrrs/MOPAC 1 > .smoke/prshow.out +grep -q "smoke body" .smoke/prshow.out +mgit pr merge ukrrs/MOPAC 1 --do squash > .smoke/merge.out +grep -q "merged pull request #1 (squash)" .smoke/merge.out +mgit pr list ukrrs/MOPAC --state closed > .smoke/prsclosed.out +grep -q "Smoke: quota accounting" .smoke/prsclosed.out +mgit pr list ukrrs/MOPAC > .smoke/prsopen.out +if grep -q "Smoke" .smoke/prsopen.out; then + echo "smoke: merged PR still listed as open" >&2; exit 1 +fi + +echo "--- failure path: missing repo (exit 2, one-line stderr with http code)" +if mgit repo show ukrrs/absent > .smoke/missing.out 2> .smoke/missing.err; then + echo "smoke: missing repo 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 token (exit 2, no token material in stderr)" +printf 'GITEA_URL=http://127.0.0.1:%s\nGITEA_KEY=wrong-token-abcdef\n' "$PORT" > .smoke/bad.env +chmod 600 .smoke/bad.env +if ./bin/mgit --config .smoke/bad.env repo list 2> .smoke/bad.err; then + echo "smoke: wrong token should fail" >&2; exit 1 +fi +grep -q "http 401" .smoke/bad.err + +echo "--- failure path: loose env file (exit 1)" +printf 'GITEA_URL=http://127.0.0.1:%s\nGITEA_KEY=%s\n' "$PORT" "$KEY" > .smoke/loose.env +chmod 644 .smoke/loose.env +if ./bin/mgit --config .smoke/loose.env repo list 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 token material in any captured output" +for f in .smoke/*.out .smoke/*.err; do + grep -qF "$KEY" "$f" && { echo "smoke: token leaked into $f" >&2; exit 1; } +done +grep -qF "wrong-token-abcdef" .smoke/bad.err && { echo "smoke: wrong token echoed" >&2; exit 1; } + +echo "smoke: OK"