1package domainsecrets
2
3import (
4 "context"
5 "fmt"
6 "os/exec"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/bnema/gordon/internal/domain"
14)
15
16func passCmd(args ...string) error {
17 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
18 defer cancel()
19 return exec.CommandContext(ctx, "pass", args...).Run()
20}
21
22func requirePass(t *testing.T) {
23 if err := passCmd("version"); err != nil {
24 t.Skip("pass not available")
25 }
26 if err := passCmd("ls"); err != nil {
27 t.Skip("pass store not initialized")
28 }
29}
30
31func cleanupPassDomain(_ *testing.T, domainName string, keys []string) {
32 safeDomain, err := domain.SanitizeDomainForEnvFile(domainName)
33 if err != nil {
34 return
35 }
36
37 for _, key := range keys {
38 path := fmt.Sprintf("%s/%s/%s", PassDomainSecretsPath, safeDomain, key)
39 _ = passCmd("rm", "-f", path)
40 }
41
42 manifestPath := fmt.Sprintf("%s/%s/.keys", PassDomainSecretsPath, safeDomain)
43 _ = passCmd("rm", "-f", manifestPath)
44}
45
46func TestPassStore_SetGetDelete(t *testing.T) {
47 requirePass(t)
48
49 store, err := NewPassStore(testLogger())
50 require.NoError(t, err)
51
52 domainName := fmt.Sprintf("test.%d.example.com", time.Now().UnixNano())
53 keys := []string{"API_KEY", "DB_PASSWORD"}
54 defer cleanupPassDomain(t, domainName, keys)
55
56 secretsMap := map[string]string{
57 "API_KEY": "alpha",
58 "DB_PASSWORD": "bravo",
59 }
60
61 err = store.Set(domainName, secretsMap)
62 require.NoError(t, err)
63
64 keysList, err := store.ListKeys(domainName)
65 require.NoError(t, err)
66 assert.Len(t, keysList, 2)
67 assert.ElementsMatch(t, keys, keysList)
68
69 values, err := store.GetAll(domainName)
70 require.NoError(t, err)
71 assert.Equal(t, "alpha", values["API_KEY"])
72 assert.Equal(t, "bravo", values["DB_PASSWORD"])
73
74 err = store.Delete(domainName, "DB_PASSWORD")
75 require.NoError(t, err)
76
77 keysList, err = store.ListKeys(domainName)
78 require.NoError(t, err)
79 assert.ElementsMatch(t, []string{"API_KEY"}, keysList)
80}
81
82func TestPassStore_SetGetAttachment(t *testing.T) {
83 requirePass(t)
84
85 store, err := NewPassStore(testLogger())
86 require.NoError(t, err)
87
88 containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
89 keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
90 defer CleanupPassAttachment(t, containerName, keys)
91
92 secretsMap := map[string]string{
93 "POSTGRES_USER": "gitea",
94 "POSTGRES_PASSWORD": "secret123",
95 }
96
97 err = store.SetAttachment(containerName, secretsMap)
98 require.NoError(t, err)
99
100 values, err := store.GetAllAttachment(containerName)
101 require.NoError(t, err)
102 assert.Equal(t, "gitea", values["POSTGRES_USER"])
103 assert.Equal(t, "secret123", values["POSTGRES_PASSWORD"])
104}
105
106func TestPassStore_ListAttachmentKeys_AfterSet(t *testing.T) {
107 requirePass(t)
108
109 store, err := NewPassStore(testLogger())
110 require.NoError(t, err)
111
112 containerName := fmt.Sprintf("redis-cache-%d", time.Now().UnixNano())
113 keys := []string{"REDIS_PASSWORD"}
114 defer CleanupPassAttachment(t, containerName, keys)
115
116 secretsMap := map[string]string{
117 "REDIS_PASSWORD": "redis123",
118 }
119
120 err = store.SetAttachment(containerName, secretsMap)
121 require.NoError(t, err)
122
123 values, err := store.GetAllAttachment(containerName)
124 require.NoError(t, err)
125 assert.Len(t, values, 1)
126 assert.Equal(t, "redis123", values["REDIS_PASSWORD"])
127}
128
129func TestPassStore_DeleteAttachment(t *testing.T) {
130 requirePass(t)
131
132 store, err := NewPassStore(testLogger())
133 require.NoError(t, err)
134
135 containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
136 keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
137 defer CleanupPassAttachment(t, containerName, keys)
138
139 // Set 2 secrets
140 secretsMap := map[string]string{
141 "POSTGRES_USER": "gitea",
142 "POSTGRES_PASSWORD": "secret123",
143 }
144 err = store.SetAttachment(containerName, secretsMap)
145 require.NoError(t, err)
146
147 // Delete 1
148 err = store.DeleteAttachment(containerName, "POSTGRES_PASSWORD")
149 require.NoError(t, err)
150
151 // Verify remaining via GetAllAttachment
152 values, err := store.GetAllAttachment(containerName)
153 require.NoError(t, err)
154 assert.Len(t, values, 1)
155 assert.Equal(t, "gitea", values["POSTGRES_USER"])
156 _, exists := values["POSTGRES_PASSWORD"]
157 assert.False(t, exists)
158}
159
160func TestParsePassListOutput(t *testing.T) {
161 tests := []struct {
162 name string
163 basePath string
164 output string
165 want []passListEntry
166 }{
167 {
168 name: "simple tree",
169 basePath: "domain",
170 output: "domain\n├── key1\n└── key2",
171 want: []passListEntry{{name: "key1", depth: 1}, {name: "key2", depth: 1}},
172 },
173 {
174 name: "nested tree",
175 basePath: "domain",
176 output: "domain\n│ ├── subkey1\n│ └── subkey2\n└── key1",
177 want: []passListEntry{{name: "subkey1", depth: 2}, {name: "subkey2", depth: 2}, {name: "key1", depth: 1}},
178 },
179 {
180 name: "empty output",
181 basePath: "domain",
182 output: "",
183 want: []passListEntry{},
184 },
185 {
186 name: "ASCII fallback chars",
187 basePath: "domain",
188 output: "domain\n| |-- subkey1\n`-- key1",
189 want: []passListEntry{{name: "subkey1", depth: 2}, {name: "key1", depth: 1}},
190 },
191 }
192
193 for _, tt := range tests {
194 t.Run(tt.name, func(t *testing.T) {
195 got := parsePassListOutput(tt.basePath, tt.output)
196 assert.Equal(t, tt.want, got)
197 })
198 }
199}