summaryrefslogtreecommitdiffstatshomepage
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_canonical.py163
-rw-r--r--tests/test_canonical_desktop.py79
-rw-r--r--tests/test_canonical_profiles.py33
-rw-r--r--tests/test_canonical_record.py51
-rw-r--r--tests/test_recipes.py94
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])