diff options
| author | sommerfeld <sommerfeld@sommerfeld.dev> | 2026-09-17 15:05:36 +0100 |
|---|---|---|
| committer | sommerfeld <sommerfeld@sommerfeld.dev> | 2026-09-17 15:05:36 +0100 |
| commit | aff435a0fb3a56b1d7b738c7dede363440884b3a (patch) | |
| tree | 07acb41d37760aa169ee42dbeb69da3621cc7489 /tests | |
| parent | a7f74c1fa4b89c5829f5896842eb7248c820704c (diff) | |
| download | dotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.tar.gz dotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.tar.bz2 dotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.zip | |
Add the Canonical laptop profile and setup workflows
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_canonical.py | 163 | ||||
| -rw-r--r-- | tests/test_canonical_desktop.py | 79 | ||||
| -rw-r--r-- | tests/test_canonical_profiles.py | 33 | ||||
| -rw-r--r-- | tests/test_canonical_record.py | 51 | ||||
| -rw-r--r-- | tests/test_recipes.py | 94 |
5 files changed, 420 insertions, 0 deletions
diff --git a/tests/test_canonical.py b/tests/test_canonical.py new file mode 100644 index 0000000..d1c2367 --- /dev/null +++ b/tests/test_canonical.py @@ -0,0 +1,163 @@ +import importlib.util +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "canonical", ROOT / "scripts/canonical.py" +) +assert SPEC and SPEC.loader +canonical = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(canonical) + + +class PackageTests(unittest.TestCase): + def test_non_corporate_role_is_rejected_before_system_access(self): + with ( + patch.object( + canonical.subprocess, + "check_output", + return_value='{"machineRole":"host"}', + ), + self.assertRaises(SystemExit), + ): + canonical.require_canonical() + + def test_classic_permission_is_explicit(self): + commands = canonical.snap_install_commands() + self.assertIn( + ["sudo", "snap", "install", "ghostty", "--channel=stable", "--classic"], + commands, + ) + self.assertEqual(sum("--classic" in command for command in commands), 1) + + def test_flatpaks_are_user_scoped(self): + commands = canonical.flatpak_install_commands() + self.assertTrue(all("--user" in command for command in commands)) + self.assertIn("im.nheko.Nheko", commands[-1]) + + def test_no_destructive_package_updates(self): + commands = canonical.update_commands() + self.assertIn(["sudo", "apt-get", "upgrade"], commands) + self.assertFalse( + any( + "autoremove" in command or "dist-upgrade" in command + for command in commands + ) + ) + self.assertFalse(any("--ignore-running" in command for command in commands)) + + +class RoleTests(unittest.TestCase): + def command(self, role: str, *args: str) -> list[str]: + return [ + "chezmoi", + "--config", + "/dev/null", + "--config-format", + "toml", + "--override-data", + json.dumps( + { + "machineRole": role, + "workName": "Work User", + "workEmail": "work@canonical.com", + "workSigningKey": "A" * 40, + } + ), + "-S", + str(ROOT), + *args, + ] + + def test_canonical_file_boundary(self): + files = subprocess.check_output( + self.command("canonical", "managed", "--include=files,scripts,symlinks"), + text=True, + ).splitlines() + for required in [ + ".ssh/config", + ".gnupg/gpg.conf", + ".config/git/config", + ".config/ghostty/config", + ".local/bin/rqr", + ]: + self.assertIn(required, files) + for path in files: + self.assertFalse( + any( + part in path + for part in [ + "sway", + "waybar", + "nym.pub", + "sshcontrol", + "pass-secret-service", + ".config/git/hooks", + "deploy-etc", + ] + ) + ) + self.assertNotIn("__pycache__", path) + self.assertNotEqual(path, ".config/nvim/nvim-pack-lock.json") + if path.endswith(".sh") and not path.startswith("."): + self.assertIn(path, ["canonical-desktop.sh", "canonical-nvim-lock.sh"]) + + def test_canonical_lockfile_is_seeded_without_overwriting(self): + rendered = subprocess.check_output( + self.command( + "canonical", + "execute-template", + "--file", + str(ROOT / "run_before_canonical-nvim-lock.sh.tmpl"), + ), + text=True, + ) + with tempfile.TemporaryDirectory() as directory: + env = { + **os.environ, + "HOME": directory, + "XDG_CONFIG_HOME": directory + "/config", + } + target = Path(directory) / "config/nvim/nvim-pack-lock.json" + subprocess.run(["sh", "-c", rendered], env=env, check=True) + self.assertEqual( + target.read_bytes(), + (ROOT / "dot_config/nvim/nvim-pack-lock.json").read_bytes(), + ) + target.write_text('{"local": true}\n') + subprocess.run(["sh", "-c", rendered], env=env, check=True) + self.assertEqual(target.read_text(), '{"local": true}\n') + + def test_work_identity_is_rendered_without_personal_identity(self): + for source in [ + "dot_config/git/config.tmpl", + "private_dot_ssh/config.tmpl", + "private_dot_gnupg/gpg.conf.tmpl", + ]: + rendered = subprocess.check_output( + self.command( + "canonical", "execute-template", "--file", str(ROOT / source) + ), + text=True, + ) + self.assertNotIn("sommerfeld", rendered) + self.assertNotIn("nym.pub", rendered) + self.assertNotIn("proton/", rendered) + + def test_host_and_vm_do_not_receive_corporate_autostart(self): + for role in ["host", "vm"]: + files = subprocess.check_output( + self.command(role, "managed", "--include=files,symlinks"), text=True + ) + self.assertNotIn(".config/autostart/dotfiles-", files) + self.assertNotIn("gpg-agent.service.d/canonical.conf", files) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_canonical_desktop.py b/tests/test_canonical_desktop.py new file mode 100644 index 0000000..cdbc812 --- /dev/null +++ b/tests/test_canonical_desktop.py @@ -0,0 +1,79 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "desktop", ROOT / "dot_local/lib/dotfiles/canonical_desktop.py" +) +assert SPEC and SPEC.loader +desktop = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(desktop) + + +class DesktopTests(unittest.TestCase): + def test_unknown_keys_fail_without_writing(self): + settings = MagicMock() + settings.props.settings_schema.list_keys.return_value = [] + with ( + patch.object(desktop, "settings_object", return_value=settings), + self.assertRaisesRegex(RuntimeError, "Unknown setting"), + ): + desktop.write_key("schema", "missing", "value", {}) + settings.set_value.assert_not_called() + + def test_launcher_uses_gnome_application_view_key(self): + with ( + patch.object(desktop, "merge_key"), + patch.object(desktop, "shortcuts"), + patch.object(desktop, "workspaces"), + patch.object(desktop, "write_key") as write, + ): + desktop.apply_settings({}) + write.assert_any_call( + "org.gnome.shell.keybindings", "toggle-application-view", ["<Super>d"], {} + ) + + def test_locked_keys_are_never_written(self): + settings = MagicMock() + settings.props.settings_schema.list_keys.return_value = ["key"] + settings.is_writable.return_value = False + with patch.object(desktop, "settings_object", return_value=settings): + desktop.write_key("schema", "key", "value", {}) + settings.set_value.assert_not_called() + + def test_first_snapshot_is_kept_on_repeated_apply(self): + settings = MagicMock() + settings.props.settings_schema.list_keys.return_value = ["key"] + settings.get_user_value.return_value.print_.return_value = "'original'" + glib = MagicMock() + glib.Variant.return_value.print_.return_value = "'managed'" + saved = {} + with ( + tempfile.TemporaryDirectory() as directory, + patch.object(desktop, "STATE", Path(directory) / "state.json"), + patch.object(desktop, "settings_object", return_value=settings), + patch.object(desktop.importlib, "import_module", return_value=glib), + ): + desktop.write_key("schema", "key", "managed", saved) + settings.get_user_value.return_value.print_.return_value = "'changed'" + desktop.write_key("schema", "key", "managed", saved) + self.assertEqual(next(iter(saved.values()))["before"], "'original'") + + def test_merge_keeps_unrelated_entries(self): + settings = MagicMock() + settings.get_strv.return_value = ["company", "personal"] + with ( + patch.object(desktop, "settings_object", return_value=settings), + patch.object(desktop, "write_key") as write, + ): + desktop.merge_key("schema", "key", ["personal", "new"], {}) + write.assert_called_once_with( + "schema", "key", ["company", "personal", "new"], {} + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_canonical_profiles.py b/tests/test_canonical_profiles.py new file mode 100644 index 0000000..e066661 --- /dev/null +++ b/tests/test_canonical_profiles.py @@ -0,0 +1,33 @@ +import tempfile +import unittest +from pathlib import Path + +from scripts.canonical_profiles import preferences, profiles + + +class ProfileTests(unittest.TestCase): + def test_preferences_preserve_user_lines_and_are_idempotent(self): + original = 'user_pref("local.setting", true);\n' + owned = 'user_pref("mail.biff.show_alert", true);\n' + result = preferences(original, owned) + self.assertIn(original, result) + self.assertEqual(preferences(result, owned), result) + self.assertNotIn("show_alert", preferences(result, "")) + + def test_incomplete_marker_is_not_overwritten(self): + with self.assertRaises(ValueError): + preferences("// dotfiles: begin\nlocal data", "") + + def test_profile_paths_stay_inside_the_snap_directory(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "work@canonical.com").mkdir() + (root / "profiles.ini").write_text( + "[Profile0]\nPath=work@canonical.com\nIsRelative=1\n" + "[Profile1]\nPath=/etc\nIsRelative=0\n" + ) + self.assertEqual(profiles(root), [root / "work@canonical.com"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_canonical_record.py b/tests/test_canonical_record.py new file mode 100644 index 0000000..cfc9faf --- /dev/null +++ b/tests/test_canonical_record.py @@ -0,0 +1,51 @@ +import importlib.util +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "record", ROOT / "dot_local/lib/dotfiles/record.py" +) +assert SPEC and SPEC.loader +record = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(record) + + +class RecorderTests(unittest.TestCase): + def test_start_is_idempotent(self): + with ( + patch.object(record, "active", return_value=True), + patch.object(record.subprocess, "run") as run, + ): + record.start(Path("/unused")) + run.assert_not_called() + + def test_stop_before_portal_selection_stops_only_owned_service(self): + with ( + tempfile.TemporaryDirectory() as directory, + patch.object(record, "active", return_value=True), + patch.object(record.subprocess, "run") as run, + ): + record.stop(Path(directory)) + run.assert_called_once_with( + ["systemctl", "--user", "stop", "dotfiles-record.service"], check=True + ) + + def test_stop_uses_recorder_ipc_when_available(self): + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) + (runtime / "control.sock").touch() + with ( + patch.object(record, "active", return_value=True), + patch.object(record.subprocess, "run") as run, + ): + record.stop(runtime) + command = run.call_args.args[0] + self.assertIn("--command=gsr-cli", command) + self.assertEqual(command[-1], "stop") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_recipes.py b/tests/test_recipes.py new file mode 100644 index 0000000..772833d --- /dev/null +++ b/tests/test_recipes.py @@ -0,0 +1,94 @@ +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class RecipeTests(unittest.TestCase): + def invoke(self, role, *recipes): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + commands = { + "chezmoi": 'if [ "$1" = data ]; then printf \'%s\\n\' "$ROLE_DATA"; else echo "chezmoi $*"; fi', + "sudo": 'echo "UNEXPECTED sudo"; exit 99', + "flatpak": 'echo "UNEXPECTED flatpak"; exit 99', + "pacman": 'echo "UNEXPECTED pacman"; exit 99', + } + for name, body in commands.items(): + executable = path / name + executable.write_text("#!/bin/sh\n" + body + "\n") + executable.chmod(0o755) + return subprocess.run( + ["just", *recipes], + cwd=ROOT, + env={ + **os.environ, + "PATH": f"{path}:{os.environ['PATH']}", + "ROLE_DATA": json.dumps({"machineRole": role}), + }, + capture_output=True, + text=True, + check=False, + ) + + def test_invalid_role_stops_package_commands(self): + for recipe in ["pkg-apply", "pkg-fix", "flatpak-update"]: + with self.subTest(recipe=recipe): + result = self.invoke("invalid", recipe) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("UNEXPECTED", result.stdout) + + def test_non_host_maintenance_only_uses_chezmoi(self): + for role in ["vm", "canonical"]: + for recipe in ["diff", "merge", "re-add"]: + with self.subTest(role=role, recipe=recipe): + result = self.invoke(role, recipe) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("chezmoi", result.stdout) + self.assertNotIn("UNEXPECTED", result.stdout) + + def test_non_host_etc_paths_fail_before_home_changes(self): + for recipe in ["diff", "merge", "re-add"]: + result = self.invoke("canonical", recipe, ".config/zsh", "etc/hosts") + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("chezmoi", result.stdout) + + def test_vm_migration_initializes_role_before_switch(self): + result = subprocess.check_output( + ["just", "--justfile", "nix/justfile", "--dry-run", "migrate-chezmoi"], + cwd=ROOT, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertLess(result.index("chezmoi init"), result.index("switch.sh")) + + def test_host_home_paths_do_not_select_etc(self): + for recipe in ["diff", "merge", "re-add"]: + result = self.invoke("host", recipe, ".config/zsh") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(len(result.stdout.splitlines()), 1) + self.assertIn(".config/zsh", result.stdout) + + def test_host_mixed_paths_are_split_by_domain(self): + for domain, expected in [("home", ".config/zsh"), ("etc", "etc/hosts")]: + result = subprocess.check_output( + [ + "bash", + "-c", + ( + "source scripts/maintenance-lib.sh; " + "_machine_role() { echo host; }; " + '_maintenance_select auto "$1" .config/zsh etc/hosts; ' + 'printf "%s\\n" "$maintenance_run" "${args[@]}"' + ), + "test", + domain, + ], + cwd=ROOT, + text=True, + ) + self.assertEqual(result.splitlines(), ["true", expected]) |
