From ff3147e73bc9458cbd628e9f6e8382e1658210ba Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 08:00:15 -0700 Subject: [PATCH] M3: add unit tests for config resolution and cryptsetup helpers Tests cover: alias resolution (exact match, device path match, unknown, empty methods default), AliasFor lookup, Load with missing/valid YAML, MapperName generation, and token plugin directory detection. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/config/config_test.go | 128 +++++++++++++++++++++++++ internal/cryptsetup/cryptsetup_test.go | 48 ++++++++++ 2 files changed, 176 insertions(+) create mode 100644 internal/config/config_test.go create mode 100644 internal/cryptsetup/cryptsetup_test.go diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..8db4281 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,128 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveDevice_ExactAlias(t *testing.T) { + cfg := &Config{ + Devices: map[string]DeviceConfig{ + "backup": {UUID: "abc-123", Methods: []string{"fido2", "passphrase"}}, + }, + } + r := cfg.ResolveDevice("backup") + if r.UUID != "abc-123" { + t.Errorf("UUID = %q, want %q", r.UUID, "abc-123") + } + if len(r.Methods) != 2 || r.Methods[0] != "fido2" { + t.Errorf("Methods = %v, want [fido2 passphrase]", r.Methods) + } +} + +func TestResolveDevice_DevicePathMatch(t *testing.T) { + cfg := &Config{ + Devices: map[string]DeviceConfig{ + "sda1": {UUID: "abc-123", Methods: []string{"passphrase"}}, + }, + } + r := cfg.ResolveDevice("/dev/sda1") + if r.UUID != "abc-123" { + t.Errorf("UUID = %q, want %q", r.UUID, "abc-123") + } +} + +func TestResolveDevice_UnknownReturnsDefaults(t *testing.T) { + cfg := &Config{Devices: map[string]DeviceConfig{}} + r := cfg.ResolveDevice("nonexistent") + if r.UUID != "" { + t.Errorf("UUID = %q, want empty", r.UUID) + } + if len(r.Methods) != 1 || r.Methods[0] != "passphrase" { + t.Errorf("Methods = %v, want [passphrase]", r.Methods) + } +} + +func TestResolveDevice_EmptyMethodsDefaultsToPassphrase(t *testing.T) { + cfg := &Config{ + Devices: map[string]DeviceConfig{ + "backup": {UUID: "abc-123"}, + }, + } + r := cfg.ResolveDevice("backup") + if len(r.Methods) != 1 || r.Methods[0] != "passphrase" { + t.Errorf("Methods = %v, want [passphrase]", r.Methods) + } +} + +func TestAliasFor_Found(t *testing.T) { + cfg := &Config{ + Devices: map[string]DeviceConfig{ + "backup": {UUID: "abc-123"}, + }, + } + if alias := cfg.AliasFor("abc-123"); alias != "backup" { + t.Errorf("AliasFor = %q, want %q", alias, "backup") + } +} + +func TestAliasFor_NotFound(t *testing.T) { + cfg := &Config{ + Devices: map[string]DeviceConfig{ + "backup": {UUID: "abc-123"}, + }, + } + if alias := cfg.AliasFor("unknown"); alias != "" { + t.Errorf("AliasFor = %q, want empty", alias) + } +} + +func TestLoad_MissingFile(t *testing.T) { + // Point XDG_CONFIG_HOME to a temp dir with no config. + tmp := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tmp) + + cfg := Load() + if cfg == nil { + t.Fatal("Load returned nil") + } + if len(cfg.Devices) != 0 { + t.Errorf("Devices = %v, want empty", cfg.Devices) + } +} + +func TestLoad_ValidYAML(t *testing.T) { + tmp := t.TempDir() + dir := filepath.Join(tmp, "arca") + os.MkdirAll(dir, 0o755) + os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(` +devices: + backup: + uuid: "abc-123" + mountpoint: "/mnt/backup" + methods: + - fido2 + - passphrase + keyfile: "/path/to/key" +`), 0o644) + t.Setenv("XDG_CONFIG_HOME", tmp) + + cfg := Load() + dev, ok := cfg.Devices["backup"] + if !ok { + t.Fatal("device 'backup' not found in config") + } + if dev.UUID != "abc-123" { + t.Errorf("UUID = %q, want %q", dev.UUID, "abc-123") + } + if dev.Mountpoint != "/mnt/backup" { + t.Errorf("Mountpoint = %q, want %q", dev.Mountpoint, "/mnt/backup") + } + if len(dev.Methods) != 2 { + t.Errorf("Methods = %v, want 2 entries", dev.Methods) + } + if dev.Keyfile != "/path/to/key" { + t.Errorf("Keyfile = %q, want %q", dev.Keyfile, "/path/to/key") + } +} diff --git a/internal/cryptsetup/cryptsetup_test.go b/internal/cryptsetup/cryptsetup_test.go new file mode 100644 index 0000000..6e6fcfd --- /dev/null +++ b/internal/cryptsetup/cryptsetup_test.go @@ -0,0 +1,48 @@ +package cryptsetup + +import ( + "os" + "path/filepath" + "testing" +) + +func TestMapperName(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"/dev/sda1", "arca-sda1"}, + {"/dev/nvme0n1p2", "arca-nvme0n1p2"}, + {"/dev/dm-0", "arca-dm-0"}, + {"sda1", "arca-sda1"}, + } + for _, tt := range tests { + got := MapperName(tt.input) + if got != tt.want { + t.Errorf("MapperName(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} + +func TestHasTokenPlugins_WithPlugins(t *testing.T) { + tmp := t.TempDir() + os.WriteFile(filepath.Join(tmp, "libcryptsetup-token-systemd-fido2.so"), []byte("fake"), 0o644) + + if !hasTokenPlugins(tmp) { + t.Error("hasTokenPlugins returned false for dir with plugin") + } +} + +func TestHasTokenPlugins_EmptyDir(t *testing.T) { + tmp := t.TempDir() + + if hasTokenPlugins(tmp) { + t.Error("hasTokenPlugins returned true for empty dir") + } +} + +func TestHasTokenPlugins_NonexistentDir(t *testing.T) { + if hasTokenPlugins("/nonexistent/path") { + t.Error("hasTokenPlugins returned true for nonexistent dir") + } +}