Files
arr-init/tests/naming.nix
Simon Gardling 6dde2a3e0d servarr: add configXml option with preStart hook
Adds services.arrInit.<name>.configXml for declaratively ensuring XML
elements exist in a Servarr config.xml before the service starts.

Generates a preStart hook on the main service that runs a Python helper
to patch or create config.xml. Undeclared elements are preserved;
declared elements are written with exact values.

Primary use case: preventing recurring Prowlarr 'not listening on port'
failures when config.xml loses the <Port> element — now guaranteed to
exist before Prowlarr starts.

Hardening:
- Atomic writes (tmp + rename): power loss cannot corrupt config.xml
- Malformed XML recovery: fresh <Config> root instead of blocking boot
- Secure default mode (0600) for new files containing ApiKey
- Preserves existing file mode on rewrite
- Assertion against duplicate serviceName targeting

Tests (10 subtests): creates-from-missing, patches-existing, preserves-
undeclared, corrects-tampered, idempotent, malformed-recovery,
ownership-preserved, not-world-readable.
2026-04-17 00:45:21 -04:00

69 lines
2.4 KiB
Nix

{
pkgs,
lib,
self,
}:
pkgs.testers.runNixOSTest {
name = "arr-init-naming";
nodes.machine =
{ pkgs, lib, ... }:
{
imports = [ self.nixosModules.default ];
system.stateVersion = "24.11";
virtualisation.memorySize = 4096;
environment.systemPackages = with pkgs; [
curl
jq
gnugrep
];
services.sonarr = {
enable = true;
dataDir = "/var/lib/sonarr/.config/NzbDrone";
settings.server.port = lib.mkDefault 8989;
};
services.arrInit.sonarr = {
enable = true;
serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989;
naming = {
renameEpisodes = true;
standardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}";
seasonFolderFormat = "Season {season}";
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("sonarr.service")
machine.wait_until_succeeds(
"API_KEY=$(grep -oP '(?<=<ApiKey>)[^<]+' /var/lib/sonarr/.config/NzbDrone/config.xml) && "
"curl -sf http://localhost:8989/api/v3/system/status -H \"X-Api-Key: $API_KEY\"",
timeout=120,
)
machine.succeed("systemctl restart sonarr-init.service")
machine.wait_for_unit("sonarr-init.service")
with subtest("Naming configuration was applied"):
machine.succeed(
"API_KEY=$(grep -oP '(?<=<ApiKey>)[^<]+' /var/lib/sonarr/.config/NzbDrone/config.xml) && "
"curl -sf http://localhost:8989/api/v3/config/naming -H \"X-Api-Key: $API_KEY\" | "
"jq -e '.renameEpisodes == true'"
)
machine.succeed(
"API_KEY=$(grep -oP '(?<=<ApiKey>)[^<]+' /var/lib/sonarr/.config/NzbDrone/config.xml) && "
"curl -sf http://localhost:8989/api/v3/config/naming -H \"X-Api-Key: $API_KEY\" | "
"jq -e '.seasonFolderFormat == \"Season {season}\"'"
)
with subtest("Naming idempotency - second run does not change anything"):
machine.succeed("systemctl restart sonarr-init.service")
machine.wait_for_unit("sonarr-init.service")
journal = machine.succeed("journalctl -u sonarr-init.service --no-pager")
assert "already correct" in journal.lower(), \
f"Expected 'already correct' on idempotent run, got: {journal[-500:]}"
'';
}