test: add a master-protection guard, fix three stale assertions

Responds to the independent review of the writer-branch PR.

Adopted (the reviewer's strongest recommendation): a test that PROVES master is
protected, rather than documenting the setting. It attempts a direct push to
master with the deploy key and requires it to be rejected - the same way an
attacker would find out. It currently FAILS, which is the correct result: branch
protection is genuinely not enabled on armistace/blog, and the failure is now
visible in every test run instead of buried in prose.

Cannot fix from here: setting branch protection on armistace/blog returns
 403 "user should be an owner or a collaborator with admin write"
for the bot account (it is a collaborator, not the owner). This is an operator
action, documented in docs/writer-branch-workflow.md with the exact Gitea
settings. Until it is done the writer-branch model is advisory, not enforced.

Also fixed three assertions that were stale rather than wrong:
  - "installer markers present" now accepts the post-install state (config.php
    populated) as valid, since the operator has completed the installer.
  - "posts found == 20" is now ">= 20" - the operator has added a file, so an
    exact count was always going to fail.
  - "branch == master" is now master|atheos-writer - the writer branch is the
    intended steady state under the new workflow, so asserting master was
    asserting the opposite of the design.

Result: 40 passed, 1 failed - the single failure being the branch-protection gap.
This commit is contained in:
2026-09-29 16:09:47 +10:00
parent 1c7db911df
commit 0b72cf48b1
+37 -3
View File
@@ -17,7 +17,14 @@ echo "=============================================================="
CODE=$(wget -qO /tmp/idx.html --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1)
chk "HTTP status for /" "$CODE" "200"
if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then ok "first-run installer/login markers present"; else bad "no installer markers"; fi
# Either state is valid: pre-install shows the installer, post-install shows the login/app.
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then
ok "installer markers present (pre-install state)"
elif [ -f "$W/config.php" ] && [ -s "$W/config.php" ]; then
ok "installer already completed (config.php populated)"
else
bad "neither installer markers nor a populated config.php"
fi
SOCK=$(sed -n 's/^listen = //p' /etc/php83/php-fpm.d/www.conf | head -1)
NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1)
echo " --- socket contract (I-1):"
@@ -29,7 +36,8 @@ echo
echo "=============================================================="
echo "AC-3 'blog' is present with files that can be opened"
echo "=============================================================="
chk "posts found" "$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')" "20"
POSTS=$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')
if [ "$POSTS" -ge 20 ]; then ok "posts found ($POSTS, expected >= 20)"; else bad "posts found ($POSTS, expected >= 20)"; fi
if [ -d "$W/workspace/blog/.git" ]; then ok "it is a git working copy"; else bad "not a git working copy"; fi
chk "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog"
chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos"
@@ -56,7 +64,13 @@ case "$REMOTE" in
gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;;
*) bad "remote does not target the blog repo" ;;
esac
chk "branch" "$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)" "master"
# A repo may legitimately sit on master, or on the long-lived writer branch (docs/writer-branch-workflow.md).
BR=$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)
case "$BR" in
master|main) ok "branch ($BR)" ;;
atheos-writer) ok "branch ($BR - writer-branch workflow)" ;;
*) bad "branch is unexpected: $BR" ;;
esac
# Phase 2: once the SSH alias is in use, prove the transport actually authenticates
case "$REMOTE" in
@@ -92,6 +106,26 @@ for d in mysqld mariadbd postgres redis-server; do
if command -v "$d" >/dev/null 2>&1; then bad "$d present - unexpected"; else ok "$d absent (correct)"; fi
done
echo
echo "=============================================================="
echo "GUARD: is master PROTECTED on the blog repo? (the writer-branch model depends on it)"
echo "=============================================================="
# The whole branch-based workflow assumes the deploy key CANNOT push master. That is a
# repo-side setting we cannot verify from here by inspection - so test it the way an
# attacker would: attempt a direct push to master and require it to be REJECTED.
if [ "${SKIP_MASTER_PUSH_TEST:-0}" = "1" ]; then
echo " SKIPPED (SKIP_MASTER_PUSH_TEST=1)"
else
PUSH_OUT=$(asuser "git -C $B push origin HEAD:master --dry-run" 2>&1)
if echo "$PUSH_OUT" | grep -qiE 'protected|not allowed|denied|forbidden|remote rejected'; then
ok "master is protected - the deploy key CANNOT push it"
else
bad "master is NOT protected - the deploy key could push master directly!"
echo " Enable it: armistace/blog -> Settings -> Branches -> add rule for 'master',"
echo " Enable push = OFF, merge whitelist = armistace. See docs/writer-branch-workflow.md"
fi
fi
echo
echo "=============================================================="
echo "FR-11 GUARD: agent created NO identity or armistace credential"