1package secrets
2
3import (
4 "context"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zerowrap"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/mock"
11
12 "github.com/bnema/gordon/internal/boundaries/out"
13 outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
14 "github.com/bnema/gordon/internal/domain"
15)
16
17func testLogger() zerowrap.Logger {
18 return zerowrap.Default()
19}
20
21func TestValidateDomain(t *testing.T) {
22 tests := []struct {
23 name string
24 domain string
25 wantErr error
26 }{
27 // Valid domains
28 {
29 name: "valid simple domain",
30 domain: "example.com",
31 wantErr: nil,
32 },
33 {
34 name: "valid subdomain",
35 domain: "app.example.com",
36 wantErr: nil,
37 },
38 {
39 name: "valid domain with port",
40 domain: "app.example.com:8080",
41 wantErr: nil,
42 },
43 {
44 name: "valid domain with hyphens",
45 domain: "my-app.example-site.com",
46 wantErr: nil,
47 },
48 {
49 name: "valid single label",
50 domain: "localhost",
51 wantErr: nil,
52 },
53 // Empty domain
54 {
55 name: "empty domain",
56 domain: "",
57 wantErr: ErrDomainEmpty,
58 },
59 // Path traversal attempts
60 {
61 name: "path traversal with double dots",
62 domain: "../etc/passwd",
63 wantErr: ErrDomainPathTraversal,
64 },
65 {
66 name: "path traversal in middle",
67 domain: "app/../etc/passwd",
68 wantErr: ErrDomainPathTraversal,
69 },
70 {
71 name: "path traversal at end",
72 domain: "app.example.com/..",
73 wantErr: ErrDomainPathTraversal,
74 },
75 {
76 name: "multiple path traversal",
77 domain: "..../....//etc/passwd",
78 wantErr: ErrDomainPathTraversal,
79 },
80 {
81 name: "encoded path traversal still blocked",
82 domain: "app..example.com",
83 wantErr: ErrDomainPathTraversal,
84 },
85 // Domain too long
86 {
87 name: "domain at max length (253)",
88 domain: strings.Repeat("a", 253),
89 wantErr: nil,
90 },
91 {
92 name: "domain exceeds max length",
93 domain: strings.Repeat("a", 254),
94 wantErr: ErrDomainTooLong,
95 },
96 {
97 name: "domain way too long",
98 domain: strings.Repeat("a", 1000),
99 wantErr: ErrDomainTooLong,
100 },
101 // Invalid characters
102 {
103 name: "null byte in domain",
104 domain: "app\x00.example.com",
105 wantErr: ErrDomainInvalidChars,
106 },
107 {
108 name: "null byte at start",
109 domain: "\x00example.com",
110 wantErr: ErrDomainInvalidChars,
111 },
112 {
113 name: "underscore rejected to avoid env filename collision",
114 domain: "app_example.com",
115 wantErr: ErrDomainInvalidChars,
116 },
117 }
118
119 for _, tt := range tests {
120 t.Run(tt.name, func(t *testing.T) {
121 err := ValidateDomain(tt.domain)
122 if tt.wantErr != nil {
123 assert.ErrorIs(t, err, tt.wantErr)
124 } else {
125 assert.NoError(t, err)
126 }
127 })
128 }
129}
130
131func TestValidateDomain_DistinguishesSeparatorsForEnvStorage(t *testing.T) {
132 for _, domain := range []string{"app.example.com", "app:example:com", "app/example/com"} {
133 assert.NoError(t, ValidateDomain(domain), "expected %q to remain valid", domain)
134 }
135}
136
137func TestService_ListKeys_ValidationErrors(t *testing.T) {
138 store := outmocks.NewMockDomainSecretStore(t)
139 svc := NewService(store, testLogger(), nil)
140
141 tests := []struct {
142 name string
143 domain string
144 wantErr error
145 }{
146 {
147 name: "empty domain",
148 domain: "",
149 wantErr: ErrDomainEmpty,
150 },
151 {
152 name: "path traversal",
153 domain: "../etc/passwd",
154 wantErr: ErrDomainPathTraversal,
155 },
156 {
157 name: "domain too long",
158 domain: strings.Repeat("a", 300),
159 wantErr: ErrDomainTooLong,
160 },
161 }
162
163 for _, tt := range tests {
164 t.Run(tt.name, func(t *testing.T) {
165 keys, err := svc.ListKeys(context.Background(), tt.domain)
166 assert.Nil(t, keys)
167 assert.ErrorIs(t, err, tt.wantErr)
168 })
169 }
170}
171
172func TestService_ListKeys_Success(t *testing.T) {
173 store := outmocks.NewMockDomainSecretStore(t)
174 svc := NewService(store, testLogger(), nil)
175
176 expectedKeys := []string{"API_KEY", "DB_PASSWORD"}
177 store.EXPECT().ListKeys("app.example.com").Return(expectedKeys, nil)
178
179 keys, err := svc.ListKeys(context.Background(), "app.example.com")
180 assert.NoError(t, err)
181 assert.Equal(t, expectedKeys, keys)
182}
183
184func TestService_ListKeysWithAttachments_ValidationErrors(t *testing.T) {
185 store := outmocks.NewMockDomainSecretStore(t)
186 svc := NewService(store, testLogger(), nil)
187
188 tests := []struct {
189 name string
190 domain string
191 wantErr error
192 }{
193 {
194 name: "empty domain",
195 domain: "",
196 wantErr: ErrDomainEmpty,
197 },
198 {
199 name: "path traversal",
200 domain: "../etc/passwd",
201 wantErr: ErrDomainPathTraversal,
202 },
203 {
204 name: "domain too long",
205 domain: strings.Repeat("a", 300),
206 wantErr: ErrDomainTooLong,
207 },
208 }
209
210 for _, tt := range tests {
211 t.Run(tt.name, func(t *testing.T) {
212 keys, attachments, err := svc.ListKeysWithAttachments(context.Background(), tt.domain)
213 assert.Nil(t, keys)
214 assert.Nil(t, attachments)
215 assert.ErrorIs(t, err, tt.wantErr)
216 })
217 }
218}
219
220func TestService_ListKeysWithAttachments_Success(t *testing.T) {
221 t.Run("returns both domain and attachment secrets", func(t *testing.T) {
222 store := outmocks.NewMockDomainSecretStore(t)
223 svc := NewService(store, testLogger(), nil)
224
225 expectedKeys := []string{"API_KEY", "DB_PASSWORD"}
226 expectedAttachments := []out.AttachmentSecrets{
227 {Service: "postgres", Keys: []string{"POSTGRES_PASSWORD"}},
228 {Service: "redis", Keys: []string{"REDIS_PASSWORD", "REDIS_USER"}},
229 }
230
231 store.EXPECT().ListKeys("app.example.com").Return(expectedKeys, nil)
232 store.EXPECT().ListAttachmentKeys("app.example.com").Return(expectedAttachments, nil)
233
234 keys, attachments, err := svc.ListKeysWithAttachments(context.Background(), "app.example.com")
235 assert.NoError(t, err)
236 assert.Equal(t, expectedKeys, keys)
237 assert.Equal(t, expectedAttachments, attachments)
238 })
239
240 t.Run("returns empty attachments when none exist", func(t *testing.T) {
241 store := outmocks.NewMockDomainSecretStore(t)
242 svc := NewService(store, testLogger(), nil)
243
244 expectedKeys := []string{"API_KEY"}
245
246 store.EXPECT().ListKeys("app.example.com").Return(expectedKeys, nil)
247 store.EXPECT().ListAttachmentKeys("app.example.com").Return(nil, nil)
248
249 keys, attachments, err := svc.ListKeysWithAttachments(context.Background(), "app.example.com")
250 assert.NoError(t, err)
251 assert.Equal(t, expectedKeys, keys)
252 assert.Nil(t, attachments)
253 })
254
255 t.Run("continues without attachments on attachment error", func(t *testing.T) {
256 store := outmocks.NewMockDomainSecretStore(t)
257 svc := NewService(store, testLogger(), nil)
258
259 expectedKeys := []string{"API_KEY"}
260 attachmentErr := assert.AnError
261
262 store.EXPECT().ListKeys("app.example.com").Return(expectedKeys, nil)
263 store.EXPECT().ListAttachmentKeys("app.example.com").Return(nil, attachmentErr)
264
265 // Should still succeed, just without attachments
266 keys, attachments, err := svc.ListKeysWithAttachments(context.Background(), "app.example.com")
267 assert.NoError(t, err)
268 assert.Equal(t, expectedKeys, keys)
269 assert.Nil(t, attachments)
270 })
271}
272
273func TestService_ListKeysWithAttachments_StoreError(t *testing.T) {
274 store := outmocks.NewMockDomainSecretStore(t)
275 svc := NewService(store, testLogger(), nil)
276
277 storeErr := assert.AnError
278 store.EXPECT().ListKeys("app.example.com").Return(nil, storeErr)
279
280 keys, attachments, err := svc.ListKeysWithAttachments(context.Background(), "app.example.com")
281 assert.Nil(t, keys)
282 assert.Nil(t, attachments)
283 assert.ErrorIs(t, err, storeErr)
284}
285
286func TestService_GetAll_ValidationErrors(t *testing.T) {
287 store := outmocks.NewMockDomainSecretStore(t)
288 svc := NewService(store, testLogger(), nil)
289
290 tests := []struct {
291 name string
292 domain string
293 wantErr error
294 }{
295 {
296 name: "empty domain",
297 domain: "",
298 wantErr: ErrDomainEmpty,
299 },
300 {
301 name: "path traversal",
302 domain: "app/../secret",
303 wantErr: ErrDomainPathTraversal,
304 },
305 }
306
307 for _, tt := range tests {
308 t.Run(tt.name, func(t *testing.T) {
309 secrets, err := svc.GetAll(context.Background(), tt.domain)
310 assert.Nil(t, secrets)
311 assert.ErrorIs(t, err, tt.wantErr)
312 })
313 }
314}
315
316func TestService_GetAll_Success(t *testing.T) {
317 store := outmocks.NewMockDomainSecretStore(t)
318 svc := NewService(store, testLogger(), nil)
319
320 expectedSecrets := map[string]string{"API_KEY": "secret123", "DB_PASSWORD": "pass456"}
321 store.EXPECT().GetAll("app.example.com").Return(expectedSecrets, nil)
322
323 secrets, err := svc.GetAll(context.Background(), "app.example.com")
324 assert.NoError(t, err)
325 assert.Equal(t, expectedSecrets, secrets)
326}
327
328func TestService_Set_ValidationErrors(t *testing.T) {
329 store := outmocks.NewMockDomainSecretStore(t)
330 svc := NewService(store, testLogger(), nil)
331
332 secrets := map[string]string{"API_KEY": "secret123"}
333
334 tests := []struct {
335 name string
336 domain string
337 wantErr error
338 }{
339 {
340 name: "empty domain",
341 domain: "",
342 wantErr: ErrDomainEmpty,
343 },
344 {
345 name: "path traversal",
346 domain: "../../../etc/passwd",
347 wantErr: ErrDomainPathTraversal,
348 },
349 }
350
351 for _, tt := range tests {
352 t.Run(tt.name, func(t *testing.T) {
353 err := svc.Set(context.Background(), tt.domain, secrets)
354 assert.ErrorIs(t, err, tt.wantErr)
355 })
356 }
357}
358
359func TestService_Set_Success(t *testing.T) {
360 store := outmocks.NewMockDomainSecretStore(t)
361 eventBus := outmocks.NewMockEventPublisher(t)
362 svc := NewService(store, testLogger(), eventBus)
363
364 secrets := map[string]string{"API_KEY": "secret123"}
365 store.EXPECT().Set("app.example.com", secrets).Return(nil)
366 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.AnythingOfType("domain.SecretsChangedPayload")).Return(nil)
367
368 err := svc.Set(context.Background(), "app.example.com", secrets)
369 assert.NoError(t, err)
370}
371
372func TestService_Set_PublishesSecretsChangedEvent(t *testing.T) {
373 store := outmocks.NewMockDomainSecretStore(t)
374 eventBus := outmocks.NewMockEventPublisher(t)
375 svc := NewService(store, testLogger(), eventBus)
376
377 secrets := map[string]string{"API_KEY": "secret123", "DB_PASSWORD": "pass456"}
378 store.EXPECT().Set("app.example.com", secrets).Return(nil)
379 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.MatchedBy(func(payload domain.SecretsChangedPayload) bool {
380 assert.Equal(t, "app.example.com", payload.Domain)
381 assert.Equal(t, "set", payload.Operation)
382 assert.ElementsMatch(t, []string{"API_KEY", "DB_PASSWORD"}, payload.Keys)
383 return true
384 })).Return(nil)
385
386 err := svc.Set(context.Background(), "app.example.com", secrets)
387 assert.NoError(t, err)
388}
389
390func TestService_Delete_ValidationErrors(t *testing.T) {
391 store := outmocks.NewMockDomainSecretStore(t)
392 svc := NewService(store, testLogger(), nil)
393
394 tests := []struct {
395 name string
396 domain string
397 wantErr error
398 }{
399 {
400 name: "empty domain",
401 domain: "",
402 wantErr: ErrDomainEmpty,
403 },
404 {
405 name: "path traversal",
406 domain: "..%2F..%2Fetc%2Fpasswd",
407 wantErr: ErrDomainPathTraversal,
408 },
409 }
410
411 for _, tt := range tests {
412 t.Run(tt.name, func(t *testing.T) {
413 err := svc.Delete(context.Background(), tt.domain, "API_KEY")
414 assert.ErrorIs(t, err, tt.wantErr)
415 })
416 }
417}
418
419func TestService_Delete_Success(t *testing.T) {
420 store := outmocks.NewMockDomainSecretStore(t)
421 eventBus := outmocks.NewMockEventPublisher(t)
422 svc := NewService(store, testLogger(), eventBus)
423
424 store.EXPECT().Delete("app.example.com", "API_KEY").Return(nil)
425 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.AnythingOfType("domain.SecretsChangedPayload")).Return(nil)
426
427 err := svc.Delete(context.Background(), "app.example.com", "API_KEY")
428 assert.NoError(t, err)
429}
430
431func TestService_Delete_PublishesSecretsChangedEvent(t *testing.T) {
432 store := outmocks.NewMockDomainSecretStore(t)
433 eventBus := outmocks.NewMockEventPublisher(t)
434 svc := NewService(store, testLogger(), eventBus)
435
436 store.EXPECT().Delete("app.example.com", "API_KEY").Return(nil)
437 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.MatchedBy(func(payload domain.SecretsChangedPayload) bool {
438 assert.Equal(t, "app.example.com", payload.Domain)
439 assert.Equal(t, "delete", payload.Operation)
440 assert.Equal(t, []string{"API_KEY"}, payload.Keys)
441 return true
442 })).Return(nil)
443
444 err := svc.Delete(context.Background(), "app.example.com", "API_KEY")
445 assert.NoError(t, err)
446}
447
448func TestService_Set_SucceedsEvenIfEventPublishFails(t *testing.T) {
449 store := outmocks.NewMockDomainSecretStore(t)
450 eventBus := outmocks.NewMockEventPublisher(t)
451 svc := NewService(store, testLogger(), eventBus)
452
453 secrets := map[string]string{"API_KEY": "secret123"}
454 store.EXPECT().Set("app.example.com", secrets).Return(nil)
455 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.AnythingOfType("domain.SecretsChangedPayload")).Return(assert.AnError)
456
457 err := svc.Set(context.Background(), "app.example.com", secrets)
458 assert.NoError(t, err)
459}
460
461func TestService_NilEventPublisher_DoesNotPanic(t *testing.T) {
462 store := outmocks.NewMockDomainSecretStore(t)
463 svc := NewService(store, testLogger(), nil)
464
465 secrets := map[string]string{"API_KEY": "secret123"}
466 store.EXPECT().Set("app.example.com", secrets).Return(nil)
467
468 assert.NotPanics(t, func() {
469 err := svc.Set(context.Background(), "app.example.com", secrets)
470 assert.NoError(t, err)
471 })
472}
473
474func TestService_StoreError_PropagatedCorrectly(t *testing.T) {
475 store := outmocks.NewMockDomainSecretStore(t)
476 svc := NewService(store, testLogger(), nil)
477
478 storeErr := assert.AnError
479 store.EXPECT().ListKeys("app.example.com").Return(nil, storeErr)
480
481 keys, err := svc.ListKeys(context.Background(), "app.example.com")
482 assert.Nil(t, keys)
483 assert.ErrorIs(t, err, storeErr)
484}
485
486// TestPathTraversalVariants tests various path traversal attack patterns.
487func TestPathTraversalVariants(t *testing.T) {
488 tests := []struct {
489 name string
490 domain string
491 }{
492 {"simple parent dir", ".."},
493 {"parent dir prefix", "../secret"},
494 {"parent dir suffix", "secret/.."},
495 {"parent dir in middle", "app/../secret"},
496 {"multiple parent dirs", "../../etc/passwd"},
497 {"windows style", "..\\..\\etc\\passwd"},
498 {"mixed slashes with dots", "app/./../../etc"},
499 {"double dots only", "...."},
500 {"triple dots", "..."},
501 {"dot dot in subdomain style", "app..evil.com"},
502 }
503
504 for _, tt := range tests {
505 t.Run(tt.name, func(t *testing.T) {
506 err := ValidateDomain(tt.domain)
507 // All these should be rejected (either path traversal or other validation)
508 // The main goal is that ".." patterns are caught
509 if strings.Contains(tt.domain, "..") {
510 assert.ErrorIs(t, err, ErrDomainPathTraversal, "domain %q should be rejected for path traversal", tt.domain)
511 }
512 })
513 }
514}
515
516// TestService_ValidationHappensBeforeStore ensures validation runs before any store calls.
517func TestService_ValidationHappensBeforeStore(t *testing.T) {
518 store := outmocks.NewMockDomainSecretStore(t)
519 svc := NewService(store, testLogger(), nil)
520
521 // With path traversal domain, store methods should never be called
522 // We don't set up any expectations, so if store is called, the test will fail
523
524 _, err := svc.ListKeys(context.Background(), "../etc/passwd")
525 assert.ErrorIs(t, err, ErrDomainPathTraversal)
526
527 _, err = svc.GetAll(context.Background(), "../etc/passwd")
528 assert.ErrorIs(t, err, ErrDomainPathTraversal)
529
530 err = svc.Set(context.Background(), "../etc/passwd", map[string]string{"key": "value"})
531 assert.ErrorIs(t, err, ErrDomainPathTraversal)
532
533 err = svc.Delete(context.Background(), "../etc/passwd", "key")
534 assert.ErrorIs(t, err, ErrDomainPathTraversal)
535
536 // Verify no store methods were called (mockery would fail if unexpected calls happened)
537 mock.AssertExpectationsForObjects(t, store)
538}
539
540func TestService_SetAttachment(t *testing.T) {
541 store := outmocks.NewMockDomainSecretStore(t)
542 eventBus := outmocks.NewMockEventPublisher(t)
543 svc := NewService(store, testLogger(), eventBus)
544
545 secrets := map[string]string{"POSTGRES_PASSWORD": "secret123"}
546 store.EXPECT().SetAttachment("gordon-app__example__com-postgres", secrets).Return(nil)
547 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.AnythingOfType("domain.SecretsChangedPayload")).Return(nil)
548
549 err := svc.SetAttachment(context.Background(), "app.example.com", "postgres", secrets)
550 assert.NoError(t, err)
551}
552
553func TestService_DeleteAttachment(t *testing.T) {
554 store := outmocks.NewMockDomainSecretStore(t)
555 eventBus := outmocks.NewMockEventPublisher(t)
556 svc := NewService(store, testLogger(), eventBus)
557
558 store.EXPECT().DeleteAttachment("gordon-app__example__com-postgres", "OLD_KEY").Return(nil)
559 eventBus.EXPECT().Publish(domain.EventSecretsChanged, mock.AnythingOfType("domain.SecretsChangedPayload")).Return(nil)
560
561 err := svc.DeleteAttachment(context.Background(), "app.example.com", "postgres", "OLD_KEY")
562 assert.NoError(t, err)
563}
564
565func TestService_SetAttachment_InvalidDomain(t *testing.T) {
566 store := outmocks.NewMockDomainSecretStore(t)
567 svc := NewService(store, testLogger(), nil)
568
569 err := svc.SetAttachment(context.Background(), "", "postgres", map[string]string{"KEY": "val"})
570 assert.ErrorIs(t, err, ErrDomainEmpty)
571}
572
573func TestService_SetAttachment_EmptyService(t *testing.T) {
574 store := outmocks.NewMockDomainSecretStore(t)
575 svc := NewService(store, testLogger(), nil)
576
577 err := svc.SetAttachment(context.Background(), "app.example.com", "", map[string]string{"KEY": "val"})
578 assert.ErrorIs(t, err, ErrServiceEmpty)
579}
580
581func TestService_DeleteAttachment_InvalidDomain(t *testing.T) {
582 store := outmocks.NewMockDomainSecretStore(t)
583 svc := NewService(store, testLogger(), nil)
584
585 err := svc.DeleteAttachment(context.Background(), "", "postgres", "SOME_KEY")
586 assert.ErrorIs(t, err, ErrDomainEmpty)
587}
588
589func TestService_DeleteAttachment_EmptyService(t *testing.T) {
590 store := outmocks.NewMockDomainSecretStore(t)
591 svc := NewService(store, testLogger(), nil)
592
593 err := svc.DeleteAttachment(context.Background(), "app.example.com", "", "SOME_KEY")
594 assert.ErrorIs(t, err, ErrServiceEmpty)
595}
596
597func TestService_SetAttachment_InvalidServiceName(t *testing.T) {
598 store := outmocks.NewMockDomainSecretStore(t)
599 svc := NewService(store, testLogger(), nil)
600
601 err := svc.SetAttachment(context.Background(), "app.example.com", "../evil", map[string]string{"K": "V"})
602 assert.ErrorIs(t, err, ErrInvalidServiceName)
603}
604
605func TestService_DeleteAttachment_InvalidServiceName(t *testing.T) {
606 store := outmocks.NewMockDomainSecretStore(t)
607 svc := NewService(store, testLogger(), nil)
608
609 err := svc.DeleteAttachment(context.Background(), "app.example.com", "foo/bar", "SOME_KEY")
610 assert.ErrorIs(t, err, ErrInvalidServiceName)
611}
612
613func TestService_SetAttachment_ServiceNameTooLong(t *testing.T) {
614 store := outmocks.NewMockDomainSecretStore(t)
615 svc := NewService(store, testLogger(), nil)
616
617 // Service name exceeds DNS label limit of 63 characters
618 longService := strings.Repeat("a", 64)
619 err := svc.SetAttachment(context.Background(), "app.example.com", longService, map[string]string{"K": "V"})
620 assert.ErrorIs(t, err, ErrInvalidServiceName)
621}
622
623func TestService_SetAttachment_ServiceNameTrailingHyphen(t *testing.T) {
624 store := outmocks.NewMockDomainSecretStore(t)
625 svc := NewService(store, testLogger(), nil)
626
627 err := svc.SetAttachment(context.Background(), "app.example.com", "postgres-", map[string]string{"K": "V"})
628 assert.ErrorIs(t, err, ErrInvalidServiceName)
629}
630
631func TestService_DeleteAttachment_ServiceNameTooLong(t *testing.T) {
632 store := outmocks.NewMockDomainSecretStore(t)
633 svc := NewService(store, testLogger(), nil)
634
635 // Service name exceeds DNS label limit of 63 characters
636 longService := strings.Repeat("a", 64)
637 err := svc.DeleteAttachment(context.Background(), "app.example.com", longService, "SOME_KEY")
638 assert.ErrorIs(t, err, ErrInvalidServiceName)
639}
640
641func TestService_DeleteAttachment_ServiceNameTrailingHyphen(t *testing.T) {
642 store := outmocks.NewMockDomainSecretStore(t)
643 svc := NewService(store, testLogger(), nil)
644
645 err := svc.DeleteAttachment(context.Background(), "app.example.com", "redis-", "SOME_KEY")
646 assert.ErrorIs(t, err, ErrInvalidServiceName)
647}
648
649func TestService_AttachmentValidationHappensBeforeStore(t *testing.T) {
650 store := outmocks.NewMockDomainSecretStore(t)
651 svc := NewService(store, testLogger(), nil)
652
653 // With invalid service name, store methods should never be called
654 err := svc.SetAttachment(context.Background(), "app.example.com", "../evil", map[string]string{"key": "value"})
655 assert.ErrorIs(t, err, ErrInvalidServiceName)
656
657 err = svc.DeleteAttachment(context.Background(), "app.example.com", "../evil", "key")
658 assert.ErrorIs(t, err, ErrInvalidServiceName)
659
660 // Verify no store methods were called
661 mock.AssertExpectationsForObjects(t, store)
662}