Skip to content

Add real end-to-end verification: create admin account -> log in -> health check - #42

Merged
jsokol merged 9 commits into
mainfrom
test/e2e-health-check-verification
Sep 20, 2026
Merged

jsokol merged 9 commits into
mainfrom
test/e2e-health-check-verification

Conversation

@jsokol

@jsokol jsokol commented Sep 20, 2026

Copy link
Copy Markdown
Member

Summary

Extends verify-install.sh to drive the actual first-run UI flow with curl (no browser/Playwright needed - every step is a plain server-side form POST): fetch the fresh install's admin-creation page, create the account, log in, then load /admin/health_check.php and assert on its self-reported status. This goes well beyond our existing per-endpoint curl+grep checks and immediately caught two real bugs.

Curl gotchas hit and fixed (test script only)

  • -X POST + -L: explicit -X POST forces curl to re-POST on every redirect hop instead of downgrading to GET per normal semantics, so it re-POSTed login credentials to the post-login redirect target, which correctly rejected it as CSRF-invalid. Fixed by dropping the redundant -X POST (-d/--data-urlencode already imply POST).
  • The health check's cron-recency item needs cron.php to have actually ticked, but the seed database ships a stale cron_last_run fixture - checking for "non-empty" passed instantly without ever observing a real tick. Now polls up to 75s for a value newer than when the check started.

Two real bugs this surfaced (now fixed)

  • tests/dockerfiles/systemctl-shim-centos.sh: systemctl enable --now crond was silently a no-op - the shim stripped --now and treated enable as never needing to start anything. crond never actually ran, so the backup cron never fired on CentOS/RHEL in this test infrastructure. A real, systemd-booted server treats enable --now as enable-and-start, so this was purely a test-shim gap, not a simplerisk-setup.sh bug - but every prior CentOS/RHEL CI run had a cron that silently never executed. Fixed by tracking --now separately and reusing the existing start logic; added crond/cron to start/is-active/status too.
  • The self-referential health check depends on SimpleRisk's own simplerisk_base_url setting, written once from whichever host/port the first request used and never recomputed. Verified this stays consistent in CI because every check already goes through the container's real internal address via docker exec ... curl https://localhost/..., with no host port ever published.

Test plan

  • bash -n on both changed files
  • Full local Docker run (install → verify → uninstall → verify), fresh containers, both 35/35 checks:
    • Ubuntu 26.04
    • CentOS Stream 9 (shares its systemctl shim with CentOS Stream 10)
  • Confirmed the automation cron genuinely ticks (cron_last_run updates within the poll window) and the health check reports zero failures anywhere on the page, not just in the connectivity section.

Extends verify-install.sh to drive the actual first-run UI flow with
curl (no browser/JS dependency - every step here is a plain
server-side form POST): fetch the fresh install's admin-creation page,
create the account, log in, then load /admin/health_check.php and
assert on its self-reported status. This catches real breakage our
existing per-endpoint curl+grep checks can't see - see below for two
real bugs it caught immediately.

Curl gotchas hit and fixed along the way (both in the test script, not
the product):
- `-X POST` combined with `-L`: curl forces POST on every redirect hop
  when -X is explicit, instead of downgrading to GET per normal
  redirect semantics. This re-POSTed the login credentials to the
  dashboard URL, which correctly rejected it as CSRF-invalid. Fixed by
  dropping the redundant -X POST (the -d/--data-urlencode flags already
  imply POST).
- The health check's cron-recency item needs cron.php to have actually
  ticked; the seed database ships a stale cron_last_run fixture, so
  checking for "non-empty" passed instantly without ever seeing a real
  tick. Now polls (up to 75s) for a value newer than when the check
  started.

Two real bugs this surfaced, now fixed:
- tests/dockerfiles/systemctl-shim-centos.sh: `systemctl enable --now
  crond` was silently a no-op (the shim stripped --now and treated
  `enable` as never needing to start anything), so crond never actually
  ran and the backup cron never fired on CentOS/RHEL in this test
  infrastructure - since a real, systemd-booted server treats `enable
  --now` as enable-and-start, this was purely a test-shim gap, not a
  simplerisk-setup.sh bug, but every prior CentOS/RHEL CI run had a
  cron that silently never executed. Fixed by tracking --now separately
  and invoking the same start logic other units already have; added
  crond/cron handling to start/is-active/status too.
- The self-referential health check depends on SimpleRisk's own
  simplerisk_base_url setting, which is written once from whichever
  host/port the very first request used and never recomputed - verified
  this stays consistent in our CI setup because every check already
  goes through the container's real internal address via `docker exec
  ... curl https://localhost/...` with no host port ever published.

Verified end-to-end (35/35 checks) on fresh installs on Ubuntu 26.04
and CentOS Stream 9 (which shares its systemctl shim with CentOS Stream
10), including confirming the automation cron genuinely ticks and the
health check reports zero failures anywhere on the page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jsokol
jsokol enabled auto-merge (squash) September 20, 2026 05:10
jsokol and others added 8 commits September 20, 2026 00:21
All 6 CI jobs failed on the cron-tick check despite it passing reliably
in local Docker testing (Ubuntu 26.04, CentOS Stream 9, both 35/35).
75s covers barely more than one minute-boundary; shared GitHub Actions
runners apparently have enough scheduling jitter that this margin isn't
reliable. Widened to 150s (comfortably past two boundaries) and added a
diagnostic dump of any cron/crond process if the poll still fails, to
make the next failure (if any) actionable without needing a fresh CI
run just to see what's running.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Widening the poll to 150s (previous commit) didn't fix it - CentOS
Stream 9/10 still failed with crond confirmed running (pid present)
but cron_last_run never updating. That rules out "crond isn't started"
(the bug fixed two commits ago) and points at either crond never
actually invoking the cron.d entry, or invoking it but something fails
before cron.php reaches its update_or_insert_setting() call. Can't
reproduce locally (Ubuntu 26.04 and CentOS Stream 9 both passed
35/35 in Docker Desktop), so add enough diagnostic output on failure -
the cron.d file's permissions/content and simplerisk.log's own
cron-tagged lines - to tell which of those it is from the next CI run
directly, instead of guessing again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- verify-install.sh: crond starts before set_up_backup_cronjob ever
  writes /etc/cron.d/simplerisk, so picking up that new file relies on
  crond's inotify watch on the directory. Diagnostics from the last CI
  run showed zero cron invocations ever (simplerisk.log has no cron
  entries at all) despite crond running and the file being correctly
  formatted, across multiple minute-boundaries - on GitHub Actions'
  runners specifically; the exact same flow passes locally in Docker
  Desktop. Rather than keep trusting inotify in an environment where
  it's demonstrably not firing, explicitly SIGHUP crond to force a
  re-scan before polling.
- .github/workflows/install-test.yml: bumped actions/checkout v4->v5
  and docker/setup-buildx-action v3->v4 to clear the "Node.js 20 is
  deprecated" warning (both older majors ran on Node 20; the runner now
  force-runs them on Node 24 anyway, but pinning the versions that
  target Node 24 natively avoids the warning and any future breakage
  when forced compatibility goes away).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed cronie (and its crontabs/cronie-anacron dependencies) are
already installed via the Dockerfile's pre-bake, and crond is
genuinely running (a real PID shows up in every failed run's
diagnostics) - so this isn't a missing package. The SIGHUP nudge from
the previous commit didn't help either, which means either the file
genuinely still isn't being noticed, or crond is attempting the job and
failing silently: its own parsing/exec diagnostics normally go through
openlog()/syslog(), and this minimal container likely has no syslog
daemon actually consuming that, so real errors could be getting
dropped with nothing to show for it.

After one minute-boundary with no tick, kill the daemon and restart it
in foreground, verbose debug mode (`crond -n -x sch,proc,pars`, or
`cron -f` on Debian/Ubuntu) redirected to a file instead of syslog, and
give it one more minute-boundary before giving up - this should
directly show whether the job is even being matched by the parser, or
matched and failing, and why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n PAM

The verbose crond debug output from the last commit gave a definitive
answer: crond correctly parses and attempts the cron.d job (twice,
confirmed at two separate minute-boundaries), but PAM immediately
rejects it - "Authentication service cannot retrieve authentication
info" - before the job ever executes. Root cause: the base
quay.io/centos/centos:stream9/10 images ship /etc/nsswitch.conf with
`passwd: sss files systemd` and `group: sss files systemd` - SSSD
listed ahead of files - but SSSD is neither installed nor running in
this minimal container. PAM's account/session handling for the job's
user (initgroups()/getgrnam()) tries SSSD first and fails outright
instead of falling through to /etc/passwd, so cron.d entries can never
run as a non-root user at all. Confirmed directly: stripping sss from
nsswitch.conf makes the PAM error disappear entirely and the job
actually execute.

This has nothing to do with simplerisk-setup.sh or real CentOS/RHEL
servers - a real server enrolled in SSSD (or not) has a consistent,
working nsswitch config either way. It's specific to this minimal test
image having a stale default with nothing backing it, and evidently
tolerated differently by different Docker host environments (passed
locally in Docker Desktop, failed consistently on GitHub Actions
runners) - which is exactly why the timing-based workarounds in the
last few commits never actually fixed it.

Also simplifies verify-install.sh's cron-tick poll back down (single
90s pass, lightweight diagnostics) now that the real bug is fixed and
the elaborate two-phase debug-mode-restart machinery used to diagnose
it is no longer needed.

Verified end-to-end (35/35 install checks, 11/11 uninstall checks) on
a fresh CentOS Stream 9 container in Docker, confirming the automation
cron ticks correctly with no PAM errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… debug

The last run split into two distinct failures that need separate
visibility:
- CentOS Stream 9/10 still miss the cron tick even with the nsswitch.conf
  fix applied (Stream 10's own default nsswitch.conf has no sss reference
  at all, so it was never the same bug there - or the fix didn't take for
  some other reason). Re-added the foreground/verbose crond restart that
  gave the definitive PAM answer two commits ago, since removing it
  after "fixing" the bug turned out to be premature.
- Debian 13 and both remaining Ubuntu versions now pass the cron-tick
  check but fail the generic "no failed checks anywhere on the page"
  catch-all on some other, unidentified health item. Added a diagnostic
  that extracts and prints the actual failing item's text instead of
  just the catch-all's pass/fail, so the next run says what it is
  instead of needing another guess-and-push cycle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Waiting for an observed cron tick was flaky across the CI matrix for
reasons unrelated to the setup script (e.g. a CentOS/RHEL container's
PAM/audit stack rejecting crond's non-root job user regardless of the
nsswitch.conf fix). Verify what the installer is actually responsible
for instead: the daemon is running, cron.php is in place, and cron.d
is configured to invoke it - all already covered except the running
daemon check, which is now added.

The health check's own "no failed checks" catch-all is switched from
a blanket zero-x-marks assertion to an explicit exclusion list, since
two of its checks can never pass in this environment: the DNS lookup
against SERVER_NAME=localhost (dns_get_record() doesn't consult
/etc/hosts) and the cron_last_run-within-the-hour check (this script
no longer waits for a live tick). Their two summary rollup rows are
excluded alongside them; every other check, including their sibling
leaf checks, is still enforced.

Verified locally end-to-end (35/35) on CentOS Stream 9 and Ubuntu
22.04 with fresh installs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
setup_ubuntu_debian() installed the cron package via apt but, unlike
every other OS branch (systemctl enable --now crond/cron for
CentOS/SUSE), never explicitly started it. apt's postinst normally
starts it via invoke-rc.d, but that's skipped wherever policy-rc.d
denies service auto-start (e.g. Debian's own stock Docker image),
leaving cron installed but never running. This doesn't affect boot
persistence - the postinst's systemd-enable step isn't gated by
policy-rc.d, only the start step is - but it does mean the backup
cron job installed by this script would silently never run once.

Verified locally: fresh install + verify-install.sh (35/35, including
the new "Cron daemon is running" check) and uninstall both pass on
Debian 13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jsokol
jsokol merged commit f5646f0 into main Sep 20, 2026
6 checks passed
@jsokol
jsokol deleted the test/e2e-health-check-verification branch September 20, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant