fix(secrets): improve error handling in attachment file path resolution
Change getAttachmentEnvFilePath to return (string, error) instead of just string, preserving error context instead of returning empty string on validation failures. This prevents loss of error information and improves debugging.
- commit
- 4d94bdc58539aca4882f14c1e96c84e2b889ab3f
- author
- bnema · · 2026-01-31 22:20 UTC
- parent
- 584c131
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +177 -0
M internal/adapters/out/domainsecrets/store_test.go +177 -0
| @@ -116,6 +116,183 @@ func TestFileStore_PathContainment(t *testing.T) { | |||
| 116 | 116 | } | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | + | func TestSanitizeDomainForContainer(t *testing.T) { | |
| 120 | + | tests := []struct { | |
| 121 | + | domain string | |
| 122 | + | expected string | |
| 123 | + | description string | |
| 124 | + | }{ | |
| 125 | + | { | |
| 126 | + | domain: "git.example.com", | |
| 127 | + | expected: "git__example__com", | |
| 128 | + | description: "Dots become double underscores", | |
| 129 | + | }, | |
| 130 | + | { | |
| 131 | + | domain: "git-example.com", | |
| 132 | + | expected: "git-example__com", | |
| 133 | + | description: "Hyphens preserved, dots become underscores", | |
| 134 | + | }, | |
| 135 | + | { | |
| 136 | + | domain: "app:8080.example.com", | |
| 137 | + | expected: "app-_8080__example__com", | |
| 138 | + | description: "Colons become hyphen-underscore", | |
| 139 | + | }, | |
| 140 | + | { | |
| 141 | + | domain: "git.example.com:3000", | |
| 142 | + | expected: "git__example__com-_3000", | |
| 143 | + | description: "Multiple separators handled distinctly", | |
| 144 | + | }, | |
| 145 | + | { | |
| 146 | + | domain: "simple.com", | |
| 147 | + | expected: "simple__com", | |
| 148 | + | description: "Simple domain", | |
| 149 | + | }, | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | for _, tt := range tests { | |
| 153 | + | t.Run(tt.domain, func(t *testing.T) { | |
| 154 | + | result := domain.SanitizeDomainForContainer(tt.domain) | |
| 155 | + | assert.Equal(t, tt.expected, result, "sanitization should match expected") | |
| 156 | + | }) | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // Verify no collisions between potentially conflicting domains | |
| 160 | + | t.Run("NoCollisions", func(t *testing.T) { | |
| 161 | + | domains := []string{ | |
| 162 | + | "git.example.com", | |
| 163 | + | "git-example.com", | |
| 164 | + | "app:8080.example.com", | |
| 165 | + | "app-8080-example.com", | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | results := make(map[string]string) | |
| 169 | + | for _, d := range domains { | |
| 170 | + | result := domain.SanitizeDomainForContainer(d) | |
| 171 | + | if original, exists := results[result]; exists { | |
| 172 | + | t.Errorf("COLLISION: %q and %q both sanitize to %q", original, d, result) | |
| 173 | + | } | |
| 174 | + | results[result] = d | |
| 175 | + | } | |
| 176 | + | }) | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | func TestFileStore_AttachmentOperations(t *testing.T) { | |
| 180 | + | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | |
| 181 | + | require.NoError(t, err) | |
| 182 | + | defer os.RemoveAll(tmpDir) | |
| 183 | + | ||
| 184 | + | store, err := NewFileStore(tmpDir, testLogger()) | |
| 185 | + | require.NoError(t, err) | |
| 186 | + | ||
| 187 | + | containerName := "gitea-postgres" | |
| 188 | + | ||
| 189 | + | t.Run("SetAttachment creates new file", func(t *testing.T) { | |
| 190 | + | err := store.SetAttachment(containerName, map[string]string{ | |
| 191 | + | "POSTGRES_DB": "gitea", | |
| 192 | + | "POSTGRES_PASSWORD": "secret123", | |
| 193 | + | }) | |
| 194 | + | require.NoError(t, err) | |
| 195 | + | ||
| 196 | + | // Verify file exists | |
| 197 | + | expectedFile := filepath.Join(tmpDir, "gordon-"+containerName+".env") | |
| 198 | + | _, err = os.Stat(expectedFile) | |
| 199 | + | assert.NoError(t, err, "expected attachment env file to exist at %s", expectedFile) | |
| 200 | + | }) | |
| 201 | + | ||
| 202 | + | t.Run("GetAllAttachment retrieves secrets", func(t *testing.T) { | |
| 203 | + | secrets, err := store.GetAllAttachment(containerName) | |
| 204 | + | require.NoError(t, err) | |
| 205 | + | assert.Equal(t, "gitea", secrets["POSTGRES_DB"]) | |
| 206 | + | assert.Equal(t, "secret123", secrets["POSTGRES_PASSWORD"]) | |
| 207 | + | }) | |
| 208 | + | ||
| 209 | + | t.Run("SetAttachment merges with existing", func(t *testing.T) { | |
| 210 | + | err := store.SetAttachment(containerName, map[string]string{ | |
| 211 | + | "NEW_VAR": "new_value", | |
| 212 | + | }) | |
| 213 | + | require.NoError(t, err) | |
| 214 | + | ||
| 215 | + | // Should have both old and new secrets | |
| 216 | + | secrets, err := store.GetAllAttachment(containerName) | |
| 217 | + | require.NoError(t, err) | |
| 218 | + | assert.Equal(t, "gitea", secrets["POSTGRES_DB"]) | |
| 219 | + | assert.Equal(t, "secret123", secrets["POSTGRES_PASSWORD"]) | |
| 220 | + | assert.Equal(t, "new_value", secrets["NEW_VAR"]) | |
| 221 | + | }) | |
| 222 | + | ||
| 223 | + | t.Run("GetAllAttachment returns empty map for non-existent container", func(t *testing.T) { | |
| 224 | + | secrets, err := store.GetAllAttachment("non-existent") | |
| 225 | + | require.NoError(t, err) | |
| 226 | + | assert.Empty(t, secrets) | |
| 227 | + | }) | |
| 228 | + | ||
| 229 | + | t.Run("SetAttachment with hyphenated container name", func(t *testing.T) { | |
| 230 | + | hyphenatedContainer := "gordon-git-example-com-gitea-postgres" | |
| 231 | + | err := store.SetAttachment(hyphenatedContainer, map[string]string{ | |
| 232 | + | "KEY": "value", | |
| 233 | + | }) | |
| 234 | + | require.NoError(t, err) | |
| 235 | + | ||
| 236 | + | secrets, err := store.GetAllAttachment(hyphenatedContainer) | |
| 237 | + | require.NoError(t, err) | |
| 238 | + | assert.Equal(t, "value", secrets["KEY"]) | |
| 239 | + | }) | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func TestFileStore_AttachmentPathTraversal(t *testing.T) { | |
| 243 | + | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | |
| 244 | + | require.NoError(t, err) | |
| 245 | + | defer os.RemoveAll(tmpDir) | |
| 246 | + | ||
| 247 | + | store, err := NewFileStore(tmpDir, testLogger()) | |
| 248 | + | require.NoError(t, err) | |
| 249 | + | ||
| 250 | + | tests := []struct { | |
| 251 | + | name string | |
| 252 | + | container string | |
| 253 | + | wantSetErr bool | |
| 254 | + | wantGetErr bool | |
| 255 | + | }{ | |
| 256 | + | // Valid container names | |
| 257 | + | {"simple", "postgres", false, false}, | |
| 258 | + | {"with hyphen", "gitea-postgres", false, false}, | |
| 259 | + | {"with underscore", "gitea_postgres", false, false}, | |
| 260 | + | {"complex real container", "gordon-git-example-com-gitea-postgres", false, false}, | |
| 261 | + | ||
| 262 | + | // Invalid container names - should fail | |
| 263 | + | {"empty", "", true, true}, | |
| 264 | + | {"starts with number", "1container", true, true}, | |
| 265 | + | {"starts with hyphen", "-container", true, true}, | |
| 266 | + | {"starts with underscore", "_container", true, true}, | |
| 267 | + | {"path traversal", "../etc", true, true}, | |
| 268 | + | {"contains slash", "container/name", true, true}, | |
| 269 | + | {"contains backslash", "container\\name", true, true}, | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | for _, tt := range tests { | |
| 273 | + | t.Run(tt.name, func(t *testing.T) { | |
| 274 | + | // Test SetAttachment | |
| 275 | + | err := store.SetAttachment(tt.container, map[string]string{"KEY": "value"}) | |
| 276 | + | if tt.wantSetErr { | |
| 277 | + | require.Error(t, err) | |
| 278 | + | } else { | |
| 279 | + | assert.NoError(t, err) | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // Test GetAllAttachment | |
| 283 | + | _, err = store.GetAllAttachment(tt.container) | |
| 284 | + | if tt.wantGetErr { | |
| 285 | + | require.Error(t, err) | |
| 286 | + | } else { | |
| 287 | + | if !tt.wantSetErr { | |
| 288 | + | // Only check no error if we successfully set it | |
| 289 | + | assert.NoError(t, err) | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | }) | |
| 293 | + | } | |
| 294 | + | } | |
| 295 | + | ||
| 119 | 296 | func TestFileStore_ValidDomainOperations(t *testing.T) { | |
| 120 | 297 | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | |
| 121 | 298 | require.NoError(t, err) | |
| @@ -116,6 +116,183 @@ func TestFileStore_PathContainment(t *testing.T) { | |||||
| 116 | } | 116 | } | ||
| 117 | } | 117 | } | ||
| 118 | 118 | ||||
| 119 | + | func TestSanitizeDomainForContainer(t *testing.T) { | |||
| 120 | + | tests := []struct { | |||
| 121 | + | domain string | |||
| 122 | + | expected string | |||
| 123 | + | description string | |||
| 124 | + | }{ | |||
| 125 | + | { | |||
| 126 | + | domain: "git.example.com", | |||
| 127 | + | expected: "git__example__com", | |||
| 128 | + | description: "Dots become double underscores", | |||
| 129 | + | }, | |||
| 130 | + | { | |||
| 131 | + | domain: "git-example.com", | |||
| 132 | + | expected: "git-example__com", | |||
| 133 | + | description: "Hyphens preserved, dots become underscores", | |||
| 134 | + | }, | |||
| 135 | + | { | |||
| 136 | + | domain: "app:8080.example.com", | |||
| 137 | + | expected: "app-_8080__example__com", | |||
| 138 | + | description: "Colons become hyphen-underscore", | |||
| 139 | + | }, | |||
| 140 | + | { | |||
| 141 | + | domain: "git.example.com:3000", | |||
| 142 | + | expected: "git__example__com-_3000", | |||
| 143 | + | description: "Multiple separators handled distinctly", | |||
| 144 | + | }, | |||
| 145 | + | { | |||
| 146 | + | domain: "simple.com", | |||
| 147 | + | expected: "simple__com", | |||
| 148 | + | description: "Simple domain", | |||
| 149 | + | }, | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | for _, tt := range tests { | |||
| 153 | + | t.Run(tt.domain, func(t *testing.T) { | |||
| 154 | + | result := domain.SanitizeDomainForContainer(tt.domain) | |||
| 155 | + | assert.Equal(t, tt.expected, result, "sanitization should match expected") | |||
| 156 | + | }) | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | // Verify no collisions between potentially conflicting domains | |||
| 160 | + | t.Run("NoCollisions", func(t *testing.T) { | |||
| 161 | + | domains := []string{ | |||
| 162 | + | "git.example.com", | |||
| 163 | + | "git-example.com", | |||
| 164 | + | "app:8080.example.com", | |||
| 165 | + | "app-8080-example.com", | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | results := make(map[string]string) | |||
| 169 | + | for _, d := range domains { | |||
| 170 | + | result := domain.SanitizeDomainForContainer(d) | |||
| 171 | + | if original, exists := results[result]; exists { | |||
| 172 | + | t.Errorf("COLLISION: %q and %q both sanitize to %q", original, d, result) | |||
| 173 | + | } | |||
| 174 | + | results[result] = d | |||
| 175 | + | } | |||
| 176 | + | }) | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | func TestFileStore_AttachmentOperations(t *testing.T) { | |||
| 180 | + | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | |||
| 181 | + | require.NoError(t, err) | |||
| 182 | + | defer os.RemoveAll(tmpDir) | |||
| 183 | + | ||||
| 184 | + | store, err := NewFileStore(tmpDir, testLogger()) | |||
| 185 | + | require.NoError(t, err) | |||
| 186 | + | ||||
| 187 | + | containerName := "gitea-postgres" | |||
| 188 | + | ||||
| 189 | + | t.Run("SetAttachment creates new file", func(t *testing.T) { | |||
| 190 | + | err := store.SetAttachment(containerName, map[string]string{ | |||
| 191 | + | "POSTGRES_DB": "gitea", | |||
| 192 | + | "POSTGRES_PASSWORD": "secret123", | |||
| 193 | + | }) | |||
| 194 | + | require.NoError(t, err) | |||
| 195 | + | ||||
| 196 | + | // Verify file exists | |||
| 197 | + | expectedFile := filepath.Join(tmpDir, "gordon-"+containerName+".env") | |||
| 198 | + | _, err = os.Stat(expectedFile) | |||
| 199 | + | assert.NoError(t, err, "expected attachment env file to exist at %s", expectedFile) | |||
| 200 | + | }) | |||
| 201 | + | ||||
| 202 | + | t.Run("GetAllAttachment retrieves secrets", func(t *testing.T) { | |||
| 203 | + | secrets, err := store.GetAllAttachment(containerName) | |||
| 204 | + | require.NoError(t, err) | |||
| 205 | + | assert.Equal(t, "gitea", secrets["POSTGRES_DB"]) | |||
| 206 | + | assert.Equal(t, "secret123", secrets["POSTGRES_PASSWORD"]) | |||
| 207 | + | }) | |||
| 208 | + | ||||
| 209 | + | t.Run("SetAttachment merges with existing", func(t *testing.T) { | |||
| 210 | + | err := store.SetAttachment(containerName, map[string]string{ | |||
| 211 | + | "NEW_VAR": "new_value", | |||
| 212 | + | }) | |||
| 213 | + | require.NoError(t, err) | |||
| 214 | + | ||||
| 215 | + | // Should have both old and new secrets | |||
| 216 | + | secrets, err := store.GetAllAttachment(containerName) | |||
| 217 | + | require.NoError(t, err) | |||
| 218 | + | assert.Equal(t, "gitea", secrets["POSTGRES_DB"]) | |||
| 219 | + | assert.Equal(t, "secret123", secrets["POSTGRES_PASSWORD"]) | |||
| 220 | + | assert.Equal(t, "new_value", secrets["NEW_VAR"]) | |||
| 221 | + | }) | |||
| 222 | + | ||||
| 223 | + | t.Run("GetAllAttachment returns empty map for non-existent container", func(t *testing.T) { | |||
| 224 | + | secrets, err := store.GetAllAttachment("non-existent") | |||
| 225 | + | require.NoError(t, err) | |||
| 226 | + | assert.Empty(t, secrets) | |||
| 227 | + | }) | |||
| 228 | + | ||||
| 229 | + | t.Run("SetAttachment with hyphenated container name", func(t *testing.T) { | |||
| 230 | + | hyphenatedContainer := "gordon-git-example-com-gitea-postgres" | |||
| 231 | + | err := store.SetAttachment(hyphenatedContainer, map[string]string{ | |||
| 232 | + | "KEY": "value", | |||
| 233 | + | }) | |||
| 234 | + | require.NoError(t, err) | |||
| 235 | + | ||||
| 236 | + | secrets, err := store.GetAllAttachment(hyphenatedContainer) | |||
| 237 | + | require.NoError(t, err) | |||
| 238 | + | assert.Equal(t, "value", secrets["KEY"]) | |||
| 239 | + | }) | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | func TestFileStore_AttachmentPathTraversal(t *testing.T) { | |||
| 243 | + | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | |||
| 244 | + | require.NoError(t, err) | |||
| 245 | + | defer os.RemoveAll(tmpDir) | |||
| 246 | + | ||||
| 247 | + | store, err := NewFileStore(tmpDir, testLogger()) | |||
| 248 | + | require.NoError(t, err) | |||
| 249 | + | ||||
| 250 | + | tests := []struct { | |||
| 251 | + | name string | |||
| 252 | + | container string | |||
| 253 | + | wantSetErr bool | |||
| 254 | + | wantGetErr bool | |||
| 255 | + | }{ | |||
| 256 | + | // Valid container names | |||
| 257 | + | {"simple", "postgres", false, false}, | |||
| 258 | + | {"with hyphen", "gitea-postgres", false, false}, | |||
| 259 | + | {"with underscore", "gitea_postgres", false, false}, | |||
| 260 | + | {"complex real container", "gordon-git-example-com-gitea-postgres", false, false}, | |||
| 261 | + | ||||
| 262 | + | // Invalid container names - should fail | |||
| 263 | + | {"empty", "", true, true}, | |||
| 264 | + | {"starts with number", "1container", true, true}, | |||
| 265 | + | {"starts with hyphen", "-container", true, true}, | |||
| 266 | + | {"starts with underscore", "_container", true, true}, | |||
| 267 | + | {"path traversal", "../etc", true, true}, | |||
| 268 | + | {"contains slash", "container/name", true, true}, | |||
| 269 | + | {"contains backslash", "container\\name", true, true}, | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | for _, tt := range tests { | |||
| 273 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 274 | + | // Test SetAttachment | |||
| 275 | + | err := store.SetAttachment(tt.container, map[string]string{"KEY": "value"}) | |||
| 276 | + | if tt.wantSetErr { | |||
| 277 | + | require.Error(t, err) | |||
| 278 | + | } else { | |||
| 279 | + | assert.NoError(t, err) | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | // Test GetAllAttachment | |||
| 283 | + | _, err = store.GetAllAttachment(tt.container) | |||
| 284 | + | if tt.wantGetErr { | |||
| 285 | + | require.Error(t, err) | |||
| 286 | + | } else { | |||
| 287 | + | if !tt.wantSetErr { | |||
| 288 | + | // Only check no error if we successfully set it | |||
| 289 | + | assert.NoError(t, err) | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | }) | |||
| 293 | + | } | |||
| 294 | + | } | |||
| 295 | + | ||||
| 119 | func TestFileStore_ValidDomainOperations(t *testing.T) { | 296 | func TestFileStore_ValidDomainOperations(t *testing.T) { | ||
| 120 | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | 297 | tmpDir, err := os.MkdirTemp("", "domainsecrets-test-*") | ||
| 121 | require.NoError(t, err) | 298 | require.NoError(t, err) | ||