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}