diff --git a/scripts/check-phase19.5.sh b/scripts/check-phase19.5.sh index 319ffb66..a6a02143 100755 --- a/scripts/check-phase19.5.sh +++ b/scripts/check-phase19.5.sh @@ -13,8 +13,11 @@ # application workspace), its Docker-free tests by required # name, and the additive check: since PHASE195_USER_BASE no # line declaring an exported func, method or type was removed -# from a non-test .go file, and no file under updates/ changed -# (D-22). +# from a non-test .go file, and the migration check: the only +# non-test change under updates/ is the added +# email_verified_at migration (USER_ADDED_MIGRATIONS, D-22 +# relaxed by D-27), which must add its column with ADD +# COLUMN IF NOT EXISTS and write or drop nothing. # sociallogin go vet and go test -short in the social login plugin, its # Docker-free tests by required name, no gorilla/sessions and # no gopkg.in/yaml.v3 in `go list -deps .`, and no Go file @@ -69,6 +72,11 @@ DEMO_APP="${DEMO_APP:-$ROOT/../sm-demo-app}" BM_APP="${BM_APP:-$ROOT/../sm-bm-app}" # The user plugin commit the phase started from (PATTERNS.md). USER_BASE="${PHASE195_USER_BASE:-4c5d3b8}" +# The migrations the user plugin may add since USER_BASE. D-22 allowed none; +# D-27 relaxes it for this one additive migration (users.email_verified_at, +# a nullable column, no row written). Any other added, modified, renamed or +# deleted non-test file under updates/ is refused. +USER_ADDED_MIGRATIONS=(updates/202610210001_add_users_email_verified_at.go) # --------------------------------------------------------------------------- # Required test names. Each list runs with -run '^(a|b|...)$' and every name @@ -142,6 +150,9 @@ DOCKER_SOCIAL_TESTS=( # Plan 04: the PHP ports, one subtest per PHP method. TestPHPSocialAuthGate TestPHPOAuthLinkCookie TestPHPPendingRegistrationTakeover TestPHPPasswordBootstrap TestPHPProfileOAuthLink + # Plan 05 (D-26): a verified email links only onto an account whose + # owner proved the address (users.email_verified_at). + TestOwnerVerifiedEmail ) # docs stage: names of consuming applications (split so this file does not @@ -346,6 +357,34 @@ forbidden_deps() { grep -E '^(github\.com/gorilla/sessions|gopkg\.in/yaml\.v3)(/|$)' || true } +# 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 (D-22, D-27). 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 +} + +# migration_writes_rows reads the Go file $1 without its // comment lines +# and prints every line carrying an SQL word that writes or drops data. +migration_writes_rows() { + grep -vE '^[[:space:]]*//' "$1" | grep -E '\b(UPDATE|INSERT|DELETE|DROP|TRUNCATE)\b' || true +} + run_self_test() { bash -n "${BASH_SOURCE[0]}" @@ -424,6 +463,39 @@ run_self_test() { return 1 fi + # The migration check (D-22, D-27) accepts only the added + # email_verified_at migration and refuses every other change under + # updates/, and a migration that writes or drops anything. + if [[ -n "$(refused_migration_changes <<<$'A\tupdates/202610210001_add_users_email_verified_at.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/202610220001_add_users_other.go' \ + $'D\tupdates/202610210001_add_users_email_verified_at.go' \ + $'R100\tupdates/202610160002_add_handle_to_users.go\tupdates/202610210001_add_users_email_verified_at.go'; do + if [[ -z "$(refused_migration_changes <<<"$line")" ]]; then + echo "refuse: self-test refused_migration_changes missed: $line" >&2 + return 1 + fi + done + local mig + mig="$(mktemp -d)" + printf '%s\n' 'package updates' '' 'func m(tx *gorm.DB) error {' \ + ' return tx.Exec(`UPDATE users SET email_verified_at = now()`).Error' '}' >"$mig/writes.go" + printf '%s\n' 'package updates' '' '// No UPDATE here: the migration writes no rows.' 'func m(tx *gorm.DB) error {' \ + ' return tx.Exec(`ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ NULL`).Error' '}' >"$mig/clean.go" + if [[ -z "$(migration_writes_rows "$mig/writes.go")" ]]; then + rm -rf "$mig" + echo "refuse: self-test migration_writes_rows missed a row update" >&2 + return 1 + fi + if [[ -n "$(migration_writes_rows "$mig/clean.go")" ]]; then + rm -rf "$mig" + echo "refuse: self-test migration_writes_rows refused a word in a comment" >&2 + return 1 + fi + rm -rf "$mig" + local mode for mode in self-test go userplugin sociallogin demo bm fonoteka docker docs all; do grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || { @@ -464,19 +536,37 @@ run_userplugin() { return 1 fi added="$(grep -cE '^\+(func|type) ' <<<"$diff" || true)" + # D-22, D-27: under updates/ only the email_verified_at migration may be + # added (test files aside), and it adds its column and writes nothing. + local refused file writes checked=0 rc=0 - migrations="$(git -C "$USER_PLUGIN" diff --name-only "$USER_BASE" -- updates/ 2>&1)" || rc=$? + 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-only $USER_BASE -- updates/ in $USER_PLUGIN failed (exit $rc)" >&2 + echo "refuse: git diff --name-status $USER_BASE -- updates/ in $USER_PLUGIN failed (exit $rc)" >&2 return 1 fi - if [[ -n "$migrations" ]]; then - printf '%s\n' "$migrations" >&2 - echo "refuse: the user plugin changed files under updates/ since $USER_BASE (D-22: no migration change)" >&2 + 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 (D-22, D-27: only the email_verified_at migration may be added)" >&2 return 1 fi - echo "additive: no exported func, method or type removed and no migration changed in the user plugin since $USER_BASE ($added declarations added)" + for file in "${USER_ADDED_MIGRATIONS[@]}"; do + [[ -f "$USER_PLUGIN/$file" ]] || continue + checked=$((checked + 1)) + if ! grep -q 'ADD COLUMN IF NOT EXISTS email_verified_at' "$USER_PLUGIN/$file"; then + echo "refuse: $file does not add email_verified_at with ADD COLUMN IF NOT EXISTS (D-27)" >&2 + return 1 + fi + writes="$(migration_writes_rows "$USER_PLUGIN/$file")" + if [[ -n "$writes" ]]; then + printf '%s\n' "$writes" >&2 + echo "refuse: $file writes or drops data (D-27: additive, no backfill)" >&2 + return 1 + fi + done + echo "additive: no exported func, method or type removed and no migration changed beyond the allowed additions in the user plugin since $USER_BASE ($added declarations added; $checked allowed migration(s) added, column only)" echo "phase19.5 userplugin passed" }