feat(09-12): add the phase 9 acceptance gate
- Document every D-09 admin route for the OpenAPI contract. - Fail the gate on skipped tests, zero-test runs, and a missing admin path.
This commit is contained in:
379
cabana/admin_openapi.go
Normal file
379
cabana/admin_openapi.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package cabana
|
||||
|
||||
// Admin API annotations. swag reads these with the handler package so
|
||||
// docs/openapi.json lists every D-09 route. The functions are not mounted;
|
||||
// service.mount in http.go is the runtime route table, and
|
||||
// TestPhase09PermissionMatrix fails if the two lists diverge.
|
||||
|
||||
// ErrorBody is one D-10 error object.
|
||||
type ErrorBody struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]any `json:"details"`
|
||||
}
|
||||
|
||||
// ErrorEnvelope is the D-10 error envelope.
|
||||
type ErrorEnvelope struct {
|
||||
Error ErrorBody `json:"error"`
|
||||
}
|
||||
|
||||
// SuccessMeta is the D-10 meta object.
|
||||
type SuccessMeta struct {
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
LastPage int `json:"last_page,omitempty"`
|
||||
}
|
||||
|
||||
// SuccessEnvelope is the D-10 success envelope.
|
||||
type SuccessEnvelope struct {
|
||||
Data any `json:"data"`
|
||||
Meta SuccessMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// AdminLoginData is the admin login payload.
|
||||
type AdminLoginData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// AdminLoginEnvelope is the admin login success body.
|
||||
type AdminLoginEnvelope struct {
|
||||
Data AdminLoginData `json:"data"`
|
||||
Meta SuccessMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// AdminLogin documents POST /_admin/api/v1/auth/login.
|
||||
//
|
||||
// @Summary Admin login
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} AdminLoginEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/auth/login [post]
|
||||
func AdminLogin() {}
|
||||
|
||||
// AdminRefresh documents POST /_admin/api/v1/auth/refresh.
|
||||
//
|
||||
// @Summary Refresh an admin token
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} AdminLoginEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/auth/refresh [post]
|
||||
func AdminRefresh() {}
|
||||
|
||||
// AdminLogout documents POST /_admin/api/v1/auth/logout.
|
||||
//
|
||||
// @Summary Admin logout
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/auth/logout [post]
|
||||
func AdminLogout() {}
|
||||
|
||||
// AdminMe documents GET /_admin/api/v1/auth/me.
|
||||
//
|
||||
// @Summary Current admin
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/auth/me [get]
|
||||
func AdminMe() {}
|
||||
|
||||
// AdminNavigation documents GET /_admin/api/v1/navigation.
|
||||
//
|
||||
// @Summary Admin navigation
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/navigation [get]
|
||||
func AdminNavigation() {}
|
||||
|
||||
// AdminSettingsList documents GET /_admin/api/v1/settings.
|
||||
//
|
||||
// @Summary List admin settings
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/settings [get]
|
||||
func AdminSettingsList() {}
|
||||
|
||||
// AdminSettingsSchema documents GET /_admin/api/v1/settings/{code}/schema.
|
||||
//
|
||||
// @Summary Admin settings schema
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param code path string true "Settings code"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/settings/{code}/schema [get]
|
||||
func AdminSettingsSchema() {}
|
||||
|
||||
// AdminSettingsGet documents GET /_admin/api/v1/settings/{code}.
|
||||
//
|
||||
// @Summary Read admin settings
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param code path string true "Settings code"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/settings/{code} [get]
|
||||
func AdminSettingsGet() {}
|
||||
|
||||
// AdminSettingsPut documents PUT /_admin/api/v1/settings/{code}.
|
||||
//
|
||||
// @Summary Update admin settings
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param code path string true "Settings code"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/settings/{code} [put]
|
||||
func AdminSettingsPut() {}
|
||||
|
||||
// AdminListSchema documents the list schema route.
|
||||
//
|
||||
// @Summary Admin list schema
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/list [get]
|
||||
func AdminListSchema() {}
|
||||
|
||||
// AdminFormSchema documents the form schema route.
|
||||
//
|
||||
// @Summary Admin form schema
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/form [get]
|
||||
func AdminFormSchema() {}
|
||||
|
||||
// AdminRelationSchema documents the relation schema route.
|
||||
//
|
||||
// @Summary Admin relation schema
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param name path string true "Relation name"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/schema/relation/{name} [get]
|
||||
func AdminRelationSchema() {}
|
||||
|
||||
// AdminList documents the record list route.
|
||||
//
|
||||
// @Summary List admin records
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller} [get]
|
||||
func AdminList() {}
|
||||
|
||||
// AdminCreate documents the record create route.
|
||||
//
|
||||
// @Summary Create an admin record
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller} [post]
|
||||
func AdminCreate() {}
|
||||
|
||||
// AdminBulkDelete documents the bulk delete route.
|
||||
//
|
||||
// @Summary Bulk-delete admin records
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/bulk-delete [post]
|
||||
func AdminBulkDelete() {}
|
||||
|
||||
// AdminShow documents the record show route.
|
||||
//
|
||||
// @Summary Show an admin record
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Record id"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id} [get]
|
||||
func AdminShow() {}
|
||||
|
||||
// AdminUpdate documents the record update route.
|
||||
//
|
||||
// @Summary Update an admin record
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Record id"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id} [put]
|
||||
func AdminUpdate() {}
|
||||
|
||||
// AdminDelete documents the record delete route.
|
||||
//
|
||||
// @Summary Delete an admin record
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Record id"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 404 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id} [delete]
|
||||
func AdminDelete() {}
|
||||
|
||||
// AdminRelationLinked documents the linked-relation route.
|
||||
//
|
||||
// @Summary List linked relation records
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Owner id"
|
||||
// @Param name path string true "Relation name"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name} [get]
|
||||
func AdminRelationLinked() {}
|
||||
|
||||
// AdminRelationCandidates documents the relation candidate route.
|
||||
//
|
||||
// @Summary List relation candidates
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Owner id"
|
||||
// @Param name path string true "Relation name"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/candidates [get]
|
||||
func AdminRelationCandidates() {}
|
||||
|
||||
// AdminRelationLink documents the relation link route.
|
||||
//
|
||||
// @Summary Link relation records
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Owner id"
|
||||
// @Param name path string true "Relation name"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/link [post]
|
||||
func AdminRelationLink() {}
|
||||
|
||||
// AdminRelationUnlink documents the relation unlink route.
|
||||
//
|
||||
// @Summary Unlink relation records
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BackendBearer
|
||||
// @Param vendor path string true "Vendor"
|
||||
// @Param plugin path string true "Plugin"
|
||||
// @Param controller path string true "Controller"
|
||||
// @Param id path integer true "Owner id"
|
||||
// @Param name path string true "Relation name"
|
||||
// @Success 200 {object} SuccessEnvelope
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 403 {object} ErrorEnvelope
|
||||
// @Failure 422 {object} ErrorEnvelope
|
||||
// @Router /_admin/api/v1/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink [post]
|
||||
func AdminRelationUnlink() {}
|
||||
91
cabana/phase09_contract_test.go
Normal file
91
cabana/phase09_contract_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPhase09ContractInventory fails when the committed OpenAPI document
|
||||
// drops a D-09 route or a protected route's 401 response.
|
||||
func TestPhase09ContractInventory(t *testing.T) {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("caller")
|
||||
}
|
||||
specPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "fonoteka.go", "docs", "openapi.json"))
|
||||
raw, err := os.ReadFile(specPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", specPath, err)
|
||||
}
|
||||
var spec struct {
|
||||
Paths map[string]map[string]struct {
|
||||
Responses map[string]json.RawMessage `json:"responses"`
|
||||
Security []map[string]json.RawMessage `json:"security"`
|
||||
} `json:"paths"`
|
||||
Components struct {
|
||||
SecuritySchemes map[string]json.RawMessage `json:"securitySchemes"`
|
||||
} `json:"components"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := spec.Components.SecuritySchemes["BackendBearer"]; !ok {
|
||||
t.Fatal("openapi is missing the BackendBearer scheme")
|
||||
}
|
||||
public := map[string]bool{
|
||||
"POST /_admin/api/v1/auth/login": true,
|
||||
"POST /_admin/api/v1/auth/refresh": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, route := range phase09Routes {
|
||||
method, path, ok := splitRoute(route.key)
|
||||
if !ok {
|
||||
t.Fatalf("bad route key %s", route.key)
|
||||
}
|
||||
method = strings.ToLower(method)
|
||||
ops, ok := spec.Paths[path]
|
||||
if !ok {
|
||||
t.Fatalf("openapi missing %s", path)
|
||||
}
|
||||
op, ok := ops[method]
|
||||
if !ok {
|
||||
t.Fatalf("openapi missing %s %s", method, path)
|
||||
}
|
||||
key := route.key
|
||||
if seen[key] {
|
||||
t.Fatalf("duplicate contract route %s", key)
|
||||
}
|
||||
seen[key] = true
|
||||
if _, ok := op.Responses["200"]; !ok {
|
||||
t.Fatalf("%s has no 200 response", key)
|
||||
}
|
||||
if public[key] {
|
||||
if _, ok := op.Responses["401"]; !ok {
|
||||
t.Fatalf("%s has no 401 response", key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(op.Security) == 0 {
|
||||
t.Fatalf("%s has no BackendBearer security requirement", key)
|
||||
}
|
||||
if _, ok := op.Responses["401"]; !ok {
|
||||
t.Fatalf("%s has no 401 response", key)
|
||||
}
|
||||
}
|
||||
if len(seen) != len(phase09Routes) {
|
||||
t.Fatalf("contract routes=%d want %d", len(seen), len(phase09Routes))
|
||||
}
|
||||
}
|
||||
|
||||
func splitRoute(key string) (method, path string, ok bool) {
|
||||
for i := 0; i < len(key); i++ {
|
||||
if key[i] == ' ' {
|
||||
return key[:i], key[i+1:], key[i+1:] != ""
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
180
scripts/check-phase9.sh
Executable file
180
scripts/check-phase9.sh
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user