- Document every D-09 admin route for the OpenAPI contract. - Fail the gate on skipped tests, zero-test runs, and a missing admin path.
181 lines
5.0 KiB
Bash
Executable File
181 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Phase 9 fail-closed gate. Stages refuse skipped PostgreSQL tests, zero-test
|
|
# runs, and an OpenAPI document that does not list the admin surface.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
APP="$(cd "$ROOT/../fonoteka.go" && pwd)"
|
|
REVIEW="$ROOT/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-SECURITY-REVIEW.md"
|
|
VALIDATION="$ROOT/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-VALIDATION.md"
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage:
|
|
check-phase9.sh --self-test
|
|
check-phase9.sh --security
|
|
check-phase9.sh --postgres
|
|
check-phase9.sh --openapi
|
|
check-phase9.sh --evidence
|
|
check-phase9.sh --all
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
# phase9_detect reads go test -json. Exit 1 fail, 2 skip, 3 zero tests.
|
|
phase9_detect() {
|
|
python3 - "$1" <<'PY'
|
|
import json, sys
|
|
path = sys.argv[1]
|
|
saw = False
|
|
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
for raw in fh:
|
|
line = raw.strip()
|
|
if not line.startswith("{"):
|
|
continue
|
|
try:
|
|
ev = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
print("refuse: non-json test output", file=sys.stderr)
|
|
sys.exit(4)
|
|
action = ev.get("Action")
|
|
test = ev.get("Test") or ""
|
|
if action == "skip" and test:
|
|
print(f"refuse: skipped {test}", file=sys.stderr)
|
|
sys.exit(2)
|
|
if action == "fail":
|
|
name = test or ev.get("Package") or "unknown"
|
|
print(f"refuse: failed {name}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if action == "pass" and test:
|
|
saw = True
|
|
if not saw:
|
|
print("refuse: zero tests", file=sys.stderr)
|
|
sys.exit(3)
|
|
PY
|
|
}
|
|
|
|
phase9_go() {
|
|
local dir="$1"
|
|
shift
|
|
local log err
|
|
log="$(mktemp)"
|
|
err="$(mktemp)"
|
|
set +e
|
|
(cd "$dir" && go test -json -count=1 "$@") >"$log" 2>"$err"
|
|
local rc=$?
|
|
set -e
|
|
if [[ -s "$err" ]]; then
|
|
cat "$err" >&2
|
|
fi
|
|
local dc=0
|
|
phase9_detect "$log" || dc=$?
|
|
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
|
tail -n 30 "$log" >&2 || true
|
|
rm -f "$log" "$err"
|
|
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
|
exit 1
|
|
fi
|
|
rm -f "$log" "$err"
|
|
}
|
|
|
|
expect_detect_fails() {
|
|
local name="$1"
|
|
local payload="$2"
|
|
local log
|
|
log="$(mktemp)"
|
|
printf '%s\n' "$payload" >"$log"
|
|
local dc=0
|
|
phase9_detect "$log" || dc=$?
|
|
rm -f "$log"
|
|
if [[ "$dc" -eq 0 ]]; then
|
|
echo "refuse: self-test $name accepted a bad run" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_self_test() {
|
|
bash -n "${BASH_SOURCE[0]}"
|
|
local log
|
|
log="$(mktemp)"
|
|
printf '%s\n' '{"Action":"pass","Test":"TestPhase09GuardIsolation"}' >"$log"
|
|
phase9_detect "$log"
|
|
rm -f "$log"
|
|
expect_detect_fails skip '{"Action":"skip","Test":"TestPhase09MigrationsFreshRollback"}'
|
|
expect_detect_fails zero '{"Action":"pass","Package":"git.golem15.com/golem15/summercms/lagoon"}'
|
|
expect_detect_fails fail '{"Action":"fail","Test":"TestPhase09ContractInventory"}'
|
|
for flag in --self-test --security --postgres --openapi --evidence --all; do
|
|
grep -q -- "$flag" "${BASH_SOURCE[0]}" || {
|
|
echo "refuse: missing mode $flag" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
echo "phase9 self-test passed"
|
|
}
|
|
|
|
run_security() {
|
|
phase9_go "$ROOT" ./bouncer ./cabana -run '^TestPhase09'
|
|
phase9_go "$APP" ./plugins/golem15/fonoteka -run '^TestPhase09Security'
|
|
echo "phase9 security passed"
|
|
}
|
|
|
|
run_postgres() {
|
|
phase9_go "$ROOT" ./lagoon -run '^TestPhase09MigrationsFreshRollback$'
|
|
phase9_go "$APP" ./plugins/golem15/fonoteka -run '^TestPhase09AssembledAcceptance$'
|
|
echo "phase9 postgres passed"
|
|
}
|
|
|
|
run_openapi() {
|
|
(cd "$APP" && bash scripts/check-openapi.sh)
|
|
phase9_go "$ROOT" ./cabana -run '^TestPhase09ContractInventory$'
|
|
echo "phase9 openapi passed"
|
|
}
|
|
|
|
run_evidence() {
|
|
[[ -f "$REVIEW" && -f "$VALIDATION" ]] || {
|
|
echo "refuse: security review or validation file is missing" >&2
|
|
exit 1
|
|
}
|
|
python3 - "$REVIEW" "$VALIDATION" <<'PY'
|
|
import pathlib, sys
|
|
review = pathlib.Path(sys.argv[1]).read_text()
|
|
validation = pathlib.Path(sys.argv[2]).read_text()
|
|
required = [f"T-09-{i:02d}" for i in range(1, 22)] + ["T-09-SC"]
|
|
missing = [item for item in required if item not in review]
|
|
if missing:
|
|
print("refuse: review missing " + ", ".join(missing), file=sys.stderr)
|
|
sys.exit(1)
|
|
if "nyquist_compliant: true" not in validation:
|
|
print("refuse: validation is not nyquist_compliant", file=sys.stderr)
|
|
sys.exit(1)
|
|
for line in validation.splitlines():
|
|
if line.startswith("|") and "pending" in line:
|
|
print("refuse: validation row still pending: " + line, file=sys.stderr)
|
|
sys.exit(1)
|
|
for req in ("AUTH-08", "ADMIN-01", "ADMIN-02", "ADMIN-03", "ADMIN-04", "ADMIN-05"):
|
|
if req not in validation:
|
|
print(f"refuse: validation missing {req}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print("phase9 evidence files passed")
|
|
PY
|
|
run_security
|
|
run_postgres
|
|
run_openapi
|
|
echo "phase9 evidence passed"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
--self-test) run_self_test ;;
|
|
--security) run_security ;;
|
|
--postgres) run_postgres ;;
|
|
--openapi) run_openapi ;;
|
|
--evidence) run_evidence ;;
|
|
--all)
|
|
run_self_test
|
|
run_security
|
|
run_postgres
|
|
run_openapi
|
|
run_evidence
|
|
;;
|
|
*) usage ;;
|
|
esac
|