this repo has no description
1package secrets
2
3import (
4 "context"
5 "log/slog"
6 "os"
7 "testing"
8 "time"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "github.com/stretchr/testify/assert"
12)
13
14// MockOpenBaoManager is a mock implementation of Manager interface for testing
15type MockOpenBaoManager struct {
16 secrets map[string]UnlockedSecret // key: repo_key format
17 shouldError bool
18 errorToReturn error
19 stopped bool
20}
21
22func NewMockOpenBaoManager() *MockOpenBaoManager {
23 return &MockOpenBaoManager{secrets: make(map[string]UnlockedSecret)}
24}
25
26func (m *MockOpenBaoManager) SetError(err error) {
27 m.shouldError = true
28 m.errorToReturn = err
29}
30
31func (m *MockOpenBaoManager) ClearError() {
32 m.shouldError = false
33 m.errorToReturn = nil
34}
35
36func (m *MockOpenBaoManager) Stop() {
37 m.stopped = true
38}
39
40func (m *MockOpenBaoManager) IsStopped() bool {
41 return m.stopped
42}
43
44func (m *MockOpenBaoManager) buildKey(repo DidSlashRepo, key string) string {
45 return string(repo) + "_" + key
46}
47
48func (m *MockOpenBaoManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
49 if m.shouldError {
50 return m.errorToReturn
51 }
52
53 key := m.buildKey(secret.Repo, secret.Key)
54 if _, exists := m.secrets[key]; exists {
55 return ErrKeyAlreadyPresent
56 }
57
58 m.secrets[key] = secret
59 return nil
60}
61
62func (m *MockOpenBaoManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
63 if m.shouldError {
64 return m.errorToReturn
65 }
66
67 key := m.buildKey(secret.Repo, secret.Key)
68 if _, exists := m.secrets[key]; !exists {
69 return ErrKeyNotFound
70 }
71
72 delete(m.secrets, key)
73 return nil
74}
75
76func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) {
77 if m.shouldError {
78 return nil, m.errorToReturn
79 }
80
81 var result []LockedSecret
82 for _, secret := range m.secrets {
83 if secret.Repo == repo {
84 result = append(result, LockedSecret{
85 Key: secret.Key,
86 Repo: secret.Repo,
87 CreatedAt: secret.CreatedAt,
88 CreatedBy: secret.CreatedBy,
89 })
90 }
91 }
92
93 return result, nil
94}
95
96func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) {
97 if m.shouldError {
98 return nil, m.errorToReturn
99 }
100
101 var result []UnlockedSecret
102 for _, secret := range m.secrets {
103 if secret.Repo == repo {
104 result = append(result, secret)
105 }
106 }
107
108 return result, nil
109}
110
111func createTestSecretForOpenBao(repo, key, value, createdBy string) UnlockedSecret {
112 return UnlockedSecret{
113 Key: key,
114 Value: value,
115 Repo: DidSlashRepo(repo),
116 CreatedAt: time.Now(),
117 CreatedBy: syntax.DID(createdBy),
118 }
119}
120
121func TestOpenBaoManagerInterface(t *testing.T) {
122 var _ Manager = (*OpenBaoManager)(nil)
123}
124
125func TestNewOpenBaoManager(t *testing.T) {
126 tests := []struct {
127 name string
128 address string
129 roleID string
130 secretID string
131 opts []OpenBaoManagerOpt
132 expectError bool
133 errorContains string
134 }{
135 {
136 name: "empty address",
137 address: "",
138 roleID: "test-role-id",
139 secretID: "test-secret-id",
140 opts: nil,
141 expectError: true,
142 errorContains: "address cannot be empty",
143 },
144 {
145 name: "empty role_id",
146 address: "http://localhost:8200",
147 roleID: "",
148 secretID: "test-secret-id",
149 opts: nil,
150 expectError: true,
151 errorContains: "role_id cannot be empty",
152 },
153 {
154 name: "empty secret_id",
155 address: "http://localhost:8200",
156 roleID: "test-role-id",
157 secretID: "",
158 opts: nil,
159 expectError: true,
160 errorContains: "secret_id cannot be empty",
161 },
162 }
163
164 for _, tt := range tests {
165 t.Run(tt.name, func(t *testing.T) {
166 logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
167 manager, err := NewOpenBaoManager(tt.address, tt.roleID, tt.secretID, logger, tt.opts...)
168
169 if tt.expectError {
170 assert.Error(t, err)
171 assert.Nil(t, manager)
172 assert.Contains(t, err.Error(), tt.errorContains)
173 } else {
174 // For valid configurations, we expect an error during authentication
175 // since we're not connecting to a real OpenBao server
176 assert.Error(t, err)
177 assert.Nil(t, manager)
178 }
179 })
180 }
181}
182
183func TestOpenBaoManager_PathBuilding(t *testing.T) {
184 manager := &OpenBaoManager{mountPath: "secret"}
185
186 tests := []struct {
187 name string
188 repo DidSlashRepo
189 key string
190 expected string
191 }{
192 {
193 name: "simple repo path",
194 repo: DidSlashRepo("did:plc:foo/repo"),
195 key: "api_key",
196 expected: "repos/did_plc_foo_repo/api_key",
197 },
198 {
199 name: "complex repo path with dots",
200 repo: DidSlashRepo("did:web:example.com/my-repo"),
201 key: "secret_key",
202 expected: "repos/did_web_example_com_my-repo/secret_key",
203 },
204 }
205
206 for _, tt := range tests {
207 t.Run(tt.name, func(t *testing.T) {
208 result := manager.buildSecretPath(tt.repo, tt.key)
209 assert.Equal(t, tt.expected, result)
210 })
211 }
212}
213
214func TestOpenBaoManager_buildRepoPath(t *testing.T) {
215 manager := &OpenBaoManager{mountPath: "test"}
216
217 tests := []struct {
218 name string
219 repo DidSlashRepo
220 expected string
221 }{
222 {
223 name: "simple repo",
224 repo: "did:plc:test/myrepo",
225 expected: "repos/did_plc_test_myrepo",
226 },
227 {
228 name: "repo with dots",
229 repo: "did:plc:example.com/my.repo",
230 expected: "repos/did_plc_example_com_my_repo",
231 },
232 {
233 name: "complex repo",
234 repo: "did:web:example.com:8080/path/to/repo",
235 expected: "repos/did_web_example_com_8080_path_to_repo",
236 },
237 }
238
239 for _, tt := range tests {
240 t.Run(tt.name, func(t *testing.T) {
241 result := manager.buildRepoPath(tt.repo)
242 assert.Equal(t, tt.expected, result)
243 })
244 }
245}
246
247func TestWithMountPath(t *testing.T) {
248 manager := &OpenBaoManager{mountPath: "default"}
249
250 opt := WithMountPath("custom-mount")
251 opt(manager)
252
253 assert.Equal(t, "custom-mount", manager.mountPath)
254}
255
256func TestOpenBaoManager_Stop(t *testing.T) {
257 // Create a manager with minimal setup
258 manager := &OpenBaoManager{
259 mountPath: "test",
260 stopCh: make(chan struct{}),
261 }
262
263 // Verify the manager implements Stopper interface
264 var stopper Stopper = manager
265 assert.NotNil(t, stopper)
266
267 // Call Stop and verify it doesn't panic
268 assert.NotPanics(t, func() {
269 manager.Stop()
270 })
271
272 // Verify the channel was closed
273 select {
274 case <-manager.stopCh:
275 // Channel was closed as expected
276 default:
277 t.Error("Expected stop channel to be closed after Stop()")
278 }
279}
280
281func TestOpenBaoManager_StopperInterface(t *testing.T) {
282 manager := &OpenBaoManager{}
283
284 // Verify that OpenBaoManager implements the Stopper interface
285 _, ok := interface{}(manager).(Stopper)
286 assert.True(t, ok, "OpenBaoManager should implement Stopper interface")
287}
288
289// Test MockOpenBaoManager interface compliance
290func TestMockOpenBaoManagerInterface(t *testing.T) {
291 var _ Manager = (*MockOpenBaoManager)(nil)
292 var _ Stopper = (*MockOpenBaoManager)(nil)
293}
294
295func TestMockOpenBaoManager_AddSecret(t *testing.T) {
296 tests := []struct {
297 name string
298 secrets []UnlockedSecret
299 expectError bool
300 }{
301 {
302 name: "add single secret",
303 secrets: []UnlockedSecret{
304 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
305 },
306 expectError: false,
307 },
308 {
309 name: "add multiple secrets",
310 secrets: []UnlockedSecret{
311 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
312 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
313 },
314 expectError: false,
315 },
316 {
317 name: "add duplicate secret",
318 secrets: []UnlockedSecret{
319 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
320 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "newsecret", "did:plc:creator"),
321 },
322 expectError: true,
323 },
324 }
325
326 for _, tt := range tests {
327 t.Run(tt.name, func(t *testing.T) {
328 mock := NewMockOpenBaoManager()
329 ctx := context.Background()
330 var err error
331
332 for i, secret := range tt.secrets {
333 err = mock.AddSecret(ctx, secret)
334 if tt.expectError && i == 1 { // Second secret should fail for duplicate test
335 assert.Equal(t, ErrKeyAlreadyPresent, err)
336 return
337 }
338 if !tt.expectError {
339 assert.NoError(t, err)
340 }
341 }
342
343 if !tt.expectError {
344 assert.NoError(t, err)
345 }
346 })
347 }
348}
349
350func TestMockOpenBaoManager_RemoveSecret(t *testing.T) {
351 tests := []struct {
352 name string
353 setupSecrets []UnlockedSecret
354 removeSecret Secret[any]
355 expectError bool
356 }{
357 {
358 name: "remove existing secret",
359 setupSecrets: []UnlockedSecret{
360 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
361 },
362 removeSecret: Secret[any]{
363 Key: "API_KEY",
364 Repo: DidSlashRepo("did:plc:test/repo1"),
365 },
366 expectError: false,
367 },
368 {
369 name: "remove non-existent secret",
370 setupSecrets: []UnlockedSecret{},
371 removeSecret: Secret[any]{
372 Key: "API_KEY",
373 Repo: DidSlashRepo("did:plc:test/repo1"),
374 },
375 expectError: true,
376 },
377 }
378
379 for _, tt := range tests {
380 t.Run(tt.name, func(t *testing.T) {
381 mock := NewMockOpenBaoManager()
382 ctx := context.Background()
383
384 // Setup secrets
385 for _, secret := range tt.setupSecrets {
386 err := mock.AddSecret(ctx, secret)
387 assert.NoError(t, err)
388 }
389
390 // Remove secret
391 err := mock.RemoveSecret(ctx, tt.removeSecret)
392
393 if tt.expectError {
394 assert.Equal(t, ErrKeyNotFound, err)
395 } else {
396 assert.NoError(t, err)
397 }
398 })
399 }
400}
401
402func TestMockOpenBaoManager_GetSecretsLocked(t *testing.T) {
403 tests := []struct {
404 name string
405 setupSecrets []UnlockedSecret
406 queryRepo DidSlashRepo
407 expectedCount int
408 expectedKeys []string
409 expectError bool
410 }{
411 {
412 name: "get secrets from repo with secrets",
413 setupSecrets: []UnlockedSecret{
414 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
415 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
416 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"),
417 },
418 queryRepo: DidSlashRepo("did:plc:test/repo1"),
419 expectedCount: 2,
420 expectedKeys: []string{"API_KEY", "DB_PASSWORD"},
421 expectError: false,
422 },
423 {
424 name: "get secrets from empty repo",
425 setupSecrets: []UnlockedSecret{},
426 queryRepo: DidSlashRepo("did:plc:test/empty"),
427 expectedCount: 0,
428 expectedKeys: []string{},
429 expectError: false,
430 },
431 }
432
433 for _, tt := range tests {
434 t.Run(tt.name, func(t *testing.T) {
435 mock := NewMockOpenBaoManager()
436 ctx := context.Background()
437
438 // Setup
439 for _, secret := range tt.setupSecrets {
440 err := mock.AddSecret(ctx, secret)
441 assert.NoError(t, err)
442 }
443
444 // Test
445 secrets, err := mock.GetSecretsLocked(ctx, tt.queryRepo)
446
447 if tt.expectError {
448 assert.Error(t, err)
449 } else {
450 assert.NoError(t, err)
451 assert.Len(t, secrets, tt.expectedCount)
452
453 // Check keys
454 actualKeys := make([]string, len(secrets))
455 for i, secret := range secrets {
456 actualKeys[i] = secret.Key
457 }
458
459 for _, expectedKey := range tt.expectedKeys {
460 assert.Contains(t, actualKeys, expectedKey)
461 }
462 }
463 })
464 }
465}
466
467func TestMockOpenBaoManager_GetSecretsUnlocked(t *testing.T) {
468 tests := []struct {
469 name string
470 setupSecrets []UnlockedSecret
471 queryRepo DidSlashRepo
472 expectedCount int
473 expectedSecrets map[string]string // key -> value
474 expectError bool
475 }{
476 {
477 name: "get unlocked secrets from repo",
478 setupSecrets: []UnlockedSecret{
479 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
480 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
481 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"),
482 },
483 queryRepo: DidSlashRepo("did:plc:test/repo1"),
484 expectedCount: 2,
485 expectedSecrets: map[string]string{
486 "API_KEY": "secret123",
487 "DB_PASSWORD": "dbpass456",
488 },
489 expectError: false,
490 },
491 {
492 name: "get secrets from empty repo",
493 setupSecrets: []UnlockedSecret{},
494 queryRepo: DidSlashRepo("did:plc:test/empty"),
495 expectedCount: 0,
496 expectedSecrets: map[string]string{},
497 expectError: false,
498 },
499 }
500
501 for _, tt := range tests {
502 t.Run(tt.name, func(t *testing.T) {
503 mock := NewMockOpenBaoManager()
504 ctx := context.Background()
505
506 // Setup
507 for _, secret := range tt.setupSecrets {
508 err := mock.AddSecret(ctx, secret)
509 assert.NoError(t, err)
510 }
511
512 // Test
513 secrets, err := mock.GetSecretsUnlocked(ctx, tt.queryRepo)
514
515 if tt.expectError {
516 assert.Error(t, err)
517 } else {
518 assert.NoError(t, err)
519 assert.Len(t, secrets, tt.expectedCount)
520
521 // Check key-value pairs
522 actualSecrets := make(map[string]string)
523 for _, secret := range secrets {
524 actualSecrets[secret.Key] = secret.Value
525 }
526
527 for expectedKey, expectedValue := range tt.expectedSecrets {
528 actualValue, exists := actualSecrets[expectedKey]
529 assert.True(t, exists, "Expected key %s not found", expectedKey)
530 assert.Equal(t, expectedValue, actualValue)
531 }
532 }
533 })
534 }
535}
536
537func TestMockOpenBaoManager_ErrorHandling(t *testing.T) {
538 mock := NewMockOpenBaoManager()
539 ctx := context.Background()
540 testError := assert.AnError
541
542 // Test error injection
543 mock.SetError(testError)
544
545 secret := createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator")
546
547 // All operations should return the injected error
548 err := mock.AddSecret(ctx, secret)
549 assert.Equal(t, testError, err)
550
551 _, err = mock.GetSecretsLocked(ctx, "did:plc:test/repo1")
552 assert.Equal(t, testError, err)
553
554 _, err = mock.GetSecretsUnlocked(ctx, "did:plc:test/repo1")
555 assert.Equal(t, testError, err)
556
557 err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: "did:plc:test/repo1"})
558 assert.Equal(t, testError, err)
559
560 // Clear error and test normal operation
561 mock.ClearError()
562 err = mock.AddSecret(ctx, secret)
563 assert.NoError(t, err)
564}
565
566func TestMockOpenBaoManager_Stop(t *testing.T) {
567 mock := NewMockOpenBaoManager()
568
569 assert.False(t, mock.IsStopped())
570
571 mock.Stop()
572
573 assert.True(t, mock.IsStopped())
574}
575
576func TestMockOpenBaoManager_Integration(t *testing.T) {
577 tests := []struct {
578 name string
579 scenario func(t *testing.T, mock *MockOpenBaoManager)
580 }{
581 {
582 name: "complete workflow",
583 scenario: func(t *testing.T, mock *MockOpenBaoManager) {
584 ctx := context.Background()
585 repo := DidSlashRepo("did:plc:test/integration")
586
587 // Start with empty repo
588 secrets, err := mock.GetSecretsLocked(ctx, repo)
589 assert.NoError(t, err)
590 assert.Empty(t, secrets)
591
592 // Add some secrets
593 secret1 := createTestSecretForOpenBao(string(repo), "API_KEY", "secret123", "did:plc:creator")
594 secret2 := createTestSecretForOpenBao(string(repo), "DB_PASSWORD", "dbpass456", "did:plc:creator")
595
596 err = mock.AddSecret(ctx, secret1)
597 assert.NoError(t, err)
598
599 err = mock.AddSecret(ctx, secret2)
600 assert.NoError(t, err)
601
602 // Verify secrets exist
603 secrets, err = mock.GetSecretsLocked(ctx, repo)
604 assert.NoError(t, err)
605 assert.Len(t, secrets, 2)
606
607 unlockedSecrets, err := mock.GetSecretsUnlocked(ctx, repo)
608 assert.NoError(t, err)
609 assert.Len(t, unlockedSecrets, 2)
610
611 // Remove one secret
612 err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: repo})
613 assert.NoError(t, err)
614
615 // Verify only one secret remains
616 secrets, err = mock.GetSecretsLocked(ctx, repo)
617 assert.NoError(t, err)
618 assert.Len(t, secrets, 1)
619 assert.Equal(t, "DB_PASSWORD", secrets[0].Key)
620 },
621 },
622 }
623
624 for _, tt := range tests {
625 t.Run(tt.name, func(t *testing.T) {
626 mock := NewMockOpenBaoManager()
627 tt.scenario(t, mock)
628 })
629 }
630}