- 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
334 lines
11 KiB
Bash
Executable File
334 lines
11 KiB
Bash
Executable File
#!/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
|