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