1package domainsecrets
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/zerowrap"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "gordon/internal/domain"
14)
15
16func testLogger() zerowrap.Logger {
17 return zerowrap.Default()
18}
19
20func TestFileStore_PathTraversal(t *testing.T) {
21 tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*")
22 require.NoError(t, err)
23 defer os.RemoveAll(tmpDir)
24
25 store, err := NewFileStore(tmpDir, testLogger())
26 require.NoError(t, err)
27
28 tests := []struct {
29 name string
30 domain string
31 wantErr bool
32 }{
33 // Valid domains
34 {"simple domain", "example.com", false},
35 {"subdomain", "sub.example.com", false},
36 {"domain with port", "example.com:8080", false},
37 {"domain with path", "example.com/path", false},
38 {"hyphenated domain", "my-app.example.com", false},
39 {"single char", "a", false},
40
41 // Path traversal attempts - should fail
42 {"path traversal with dots", "../../../etc/passwd", true},
43 {"path traversal in middle", "example.com/../../../etc/passwd", true},
44 {"double dots only", "..", true},
45 {"path traversal at end", "example.com/..", true},
46
47 // Invalid format - should fail
48 {"empty domain", "", true},
49 {"starts with dot", ".example.com", true},
50 {"ends with dot", "example.com.", true},
51 {"special chars semicolon", "example;rm -rf /", true},
52 {"null byte", "example\x00.com", true},
53 {"newline", "example\n.com", true},
54 }
55
56 for _, tt := range tests {
57 t.Run(tt.name, func(t *testing.T) {
58 // Test ListKeys
59 _, err := store.ListKeys(tt.domain)
60 if tt.wantErr {
61 require.Error(t, err)
62 assert.True(t, errors.Is(err, domain.ErrPathTraversal), "expected ErrPathTraversal, got: %v", err)
63 } else {
64 // No error, or file not found (which is fine)
65 if err != nil {
66 assert.False(t, errors.Is(err, domain.ErrPathTraversal), "unexpected ErrPathTraversal")
67 }
68 }
69
70 // Test Set
71 err = store.Set(tt.domain, map[string]string{"KEY": "value"})
72 if tt.wantErr {
73 require.Error(t, err)
74 assert.True(t, errors.Is(err, domain.ErrPathTraversal), "expected ErrPathTraversal, got: %v", err)
75 } else {
76 assert.NoError(t, err)
77 }
78
79 // Test GetAll
80 _, err = store.GetAll(tt.domain)
81 if tt.wantErr {
82 require.Error(t, err)
83 assert.True(t, errors.Is(err, domain.ErrPathTraversal), "expected ErrPathTraversal, got: %v", err)
84 }
85
86 // Test Delete
87 err = store.Delete(tt.domain, "KEY")
88 if tt.wantErr {
89 require.Error(t, err)
90 assert.True(t, errors.Is(err, domain.ErrPathTraversal), "expected ErrPathTraversal, got: %v", err)
91 }
92 })
93 }
94}
95
96func TestFileStore_PathContainment(t *testing.T) {
97 tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*")
98 require.NoError(t, err)
99 defer os.RemoveAll(tmpDir)
100
101 store, err := NewFileStore(tmpDir, testLogger())
102 require.NoError(t, err)
103
104 // Valid domain - file should be created inside tmpDir
105 err = store.Set("example.com", map[string]string{"KEY": "value"})
106 require.NoError(t, err)
107
108 // Verify file exists inside tmpDir
109 expectedFile := filepath.Join(tmpDir, "example_com.env")
110 _, err = os.Stat(expectedFile)
111 assert.NoError(t, err, "expected env file to exist at %s", expectedFile)
112
113 // Verify no file was created outside tmpDir
114 entries, err := filepath.Glob(tmpDir + "/../*")
115 require.NoError(t, err)
116 for _, entry := range entries {
117 if filepath.Base(entry) != filepath.Base(tmpDir) {
118 // Check it's not our test file escaped
119 assert.NotContains(t, entry, ".env", "unexpected .env file outside tmpDir: %s", entry)
120 }
121 }
122}
123
124func TestFileStore_ValidDomainOperations(t *testing.T) {
125 tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*")
126 require.NoError(t, err)
127 defer os.RemoveAll(tmpDir)
128
129 store, err := NewFileStore(tmpDir, testLogger())
130 require.NoError(t, err)
131
132 domain := "test.example.com"
133
134 // Set secrets
135 err = store.Set(domain, map[string]string{
136 "DB_HOST": "localhost",
137 "DB_PASSWORD": "secret123",
138 })
139 require.NoError(t, err)
140
141 // Get all secrets
142 secrets, err := store.GetAll(domain)
143 require.NoError(t, err)
144 assert.Equal(t, "localhost", secrets["DB_HOST"])
145 assert.Equal(t, "secret123", secrets["DB_PASSWORD"])
146
147 // List keys
148 keys, err := store.ListKeys(domain)
149 require.NoError(t, err)
150 assert.Len(t, keys, 2)
151 assert.Contains(t, keys, "DB_HOST")
152 assert.Contains(t, keys, "DB_PASSWORD")
153
154 // Delete a key
155 err = store.Delete(domain, "DB_PASSWORD")
156 require.NoError(t, err)
157
158 // Verify deletion
159 secrets, err = store.GetAll(domain)
160 require.NoError(t, err)
161 assert.Len(t, secrets, 1)
162 assert.Equal(t, "localhost", secrets["DB_HOST"])
163 _, exists := secrets["DB_PASSWORD"]
164 assert.False(t, exists)
165}
166
167func TestGetEnvFilePath(t *testing.T) {
168 tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*")
169 require.NoError(t, err)
170 defer os.RemoveAll(tmpDir)
171
172 store, err := NewFileStore(tmpDir, testLogger())
173 require.NoError(t, err)
174
175 tests := []struct {
176 domain string
177 wantEmpty bool
178 wantFilename string
179 }{
180 {"example.com", false, "example_com.env"},
181 {"sub.example.com", false, "sub_example_com.env"},
182 {"example.com:8080", false, "example_com_8080.env"},
183 {"example.com/path", false, "example_com_path.env"},
184 {"../evil", true, ""},
185 {"", true, ""},
186 }
187
188 for _, tt := range tests {
189 t.Run(tt.domain, func(t *testing.T) {
190 path := store.getEnvFilePath(tt.domain)
191
192 if tt.wantEmpty {
193 assert.Empty(t, path)
194 } else {
195 assert.NotEmpty(t, path)
196 assert.Equal(t, filepath.Join(tmpDir, tt.wantFilename), path)
197 }
198 })
199 }
200}