package shared import ( "bytes" "errors" "fmt" "os" "path/filepath" "strings" "testing" "git.unkin.net/unkin/golib/keyring" "golang.org/x/sys/unix" ) // TestMain seals tokens with a run-unique key, skipping outside CI where // keyctl is unimplemented. func TestMain(m *testing.M) { KeyringDesc = fmt.Sprintf("vault-tools-test:shared:%d", os.Getpid()) k, err := keyring.Open(KeyringDesc) if errors.Is(err, unix.ENOSYS) && os.Getenv("CI") == "" { fmt.Println("skipping: kernel keyring unavailable:", err) os.Exit(0) } if err != nil { fmt.Println(err) os.Exit(1) } code := m.Run() _ = k.Forget() os.Exit(code) } func writeRaw(t *testing.T, context string, data []byte) string { t.Helper() path, err := TokenPath(context) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatal(err) } return path } func TestSaveTokenSealsAtRest(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) if err := SaveToken(&Token{Context: "sydney", Token: "hvs.secret"}); err != nil { t.Fatal(err) } path, _ := TokenPath("sydney") data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if bytes.Contains(data, []byte("hvs.secret")) { t.Fatal("token stored in plaintext") } entries, _ := os.ReadDir(filepath.Dir(path)) if len(entries) != 1 { t.Errorf("cache dir has %d entries, want only the token file", len(entries)) } } func TestLoadTokenLockedAfterKeyLoss(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err != nil { t.Fatal(err) } k, err := keyring.Open(KeyringDesc) if err != nil { t.Fatal(err) } if err := k.Forget(); err != nil { t.Fatal(err) } _, err = LoadToken("sydney") if !errors.Is(err, ErrLocked) || !strings.Contains(err.Error(), "vcm login sydney") { t.Fatalf("LoadToken = %v, want ErrLocked with login hint", err) } if path, _ := TokenPath("sydney"); !fileExists(path) { t.Error("locked token file was deleted") } } func TestLoadTokenLegacyPlaintextIsLocked(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) if err := os.MkdirAll(CacheDir(), 0o700); err != nil { t.Fatal(err) } path := writeRaw(t, "sydney", []byte(`{"context":"sydney","token":"s.x"}`+"\n")) if _, err := LoadToken("sydney"); !errors.Is(err, ErrLocked) { t.Fatalf("LoadToken = %v, want ErrLocked", err) } if !fileExists(path) { t.Error("legacy token file was deleted") } } func TestLoadTokenCorruptNamesFile(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err != nil { t.Fatal(err) } path, _ := TokenPath("sydney") data, _ := os.ReadFile(path) data[len(data)-1] ^= 0xff writeRaw(t, "sydney", data) _, err := LoadToken("sydney") if !errors.Is(err, keyring.ErrCorrupt) || !strings.Contains(err.Error(), path) { t.Fatalf("LoadToken = %v, want ErrCorrupt naming %s", err, path) } } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func TestSaveTokenRenameFailureLeavesNoTemp(t *testing.T) { t.Setenv("XDG_CACHE_HOME", t.TempDir()) path, _ := TokenPath("sydney") if err := os.MkdirAll(filepath.Join(path, "blocker"), 0o700); err != nil { t.Fatal(err) } if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err == nil { t.Fatal("expected rename onto a non-empty directory to fail") } if entries, _ := os.ReadDir(CacheDir()); len(entries) != 1 { t.Errorf("cache dir has %d entries, want temp file cleaned up", len(entries)) } }