test(12.2-05): SPA unit tests for the phase and the check-phase12.2 gate
- sessionKey, dateFormat, FileuploadField (protected thumbnails and keyboard reorder backstops), RelationChildModal, RelationPivotModal, RelationManager row actions and create-screen deferral, date and time list cells; typed deferred relation-schema and file-list fixtures - scripts/check-phase12.2.sh: go, security (named tests, refuses missing or skipped), spa, openapi, dist, docs, hygiene and app stages, a detector self-test, one PASS or FAIL line per stage under --all
This commit is contained in:
333
scripts/check-phase12.2.sh
Executable file
333
scripts/check-phase12.2.sh
Executable file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase 12.2 fail-closed gate (admin datepicker and fileupload fields,
|
||||
# relation child CRUD, deferred binding).
|
||||
#
|
||||
# Every stage exits non-zero on a failing command, a go test run that fails,
|
||||
# skips, matches zero tests or does not build, a named security test that is
|
||||
# missing, renamed or skipped, OpenAPI or dist drift, a docs checker problem
|
||||
# or a hygiene violation. --self-test proves the go test detector fails
|
||||
# closed on planted inputs. --all runs every stage, prints one PASS or FAIL
|
||||
# line per stage and stops at the first failure.
|
||||
#
|
||||
# Framework commands run in summercms.go; the application stage runs in the
|
||||
# sibling repository named by PHASE122_APP (default ../fonoteka.go).
|
||||
# Run with FORCE_COLOR unset: bonfire's colour tests read it.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
APP="${PHASE122_APP:-$ROOT/../fonoteka.go}"
|
||||
|
||||
# The named tests of the security stage, by prefix. Each prefix must match
|
||||
# at least one top-level test that passes; any skip refuses.
|
||||
SECURITY_CABANA=(TestRelationChildScope TestProtectedFile TestDeferredCommit TestFileupload TestRelationChild)
|
||||
SECURITY_LAGOON=(TestPurgeDeferred TestDeferredStore TestDeferredConcurrentFirstBind)
|
||||
SECURITY_ATTACH=(TestIsAllowedImage TestStore)
|
||||
|
||||
# Files this phase added; the hygiene stage refuses a consuming-application
|
||||
# name in them and in the framework docs.
|
||||
PHASE_FILES=(
|
||||
modules/cabana/testdata/deferred
|
||||
modules/cabana/phase122_fixture_test.go
|
||||
modules/cabana/relation_child_scope_test.go
|
||||
modules/cabana/protected_file_test.go
|
||||
modules/cabana/relation_child_test.go
|
||||
modules/cabana/fileupload_test.go
|
||||
modules/cabana/deferred_commit_test.go
|
||||
modules/cabana/datepicker_test.go
|
||||
modules/cabana/field_file.go
|
||||
modules/cabana/field_date.go
|
||||
modules/cabana/deferred.go
|
||||
modules/cabana/relation_child.go
|
||||
modules/cabana/relation_form.go
|
||||
modules/lagoon/date.go
|
||||
modules/lagoon/deferred.go
|
||||
modules/lagoon/purge.go
|
||||
modules/lagoon/schedule.go
|
||||
modules/lagoon/purge_test.go
|
||||
modules/lagoon/attach/store.go
|
||||
modules/lagoon/attach/guard.go
|
||||
modules/lagoon/attach/guard_test.go
|
||||
admin/src/app/sessionKey.ts
|
||||
admin/src/app/dateFormat.ts
|
||||
admin/src/api/files.ts
|
||||
admin/tests/app/sessionKey.test.ts
|
||||
admin/tests/app/dateFormat.test.ts
|
||||
admin/tests/form/DatepickerField.test.ts
|
||||
admin/tests/form/FileuploadField.test.ts
|
||||
admin/tests/relation/RelationChildModal.test.ts
|
||||
admin/tests/relation/RelationPivotModal.test.ts
|
||||
admin/tests/fixtures/deferred.files.json
|
||||
admin/tests/fixtures/deferred.relation-schema.json
|
||||
)
|
||||
HYGIENE_DOCS=(modules/cabana/README.md modules/lagoon/README.md modules/conga/README.md modules/pact/README.md docs admin/src)
|
||||
APP_NAMES='fonoteka|p[lł]ytarium'
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage:
|
||||
check-phase12.2.sh --self-test
|
||||
check-phase12.2.sh --go
|
||||
check-phase12.2.sh --security
|
||||
check-phase12.2.sh --spa
|
||||
check-phase12.2.sh --openapi
|
||||
check-phase12.2.sh --dist
|
||||
check-phase12.2.sh --docs
|
||||
check-phase12.2.sh --hygiene
|
||||
check-phase12.2.sh --app
|
||||
check-phase12.2.sh --all (default)
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
# detect reads go test -json. Exit 1 fail or build failure, 2 skip, 3 zero
|
||||
# tests, 4 non-JSON, 5 a required prefix has no passing top-level test.
|
||||
# REQUIRE_PREFIXES lists the prefixes.
|
||||
detect() {
|
||||
python3 - "$1" <<'PY'
|
||||
import json, os, sys
|
||||
path = sys.argv[1]
|
||||
prefixes = os.environ.get("REQUIRE_PREFIXES", "").split()
|
||||
passed = set()
|
||||
failed = []
|
||||
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 ""
|
||||
pkg = ev.get("Package") or ev.get("ImportPath") or ""
|
||||
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
|
||||
print(f"refuse: build failed {pkg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if action == "output" and "no tests to run" in (ev.get("Output") or ""):
|
||||
print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
if action == "skip" and test:
|
||||
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if action == "fail":
|
||||
failed.append(f"{pkg} {test}".strip())
|
||||
if action == "pass" and test:
|
||||
passed.add(test)
|
||||
if failed:
|
||||
print("refuse: failed " + ", ".join(failed), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not passed:
|
||||
print("refuse: zero tests", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
top = {name for name in passed if "/" not in name}
|
||||
missing = [p for p in prefixes if not any(name.startswith(p) for name in top)]
|
||||
if missing:
|
||||
print("refuse: missing named test: no passing test for " + ", ".join(missing), file=sys.stderr)
|
||||
sys.exit(5)
|
||||
PY
|
||||
}
|
||||
|
||||
# go_json DIR [go test args...] runs go test -json -count=1 through detect.
|
||||
go_json() {
|
||||
local dir="$1"
|
||||
shift
|
||||
local log err rc=0 dc=0
|
||||
log="$(mktemp)"
|
||||
err="$(mktemp)"
|
||||
(cd "$dir" && go test -json -count=1 "$@") >"$log" 2>"$err" || rc=$?
|
||||
detect "$log" || dc=$?
|
||||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||||
cat "$err" >&2 || true
|
||||
grep -v '^{' "$log" | tail -n 20 >&2 || true
|
||||
rm -f "$log" "$err"
|
||||
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
||||
return 1
|
||||
fi
|
||||
rm -f "$log" "$err"
|
||||
}
|
||||
|
||||
# named DIR PKG PREFIX... runs the tests matching the prefixes verbosely and
|
||||
# requires a passing top-level test for each one.
|
||||
named() {
|
||||
local dir="$1" pkg="$2"
|
||||
shift 2
|
||||
local regex
|
||||
regex="^($(IFS='|'; echo "$*"))"
|
||||
REQUIRE_PREFIXES="$*" go_json "$dir" "$pkg" -v -run "$regex"
|
||||
}
|
||||
|
||||
expect_detect() {
|
||||
local name="$1" want="$2" payload="$3" log dc=0
|
||||
log="$(mktemp)"
|
||||
printf '%s\n' "$payload" >"$log"
|
||||
detect "$log" 2>/dev/null || dc=$?
|
||||
rm -f "$log"
|
||||
if [[ "$dc" -ne "$want" ]]; then
|
||||
echo "refuse: self-test $name: detector exit $dc, want $want" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_self_test() {
|
||||
bash -n "${BASH_SOURCE[0]}"
|
||||
expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestRelationChildScope"}'
|
||||
expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||
{"Action":"fail","Package":"p","Test":"TestProtectedFileScope"}'
|
||||
expect_detect package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||||
{"Action":"fail","Package":"p"}'
|
||||
expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}'
|
||||
expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestRelationChildScope"}'
|
||||
expect_detect zero 3 '{"Action":"pass","Package":"p"}'
|
||||
expect_detect no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"}
|
||||
{"Action":"pass","Package":"p"}'
|
||||
expect_detect nonjson 4 '{"Action":"pass",'
|
||||
REQUIRE_PREFIXES="TestRelationChildScope TestProtectedFile" expect_detect missing-named 5 \
|
||||
'{"Action":"pass","Package":"p","Test":"TestRelationChildScope"}'
|
||||
REQUIRE_PREFIXES="TestProtectedFile" expect_detect subtest-only 5 \
|
||||
'{"Action":"pass","Package":"p","Test":"TestOther/TestProtectedFile"}'
|
||||
REQUIRE_PREFIXES="TestRelationChildScope TestProtectedFile" expect_detect named 0 \
|
||||
'{"Action":"pass","Package":"p","Test":"TestRelationChildScopeToolbar"}
|
||||
{"Action":"pass","Package":"p","Test":"TestProtectedFileHeaders"}'
|
||||
local flag
|
||||
for flag in --self-test --go --security --spa --openapi --dist --docs --hygiene --app --all; do
|
||||
grep -q -- "^ $flag)" "${BASH_SOURCE[0]}" || {
|
||||
echo "refuse: missing mode $flag" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
echo "phase12.2 self-test passed"
|
||||
}
|
||||
|
||||
run_go() {
|
||||
(cd "$ROOT" && go vet ./...)
|
||||
go_json "$ROOT" ./...
|
||||
echo "phase12.2 go passed"
|
||||
}
|
||||
|
||||
run_security() {
|
||||
named "$ROOT" ./modules/cabana "${SECURITY_CABANA[@]}"
|
||||
named "$ROOT" ./modules/lagoon "${SECURITY_LAGOON[@]}"
|
||||
named "$ROOT" ./modules/lagoon/attach "${SECURITY_ATTACH[@]}"
|
||||
echo "phase12.2 security passed"
|
||||
}
|
||||
|
||||
run_spa() {
|
||||
npm --prefix "$ROOT/admin" run typecheck
|
||||
local log rc=0
|
||||
log="$(mktemp)"
|
||||
npm --prefix "$ROOT/admin" test >"$log" 2>&1 || rc=$?
|
||||
if [[ "$rc" -ne 0 ]] || grep -qE 'No test files found|Unhandled (Errors|Rejection)|FAIL ' "$log"; then
|
||||
tail -n 60 "$log" >&2
|
||||
rm -f "$log"
|
||||
echo "refuse: admin Vitest run failed (exit $rc)" >&2
|
||||
return 1
|
||||
fi
|
||||
grep -E 'Test Files|Tests ' "$log" || true
|
||||
rm -f "$log"
|
||||
echo "phase12.2 spa passed"
|
||||
}
|
||||
|
||||
run_openapi() {
|
||||
"$ROOT/scripts/check-admin-openapi.sh" --check
|
||||
named "$ROOT" ./modules/cabana TestPhase10OpenAPIConformance TestPhase09ContractInventory TestPhase09PermissionMatrix
|
||||
echo "phase12.2 openapi passed"
|
||||
}
|
||||
|
||||
run_dist() {
|
||||
"$ROOT/scripts/check-admin-dist.sh"
|
||||
echo "phase12.2 dist passed"
|
||||
}
|
||||
|
||||
run_docs() {
|
||||
go_json "$ROOT" ./cmd/summer -run '^TestDocsTree$'
|
||||
local out
|
||||
out="$(cd "$ROOT" && go run ./cmd/summer docs:build --check 2>&1)" || {
|
||||
echo "$out" >&2
|
||||
echo "refuse: docs:build --check failed" >&2
|
||||
return 1
|
||||
}
|
||||
go_json "$ROOT" ./modules/phrasebook -run '^TestPhase10SPAKeysResolve$'
|
||||
echo "phase12.2 docs passed"
|
||||
}
|
||||
|
||||
run_hygiene() {
|
||||
local bad=0 hits
|
||||
hits="$(cd "$ROOT" && grep -rniIE "$APP_NAMES" "${HYGIENE_DOCS[@]}" "${PHASE_FILES[@]}" 2>/dev/null || true)"
|
||||
if [[ -n "$hits" ]]; then
|
||||
echo "refuse: hygiene: consuming-application names in the framework: $hits" >&2
|
||||
bad=1
|
||||
fi
|
||||
# The acme.deferred fixture lives only in _test.go files and testdata.
|
||||
hits="$(cd "$ROOT" && grep -rlnE 'acme\.deferred|dfPlugin' --include='*.go' . 2>/dev/null | grep -vE '_test\.go$' || true)"
|
||||
if [[ -n "$hits" ]]; then
|
||||
echo "refuse: hygiene: the test fixture plugin is referenced by production code: $hits" >&2
|
||||
bad=1
|
||||
fi
|
||||
# Session keys travel only in headers, never in a URL.
|
||||
hits="$(cd "$ROOT" && grep -rnE '[?&](session_key|sessionKey|child_session_key)=' admin/src modules/cabana --include='*.ts' --include='*.vue' --include='*.go' 2>/dev/null || true)"
|
||||
if [[ -n "$hits" ]]; then
|
||||
echo "refuse: hygiene: a session key in a URL: $hits" >&2
|
||||
bad=1
|
||||
fi
|
||||
hits="$(cd "$ROOT" && grep -rnE 'v-html|innerHTML|outerHTML|insertAdjacentHTML' admin/src 2>/dev/null || true)"
|
||||
if [[ -n "$hits" ]]; then
|
||||
echo "refuse: hygiene: raw-HTML sink in admin/src: $hits" >&2
|
||||
bad=1
|
||||
fi
|
||||
hits="$(cd "$ROOT" && gofmt -l modules/cabana modules/lagoon modules/conga modules/pact 2>/dev/null || true)"
|
||||
if [[ -n "$hits" ]]; then
|
||||
echo "refuse: hygiene: gofmt: $hits" >&2
|
||||
bad=1
|
||||
fi
|
||||
[[ "$bad" -eq 0 ]] || return 1
|
||||
echo "phase12.2 hygiene passed"
|
||||
}
|
||||
|
||||
run_app() {
|
||||
[[ -d "$APP" ]] || {
|
||||
echo "refuse: application repository $APP not found (set PHASE122_APP)" >&2
|
||||
return 1
|
||||
}
|
||||
go -C "$APP" build ./...
|
||||
go -C "$APP" vet ./...
|
||||
go_json "$APP" ./plugins/golem15/fonoteka -run Admin
|
||||
REQUIRE_PREFIXES="TestSchemaMatchesPHPSnapshot TestMigrateSeedsCanonicalGenres" \
|
||||
go_json "$APP" ./parity -run '^(TestSchemaMatchesPHPSnapshot|TestMigrateSeedsCanonicalGenres)$'
|
||||
if [[ -n "$(git -C "$APP" status --porcelain)" ]]; then
|
||||
git -C "$APP" status --short >&2
|
||||
echo "refuse: the application repository has uncommitted changes" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "phase12.2 app passed"
|
||||
}
|
||||
|
||||
run_all() {
|
||||
local stage
|
||||
for stage in self-test go security spa openapi dist docs hygiene app; do
|
||||
# Each stage runs in its own process, so errexit stays in force
|
||||
# inside it (bash ignores set -e in a function called from an if).
|
||||
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||||
echo "PASS $stage"
|
||||
else
|
||||
echo "FAIL $stage"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "phase12.2 all stages passed"
|
||||
}
|
||||
|
||||
case "${1:---all}" in
|
||||
--self-test) run_self_test ;;
|
||||
--go) run_go ;;
|
||||
--security) run_security ;;
|
||||
--spa) run_spa ;;
|
||||
--openapi) run_openapi ;;
|
||||
--dist) run_dist ;;
|
||||
--docs) run_docs ;;
|
||||
--hygiene) run_hygiene ;;
|
||||
--app) run_app ;;
|
||||
--all) run_all ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user