Files
hortusfox-web-nixos/tests/vm-test.nix
T
awkawbandClaude Sonnet 5 38ec3c76f7 Initial import of HortusFox Nix flake packaging
Packages HortusFox from source via php.buildComposerProject and provides
a NixOS module (services.hortusfox-web) to deploy it as a full service
(mariadb + phpfpm + nginx/caddy + ACME + cron timers).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 17:14:22 -04:00

97 lines
4.3 KiB
Nix

# One nixosTest per proxy backend: boots the service, waits for the
# backend-specific web server unit, and exercises both a dynamic (PHP-FPM)
# and a static (nginx/caddy's own) request path. Called once per backend
# from flake.nix's `checks`.
{ pkgs, nixosModule, proxyServer }:
let
# Known plaintext, embedded directly into the curl login POST below --
# this is a throwaway test VM's password, not a real secret.
adminPassword = "correct-horse-battery-staple";
in
pkgs.testers.nixosTest {
name = "hortusfox-web-${proxyServer}";
nodes.machine = {
imports = [ nixosModule ];
services.hortusfox-web = {
enable = true;
proxy.server = proxyServer;
proxy.ssl = false; # no internet/ACME inside the test VM
settings.APP_DEBUG = true; # required for `php asatru migrate:*` to run
adminAccount.passwordFile = pkgs.writeText "hortusfox-test-admin-password" adminPassword;
};
};
testScript = ''
machine.wait_for_unit("mysql.service")
machine.wait_for_unit("hortusfox-setup.service")
machine.wait_for_unit("phpfpm-hortusfox.service")
machine.wait_for_unit("${proxyServer}.service")
machine.wait_for_open_port(80)
# -L: "/" redirects to the login page (/auth) for a logged-out
# user -- following it is what actually exercises the page that
# broke before (a bare "/" 302 response has no exception text
# to catch).
response = machine.succeed("curl -sfL http://localhost/")
# A <400 response alone isn't enough -- Asatru renders uncaught
# exceptions as HTML without failing the HTTP status, so a plain
# `curl -sf` above would happily "succeed" on a broken app (this
# is exactly how the runtime-tree symlink-vs-copy bug slipped
# past this check before). Fail loudly on that page instead.
assert "Asatru PHP - Exception" not in response, (
f"got an Asatru exception page instead of the real site:\n{response}"
)
# Also fetch a real static asset directly (the proxy's own job, not
# PHP-FPM's) -- the login page's background image, which ships in
# the package itself. This is what actually exercises the proxy
# reading into stateDir's writable subdirs (public/img here), as
# opposed to the runtime tree checked above.
machine.succeed("curl -sf http://localhost/img/pattern.jpg -o /dev/null")
# PHP-FPM runs as the hortusfox user and can serve pages even when
# the proxy (a different user) can't read the runtime tree at all --
# so a passing response above doesn't rule this out on its own. Check
# the proxy's own log directly, and only after every request made so
# far, so a permission error on any of them (runtime tree or
# persistent state dir) gets caught here.
machine.fail("journalctl -u ${proxyServer} --no-pager | grep -qi 'permission denied'")
# Exercise the actual point of adminAccount.passwordFile: log in as the
# seeded admin, not just confirm a login form renders. migrate:fresh
# alone creates no user rows, so this is the only way any account gets
# to exist at all -- worth proving it actually works end to end.
machine.succeed(
"curl -sc /tmp/cookies -o /tmp/auth.html -L http://localhost/"
)
csrf_token = machine.succeed(
"sed -n 's/.*name=\"csrf_token\" value=\"\([^\"]*\)\".*/\\1/p' /tmp/auth.html | head -1"
).strip()
assert csrf_token, "could not find a csrf_token on the /auth page"
# -w appends the final URL after a marker, so we can tell *where* we
# landed -- the dashboard itself also has a (normally hidden) change-
# password form in its markup, so checking for a password input isn't
# a reliable way to tell it apart from the login page.
raw = machine.succeed(
"curl -sb /tmp/cookies -c /tmp/cookies -L -w '\\n===URL===%{url_effective}' "
"--data-urlencode email=admin@localhost "
"--data-urlencode password=${adminPassword} "
f"--data-urlencode csrf_token={csrf_token} "
"--data-urlencode redirect=/ "
"http://localhost/login"
)
login_response, final_url = raw.rsplit("\n===URL===", 1)
assert "Asatru PHP - Exception" not in login_response, (
f"login crashed instead of succeeding:\n{login_response}"
)
assert "/auth" not in final_url, (
f"login didn't redirect away from /auth -- login failed (landed on {final_url}):\n{login_response}"
)
'';
}