|
|
|
@@ -0,0 +1,96 @@
|
|
|
|
|
# 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}"
|
|
|
|
|
)
|
|
|
|
|
'';
|
|
|
|
|
}
|