Exercises the chromium/snappdf pipeline via POST /api/v1/preview with no entity, which falls back to PdfMock's self-contained mock PDF - verified this needs no client/invoice test data, since a fresh account has neither. Asserts the response starts with the %PDF magic bytes. This was previously the biggest untested path in the module: an actual binary runtime dependency (chromium) rather than just config, and the kind of thing that silently breaks in a minimal NixOS closure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
5.0 KiB
Nix
102 lines
5.0 KiB
Nix
{ nixpkgsPath, system }:
|
|
|
|
let
|
|
# nixosTest forbids setting `nixpkgs.config` on a node once `pkgs` is externally supplied,
|
|
# so allowUnfree (required to build pkgs.chromium) has to be baked into this pkgs instance
|
|
# up front rather than set from within common.nix like the interactive VM does.
|
|
pkgs = import nixpkgsPath {
|
|
inherit system;
|
|
config.allowUnfree = true;
|
|
};
|
|
|
|
adminEmail = "admin@email.com";
|
|
adminPassword = pkgs.lib.strings.trim (builtins.readFile ./invoice_ninja_test_password);
|
|
in
|
|
pkgs.testers.nixosTest {
|
|
name = "invoiceninja";
|
|
|
|
nodes = {
|
|
nginx.imports = [ (import ./common.nix { proxyServer = "nginx"; }) ];
|
|
caddy.imports = [ (import ./common.nix { proxyServer = "caddy"; }) ];
|
|
};
|
|
|
|
testScript = ''
|
|
import json
|
|
|
|
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)
|
|
|
|
# phpfpm-invoiceninja/invoiceninja-queue-worker both Require= invoiceninja-data-setup,
|
|
# so systemd already waited for it to finish above. Confirm the one-time setup steps
|
|
# it guards actually completed, rather than just that the unit exited.
|
|
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"):
|
|
response = machine.succeed("curl -sf http://localhost/")
|
|
assert "Invoice Ninja" in response, f"unexpected response from /: {response}"
|
|
|
|
with subtest("web UI login page is served"):
|
|
# The UI is a client-side-routed React SPA, so curl (no JS) only sees the same
|
|
# 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("invoiceninja-manage wrapper can run artisan commands"):
|
|
machine.succeed("invoiceninja-manage migrate:status")
|
|
|
|
with subtest("scheduler timer is enabled and schedule:run succeeds"):
|
|
# 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("admin account can log in via the API"):
|
|
login_response = machine.succeed(
|
|
"curl -sf -X POST http://localhost/api/v1/login "
|
|
+ "-H 'Content-Type: application/json' "
|
|
+ "-d '{\"email\":\"${adminEmail}\",\"password\":\"${adminPassword}\"}'"
|
|
)
|
|
assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}"
|
|
|
|
with subtest("full language list is available, not just the af/ca/id migration's 3"):
|
|
# Regression check: migration 2025_08_31_224356_additional_languages_af_ca_id.php runs
|
|
# during `migrate`, before db:seed's LanguageSeeder has populated the other 42 rows, and
|
|
# does Cache::forever('languages', Language::all()) - permanently caching just the 3
|
|
# languages it sees at that moment. invoiceninja-data-setup works around this with
|
|
# `invoiceninja-manage cache:forget languages`; this confirms the workaround is in place.
|
|
token = json.loads(login_response)["data"][0]["token"]["token"]
|
|
statics = machine.succeed(
|
|
f"curl -sf http://localhost/api/v1/statics -H 'X-API-TOKEN: {token}'"
|
|
)
|
|
language_count = len(json.loads(statics)["languages"])
|
|
assert language_count > 40, f"expected ~45 languages, got {language_count}"
|
|
|
|
with subtest("PDF generation works (chromium/snappdf)"):
|
|
# /api/v1/preview with no entity falls back to a self-contained mock PDF (no invoice/client
|
|
# setup needed), exercising the same chromium/snappdf pipeline real invoice PDFs use.
|
|
machine.succeed(
|
|
"curl -sf -X POST http://localhost/api/v1/preview "
|
|
+ f"-H 'X-API-TOKEN: {token}' -H 'Content-Type: application/json' "
|
|
+ "-d '{}' -o /tmp/preview.pdf"
|
|
)
|
|
magic_bytes = machine.succeed("head -c 4 /tmp/preview.pdf")
|
|
assert magic_bytes == "%PDF", f"expected a PDF, got: {magic_bytes!r}"
|
|
|
|
|
|
nginx.start()
|
|
caddy.start()
|
|
|
|
check_invoiceninja(nginx, "nginx.service")
|
|
check_invoiceninja(caddy, "caddy.service")
|
|
'';
|
|
}
|