diff --git a/flake.nix b/flake.nix index 6018ee5..7c23a00 100644 --- a/flake.nix +++ b/flake.nix @@ -34,6 +34,13 @@ in { nixosModules.default.imports = [./nixarr vpnconfinement.nixosModules.default]; + # Add tests attribute to the flake outputs + checks = forAllSystems ({pkgs}: { + permissions-test = pkgs.callPackage ./tests/permissions-test.nix { + inherit (self) nixosModules; + }; + }); + devShells = forAllSystems ({pkgs}: { default = pkgs.mkShell { packages = with pkgs; [ diff --git a/tests/permissions-test.nix b/tests/permissions-test.nix new file mode 100644 index 0000000..30fe33b --- /dev/null +++ b/tests/permissions-test.nix @@ -0,0 +1,160 @@ +# Comprehensive test for Nixarr file permissions, user/group creation, and directory structure +{ + pkgs, + nixosModules, + lib ? pkgs.lib, +}: +pkgs.nixosTest { + name = "nixarr-permissions-test"; + + nodes.machine = { + config, + pkgs, + ... + }: { + imports = [nixosModules.default]; + + networking.firewall.enable = false; + + nixarr = { + enable = true; + stateDir = "/data/.state/nixarr"; + mediaDir = "/data/media"; + mediaUsers = ["testuser"]; + + # Enable key services to trigger tmpfiles directory creation + jellyfin.enable = true; + + transmission = { + enable = true; + vpn.enable = false; + privateTrackers.cross-seed.enable = true; + }; + + sonarr = { + enable = true; + vpn.enable = false; + }; + + radarr = { + enable = true; + vpn.enable = false; + }; + + lidarr = { + enable = true; + vpn.enable = false; + }; + + prowlarr = { + enable = true; + vpn.enable = false; + }; + }; + + # Create a test user to verify mediaUsers functionality + users.users.testuser = { + isNormalUser = true; + home = "/home/testuser"; + }; + }; + + testScript = '' + machine.wait_for_unit("multi-user.target") + print("Starting Nixarr permissions test...") + + # Test 1: Verify key users and groups exist + print("\n=== Testing User/Group Creation ===") + + # Check essential users exist + key_users = ["jellyfin", "transmission", "cross-seed", "sonarr", "radarr", "testuser"] + for user in key_users: + machine.succeed(f"id {user}") + print(f"✓ User {user} exists") + + # Check media group exists and has correct members + media_members = machine.succeed("getent group media | cut -d: -f4").strip() + expected_members = ["jellyfin", "transmission", "sonarr", "radarr", "lidarr", "testuser"] + for member in expected_members: + if member in media_members: + print(f"✓ {member} is in media group") + else: + machine.fail(f"{member} not in media group") + + # Check cross-seed group membership + cross_seed_members = machine.succeed("getent group cross-seed | cut -d: -f4").strip() + if "transmission" in cross_seed_members: + print("✓ transmission is in cross-seed group") + + # Test 2: Verify directory structure and ownership + print("\n=== Testing Directory Permissions ===") + + def check_dir(path, expected_user, expected_group, description): + stat_output = machine.succeed(f"stat -c '%U:%G' '{path}'").strip() + user, group = stat_output.split(":") + if user == expected_user and group == expected_group: + print(f"✓ {description}: {user}:{group}") + else: + machine.fail(f"{description} has wrong ownership: {user}:{group}, expected {expected_user}:{expected_group}") + + # Check key directories exist with correct ownership + check_dir("/data/media", "root", "media", "Media root directory") + check_dir("/data/media/library/movies", "root", "media", "Movies directory") + check_dir("/data/media/library/shows", "root", "media", "Shows directory") + check_dir("/data/media/library/music", "root", "media", "Music directory") + check_dir("/data/media/torrents", "transmission", "media", "Torrents directory") + + if machine.succeed("test -d '/data/media/torrents/.cross-seed' && echo 'exists' || echo 'missing'").strip() == "exists": + check_dir("/data/media/torrents/.cross-seed", "cross-seed", "cross-seed", "Cross-seed directory") + + # Test 3: Verify service file access + print("\n=== Testing Service File Access ===") + + # Test Jellyfin can write to media directories + test_dirs = ["/data/media/library/movies", "/data/media/library/shows"] + for test_dir in test_dirs: + if machine.succeed(f"test -d '{test_dir}' && echo 'exists' || echo 'missing'").strip() == "exists": + test_file = f"{test_dir}/jellyfin-test.txt" + machine.succeed(f"sudo -u jellyfin touch '{test_file}'") + machine.succeed(f"sudo -u jellyfin sh -c 'echo test > {test_file}'") + content = machine.succeed(f"cat '{test_file}'").strip() + if content != "test": + machine.fail(f"Expected 'test' but got '{content}'") + machine.succeed(f"sudo -u jellyfin rm '{test_file}'") + print(f"✓ Jellyfin can write/read/delete in {test_dir}") + + # Test cross-seed can access transmission state + trans_state = "/data/.state/nixarr/transmission" + if machine.succeed(f"test -d '{trans_state}' && echo 'exists' || echo 'missing'").strip() == "exists": + result = machine.succeed(f"sudo -u cross-seed test -r '{trans_state}' && echo 'readable' || echo 'not-readable'").strip() + if result == "readable": + print("✓ cross-seed can read transmission state directory") + else: + machine.fail("cross-seed cannot read transmission state directory") + + # Test 4: Verify fix-permissions command + print("\n=== Testing fix-permissions Command ===") + + # Create file with wrong permissions + test_file = "/data/media/library/movies/test-wrong-perms.txt" + if machine.succeed("test -d '/data/media/library/movies' && echo 'exists' || echo 'missing'").strip() == "exists": + machine.succeed(f"umask 077 && touch '{test_file}'") + + # Verify initial permissions are wrong + initial_perms = machine.succeed(f"stat -c '%a' '{test_file}'").strip() + if initial_perms != "600": + machine.fail(f"Expected 600 permissions, got {initial_perms}") + + machine.succeed("nixarr fix-permissions") + + # Verify permissions were fixed + fixed_perms = machine.succeed(f"stat -c '%a' '{test_file}'").strip() + if fixed_perms not in ["644", "664"]: + machine.fail(f"fix-permissions failed: permissions are {fixed_perms}, expected 644 or 664") + + machine.succeed(f"rm '{test_file}'") + print(f"✓ fix-permissions corrected file permissions from 600 to {fixed_perms}") + + print("\n=== All Permission Tests Completed ===") + ''; +}