Add Nix package, NixOS module, and tests for bambuddy

- packages.bambuddy: FastAPI backend + prebuilt React frontend assembled
  into the app-root layout upstream expects, behind a single `bambuddy`
  launcher (uvicorn --loop asyncio, ffmpeg on PATH, XDG state defaults).
  Pins upstream maziggy/bambuddy v1.2.5.7.
- nixosModules.default: services.bambuddy with host/port/dataDir,
  openFirewall, environment + environmentFile, extraPackages,
  DynamicUser + StateDirectory, systemd hardening, and
  CAP_NET_BIND_SERVICE for the virtual-printer FTPS/RTSP ports.
- checks.bambuddy-import: backend import + admin CLI smoke test.
- checks.bambuddy-service: two-node NixOS VM test covering the health
  endpoint, UI/static assets/manifest, SQLite + logs in the
  StateDirectory, journal startup check, cross-node reachability with
  openFirewall, custom host/port/env node, and loopback-only binding.
- overlay, run app, devShell, README, .gitignore.
This commit is contained in:
0m.ax 2026-11-01 19:54:55 +01:00
commit 5fb8e8d724
9 changed files with 761 additions and 0 deletions

92
tests/service-test.nix Normal file
View file

@ -0,0 +1,92 @@
{
lib,
pkgs,
bambuddy,
nixosModule,
}:
# Integration test: boots two NixOS VMs with the bambuddy NixOS module.
#
# * `server` uses the default configuration (port 8000, firewall open) and
# checks that the service starts, the API responds, the prebuilt React
# frontend is served with its static assets, and persistent state
# (SQLite database, logs) lands in the StateDirectory.
# * `custom` exercises the module options (custom host/port, extra
# environment) and verifies the dashboard is reachable cross-node once
# the firewall is opened.
pkgs.testers.runNixOSTest (
{ ... }:
{
name = "bambuddy-service";
nodes = {
server =
{ pkgs, ... }:
{
imports = [ nixosModule ];
services.bambuddy = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.curl ];
};
custom =
{ pkgs, ... }:
{
imports = [ nixosModule ];
services.bambuddy = {
enable = true;
host = "127.0.0.1";
port = 8123;
environment.LOG_LEVEL = "DEBUG";
};
environment.systemPackages = with pkgs; [
curl
iproute2
];
};
};
testScript = ''
server.start()
custom.start()
server.wait_for_unit("bambuddy.service")
server.wait_for_open_port(8000)
with subtest("health endpoint responds"):
server.succeed("curl -fsS http://127.0.0.1:8000/health | grep -q healthy")
with subtest("uvicorn started cleanly"):
server.succeed(
"journalctl -u bambuddy.service --no-pager | grep -q 'Application startup complete'"
)
with subtest("web UI is served from the packaged static build"):
server.succeed("curl -fsS http://127.0.0.1:8000/ | grep -q '<title>Bambuddy</title>'")
server.succeed("curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/img/favicon-32x32.png | grep -q '^200$'")
server.succeed("curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8000/fonts/inter-latin.woff2 | grep -q '^200$'")
server.succeed("curl -fsS http://127.0.0.1:8000/manifest.json | grep -qi bambuddy")
with subtest("persistent state lands in the StateDirectory"):
server.succeed("test -f /var/lib/bambuddy/bambuddy.db")
server.succeed("test -d /var/lib/bambuddy/logs")
with subtest("dashboard is reachable from another machine (openFirewall)"):
custom.succeed("curl -m 10 -fsS http://server:8000/health | grep -q healthy")
with subtest("custom host/port options apply"):
custom.wait_for_unit("bambuddy.service")
custom.wait_for_open_port(8123)
custom.succeed("curl -fsS http://127.0.0.1:8123/health | grep -q healthy")
# host = 127.0.0.1 must not appear on any external address
custom.fail("ss -tln | grep -E '0\\.0\\.0\\.0:8123|\\[::\\]:8123|\\*:8123'")
server.fail("curl -m 5 -fsS http://custom:8123/health")
with subtest("extra environment reaches the service"):
custom.succeed(
"systemctl show bambuddy.service -p Environment | grep -q LOG_LEVEL=DEBUG"
)
'';
}
)