返回 DeepSeek-Reasonix
credential_store_recovery_test.go
根目录 / internal / config / credential_store_recovery_test.go
1 //go:build !windows
2
3 package config
4
5 import (
6 "errors"
7 "io/fs"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 )
13
14 // lockedCredentialStore writes a global store the process cannot read. The
15 // Windows branch of the reader is selected explicitly; a mode-0 file is the
16 // portable stand-in for a deny ACE.
17 func lockedCredentialStore(t *testing.T) string {
18 t.Helper()
19 setRuntimeGOOS(t, "windows")
20 t.Setenv("REASONIX_HOME", t.TempDir())
21 t.Setenv("RECOVERED_KEY", "")
22 path := UserCredentialsPath()
23 if err := os.WriteFile(path, []byte("EXISTING_KEY=old\n"), 0o600); err != nil {
24 t.Fatal(err)
25 }
26 if err := os.Chmod(path, 0); err != nil {
27 t.Fatal(err)
28 }
29 t.Cleanup(func() { _ = os.Chmod(path, 0o600) })
30 if _, err := os.ReadFile(path); err == nil {
31 t.Skip("this user can read a mode-0 file; cannot simulate a locked store")
32 }
33 return path
34 }
35
36 func stubCredentialRecovery(t *testing.T, reset func(string) error, quarantine func(string) (string, error)) {
37 t.Helper()
38 prevReset, prevQuarantine := credentialStoreReset, credentialStoreQuarantine
39 credentialStoreReset, credentialStoreQuarantine = reset, quarantine
40 t.Cleanup(func() { credentialStoreReset, credentialStoreQuarantine = prevReset, prevQuarantine })
41 }
42
43 func TestCredentialReadKeepsPermissionErrorAndNeverResets(t *testing.T) {
44 path := lockedCredentialStore(t)
45 stubCredentialRecovery(t,
46 func(string) error { t.Fatal("plain read must not reset the ACL"); return nil },
47 func(string) (string, error) { t.Fatal("plain read must not quarantine the store"); return "", nil })
48 if _, err := readCredentialFile(path); !errors.Is(err, fs.ErrPermission) {
49 t.Fatalf("read error = %v, want the original permission error", err)
50 }
51 if revision := CredentialStoreRevision(); revision != "unreadable" {
52 t.Fatalf("revision = %q, want unreadable", revision)
53 }
54 }
55
56 func TestCredentialSaveResetsLockedStoreACL(t *testing.T) {
57 path := lockedCredentialStore(t)
58 stubCredentialRecovery(t,
59 func(p string) error { return os.Chmod(p, 0o600) },
60 func(string) (string, error) {
61 t.Fatal("quarantine must not run when the reset succeeds")
62 return "", nil
63 })
64 if _, err := SetCredential("RECOVERED_KEY", "new"); err != nil {
65 t.Fatalf("SetCredential after ACL reset: %v", err)
66 }
67 data, err := os.ReadFile(path)
68 if err != nil {
69 t.Fatal(err)
70 }
71 if got := string(data); !strings.Contains(got, "EXISTING_KEY=old") || !strings.Contains(got, "RECOVERED_KEY=new") {
72 t.Fatalf("store after reset = %q, want existing and new keys", got)
73 }
74 }
75
76 func TestCredentialSaveQuarantinesUnrecoverableStore(t *testing.T) {
77 path := lockedCredentialStore(t)
78 stubCredentialRecovery(t, func(string) error { return errors.New("WRITE_DAC denied") }, quarantineCredentialStore)
79 if _, err := SetCredential("RECOVERED_KEY", "new"); err != nil {
80 t.Fatalf("SetCredential after quarantine: %v", err)
81 }
82 data, err := os.ReadFile(path)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if got := string(data); strings.Contains(got, "EXISTING_KEY") || !strings.Contains(got, "RECOVERED_KEY=new") {
87 t.Fatalf("fresh store = %q, want only the new key", got)
88 }
89 quarantined, _ := filepath.Glob(path + ".locked-*")
90 if len(quarantined) != 1 {
91 t.Fatalf("quarantined copies = %v, want exactly one", quarantined)
92 }
93 t.Cleanup(func() { _ = os.Chmod(quarantined[0], 0o600) })
94 }
95
96 func TestCredentialSaveReportsBothFailuresWhenNothingRecovers(t *testing.T) {
97 lockedCredentialStore(t)
98 stubCredentialRecovery(t,
99 func(string) error { return errors.New("WRITE_DAC denied") },
100 func(string) (string, error) { return "", errors.New("rename denied") })
101 _, err := SetCredential("RECOVERED_KEY", "new")
102 if err == nil || !errors.Is(err, fs.ErrPermission) || !strings.Contains(err.Error(), "rename denied") {
103 t.Fatalf("SetCredential error = %v, want the permission error with the quarantine failure", err)
104 }
105 }
106
106 lines GO