- stages go, userplugin, mfa, sociallogin, demo, demo-web, bm, fonoteka, docker and docs across the seven repositories the phase touched - go test -json detector refuses fail, skip, zero tests and missing names; --self-test proves it, the additive and migration checks and the docker NOT RUN rule on planted input
886 lines
36 KiB
Bash
Executable File
886 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# check-phase19.6: fail-closed gate for Phase 19.6, second factors in a new
|
||
# shared plugin and device login in the shared user plugin. It proves the
|
||
# phase across seven repositories: this framework (unchanged by the phase,
|
||
# its decision note and docs), the shared user plugin (the login-gate seam,
|
||
# the password-change hook, the reset slot and the device flow, additive
|
||
# only), the second-factor plugin, the social login plugin (its login-gate
|
||
# call), the demo application (Go and its Nuxt frontend), the student
|
||
# application and the application whose workspace carries the user plugin
|
||
# checkout.
|
||
#
|
||
# Stages:
|
||
# go go vet ./... and go test -short ./... here; the framework's
|
||
# go.mod requires none of the three second-factor libraries.
|
||
# userplugin go vet and go test -short of the user plugin (through the
|
||
# application workspace), its Docker-free tests by required
|
||
# name, the additive check (since PHASE196_USER_BASE no line
|
||
# declaring an exported func, method, type, const or var was
|
||
# removed from a non-test .go file without coming back
|
||
# verbatim), and the migration check (the only non-test
|
||
# change under updates/ is the added device-session
|
||
# migration, USER_ADDED_MIGRATIONS).
|
||
# mfa go vet and go test -short in the second-factor plugin, its
|
||
# Docker-free tests by required name, no gopkg.in/yaml.v3 in
|
||
# `go list -deps .`, and go.mod requires pquerna/otp v1.5.0,
|
||
# boombuler/barcode v1.1.0 and go-webauthn/webauthn v0.18.2.
|
||
# sociallogin go vet and go test -short in the social login plugin, its
|
||
# Docker-free tests by required name, and its Phase 19.5
|
||
# rules: no gorilla/sessions and no gopkg.in/yaml.v3 in
|
||
# `go list -deps .`, no goth import outside
|
||
# classes/providers_goth.go and its tests.
|
||
# demo the demo application: go build ./..., go vet ., go test
|
||
# -short . with its boot, go.mod and mount tests by required
|
||
# name (TestMFAMounted only when summer.yaml mounts
|
||
# golem15.mfa; NOT MOUNTED is printed otherwise).
|
||
# demo-web the demo's Nuxt frontend: `pnpm test` (vitest) must pass
|
||
# every test and report app/lib/twoFactor.test.ts.
|
||
# bm the student application: go build ./..., go vet ., go test
|
||
# -short . with its social login and mount tests by name.
|
||
# fonoteka the application workspace that carries the user plugin:
|
||
# go vet and go test -short of the root module and of every
|
||
# workspace module, its summer.yaml does not mount
|
||
# golem15.mfa, and `go list -m all` lists no go-webauthn,
|
||
# pquerna or boombuler module (D-01).
|
||
# docker Docker host only: the Postgres tests by required name (user
|
||
# plugin root and updates packages, second-factor plugin,
|
||
# social login plugin), without -short.
|
||
# docs the decision note names the three pins, STACK.md carries
|
||
# the three rows, TestDocsTree, summer docs:build --check,
|
||
# and no framework file under modules/ or docs/ and no README
|
||
# of the second-factor or user plugin names a consuming
|
||
# application.
|
||
#
|
||
# A Go stage exits non-zero on a failing command, or on a go test -json run
|
||
# that fails, skips a test (a subtest included), matches zero tests, prints
|
||
# "no tests to run" or misses a required name (a subtest pass never counts
|
||
# for its parent's name). The vitest detector refuses a run without a
|
||
# passing line for every required file or with any skipped or failed test.
|
||
# --self-test proves the detectors on planted output and the docker NOT RUN
|
||
# rule on a faked PATH without a working docker.
|
||
#
|
||
# --all runs self-test, go, userplugin, mfa, sociallogin, demo, demo-web,
|
||
# bm, fonoteka and docs, then docker when a Docker daemon answers (NOT RUN
|
||
# docker otherwise, a failure with PHASE196_REQUIRE_DOCKER=1).
|
||
#
|
||
# Out of scope (deferred-items.md, failures that predate this phase): the
|
||
# five Docker-backed tests of the application workspace's own domain plugin
|
||
# that fail identically at the user plugin's v0.1.10 pointer are not run
|
||
# here (the fonoteka stage is -short), and the demo's `scripts/build.sh
|
||
# release`, which refuses two leftover submodule moves of another phase, is
|
||
# not part of this gate. Neither exclusion skips a test this phase added.
|
||
#
|
||
# Environment overrides (default: the sibling checkouts next to this
|
||
# repository): PHASE196_ROOT (this repository), USER_PLUGIN, MFA_PLUGIN,
|
||
# SOCIALLOGIN_PLUGIN, DEMO_APP, DEMO_WEB, BM_APP, FONOTEKA (the application
|
||
# workspace that carries the user plugin), PHASE196_USER_BASE (the user
|
||
# plugin commit the additive check compares against),
|
||
# PHASE196_REQUIRE_DOCKER.
|
||
set -euo pipefail
|
||
|
||
unset FORCE_COLOR
|
||
export NO_COLOR=1
|
||
|
||
ROOT="${PHASE196_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||
FONOTEKA="${FONOTEKA:-$ROOT/../fono""teka.go}"
|
||
USER_PLUGIN="${USER_PLUGIN:-$FONOTEKA/plugins/golem15/user}"
|
||
MFA_PLUGIN="${MFA_PLUGIN:-$ROOT/../sm-mfa-plugin}"
|
||
SOCIALLOGIN_PLUGIN="${SOCIALLOGIN_PLUGIN:-$ROOT/../sm-sociallogin-plugin}"
|
||
DEMO_APP="${DEMO_APP:-$ROOT/../sm-demo-app}"
|
||
DEMO_WEB="${DEMO_WEB:-$DEMO_APP/vue-demo-app}"
|
||
BM_APP="${BM_APP:-$ROOT/../sm-bm-app}"
|
||
# The user plugin commit the phase started from (19.6-01-SUMMARY.md: v0.1.11).
|
||
USER_BASE="${PHASE196_USER_BASE:-50da92c}"
|
||
# The migrations the user plugin may add since USER_BASE. The plan named
|
||
# updates/202610210002_create_device_auth_sessions.go; plan 01 renamed it to
|
||
# 202610260001 so it sorts after the existing 202610220001. Any other added,
|
||
# modified, renamed or deleted non-test file under updates/ is refused.
|
||
USER_ADDED_MIGRATIONS=(updates/202610260001_create_device_auth_sessions.go)
|
||
# The workspace modules of the application workspace (its go.work).
|
||
FONOTEKA_MODULES=(./plugins/golem15/feedback ./plugins/golem15/fono""teka ./plugins/golem15/golem ./plugins/golem15/user)
|
||
# The second-factor plugin's pinned libraries (D-04 decision note).
|
||
MFA_PINS=("github.com/pquerna/otp v1.5.0" "github.com/boombuler/barcode v1.1.0" "github.com/go-webauthn/webauthn v0.18.2")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Required test names. Each list runs with -run '^(a|b|...)$' and every name
|
||
# must pass; a name that skips or does not run fails the stage.
|
||
|
||
# userplugin stage: the user plugin's Docker-free tests (-short).
|
||
USER_ROOT_TESTS=(
|
||
# Plan 01.
|
||
TestLoginGatesSmoke TestDeviceCodesSmoke
|
||
)
|
||
USER_CLASSES_TESTS=()
|
||
|
||
# mfa stage: the second-factor plugin's Docker-free tests (-short).
|
||
MFA_TESTS=(
|
||
# Plans 02 and 03.
|
||
TestQRSVGSmoke TestWebAuthnCeremonySmoke
|
||
)
|
||
|
||
# sociallogin stage: the plugin's Docker-free tests (-short), Phase 19.5's.
|
||
SOCIAL_TESTS=(
|
||
TestGothProvidersSmoke TestRegistry TestNormalizeFrontendCallback
|
||
TestNormalizeReturnTo TestCodes TestEmailVerified TestGothProviders
|
||
TestErrorMessages TestPHPMethodMap
|
||
)
|
||
|
||
# demo stage (-short); TestMFAMounted is added when the demo mounts mfa.
|
||
DEMO_TESTS=(TestBootAllPlugins TestGoModRequiresTags)
|
||
DEMO_MFA_TESTS=(TestMFAMounted)
|
||
|
||
# demo-web stage: vitest files that must pass.
|
||
DEMO_WEB_FILES=(app/lib/twoFactor.test.ts)
|
||
|
||
# bm stage (-short).
|
||
BM_TESTS=(TestSocialLoginMounted TestMFAMounted)
|
||
|
||
# docker stage: testcontainers Postgres suites.
|
||
DOCKER_USER_ROOT_TESTS=(
|
||
# Plan 01.
|
||
TestLoginGateSmoke TestPasswordChangedEventSmoke TestResetTwoFactorSlotSmoke
|
||
TestDeviceFlowSmoke TestDeviceRevokeSmoke
|
||
)
|
||
DOCKER_USER_UPDATES_TESTS=()
|
||
DOCKER_MFA_TESTS=(
|
||
# Plans 02 and 03.
|
||
TestTracerSmoke TestChallengeSmoke TestTrustedSettingsSmoke
|
||
TestWebAuthnSmoke TestPasswordlessSmoke
|
||
)
|
||
DOCKER_SOCIAL_TESTS=(
|
||
# Plan 03.
|
||
TestSocialGateSmoke
|
||
)
|
||
|
||
# docs stage: names of consuming applications (split so this file does not
|
||
# match itself), the decision note and the dependency names it must carry.
|
||
APP_NAMES=("p[lł]yta""rium" "fono""teka" "wave""path" "horo""skopia" "quest""stream" "bm\\.jakub""zych")
|
||
DECISION_NOTE=".planning/notes/2026-10-21-mfa-otp-webauthn.md"
|
||
DECISION_NAMES=("pquerna/otp" "v1.5.0" "go-webauthn/webauthn" "v0.18.2" "boombuler/barcode" "v1.1.0")
|
||
STACK_FILE=".planning/research/STACK.md"
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
usage:
|
||
check-phase19.6.sh --self-test
|
||
check-phase19.6.sh --go | --userplugin | --mfa | --sociallogin | --demo | --demo-web | --bm | --fonoteka | --docker | --docs
|
||
check-phase19.6.sh --all
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
# detect_go reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests,
|
||
# 4 non-JSON, 5 missing required name.
|
||
detect_go() {
|
||
python3 - "$1" <<'PY'
|
||
import json, os, sys
|
||
path = sys.argv[1]
|
||
require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n]
|
||
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)
|
||
text = ev.get("Output") or ""
|
||
if action == "output" and "no tests to run" in text:
|
||
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)
|
||
missing = [n for n in require if n not in passed]
|
||
if missing:
|
||
print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr)
|
||
sys.exit(5)
|
||
PY
|
||
}
|
||
|
||
# detect_vitest reads a vitest run's output. Exit 1 failed or skipped
|
||
# tests, 3 no summary, 5 a required file did not pass. REQUIRE_FILES is a
|
||
# space-separated list.
|
||
detect_vitest() {
|
||
python3 - "$1" <<'PY'
|
||
import os, re, sys
|
||
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
||
need = os.environ.get("REQUIRE_FILES", "").split()
|
||
if re.search(r"\bfailed\b|\bskipped\b|\btodo\b|\bFAIL\b", text):
|
||
print("refuse: vitest reports failed, skipped or todo tests", file=sys.stderr)
|
||
sys.exit(1)
|
||
m = re.search(r"^\s*Tests\s+(\d+) passed \((\d+)\)\s*$", text, re.M)
|
||
if not m or m.group(1) != m.group(2) or m.group(1) == "0":
|
||
print("refuse: no all-passing vitest summary", file=sys.stderr)
|
||
sys.exit(3)
|
||
missing = [f for f in need if not re.search(r"^\s*✓ " + re.escape(f) + r" \(\d+ tests?\)", text, re.M)]
|
||
if missing:
|
||
print("refuse: did not pass: " + ", ".join(missing), file=sys.stderr)
|
||
sys.exit(5)
|
||
PY
|
||
}
|
||
|
||
# go_json DIR ARGS... runs go test -json ARGS in DIR and checks the stream
|
||
# with detect_go.
|
||
go_json() {
|
||
local dir="$1"
|
||
shift
|
||
local log err rc=0 dc=0
|
||
log="$(mktemp)"
|
||
err="$(mktemp)"
|
||
(cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$?
|
||
detect_go "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
cat "$err" >&2 || true
|
||
grep -v '^{' "$log" | tail -n 40 >&2 || true
|
||
python3 -c 'import json,sys
|
||
for raw in open(sys.argv[1], errors="replace"):
|
||
try: ev = json.loads(raw)
|
||
except ValueError: continue
|
||
if ev.get("Action") == "output" and ("FAIL" in ev.get("Output", "") or "SKIP" in ev.get("Output", "") or "_test.go:" in ev.get("Output", "")):
|
||
sys.stderr.write(ev["Output"])' "$log" 2>&1 | tail -n 40 >&2 || true
|
||
rm -f "$log" "$err"
|
||
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
echo "go: $(grep -c '"Action":"pass","Package":"[^"]*","Test":"[^"/]*","Elapsed"' "$log") top-level tests passed in $dir ($*)"
|
||
rm -f "$log" "$err"
|
||
}
|
||
|
||
# go_named DIR PACKAGES FLAGS NAME... runs exactly the named top-level tests
|
||
# and requires every one of them to pass. PACKAGES and FLAGS are word-split.
|
||
go_named() {
|
||
local dir="$1" pkgs="$2" flags="$3"
|
||
shift 3
|
||
if [[ "$#" -eq 0 ]]; then
|
||
echo "refuse: go_named $pkgs in $dir without required names" >&2
|
||
return 1
|
||
fi
|
||
local names=("$@") regex
|
||
regex="^($(IFS='|'; echo "${names[*]}"))\$"
|
||
# shellcheck disable=SC2086
|
||
REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" $pkgs
|
||
}
|
||
|
||
# go_short DIR PACKAGES runs go test -short PACKAGES in DIR; on failure it
|
||
# prints the tail of the output and fails.
|
||
go_short() {
|
||
local dir="$1" pkgs="${2:-./...}" out
|
||
# shellcheck disable=SC2086
|
||
if ! out="$(cd "$dir" && go test -count=1 -short $pkgs 2>&1)"; then
|
||
printf '%s\n' "$out" | grep -v '^ok ' | tail -n 60 >&2
|
||
echo "refuse: go test -short $pkgs in $dir" >&2
|
||
return 1
|
||
fi
|
||
echo "short: $dir ($pkgs)"
|
||
}
|
||
|
||
go_vet() {
|
||
local dir="$1"
|
||
shift
|
||
(cd "$dir" && go vet "$@")
|
||
echo "vet: $dir ($*)"
|
||
}
|
||
|
||
go_build() {
|
||
local dir="$1"
|
||
shift
|
||
(cd "$dir" && go build "$@")
|
||
echo "build: $dir ($*)"
|
||
}
|
||
|
||
require_dir() {
|
||
[[ -d "$1" ]] || {
|
||
echo "refuse: $2 $1 not found" >&2
|
||
return 1
|
||
}
|
||
}
|
||
|
||
expect_detector() {
|
||
local detector="$1" name="$2" want="$3" payload="$4" log dc=0
|
||
log="$(mktemp)"
|
||
printf '%s\n' "$payload" >"$log"
|
||
"$detector" "$log" >/dev/null 2>&1 || dc=$?
|
||
rm -f "$log"
|
||
if [[ "$dc" -ne "$want" ]]; then
|
||
echo "refuse: self-test $detector $name: exit $dc, want $want" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# expect_mode NAME WANT_EXIT WANT_TEXT MODE ENV... runs this script in MODE
|
||
# with ENV and checks its exit status and that its output carries WANT_TEXT.
|
||
expect_mode() {
|
||
local name="$1" want="$2" text="$3" mode="$4"
|
||
shift 4
|
||
local out rc=0
|
||
out="$(env "$@" bash "${BASH_SOURCE[0]}" "$mode" 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne "$want" ]] || ! grep -qF -- "$text" <<<"$out"; then
|
||
printf '%s\n' "$out" | tail -n 10 >&2
|
||
echo "refuse: self-test $name: exit $rc (want $want), output must carry \"$text\"" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# removed_exports reads a unified diff and prints every removed line that
|
||
# declares an exported func, method, type, const or var (a top-level
|
||
# declaration, or a `Name [Type] = value` spec inside a const or var block)
|
||
# and does not come back as an added line with the same text once
|
||
# whitespace is collapsed. Moving a declaration or a gofmt realignment of a
|
||
# const block passes; a changed signature, a renamed or removed
|
||
# declaration is printed.
|
||
removed_exports() {
|
||
python3 -c '
|
||
import re, sys
|
||
decl = re.compile(r"^(func ([A-Z]|\([^)]*\) [A-Z])|type [A-Z]|const [A-Z]|var [A-Z]|\t[A-Z][A-Za-z0-9_]*(\s+[A-Za-z0-9_.*\[\]]+)?\s*=)")
|
||
norm = lambda s: " ".join(s.split())
|
||
removed, added = [], set()
|
||
for raw in sys.stdin.read().splitlines():
|
||
if raw.startswith("---") or raw.startswith("+++"):
|
||
continue
|
||
if raw.startswith("-") and decl.match(raw[1:]):
|
||
removed.append(raw)
|
||
elif raw.startswith("+"):
|
||
added.add(norm(raw[1:]))
|
||
for line in removed:
|
||
if norm(line[1:]) not in added:
|
||
print(line)
|
||
'
|
||
}
|
||
|
||
# forbidden_deps reads `go list -deps` output and prints every forbidden
|
||
# package: gorilla/sessions (goth's session helper) and the archived
|
||
# gopkg.in/yaml.v3.
|
||
forbidden_deps() {
|
||
grep -E '^(github\.com/gorilla/sessions|gopkg\.in/yaml\.v3)(/|$)' || true
|
||
}
|
||
|
||
# second_factor_modules reads `go list -m all` output and prints every
|
||
# second-factor module (D-01: an application without golem15.mfa carries
|
||
# none of them).
|
||
second_factor_modules() {
|
||
grep -E '^(github\.com/go-webauthn/|github\.com/pquerna/|github\.com/boombuler/)' || true
|
||
}
|
||
|
||
# missing_pins FILE prints every MFA_PINS entry that FILE (a go.mod) does
|
||
# not require directly at exactly that version.
|
||
missing_pins() {
|
||
local pin
|
||
for pin in "${MFA_PINS[@]}"; do
|
||
if ! grep -qE "^[[:space:]]*(require[[:space:]]+)?${pin//./\\.}[[:space:]]*$" "$1"; then
|
||
echo "$pin"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# refused_migration_changes reads `git diff --name-status` lines and prints
|
||
# every line whose status is not A or whose path is not one of
|
||
# USER_ADDED_MIGRATIONS. A rename (R...) or copy carries two paths and is
|
||
# always refused.
|
||
refused_migration_changes() {
|
||
local status path rest allowed ok
|
||
while IFS=$'\t' read -r status path rest || [[ -n "$status" ]]; do
|
||
[[ -n "$status" ]] || continue
|
||
ok=0
|
||
if [[ "$status" == "A" && -z "$rest" ]]; then
|
||
for allowed in "${USER_ADDED_MIGRATIONS[@]}"; do
|
||
if [[ "$path" == "$allowed" ]]; then
|
||
ok=1
|
||
fi
|
||
done
|
||
fi
|
||
if [[ "$ok" -eq 0 ]]; then
|
||
printf '%s\t%s%s\n' "$status" "$path" "${rest:+$'\t'$rest}"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# mounts_mfa FILE succeeds when the summer.yaml FILE mounts golem15.mfa.
|
||
mounts_mfa() {
|
||
grep -qE '^[[:space:]]*-?[[:space:]]*id:[[:space:]]*golem15\.mfa[[:space:]]*$' "$1"
|
||
}
|
||
|
||
run_self_test() {
|
||
bash -n "${BASH_SOURCE[0]}"
|
||
|
||
expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestTracerSmoke"}'
|
||
expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"fail","Package":"p","Test":"TestGate"}'
|
||
expect_detector detect_go package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"fail","Package":"p"}'
|
||
expect_detector detect_go build 1 '{"Action":"build-fail","ImportPath":"p"}'
|
||
expect_detector detect_go skip 2 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"skip","Package":"p","Test":"TestDeviceFlowSmoke"}'
|
||
expect_detector detect_go subtest-skip 2 '{"Action":"skip","Package":"p","Test":"TestPasswordless/uv_missing"}
|
||
{"Action":"pass","Package":"p","Test":"TestPasswordless"}'
|
||
expect_detector detect_go zero 3 '{"Action":"pass","Package":"p"}'
|
||
expect_detector detect_go no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"}
|
||
{"Action":"pass","Package":"p"}'
|
||
expect_detector detect_go nonjson 4 '{"Action":"pass",'
|
||
REQUIRE_TESTS="TestTracerSmoke TestWebAuthnCeremonySmoke" expect_detector detect_go missing-named 5 \
|
||
'{"Action":"pass","Package":"p","Test":"TestTracerSmoke"}'
|
||
REQUIRE_TESTS="TestDevice" expect_detector detect_go prefix-is-not-the-test 5 \
|
||
'{"Action":"pass","Package":"p","Test":"TestDeviceStatus"}'
|
||
REQUIRE_TESTS="TestGate" expect_detector detect_go subtest-is-not-the-test 5 \
|
||
'{"Action":"pass","Package":"p","Test":"TestOuter/TestGate"}
|
||
{"Action":"pass","Package":"p","Test":"TestOther"}'
|
||
|
||
local vitest_ok vitest_skip vitest_fail vitest_missing
|
||
local vitest_files="${DEMO_WEB_FILES[*]}"
|
||
vitest_ok=$' ✓ app/lib/realtime.test.ts (3 tests) 21ms\n ✓ app/lib/twoFactor.test.ts (14 tests) 15ms\n\n Test Files 2 passed (2)\n Tests 17 passed (17)'
|
||
vitest_skip=$' ✓ app/lib/twoFactor.test.ts (14 tests | 1 skipped) 15ms\n\n Test Files 1 passed (1)\n Tests 13 passed | 1 skipped (14)'
|
||
vitest_fail=$' ❯ app/lib/twoFactor.test.ts (14 tests | 1 failed) 15ms\n\n Test Files 1 failed (1)\n Tests 1 failed | 13 passed (14)'
|
||
vitest_missing=$' ✓ app/lib/realtime.test.ts (3 tests) 21ms\n\n Test Files 1 passed (1)\n Tests 3 passed (3)'
|
||
REQUIRE_FILES="$vitest_files" expect_detector detect_vitest pass 0 "$vitest_ok"
|
||
REQUIRE_FILES="$vitest_files" expect_detector detect_vitest skip 1 "$vitest_skip"
|
||
REQUIRE_FILES="$vitest_files" expect_detector detect_vitest fail 1 "$vitest_fail"
|
||
REQUIRE_FILES="$vitest_files" expect_detector detect_vitest missing-file 5 "$vitest_missing"
|
||
REQUIRE_FILES="$vitest_files" expect_detector detect_vitest no-summary 3 ' ✓ app/lib/twoFactor.test.ts (14 tests) 15ms'
|
||
|
||
# The NOT RUN rule, on a faked host: a docker binary that never answers.
|
||
local tmp
|
||
tmp="$(mktemp -d)"
|
||
mkdir -p "$tmp/bin"
|
||
printf '#!/bin/sh\nexit 1\n' >"$tmp/bin/docker"
|
||
chmod +x "$tmp/bin/docker"
|
||
expect_mode docker-not-run 0 "NOT RUN docker" --docker-gate PATH="$tmp/bin:$PATH" PHASE196_REQUIRE_DOCKER=0
|
||
expect_mode docker-required 1 "FAIL docker" --docker-gate PATH="$tmp/bin:$PATH" PHASE196_REQUIRE_DOCKER=1
|
||
expect_mode docker-stage-needs-daemon 1 "needs a reachable Docker daemon" --docker PATH="$tmp/bin:$PATH"
|
||
rm -rf "$tmp"
|
||
|
||
# The additive check refuses a removed or changed export and accepts
|
||
# additions, moves and gofmt realignment.
|
||
local diff_bad diff_ok line
|
||
diff_bad=$'--- a/controllers/x.go\n+++ b/controllers/x.go\n-func FinishLogin(ctx context.Context) error {\n+func finishLogin(ctx context.Context) error {'
|
||
diff_ok=$'--- a/controllers/x.go\n+++ b/controllers/x.go\n+func RefreshTTL(app *backpack.App) time.Duration {\n-func helper() {}\n-\t// func Old() is gone\n-\tLoginSourcePassword = "password"\n+\tLoginSourcePassword = "password"\n-func IssueToken(a int) string {\n+func IssueToken(a int) string {'
|
||
if [[ -z "$(removed_exports <<<"$diff_bad")" ]]; then
|
||
echo "refuse: self-test removed_exports missed a removed exported func" >&2
|
||
return 1
|
||
fi
|
||
if [[ -n "$(removed_exports <<<"$diff_ok")" ]]; then
|
||
echo "refuse: self-test removed_exports refused an additive diff" >&2
|
||
return 1
|
||
fi
|
||
for line in '-type LoginGate interface {' '-func (u *User) AvatarURL(size int) string {' '-func (User) Name() string {' \
|
||
'-const DeviceAuthTTL = 5 * time.Minute' '-var ErrNotFound = errors.New("x")' $'-\tDeviceStatusPending = "pending"' \
|
||
$'-\tMaxAttempts int = 5'; do
|
||
if [[ -z "$(removed_exports <<<"$line")" ]]; then
|
||
echo "refuse: self-test removed_exports missed: $line" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
# The dependency checks refuse gorilla/sessions, gopkg.in/yaml.v3 and,
|
||
# for the application workspace, the second-factor modules.
|
||
if [[ -z "$(forbidden_deps <<<$'net/http\ngithub.com/gorilla/sessions')" ]] ||
|
||
[[ -z "$(forbidden_deps <<<'gopkg.in/yaml.v3')" ]]; then
|
||
echo "refuse: self-test forbidden_deps missed a forbidden package" >&2
|
||
return 1
|
||
fi
|
||
if [[ -n "$(forbidden_deps <<<$'go.yaml.in/yaml/v3\ngithub.com/goccy/go-yaml\ngithub.com/markbates/goth')" ]]; then
|
||
echo "refuse: self-test forbidden_deps refused an allowed package" >&2
|
||
return 1
|
||
fi
|
||
for line in 'github.com/go-webauthn/webauthn v0.18.2' 'github.com/pquerna/otp v1.5.0' 'github.com/boombuler/barcode v1.1.0'; do
|
||
if [[ -z "$(second_factor_modules <<<$'golang.org/x/crypto v0.57.0\n'"$line")" ]]; then
|
||
echo "refuse: self-test second_factor_modules missed: $line" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
if [[ -n "$(second_factor_modules <<<$'github.com/golang-jwt/jwt/v5 v5.3.1\ngithub.com/fxamacker/cbor/v2 v2.9.4')" ]]; then
|
||
echo "refuse: self-test second_factor_modules refused an unrelated module" >&2
|
||
return 1
|
||
fi
|
||
|
||
# The pin check accepts the three pins and refuses another version or an
|
||
# indirect requirement.
|
||
local gm
|
||
gm="$(mktemp -d)"
|
||
printf '%s\n' 'module x' '' 'require (' ' github.com/boombuler/barcode v1.1.0' ' github.com/go-webauthn/webauthn v0.18.2' \
|
||
' github.com/pquerna/otp v1.5.0' ')' >"$gm/ok.mod"
|
||
printf '%s\n' 'module x' '' 'require (' ' github.com/boombuler/barcode v1.1.0 // indirect' ' github.com/go-webauthn/webauthn v0.18.3' \
|
||
' github.com/pquerna/otp v1.5.0' ')' >"$gm/bad.mod"
|
||
if [[ -n "$(missing_pins "$gm/ok.mod")" ]] || [[ "$(missing_pins "$gm/bad.mod" | wc -l)" -ne 2 ]]; then
|
||
rm -rf "$gm"
|
||
echo "refuse: self-test missing_pins did not tell the pinned go.mod from the drifted one" >&2
|
||
return 1
|
||
fi
|
||
printf '%s\n' 'plugins:' ' - id: golem15.user' ' - id: golem15.mfa' >"$gm/mounted.yaml"
|
||
printf '%s\n' 'plugins:' ' - id: golem15.user' ' - id: golem15.mfa.extra' '# golem15.mfa' >"$gm/unmounted.yaml"
|
||
if ! mounts_mfa "$gm/mounted.yaml" || mounts_mfa "$gm/unmounted.yaml"; then
|
||
rm -rf "$gm"
|
||
echo "refuse: self-test mounts_mfa misread a summer.yaml" >&2
|
||
return 1
|
||
fi
|
||
rm -rf "$gm"
|
||
|
||
# The migration check accepts only the added device-session migration
|
||
# and refuses every other change under updates/.
|
||
if [[ -n "$(refused_migration_changes <<<$'A\tupdates/202610260001_create_device_auth_sessions.go')" ]]; then
|
||
echo "refuse: self-test refused_migration_changes refused the allowed migration" >&2
|
||
return 1
|
||
fi
|
||
for line in $'M\tupdates/00_base.go' $'A\tupdates/202610210002_create_device_auth_sessions.go' \
|
||
$'M\tupdates/202610260001_create_device_auth_sessions.go' \
|
||
$'D\tupdates/202610220001_create_api_tokens.go' \
|
||
$'R100\tupdates/202610160002_add_handle_to_users.go\tupdates/202610260001_create_device_auth_sessions.go'; do
|
||
if [[ -z "$(refused_migration_changes <<<"$line")" ]]; then
|
||
echo "refuse: self-test refused_migration_changes missed: $line" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
local mode
|
||
for mode in self-test go userplugin mfa sociallogin demo demo-web bm fonoteka docker docs all; do
|
||
grep -q -- "^ --$mode)" "${BASH_SOURCE[0]}" || {
|
||
echo "refuse: missing mode --$mode" >&2
|
||
return 1
|
||
}
|
||
done
|
||
echo "phase19.6 self-test passed"
|
||
}
|
||
|
||
run_go() {
|
||
go_vet "$ROOT" ./...
|
||
go_short "$ROOT"
|
||
local pin mod
|
||
for pin in "${MFA_PINS[@]}"; do
|
||
mod="${pin%% *}"
|
||
if grep -qF -- "$mod" "$ROOT/go.mod"; then
|
||
echo "refuse: the framework go.mod requires $mod (D-01: only sm-mfa-plugin may)" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
echo "deps: the framework go.mod requires none of the second-factor libraries"
|
||
echo "phase19.6 go passed"
|
||
}
|
||
|
||
run_userplugin() {
|
||
require_dir "$USER_PLUGIN" "user plugin"
|
||
require_dir "$FONOTEKA" "application workspace"
|
||
go_vet "$FONOTEKA" ./plugins/golem15/user/...
|
||
go_short "$FONOTEKA" ./plugins/golem15/user/...
|
||
go_named "$FONOTEKA" ./plugins/golem15/user -short "${USER_ROOT_TESTS[@]}"
|
||
if ((${#USER_CLASSES_TESTS[@]})); then
|
||
go_named "$FONOTEKA" ./plugins/golem15/user/classes -short "${USER_CLASSES_TESTS[@]}"
|
||
fi
|
||
# Additive only (19.4 D-05, D-17). git's status is captured before any
|
||
# filtering, so an unknown base or a broken checkout fails here instead
|
||
# of reading as an empty diff.
|
||
local diff rc=0 removed added migrations
|
||
diff="$(git -C "$USER_PLUGIN" diff "$USER_BASE" -- '*.go' ':(exclude)*_test.go' 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
printf '%s\n' "$diff" | tail -n 5 >&2
|
||
echo "refuse: git diff $USER_BASE in $USER_PLUGIN failed (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
removed="$(removed_exports <<<"$diff")"
|
||
if [[ -n "$removed" ]]; then
|
||
printf '%s\n' "$removed" >&2
|
||
echo "refuse: the user plugin removed or changed exported declarations since $USER_BASE (additive only)" >&2
|
||
return 1
|
||
fi
|
||
added="$(grep -cE '^\+(func|type|const|var) ' <<<"$diff" || true)"
|
||
# Under updates/ only the device-session migration may be added (test
|
||
# files aside).
|
||
local refused file checked=0
|
||
rc=0
|
||
migrations="$(git -C "$USER_PLUGIN" diff --name-status "$USER_BASE" -- updates/ ':(exclude)*_test.go' 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
printf '%s\n' "$migrations" | tail -n 5 >&2
|
||
echo "refuse: git diff --name-status $USER_BASE -- updates/ in $USER_PLUGIN failed (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
refused="$(refused_migration_changes <<<"$migrations")"
|
||
if [[ -n "$refused" ]]; then
|
||
printf '%s\n' "$refused" >&2
|
||
echo "refuse: the user plugin changed files under updates/ since $USER_BASE (only the device-session migration may be added)" >&2
|
||
return 1
|
||
fi
|
||
for file in "${USER_ADDED_MIGRATIONS[@]}"; do
|
||
[[ -f "$USER_PLUGIN/$file" ]] || continue
|
||
checked=$((checked + 1))
|
||
if ! grep -q 'golem15_user_device_auth_sessions' "$USER_PLUGIN/$file"; then
|
||
echo "refuse: $file does not create golem15_user_device_auth_sessions" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
echo "additive: no exported declaration removed or changed and no migration changed beyond the allowed addition in the user plugin since $USER_BASE ($added declarations added; $checked allowed migration(s) present)"
|
||
echo "phase19.6 userplugin passed"
|
||
}
|
||
|
||
run_mfa() {
|
||
require_dir "$MFA_PLUGIN" "second-factor plugin"
|
||
go_vet "$MFA_PLUGIN" ./...
|
||
# The Postgres suites skip here and run by name in the docker stage.
|
||
go_short "$MFA_PLUGIN"
|
||
USER_PLUGIN="$USER_PLUGIN" SOCIALLOGIN_PLUGIN="$SOCIALLOGIN_PLUGIN" \
|
||
go_named "$MFA_PLUGIN" . -short "${MFA_TESTS[@]}"
|
||
local deps rc=0 bad missing
|
||
deps="$(cd "$MFA_PLUGIN" && go list -deps . 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
printf '%s\n' "$deps" | tail -n 5 >&2
|
||
echo "refuse: go list -deps . in $MFA_PLUGIN failed (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
bad="$(forbidden_deps <<<"$deps")"
|
||
if [[ -n "$bad" ]]; then
|
||
printf '%s\n' "$bad" >&2
|
||
echo "refuse: the second-factor plugin links gorilla/sessions or gopkg.in/yaml.v3" >&2
|
||
return 1
|
||
fi
|
||
missing="$(missing_pins "$MFA_PLUGIN/go.mod")"
|
||
if [[ -n "$missing" ]]; then
|
||
printf '%s\n' "$missing" >&2
|
||
echo "refuse: $MFA_PLUGIN/go.mod does not require the pinned versions above directly (D-04)" >&2
|
||
return 1
|
||
fi
|
||
echo "deps: no gopkg.in/yaml.v3; go.mod pins ${MFA_PINS[*]}"
|
||
echo "phase19.6 mfa passed"
|
||
}
|
||
|
||
run_sociallogin() {
|
||
require_dir "$SOCIALLOGIN_PLUGIN" "social login plugin"
|
||
go_vet "$SOCIALLOGIN_PLUGIN" ./...
|
||
# The Postgres suites skip here and run by name in the docker stage.
|
||
go_short "$SOCIALLOGIN_PLUGIN"
|
||
USER_PLUGIN="$USER_PLUGIN" go_named "$SOCIALLOGIN_PLUGIN" . -short "${SOCIAL_TESTS[@]}"
|
||
local deps rc=0 bad importers
|
||
deps="$(cd "$SOCIALLOGIN_PLUGIN" && go list -deps . 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
printf '%s\n' "$deps" | tail -n 5 >&2
|
||
echo "refuse: go list -deps . in $SOCIALLOGIN_PLUGIN failed (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
bad="$(forbidden_deps <<<"$deps")"
|
||
if [[ -n "$bad" ]]; then
|
||
printf '%s\n' "$bad" >&2
|
||
echo "refuse: the social login plugin links gorilla/sessions or gopkg.in/yaml.v3" >&2
|
||
return 1
|
||
fi
|
||
importers="$(cd "$SOCIALLOGIN_PLUGIN" && grep -rlE '"github\.com/markbates/goth(/[^"]*)?"' --include='*.go' . |
|
||
grep -vE '^\./classes/providers_goth(_[a-z_]+)?(_test)?\.go$' || true)"
|
||
if [[ -n "$importers" ]]; then
|
||
printf '%s\n' "$importers" >&2
|
||
echo "refuse: goth is imported outside classes/providers_goth.go and its tests" >&2
|
||
return 1
|
||
fi
|
||
echo "deps: no gorilla/sessions, no gopkg.in/yaml.v3; goth only in classes/providers_goth.go"
|
||
echo "phase19.6 sociallogin passed"
|
||
}
|
||
|
||
run_demo() {
|
||
require_dir "$DEMO_APP" "demo application"
|
||
go_build "$DEMO_APP" ./...
|
||
go_vet "$DEMO_APP" .
|
||
local names=("${DEMO_TESTS[@]}")
|
||
if mounts_mfa "$DEMO_APP/summer.yaml"; then
|
||
names+=("${DEMO_MFA_TESTS[@]}")
|
||
else
|
||
echo "NOT MOUNTED: $DEMO_APP/summer.yaml has no golem15.mfa; ${DEMO_MFA_TESTS[*]} not required"
|
||
fi
|
||
go_named "$DEMO_APP" . -short "${names[@]}"
|
||
echo "phase19.6 demo passed"
|
||
}
|
||
|
||
run_demo_web() {
|
||
require_dir "$DEMO_WEB" "demo frontend"
|
||
local log rc=0 dc=0
|
||
log="$(mktemp)"
|
||
pnpm --dir "$DEMO_WEB" test >"$log" 2>&1 || rc=$?
|
||
REQUIRE_FILES="${DEMO_WEB_FILES[*]}" detect_vitest "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
tail -n 40 "$log" >&2
|
||
rm -f "$log"
|
||
echo "refuse: pnpm test in $DEMO_WEB (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
echo "vitest: $(grep -E '^\s*Tests\s' "$log" | tr -s ' ')"
|
||
rm -f "$log"
|
||
echo "phase19.6 demo-web passed"
|
||
}
|
||
|
||
run_bm() {
|
||
require_dir "$BM_APP" "student application"
|
||
go_build "$BM_APP" ./...
|
||
go_vet "$BM_APP" .
|
||
go_named "$BM_APP" . -short "${BM_TESTS[@]}"
|
||
echo "phase19.6 bm passed"
|
||
}
|
||
|
||
run_fonoteka() {
|
||
require_dir "$FONOTEKA" "application workspace"
|
||
local mod
|
||
go_vet "$FONOTEKA" ./...
|
||
go_short "$FONOTEKA" ./...
|
||
for mod in "${FONOTEKA_MODULES[@]}"; do
|
||
go_vet "$FONOTEKA" "$mod/..."
|
||
go_short "$FONOTEKA" "$mod/..."
|
||
done
|
||
if mounts_mfa "$FONOTEKA/summer.yaml" || grep -qE '(^|[^a-z.])golem15\.mfa([^a-z.]|$)' "$FONOTEKA/summer.yaml"; then
|
||
grep -nE 'golem15\.mfa' "$FONOTEKA/summer.yaml" >&2
|
||
echo "refuse: $FONOTEKA/summer.yaml mounts golem15.mfa (D-17)" >&2
|
||
return 1
|
||
fi
|
||
local mods rc=0 bad
|
||
mods="$(cd "$FONOTEKA" && go list -m all 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
printf '%s\n' "$mods" | tail -n 5 >&2
|
||
echo "refuse: go list -m all in $FONOTEKA failed (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
bad="$(second_factor_modules <<<"$mods")"
|
||
if [[ -n "$bad" ]]; then
|
||
printf '%s\n' "$bad" >&2
|
||
echo "refuse: the application workspace's module graph carries a second-factor module (D-01)" >&2
|
||
return 1
|
||
fi
|
||
echo "mount: $FONOTEKA/summer.yaml does not mount golem15.mfa; no go-webauthn, pquerna or boombuler module"
|
||
echo "phase19.6 fonoteka passed"
|
||
}
|
||
|
||
docker_up() {
|
||
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
|
||
}
|
||
|
||
run_docker() {
|
||
if ! docker_up; then
|
||
echo "refuse: the docker stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2
|
||
return 1
|
||
fi
|
||
require_dir "$FONOTEKA" "application workspace"
|
||
require_dir "$MFA_PLUGIN" "second-factor plugin"
|
||
require_dir "$SOCIALLOGIN_PLUGIN" "social login plugin"
|
||
go_named "$FONOTEKA" ./plugins/golem15/user "-timeout 20m" "${DOCKER_USER_ROOT_TESTS[@]}"
|
||
if ((${#DOCKER_USER_UPDATES_TESTS[@]})); then
|
||
go_named "$FONOTEKA" ./plugins/golem15/user/updates "-timeout 20m" "${DOCKER_USER_UPDATES_TESTS[@]}"
|
||
fi
|
||
USER_PLUGIN="$USER_PLUGIN" SOCIALLOGIN_PLUGIN="$SOCIALLOGIN_PLUGIN" \
|
||
go_named "$MFA_PLUGIN" . "-timeout 30m" "${DOCKER_MFA_TESTS[@]}"
|
||
USER_PLUGIN="$USER_PLUGIN" go_named "$SOCIALLOGIN_PLUGIN" . "-timeout 30m" "${DOCKER_SOCIAL_TESTS[@]}"
|
||
echo "phase19.6 docker passed"
|
||
}
|
||
|
||
# docker_gate is the docker step of --all: run the stage when a daemon
|
||
# answers, fail with PHASE196_REQUIRE_DOCKER=1, print NOT RUN otherwise.
|
||
docker_gate() {
|
||
if docker_up; then
|
||
if bash "${BASH_SOURCE[0]}" --docker; then
|
||
echo "PASS docker"
|
||
return 0
|
||
fi
|
||
echo "FAIL docker"
|
||
return 1
|
||
fi
|
||
if [[ "${PHASE196_REQUIRE_DOCKER:-0}" == "1" ]]; then
|
||
echo "FAIL docker: no reachable Docker daemon and PHASE196_REQUIRE_DOCKER=1"
|
||
return 1
|
||
fi
|
||
cat >&2 <<'EOF'
|
||
************************************************************************
|
||
NOT RUN docker: no reachable Docker daemon on this host.
|
||
The testcontainers Postgres suites of the user plugin, the second-factor
|
||
plugin and the social login plugin did NOT run. Run on a Docker host
|
||
(through sudo -u golem when the socket is denied) before /msd-verify-work:
|
||
bash scripts/check-phase19.6.sh --docker
|
||
************************************************************************
|
||
EOF
|
||
echo "NOT RUN docker (Docker-host only; see the notice above)"
|
||
}
|
||
|
||
run_docs() {
|
||
local name
|
||
for name in "${DECISION_NAMES[@]}"; do
|
||
if ! grep -qF -- "$name" "$ROOT/$DECISION_NOTE"; then
|
||
echo "refuse: the decision note $DECISION_NOTE does not name $name" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
echo "note: $DECISION_NOTE names ${DECISION_NAMES[*]}"
|
||
local pin mod ver
|
||
for pin in "${MFA_PINS[@]}"; do
|
||
mod="${pin%% *}"
|
||
ver="${pin##* }"
|
||
if ! grep -E "^\| \`${mod//./\\.}\`" "$ROOT/$STACK_FILE" | grep -qF -- "| $ver |"; then
|
||
echo "refuse: $STACK_FILE has no row for $mod at $ver" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
echo "stack: $STACK_FILE carries ${MFA_PINS[*]}"
|
||
go_named "$ROOT" ./cmd/summer "" TestDocsTree
|
||
(cd "$ROOT" && go run ./cmd/summer docs:build --check)
|
||
local pattern hits files=()
|
||
pattern="$(IFS='|'; echo "${APP_NAMES[*]}")"
|
||
while IFS= read -r -d '' f; do
|
||
files+=("$f")
|
||
done < <(find "$ROOT/modules" "$ROOT/docs" -type f ! -name '*_test.go' ! -path '*/node_modules/*' -print0)
|
||
local readme
|
||
for readme in "$MFA_PLUGIN/README.md" "$USER_PLUGIN/README.md"; do
|
||
[[ -f "$readme" ]] || {
|
||
echo "refuse: $readme not found" >&2
|
||
return 1
|
||
}
|
||
files+=("$readme")
|
||
done
|
||
hits="$(grep -inHIE "$pattern" "${files[@]}" || true)"
|
||
if [[ -n "$hits" ]]; then
|
||
printf '%s\n' "$hits" | head -n 20 >&2
|
||
echo "refuse: a framework source, docs page or plugin README names a consuming application" >&2
|
||
return 1
|
||
fi
|
||
echo "names: ${#files[@]} files under modules/, docs/ and the two plugin READMEs checked, no consuming application named"
|
||
echo "phase19.6 docs passed"
|
||
}
|
||
|
||
run_all() {
|
||
local stage
|
||
for stage in self-test go userplugin mfa sociallogin demo demo-web bm fonoteka docs; do
|
||
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||
echo "PASS $stage"
|
||
else
|
||
echo "FAIL $stage"
|
||
exit 1
|
||
fi
|
||
done
|
||
docker_gate || exit 1
|
||
echo "Phase 19.6 gate passed"
|
||
}
|
||
|
||
case "${1:---all}" in
|
||
--self-test) run_self_test ;;
|
||
--go) run_go ;;
|
||
--userplugin) run_userplugin ;;
|
||
--mfa) run_mfa ;;
|
||
--sociallogin) run_sociallogin ;;
|
||
--demo) run_demo ;;
|
||
--demo-web) run_demo_web ;;
|
||
--bm) run_bm ;;
|
||
--fonoteka) run_fonoteka ;;
|
||
--docker) run_docker ;;
|
||
--docker-gate) docker_gate ;;
|
||
--docs) run_docs ;;
|
||
--all) run_all ;;
|
||
*) usage ;;
|
||
esac
|