store_test.go RawHistoryFolder

1package secretfiles_test
2
3import (
4	"context"
5	"os"
6	"path/filepath"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/gordon/internal/adapters/out/secretfiles"
13	"github.com/bnema/gordon/internal/boundaries/out"
14)
15
16func newStore(t *testing.T) (*secretfiles.Store, string) {
17	t.Helper()
18	dataDir := t.TempDir()
19	store, err := secretfiles.NewStore(dataDir)
20	require.NoError(t, err)
21	t.Cleanup(func() { _ = store.Close() })
22	return store, dataDir
23}
24
25func mode(t *testing.T, p string) os.FileMode {
26	t.Helper()
27	info, err := os.Stat(p)
28	require.NoError(t, err)
29	return info.Mode().Perm()
30}
31
32func TestStore_WriteModesAndRead(t *testing.T) {
33	store, dataDir := newStore(t)
34	ctx := context.Background()
35	assert.Equal(t, filepath.Join(dataDir, "secrets-run"), store.Dir())
36
37	paths, err := store.Write(ctx, "blog", "gordon-blog--web--a", map[string]string{"DB": "s3cr3t", "API": "k"})
38	require.NoError(t, err)
39	assert.Equal(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a", "DB"), paths["DB"])
40	assert.Len(t, paths, 2)
41
42	assert.Equal(t, os.FileMode(0o700), mode(t, store.Dir()))
43	assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog")))
44	assert.Equal(t, os.FileMode(0o700), mode(t, filepath.Join(store.Dir(), "blog", "gordon-blog--web--a")))
45	assert.Equal(t, os.FileMode(0o444), mode(t, paths["DB"]))
46
47	got, err := store.Read(ctx, "blog", "gordon-blog--web--a", "DB")
48	require.NoError(t, err)
49	assert.Equal(t, "s3cr3t", got)
50}
51
52func TestStore_WriteNeverReplacesExistingContainer(t *testing.T) {
53	store, _ := newStore(t)
54	ctx := context.Background()
55	_, err := store.Write(ctx, "blog", "c1", map[string]string{"K": "live"})
56	require.NoError(t, err)
57
58	_, err = store.Write(ctx, "blog", "c1", map[string]string{"K": "other"})
59	require.Error(t, err)
60	assert.NotContains(t, err.Error(), store.Dir())
61
62	got, err := store.Read(ctx, "blog", "c1", "K")
63	require.NoError(t, err)
64	assert.Equal(t, "live", got, "a live container's files stay intact")
65}
66
67func TestStore_RemoveAndList(t *testing.T) {
68	store, _ := newStore(t)
69	ctx := context.Background()
70	for _, set := range []out.SecretFileSet{{App: "blog", Container: "c2"}, {App: "blog", Container: "c1"}, {App: "alpha", Container: "z"}} {
71		_, err := store.Write(ctx, set.App, set.Container, map[string]string{"K": "v"})
72		require.NoError(t, err)
73	}
74
75	sets, err := store.List(ctx)
76	require.NoError(t, err)
77	assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}, {App: "blog", Container: "c1"}, {App: "blog", Container: "c2"}}, sets)
78
79	require.NoError(t, store.Remove(ctx, "blog", "c1"))
80	assert.NoDirExists(t, filepath.Join(store.Dir(), "blog", "c1"))
81	assert.DirExists(t, filepath.Join(store.Dir(), "blog", "c2"))
82
83	require.NoError(t, store.Remove(ctx, "blog", "c2"))
84	assert.NoDirExists(t, filepath.Join(store.Dir(), "blog"))
85
86	require.NoError(t, store.Remove(ctx, "blog", "missing"))
87	require.NoError(t, store.Remove(ctx, "ghost", "missing"))
88
89	sets, err = store.List(ctx)
90	require.NoError(t, err)
91	assert.Equal(t, []out.SecretFileSet{{App: "alpha", Container: "z"}}, sets)
92}
93
94func TestStore_InvalidNamesRejected(t *testing.T) {
95	store, _ := newStore(t)
96	ctx := context.Background()
97	cases := []struct {
98		name, app, container, key string
99	}{
100		{"app traversal", "../x", "c", "K"},
101		{"app slash", "a/b", "c", "K"},
102		{"container dot", "blog", ".", "K"},
103		{"container dotdot", "blog", "..", "K"},
104		{"container slash", "blog", "a/b", "K"},
105		{"container empty", "blog", "", "K"},
106		{"bad key", "blog", "c", "bad-KEY!"},
107		{"key traversal", "blog", "c", "../K"},
108	}
109	for _, tc := range cases {
110		t.Run(tc.name, func(t *testing.T) {
111			_, err := store.Write(ctx, tc.app, tc.container, map[string]string{tc.key: "v"})
112			require.Error(t, err)
113			assert.NotContains(t, err.Error(), store.Dir())
114		})
115	}
116	sets, err := store.List(ctx)
117	require.NoError(t, err)
118	assert.Empty(t, sets)
119}
120
121func TestStore_SymlinkEscapeRefused(t *testing.T) {
122	store, _ := newStore(t)
123	outside := t.TempDir()
124	require.NoError(t, os.Symlink(outside, filepath.Join(store.Dir(), "blog")))
125
126	_, err := store.Write(context.Background(), "blog", "c1", map[string]string{"K": "v"})
127	require.Error(t, err)
128	entries, err := os.ReadDir(outside)
129	require.NoError(t, err)
130	assert.Empty(t, entries)
131}
132
133func TestNewStore_RefusesSymlinkBase(t *testing.T) {
134	dataDir := t.TempDir()
135	require.NoError(t, os.Symlink(t.TempDir(), filepath.Join(dataDir, "secrets-run")))
136	_, err := secretfiles.NewStore(dataDir)
137	require.Error(t, err)
138}

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