# 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}" ) ''; }