An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add test plan for MM-135 NixOS module

authored by

Malpercio and committed by
Tangled
9d098e76 f4499237

+111
+111
docs/test-plans/2026-03-09-MM-135.md
··· 1 + # Human Test Plan: MM-135 NixOS Module for Relay Deployment 2 + 3 + Generated from test-analyst review of implementation plan `docs/implementation-plans/2026-03-09-MM-135/`. 4 + 5 + **Automated coverage:** 18/18 acceptance criteria verified by `nix eval` smoke tests and structural checks. 6 + 7 + **This plan covers:** Manual verification steps for criteria that require a Linux builder, runtime testing, or human code review. 8 + 9 + --- 10 + 11 + ## Prerequisites 12 + 13 + - NixOS system or VM available for runtime testing (Linux required for E2E phases) 14 + - Development shell activated: `nix develop --impure --accept-flake-config` 15 + - All Phase 3 smoke tests passing (run commands from `docs/implementation-plans/2026-03-09-MM-135/phase_03.md` Tasks 1–5) 16 + - `just nix-check` exits 0 17 + 18 + --- 19 + 20 + ## Phase 1: TOML Content Verification (Linux Only) 21 + 22 + | Step | Action | Expected | 23 + |------|--------|----------| 24 + | 1.1 | On a Linux builder, run: `nix eval --impure --accept-flake-config --raw --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; execStart = sys.config.systemd.services.ezpds.serviceConfig.ExecStart; configPath = builtins.elemAt (builtins.match ".* --config (.*)" execStart) 0; in builtins.readFile configPath'` | Output is valid TOML containing exactly: `bind_address = "0.0.0.0"`, `data_dir = "/var/lib/ezpds"`, `port = 8080`, `public_url = "https://relay.example.com"`. No `database_url` key. No `[blobs]`, `[oauth]`, or `[iroh]` sections. | 25 + | 1.2 | Repeat step 1.1 but add `services.ezpds.settings.database_url = "sqlite:///var/lib/ezpds/custom.db";` to the module config | Output TOML additionally contains `database_url = "sqlite:///var/lib/ezpds/custom.db"`. | 26 + 27 + --- 28 + 29 + ## Phase 2: Group and StateDirectory Eval Verification 30 + 31 + | Step | Action | Expected | 32 + |------|--------|----------| 33 + | 2.1 | Run: `nix eval --impure --accept-flake-config --json --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in builtins.hasAttr "ezpds" sys.config.users.groups'` | Output: `true` | 34 + | 2.2 | Run: `nix eval --impure --accept-flake-config --raw --expr 'let flake = builtins.getFlake (builtins.toString ./.); evalNixpkgs = builtins.getFlake "nixpkgs"; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ flake.nixosModules.default { services.ezpds.enable = true; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in sys.config.systemd.services.ezpds.serviceConfig.StateDirectory'` | Output: `ezpds` | 35 + 36 + --- 37 + 38 + ## Phase 3: Bare Module Success Path 39 + 40 + | Step | Action | Expected | 41 + |------|--------|----------| 42 + | 3.1 | Run: `nix eval --impure --accept-flake-config --raw --expr 'let evalNixpkgs = builtins.getFlake "nixpkgs"; pkgs = import evalNixpkgs { system = "x86_64-linux"; }; sys = evalNixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ (import ./nix/module.nix) { services.ezpds.enable = true; services.ezpds.package = pkgs.hello; services.ezpds.settings.public_url = "https://relay.example.com"; } ]; }; in sys.config.systemd.services.ezpds.serviceConfig.ExecStart'` | Eval succeeds (exit code 0). Output is a string containing `/bin/relay --config /nix/store/...-relay.toml`. Confirms the bare module is importable when `package` is explicitly set. | 43 + 44 + --- 45 + 46 + ## Phase 4: Scope Boundaries Code Review 47 + 48 + | Step | Action | Expected | 49 + |------|--------|----------| 50 + | 4.1 | Open `nix/module.nix` and search for `blobs`, `oauth`, `iroh` | Zero matches. No options for out-of-scope config sections exist. | 51 + | 4.2 | Review the `options.services.ezpds` block and confirm it declares exactly: `enable`, `package`, `configFile`, `settings.bind_address`, `settings.port`, `settings.data_dir`, `settings.public_url`, `settings.database_url` | Eight options total. No additional stub options for future sections. | 52 + 53 + --- 54 + 55 + ## Phase 5: Systemd Hardening Code Review 56 + 57 + | Step | Action | Expected | 58 + |------|--------|----------| 59 + | 5.1 | Open `nix/module.nix` and review the `serviceConfig` block | Block contains: `ProtectSystem = "strict"`, `PrivateTmp = true`, `ProtectHome = true`, `NoNewPrivileges = true`, `StateDirectoryMode = "0750"`, `Restart = "on-failure"` | 60 + 61 + --- 62 + 63 + ## End-to-End: NixOS VM Runtime Test 64 + 65 + **Purpose:** Validate that the module produces a functioning systemd service on a live NixOS system, covering the gap between eval-time correctness and runtime behavior. 66 + 67 + | Step | Action | Expected | 68 + |------|--------|----------| 69 + | E2E.1 | Create a NixOS configuration that imports `nixosModules.default` with `services.ezpds.enable = true` and `services.ezpds.settings.public_url = "https://relay.example.com"`. Build a VM: `nixos-rebuild build-vm --flake .#<config-name>` | VM image builds successfully. | 70 + | E2E.2 | Boot the VM and run `systemctl status ezpds` | Service is loaded and attempted to start. | 71 + | E2E.3 | Run `id ezpds` inside the VM | User exists, is a system user, member of group `ezpds`. | 72 + | E2E.4 | Run `stat /var/lib/ezpds` inside the VM | Directory exists, owned by `ezpds:ezpds`, mode `0750`. | 73 + | E2E.5 | Run `systemd-analyze security ezpds` inside the VM | Confirms hardening directives active: `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome=yes`, `PrivateTmp=yes`. | 74 + | E2E.6 | Run `cat $(systemctl show ezpds -p ExecStart --value \| grep -oP '/nix/store/[^ ]+relay\.toml')` inside the VM | TOML file contents match expected defaults with the configured `public_url`. | 75 + 76 + --- 77 + 78 + ## End-to-End: configFile Escape Hatch Runtime Test 79 + 80 + **Purpose:** Validate that `configFile` properly bypasses generated TOML at runtime, critical for secret injection workflows (agenix/sops-nix). 81 + 82 + | Step | Action | Expected | 83 + |------|--------|----------| 84 + | CF.1 | In a NixOS config, set `services.ezpds.configFile = "/etc/ezpds/relay.toml"` and create that file with custom contents (e.g., `public_url = "https://custom.example.com"`, `port = 9090`). | Config file created at the specified path. | 85 + | CF.2 | Run `systemctl show ezpds -p ExecStart` | ExecStart ends with `--config /etc/ezpds/relay.toml` (not a Nix store path). | 86 + | CF.3 | Start the service and check relay logs for the port binding | Relay attempts to bind on port 9090 (from the custom config), not 8080 (the NixOS module default). | 87 + 88 + --- 89 + 90 + ## Traceability 91 + 92 + | Acceptance Criterion | Automated Test | Manual Step | 93 + |----------------------|----------------|-------------| 94 + | MM-135.AC1.1 (file exists) | `git ls-files nix/module.nix` | — | 95 + | MM-135.AC1.2 (options declared) | `nix eval --impure --expr 'builtins.typeOf ...'` + Phase 3 Task 2 Step 1 | Phase 4 step 4.2 | 96 + | MM-135.AC1.3 (defaults match) | Phase 3 Task 2 Step 1 + Task 4 Steps 1–2 | Phase 1 step 1.1 | 97 + | MM-135.AC1.4 (missing public_url fails) | Phase 3 Task 3 Step 1 | — | 98 + | MM-135.AC1.5 (missing package fails) | Phase 3 Task 3 Step 2 | — | 99 + | MM-135.AC2.1 (TOML keys present) | Phase 3 Task 4 Step 2 | Phase 1 step 1.1 | 100 + | MM-135.AC2.2 (database_url absent when null) | Phase 3 Task 4 Step 1 | Phase 1 step 1.1 | 101 + | MM-135.AC2.3 (database_url present when set) | Phase 3 Task 4 Step 3 | Phase 1 step 1.2 | 102 + | MM-135.AC2.4 (ExecStart --config) | Phase 3 Task 2 Step 1 | E2E.6 | 103 + | MM-135.AC3.1 (configFile overrides) | Phase 3 Task 5 Step 1 | CF.2 | 104 + | MM-135.AC3.2 (settings isolation) | Phase 3 Task 5 Step 2 | CF.3 | 105 + | MM-135.AC4.1 (system user) | Phase 3 Task 2 Step 2 | E2E.3 | 106 + | MM-135.AC4.2 (group defined) | Structural (source review) | Phase 2 step 2.1 | 107 + | MM-135.AC4.3 (StateDirectory) | Structural (source review) | Phase 2 step 2.2, E2E.4 | 108 + | MM-135.AC5.1 (flake output) | `nix flake check` + `nix eval .#nixosModules --apply builtins.attrNames` | — | 109 + | MM-135.AC5.2 (package default) | Phase 3 Task 2 Step 1 | — | 110 + | MM-135.AC5.3 (bare module importable) | Phase 3 Task 3 Step 2 (inverse proof) | Phase 3 step 3.1 | 111 + | MM-135.AC6.1 (no stub sections) | Structural (`grep` returns no matches) | Phase 4 steps 4.1–4.2 |