v2.31.0-dev22 8d342c4

pass_store_test.go RawHistoryFolder

1package domainsecrets
2
3import (
4	"context"
5	"fmt"
6	"os/exec"
7	"strings"
8	"testing"
9	"time"
10
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13
14	"github.com/bnema/gordon/internal/domain"
15)
16
17func passCmd(args ...string) error {
18	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
19	defer cancel()
20	return exec.CommandContext(ctx, "pass", args...).Run()
21}
22
23func passInsertValue(path, value string) error {
24	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
25	defer cancel()
26	cmd := exec.CommandContext(ctx, "pass", "insert", "-m", "-f", path)
27	cmd.Stdin = strings.NewReader(value)
28	_, err := cmd.CombinedOutput()
29	return err
30}
31
32func passShow(path string) (string, error) {
33	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
34	defer cancel()
35	out, err := exec.CommandContext(ctx, "pass", "show", path).CombinedOutput()
36	if err != nil {
37		return "", err
38	}
39	return strings.TrimSpace(string(out)), nil
40}
41
42func requirePass(t *testing.T) {
43	if err := passCmd("version"); err != nil {
44		t.Skip("pass not available")
45	}
46	if err := passCmd("ls"); err != nil {
47		t.Skip("pass store not initialized")
48	}
49}
50
51func cleanupPassDomain(_ *testing.T, domainName string, keys []string) {
52	safeDomain, err := domain.SanitizeDomainForEnvFile(domainName)
53	if err != nil {
54		return
55	}
56
57	for _, key := range keys {
58		path := fmt.Sprintf("%s/%s/%s", PassDomainSecretsPath, safeDomain, key)
59		_ = passCmd("rm", "-f", path)
60	}
61
62	manifestPath := fmt.Sprintf("%s/%s/.keys", PassDomainSecretsPath, safeDomain)
63	_ = passCmd("rm", "-f", manifestPath)
64}
65
66func TestPassStore_SetGetDelete(t *testing.T) {
67	requirePass(t)
68
69	store, err := NewPassStore(testLogger())
70	require.NoError(t, err)
71
72	domainName := fmt.Sprintf("test.%d.example.com", time.Now().UnixNano())
73	keys := []string{"API_KEY", "DB_PASSWORD"}
74	defer cleanupPassDomain(t, domainName, keys)
75
76	secretsMap := map[string]string{
77		"API_KEY":     "alpha",
78		"DB_PASSWORD": "bravo",
79	}
80
81	err = store.Set(domainName, secretsMap)
82	require.NoError(t, err)
83
84	keysList, err := store.ListKeys(domainName)
85	require.NoError(t, err)
86	assert.Len(t, keysList, 2)
87	assert.ElementsMatch(t, keys, keysList)
88
89	values, err := store.GetAll(domainName)
90	require.NoError(t, err)
91	assert.Equal(t, "alpha", values["API_KEY"])
92	assert.Equal(t, "bravo", values["DB_PASSWORD"])
93
94	err = store.Delete(domainName, "DB_PASSWORD")
95	require.NoError(t, err)
96
97	keysList, err = store.ListKeys(domainName)
98	require.NoError(t, err)
99	assert.ElementsMatch(t, []string{"API_KEY"}, keysList)
100}
101
102func TestPassStore_SetGetAttachment(t *testing.T) {
103	requirePass(t)
104
105	store, err := NewPassStore(testLogger())
106	require.NoError(t, err)
107
108	containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
109	keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
110	defer CleanupPassAttachment(t, containerName, keys)
111
112	secretsMap := map[string]string{
113		"POSTGRES_USER":     "gitea",
114		"POSTGRES_PASSWORD": "secret123",
115	}
116
117	err = store.SetAttachment(containerName, secretsMap)
118	require.NoError(t, err)
119
120	values, err := store.GetAllAttachment(containerName)
121	require.NoError(t, err)
122	assert.Equal(t, "gitea", values["POSTGRES_USER"])
123	assert.Equal(t, "secret123", values["POSTGRES_PASSWORD"])
124}
125
126func TestPassStore_ListAttachmentKeys_AfterSet(t *testing.T) {
127	requirePass(t)
128
129	store, err := NewPassStore(testLogger())
130	require.NoError(t, err)
131
132	containerName := fmt.Sprintf("redis-cache-%d", time.Now().UnixNano())
133	keys := []string{"REDIS_PASSWORD"}
134	defer CleanupPassAttachment(t, containerName, keys)
135
136	secretsMap := map[string]string{
137		"REDIS_PASSWORD": "redis123",
138	}
139
140	err = store.SetAttachment(containerName, secretsMap)
141	require.NoError(t, err)
142
143	values, err := store.GetAllAttachment(containerName)
144	require.NoError(t, err)
145	assert.Len(t, values, 1)
146	assert.Equal(t, "redis123", values["REDIS_PASSWORD"])
147}
148
149func TestPassStore_DeleteAttachment(t *testing.T) {
150	requirePass(t)
151
152	store, err := NewPassStore(testLogger())
153	require.NoError(t, err)
154
155	containerName := fmt.Sprintf("gitea-postgres-%d", time.Now().UnixNano())
156	keys := []string{"POSTGRES_USER", "POSTGRES_PASSWORD"}
157	defer CleanupPassAttachment(t, containerName, keys)
158
159	// Set 2 secrets
160	secretsMap := map[string]string{
161		"POSTGRES_USER":     "gitea",
162		"POSTGRES_PASSWORD": "secret123",
163	}
164	err = store.SetAttachment(containerName, secretsMap)
165	require.NoError(t, err)
166
167	// Delete 1
168	err = store.DeleteAttachment(containerName, "POSTGRES_PASSWORD")
169	require.NoError(t, err)
170
171	// Verify remaining via GetAllAttachment
172	values, err := store.GetAllAttachment(containerName)
173	require.NoError(t, err)
174	assert.Len(t, values, 1)
175	assert.Equal(t, "gitea", values["POSTGRES_USER"])
176	_, exists := values["POSTGRES_PASSWORD"]
177	assert.False(t, exists)
178}
179
180func TestPassStore_Delete_Idempotent(t *testing.T) {
181	requirePass(t)
182
183	store, err := NewPassStore(testLogger())
184	require.NoError(t, err)
185
186	domainName := fmt.Sprintf("idempotent.%d.example.com", time.Now().UnixNano())
187	keys := []string{"API_KEY"}
188	defer cleanupPassDomain(t, domainName, keys)
189
190	err = store.Set(domainName, map[string]string{"API_KEY": "alpha"})
191	require.NoError(t, err)
192
193	err = store.Delete(domainName, "API_KEY")
194	require.NoError(t, err)
195
196	// Second delete of an already-removed key must be a no-op.
197	err = store.Delete(domainName, "API_KEY")
198	require.NoError(t, err)
199
200	values, err := store.GetAll(domainName)
201	require.NoError(t, err)
202	assert.NotContains(t, values, "API_KEY")
203}
204
205func TestPassStore_DeleteAttachment_Idempotent(t *testing.T) {
206	requirePass(t)
207
208	store, err := NewPassStore(testLogger())
209	require.NoError(t, err)
210
211	containerName := fmt.Sprintf("idempotent-attachment-%d", time.Now().UnixNano())
212	keys := []string{"POSTGRES_PASSWORD"}
213	defer CleanupPassAttachment(t, containerName, keys)
214
215	err = store.SetAttachment(containerName, map[string]string{"POSTGRES_PASSWORD": "secret123"})
216	require.NoError(t, err)
217
218	err = store.DeleteAttachment(containerName, "POSTGRES_PASSWORD")
219	require.NoError(t, err)
220
221	// Second delete of an already-removed key must be a no-op.
222	err = store.DeleteAttachment(containerName, "POSTGRES_PASSWORD")
223	require.NoError(t, err)
224
225	values, err := store.GetAllAttachment(containerName)
226	require.NoError(t, err)
227	assert.NotContains(t, values, "POSTGRES_PASSWORD")
228}
229
230func TestPassStore_SetAttachment_OverwriteValue(t *testing.T) {
231	requirePass(t)
232
233	store, err := NewPassStore(testLogger())
234	require.NoError(t, err)
235
236	containerName := fmt.Sprintf("attachment-overwrite-%d", time.Now().UnixNano())
237	keys := []string{"REDIS_PASSWORD"}
238	defer CleanupPassAttachment(t, containerName, keys)
239
240	err = store.SetAttachment(containerName, map[string]string{"REDIS_PASSWORD": "first"})
241	require.NoError(t, err)
242
243	// Re-setting same key with a different value should deterministically overwrite.
244	err = store.SetAttachment(containerName, map[string]string{"REDIS_PASSWORD": "second"})
245	require.NoError(t, err)
246
247	values, err := store.GetAllAttachment(containerName)
248	require.NoError(t, err)
249	assert.Equal(t, "second", values["REDIS_PASSWORD"])
250}
251
252func TestPassStore_ListKeys_RecoversOrphanedEntries(t *testing.T) {
253	requirePass(t)
254
255	store, err := NewPassStore(testLogger())
256	require.NoError(t, err)
257
258	domainName := fmt.Sprintf("orphan.%d.example.com", time.Now().UnixNano())
259	keys := []string{"EXISTING", "ORIGIN"}
260	defer cleanupPassDomain(t, domainName, keys)
261
262	err = store.Set(domainName, map[string]string{"EXISTING": "present"})
263	require.NoError(t, err)
264
265	safeDomain, err := domain.SanitizeDomainForEnvFile(domainName)
266	require.NoError(t, err)
267
268	orphanPath := fmt.Sprintf("%s/%s/ORIGIN", PassDomainSecretsPath, safeDomain)
269	err = passInsertValue(orphanPath, "https://example.com")
270	require.NoError(t, err)
271
272	manifestPath := fmt.Sprintf("%s/%s/.keys", PassDomainSecretsPath, safeDomain)
273	err = passInsertValue(manifestPath, "EXISTING\n")
274	require.NoError(t, err)
275
276	listed, err := store.ListKeys(domainName)
277	require.NoError(t, err)
278	assert.ElementsMatch(t, []string{"EXISTING", "ORIGIN"}, listed)
279
280	values, err := store.GetAll(domainName)
281	require.NoError(t, err)
282	assert.Equal(t, "present", values["EXISTING"])
283	assert.Equal(t, "https://example.com", values["ORIGIN"])
284
285	manifest, err := passShow(manifestPath)
286	require.NoError(t, err)
287	assert.Contains(t, manifest, "EXISTING")
288	assert.Contains(t, manifest, "ORIGIN")
289}
290
291func TestParsePassListOutput(t *testing.T) {
292	tests := []struct {
293		name     string
294		basePath string
295		output   string
296		want     []passListEntry
297	}{
298		{
299			name:     "simple tree",
300			basePath: "domain",
301			output:   "domain\n├── key1\n└── key2",
302			want:     []passListEntry{{name: "key1", depth: 1}, {name: "key2", depth: 1}},
303		},
304		{
305			name:     "nested tree",
306			basePath: "domain",
307			output:   "domain\n│   ├── subkey1\n│   └── subkey2\n└── key1",
308			want:     []passListEntry{{name: "subkey1", depth: 2}, {name: "subkey2", depth: 2}, {name: "key1", depth: 1}},
309		},
310		{
311			name:     "empty output",
312			basePath: "domain",
313			output:   "",
314			want:     []passListEntry{},
315		},
316		{
317			name:     "ASCII fallback chars",
318			basePath: "domain",
319			output:   "domain\n|   |-- subkey1\n`-- key1",
320			want:     []passListEntry{{name: "subkey1", depth: 2}, {name: "key1", depth: 1}},
321		},
322	}
323
324	for _, tt := range tests {
325		t.Run(tt.name, func(t *testing.T) {
326			got := parsePassListOutput(tt.basePath, tt.output)
327			assert.Equal(t, tt.want, got)
328		})
329	}
330}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close