tests: exercise both nginx and caddy proxy backends
Parameterizes tests/common.nix and tests/default.nix on proxyServer (default "nginx"), giving flake.nix two nixosConfigurations (test-nginx/test-caddy) and `just build-vm [proxy]` a way to pick between them for interactive testing. The automated nixosTest now boots one node per proxy backend and runs the full assertion suite (factored into check_invoiceninja()) against each, so a regression in either nginx or caddy's config gets caught. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,8 @@ editing Nix expressions.
|
|||||||
|
|
||||||
This project uses `just` (see `justfile`) and Nix directly.
|
This project uses `just` (see `justfile`) and Nix directly.
|
||||||
|
|
||||||
- `just build-vm` — cleans previous artifacts and builds the test VM (`nixosConfigurations.test`) via `nom build`.
|
- `just build-vm [proxy]` — cleans previous artifacts and builds the test VM (`nixosConfigurations.test-<proxy>`)
|
||||||
|
via `nom build`. `proxy` is `nginx` (default) or `caddy`.
|
||||||
- `just boot-vm` — boots the built VM (`result/bin/run-nixos-vm`) in the current terminal. Serves Invoice Ninja on
|
- `just boot-vm` — boots the built VM (`result/bin/run-nixos-vm`) in the current terminal. Serves Invoice Ninja on
|
||||||
`localhost:8080` (see `QEMU_NET_OPTS` in the `justfile`) with root login `toor`.
|
`localhost:8080` (see `QEMU_NET_OPTS` in the `justfile`) with root login `toor`.
|
||||||
- `just clean` — removes `result`/`nixos.qcow2` and kills any running `qemu` process.
|
- `just clean` — removes `result`/`nixos.qcow2` and kills any running `qemu` process.
|
||||||
@@ -28,10 +29,12 @@ This project uses `just` (see `justfile`) and Nix directly.
|
|||||||
### `tests/`
|
### `tests/`
|
||||||
|
|
||||||
- `common.nix` — the shared `services.invoiceninja` config used by both the interactive VM and the automated test:
|
- `common.nix` — the shared `services.invoiceninja` config used by both the interactive VM and the automated test:
|
||||||
imports the module, enables nginx, and points at the checked-in test fixtures (`invoice_ninja_test_password`,
|
imports the module and points at the checked-in test fixtures (`invoice_ninja_test_password`, `test.env`). It's a
|
||||||
`test.env`).
|
function taking `{ proxyServer ? "nginx" }` so callers can pick which proxy backend to test.
|
||||||
- `default.nix` — the interactive VM config (`nixosConfigurations.test` in `flake.nix`), used for manual,
|
- `default.nix` — the interactive VM config (`nixosConfigurations.test-nginx`/`test-caddy` in `flake.nix`), used
|
||||||
click-around verification via `just build-vm` / `just boot-vm`. Adds the qemu-guest profile, a root login, and
|
for manual, click-around verification via `just build-vm [proxy]` / `just boot-vm`. It's also a function taking
|
||||||
|
`{ proxyServer ? "nginx" }`, forwarded straight through to `common.nix`, so `flake.nix` can instantiate one
|
||||||
|
`nixosConfigurations` entry per proxy backend. Adds the qemu-guest profile, a root login, and
|
||||||
`nixpkgs.config.allowUnfree = true` (required to build `pkgs.chromium`). It also sets a dummy `fileSystems."/"`
|
`nixpkgs.config.allowUnfree = true` (required to build `pkgs.chromium`). It also sets a dummy `fileSystems."/"`
|
||||||
and disables `boot.loader.grub` purely to satisfy NixOS's built-in assertions when `nix flake check` evaluates
|
and disables `boot.loader.grub` purely to satisfy NixOS's built-in assertions when `nix flake check` evaluates
|
||||||
`config.system.build.toplevel` directly — the qemu-vm module overrides both with a tmpfs root and direct kernel
|
`config.system.build.toplevel` directly — the qemu-vm module overrides both with a tmpfs root and direct kernel
|
||||||
@@ -39,12 +42,15 @@ This project uses `just` (see `justfile`) and Nix directly.
|
|||||||
unused.
|
unused.
|
||||||
- `nixos-test.nix` — the automated test (`pkgs.testers.nixosTest`, exposed as `checks.<system>.invoiceninja` in
|
- `nixos-test.nix` — the automated test (`pkgs.testers.nixosTest`, exposed as `checks.<system>.invoiceninja` in
|
||||||
`flake.nix`). Builds its own `pkgs` (with `allowUnfree` baked in — `nixosTest` forbids setting `nixpkgs.config`
|
`flake.nix`). Builds its own `pkgs` (with `allowUnfree` baked in — `nixosTest` forbids setting `nixpkgs.config`
|
||||||
on a node once `pkgs` is externally supplied) and boots a VM with `common.nix`, then: waits for
|
on a node once `pkgs` is externally supplied) and boots two nodes from `common.nix` — one with
|
||||||
mysql/redis/phpfpm/queue-worker/nginx to be active, checks the `invoiceninja-data-setup` marker files
|
`proxyServer = "nginx"`, one with `proxyServer = "caddy"` — so both proxy backends get exercised. The shared
|
||||||
(`.initial-migration`, `.db-seeded`, `.admin-created`) to confirm the one-time setup actually completed (not just
|
assertion suite (factored into a `check_invoiceninja(machine, proxy_unit)` function in the test script) runs
|
||||||
that the oneshot unit exited), curls `/` for the app shell, exercises the `invoiceninja-manage` wrapper, and logs
|
against each node: waits for mysql/redis/phpfpm/queue-worker/proxy to be active, checks the
|
||||||
in via `/api/v1/login` using the test admin credentials as an end-to-end check that migrations/seeding/account
|
`invoiceninja-data-setup` marker files (`.initial-migration`, `.db-seeded`, `.admin-created`) to confirm the
|
||||||
creation all worked. This is the only automated test in the repo — there is no separate unit-test suite.
|
one-time setup actually completed (not just that the oneshot unit exited), curls `/` for the app shell, exercises
|
||||||
|
the `invoiceninja-manage` wrapper, and logs in via `/api/v1/login` using the test admin credentials as an
|
||||||
|
end-to-end check that migrations/seeding/account creation all worked. This is the only automated test in the
|
||||||
|
repo — there is no separate unit-test suite.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -108,8 +114,9 @@ expect Nix to report the correct hash on a failed build (or use `nix-prefetch`/`
|
|||||||
### `flake.nix`
|
### `flake.nix`
|
||||||
|
|
||||||
Standard flake shape: `overlays.default` registers the package, `packages.<system>.invoiceninja` exposes it,
|
Standard flake shape: `overlays.default` registers the package, `packages.<system>.invoiceninja` exposes it,
|
||||||
`nixosModules` re-exports `nixos-module/default.nix`, and `nixosConfigurations.test` builds the VM defined in
|
`nixosModules` re-exports `nixos-module/default.nix`, and `nixosConfigurations.test-nginx`/`test-caddy` each build
|
||||||
`tests/`. Only `x86_64-linux` is currently supported.
|
the VM defined in `tests/default.nix` with the corresponding `proxyServer`. Only `x86_64-linux` is currently
|
||||||
|
supported.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,15 @@
|
|||||||
);
|
);
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
nixosConfigurations.test = nixpkgs.lib.nixosSystem {
|
nixosConfigurations = {
|
||||||
system = "x86_64-linux";
|
test-nginx = nixpkgs.lib.nixosSystem {
|
||||||
modules = [ ./tests ];
|
system = "x86_64-linux";
|
||||||
|
modules = [ (import ./tests/default.nix { proxyServer = "nginx"; }) ];
|
||||||
|
};
|
||||||
|
test-caddy = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [ (import ./tests/default.nix { proxyServer = "caddy"; }) ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
overlays.default = final: prev: { invoiceninja = final.callPackage ./package.nix { }; };
|
overlays.default = final: prev: { invoiceninja = final.callPackage ./package.nix { }; };
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ clean:
|
|||||||
pkill qemu
|
pkill qemu
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
[doc('Build virtual machine for testing')]
|
[doc('Build virtual machine for testing (proxy: nginx or caddy)')]
|
||||||
[group('main')]
|
[group('main')]
|
||||||
build-vm: clean
|
build-vm proxy="nginx": clean
|
||||||
nom build ".#nixosConfigurations.test.config.system.build.vm"
|
nom build ".#nixosConfigurations.test-{{proxy}}.config.system.build.vm"
|
||||||
|
|
||||||
[doc('Run virtual machine in current terminal')]
|
[doc('Run virtual machine in current terminal')]
|
||||||
[group('main')]
|
[group('main')]
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
{ proxyServer ? "nginx" }:
|
||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -5,7 +6,7 @@
|
|||||||
|
|
||||||
services.invoiceninja = {
|
services.invoiceninja = {
|
||||||
enable = true;
|
enable = true;
|
||||||
proxy.server = "nginx";
|
proxy.server = proxyServer;
|
||||||
adminAccount.passwordFile = ./invoice_ninja_test_password;
|
adminAccount.passwordFile = ./invoice_ninja_test_password;
|
||||||
secretFile = ./test.env;
|
secretFile = ./test.env;
|
||||||
settings.APP_DEBUG = true;
|
settings.APP_DEBUG = true;
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
|
{ proxyServer ? "nginx" }:
|
||||||
{ pkgs, modulesPath, ... }:
|
{ pkgs, modulesPath, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
(modulesPath + "/profiles/qemu-guest.nix")
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
./common.nix
|
(import ./common.nix { inherit proxyServer; })
|
||||||
];
|
];
|
||||||
|
|
||||||
# Required to build pkgs.chromium, which the module pulls in for PDF generation.
|
# Required to build pkgs.chromium, which the module pulls in for PDF generation.
|
||||||
|
|||||||
+46
-39
@@ -15,54 +15,61 @@ in
|
|||||||
pkgs.testers.nixosTest {
|
pkgs.testers.nixosTest {
|
||||||
name = "invoiceninja";
|
name = "invoiceninja";
|
||||||
|
|
||||||
nodes.machine = {
|
nodes = {
|
||||||
imports = [ ./common.nix ];
|
nginx.imports = [ (import ./common.nix { proxyServer = "nginx"; }) ];
|
||||||
|
caddy.imports = [ (import ./common.nix { proxyServer = "caddy"; }) ];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
machine.start()
|
def check_invoiceninja(machine, proxy_unit):
|
||||||
|
machine.wait_for_unit("mysql.service")
|
||||||
|
machine.wait_for_unit("redis-invoiceninja.service")
|
||||||
|
machine.wait_for_unit("phpfpm-invoiceninja.service")
|
||||||
|
machine.wait_for_unit("invoiceninja-queue-worker.service")
|
||||||
|
machine.wait_for_unit(proxy_unit)
|
||||||
|
machine.wait_for_open_port(80)
|
||||||
|
|
||||||
machine.wait_for_unit("mysql.service")
|
# phpfpm-invoiceninja/invoiceninja-queue-worker both Require= invoiceninja-data-setup,
|
||||||
machine.wait_for_unit("redis-invoiceninja.service")
|
# so systemd already waited for it to finish above. Confirm the one-time setup steps
|
||||||
machine.wait_for_unit("phpfpm-invoiceninja.service")
|
# it guards actually completed, rather than just that the unit exited.
|
||||||
machine.wait_for_unit("invoiceninja-queue-worker.service")
|
with subtest("one-time setup (migrate, seed, admin account) completed"):
|
||||||
machine.wait_for_unit("nginx.service")
|
machine.succeed("test -f /var/lib/invoiceninja/.initial-migration")
|
||||||
machine.wait_for_open_port(80)
|
machine.succeed("test -f /var/lib/invoiceninja/.db-seeded")
|
||||||
|
machine.succeed("test -f /var/lib/invoiceninja/.admin-created")
|
||||||
|
|
||||||
# phpfpm-invoiceninja/invoiceninja-queue-worker both Require= invoiceninja-data-setup,
|
with subtest("application shell is served over HTTP"):
|
||||||
# so systemd already waited for it to finish above. Confirm the one-time setup steps
|
response = machine.succeed("curl -sf http://localhost/")
|
||||||
# it guards actually completed, rather than just that the unit exited.
|
assert "Invoice Ninja" in response, f"unexpected response from /: {response}"
|
||||||
with subtest("one-time setup (migrate, seed, admin account) completed"):
|
|
||||||
machine.succeed("test -f /var/lib/invoiceninja/.initial-migration")
|
|
||||||
machine.succeed("test -f /var/lib/invoiceninja/.db-seeded")
|
|
||||||
machine.succeed("test -f /var/lib/invoiceninja/.admin-created")
|
|
||||||
|
|
||||||
with subtest("application shell is served over HTTP"):
|
with subtest("web UI login page is served"):
|
||||||
response = machine.succeed("curl -sf http://localhost/")
|
# The UI is a client-side-routed React SPA, so curl (no JS) only sees the same
|
||||||
assert "Invoice Ninja" in response, f"unexpected response from /: {response}"
|
# index.html shell as "/" - this confirms the proxy/Laravel resolve the /login route
|
||||||
|
# to that shell instead of 404ing, which is what a browser needs before it can
|
||||||
|
# render the actual login form.
|
||||||
|
login_page = machine.succeed("curl -sf http://localhost/login")
|
||||||
|
assert "Invoice Ninja" in login_page, f"unexpected response from /login: {login_page}"
|
||||||
|
|
||||||
with subtest("web UI login page is served"):
|
with subtest("invoiceninja-manage wrapper can run artisan commands"):
|
||||||
# The UI is a client-side-routed React SPA, so curl (no JS) only sees the same
|
machine.succeed("invoiceninja-manage migrate:status")
|
||||||
# index.html shell as "/" - this confirms nginx/Laravel resolve the /login route
|
|
||||||
# to that shell instead of 404ing, which is what a browser needs before it can
|
|
||||||
# render the actual login form.
|
|
||||||
login_page = machine.succeed("curl -sf http://localhost/login")
|
|
||||||
assert "Invoice Ninja" in login_page, f"unexpected response from /login: {login_page}"
|
|
||||||
|
|
||||||
with subtest("invoiceninja-manage wrapper can run artisan commands"):
|
with subtest("scheduler timer is enabled and schedule:run succeeds"):
|
||||||
machine.succeed("invoiceninja-manage migrate:status")
|
# Without this, the app's dashboard reports "cron jobs are not enabled".
|
||||||
|
machine.succeed("systemctl is-active invoiceninja-scheduler.timer")
|
||||||
|
machine.succeed("systemctl start --wait invoiceninja-scheduler.service")
|
||||||
|
|
||||||
with subtest("scheduler timer is enabled and schedule:run succeeds"):
|
with subtest("admin account can log in via the API"):
|
||||||
# Without this, the app's dashboard reports "cron jobs are not enabled".
|
login_response = machine.succeed(
|
||||||
machine.succeed("systemctl is-active invoiceninja-scheduler.timer")
|
"curl -sf -X POST http://localhost/api/v1/login "
|
||||||
machine.succeed("systemctl start --wait invoiceninja-scheduler.service")
|
+ "-H 'Content-Type: application/json' "
|
||||||
|
+ "-d '{\"email\":\"${adminEmail}\",\"password\":\"${adminPassword}\"}'"
|
||||||
|
)
|
||||||
|
assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}"
|
||||||
|
|
||||||
with subtest("admin account can log in via the API"):
|
|
||||||
login_response = machine.succeed(
|
nginx.start()
|
||||||
"curl -sf -X POST http://localhost/api/v1/login "
|
caddy.start()
|
||||||
+ "-H 'Content-Type: application/json' "
|
|
||||||
+ "-d '{\"email\":\"${adminEmail}\",\"password\":\"${adminPassword}\"}'"
|
check_invoiceninja(nginx, "nginx.service")
|
||||||
)
|
check_invoiceninja(caddy, "caddy.service")
|
||||||
assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}"
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user