forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
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 manager, err := NewOpenBaoManager(tt.proxyAddr, logger, tt.opts...)
156
157 if tt.expectError {
158 assert.Error(t, err)
159 assert.Nil(t, manager)
160 assert.Contains(t, err.Error(), tt.errorContains)
161 } else {
162 assert.NoError(t, err)
163 assert.NotNil(t, manager)
164 }
165 })
166 }
167}
168
169func TestOpenBaoManager_PathBuilding(t *testing.T) {
170 manager := &OpenBaoManager{mountPath: "secret"}
171
172 tests := []struct {
173 name string
174 repo DidSlashRepo
175 key string
176 expected string
177 }{
178 {
179 name: "simple repo path",
180 repo: DidSlashRepo("did:plc:foo/repo"),
181 key: "api_key",
182 expected: "repos/did_plc_foo_repo/api_key",
183 },
184 {
185 name: "complex repo path with dots",
186 repo: DidSlashRepo("did:web:example.com/my-repo"),
187 key: "secret_key",
188 expected: "repos/did_web_example_com_my-repo/secret_key",
189 },
190 }
191
192 for _, tt := range tests {
193 t.Run(tt.name, func(t *testing.T) {
194 result := manager.buildSecretPath(tt.repo, tt.key)
195 assert.Equal(t, tt.expected, result)
196 })
197 }
198}
199
200func TestOpenBaoManager_buildRepoPath(t *testing.T) {
201 manager := &OpenBaoManager{mountPath: "test"}
202
203 tests := []struct {
204 name string
205 repo DidSlashRepo
206 expected string
207 }{
208 {
209 name: "simple repo",
210 repo: "did:plc:test/myrepo",
211 expected: "repos/did_plc_test_myrepo",
212 },
213 {
214 name: "repo with dots",
215 repo: "did:plc:example.com/my.repo",
216 expected: "repos/did_plc_example_com_my_repo",
217 },
218 {
219 name: "complex repo",
220 repo: "did:web:example.com:8080/path/to/repo",
221 expected: "repos/did_web_example_com_8080_path_to_repo",
222 },
223 }
224
225 for _, tt := range tests {
226 t.Run(tt.name, func(t *testing.T) {
227 result := manager.buildRepoPath(tt.repo)
228 assert.Equal(t, tt.expected, result)
229 })
230 }
231}
232
233func TestWithMountPath(t *testing.T) {
234 manager := &OpenBaoManager{mountPath: "default"}
235
236 opt := WithMountPath("custom-mount")
237 opt(manager)
238
239 assert.Equal(t, "custom-mount", manager.mountPath)
240}
241
242func TestMockOpenBaoManager_AddSecret(t *testing.T) {
243 tests := []struct {
244 name string
245 secrets []UnlockedSecret
246 expectError bool
247 }{
248 {
249 name: "add single secret",
250 secrets: []UnlockedSecret{
251 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
252 },
253 expectError: false,
254 },
255 {
256 name: "add multiple secrets",
257 secrets: []UnlockedSecret{
258 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
259 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
260 },
261 expectError: false,
262 },
263 {
264 name: "add duplicate secret",
265 secrets: []UnlockedSecret{
266 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
267 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "newsecret", "did:plc:creator"),
268 },
269 expectError: true,
270 },
271 }
272
273 for _, tt := range tests {
274 t.Run(tt.name, func(t *testing.T) {
275 mock := NewMockOpenBaoManager()
276 ctx := context.Background()
277 var err error
278
279 for i, secret := range tt.secrets {
280 err = mock.AddSecret(ctx, secret)
281 if tt.expectError && i == 1 { // Second secret should fail for duplicate test
282 assert.Equal(t, ErrKeyAlreadyPresent, err)
283 return
284 }
285 if !tt.expectError {
286 assert.NoError(t, err)
287 }
288 }
289
290 if !tt.expectError {
291 assert.NoError(t, err)
292 }
293 })
294 }
295}
296
297func TestMockOpenBaoManager_RemoveSecret(t *testing.T) {
298 tests := []struct {
299 name string
300 setupSecrets []UnlockedSecret
301 removeSecret Secret[any]
302 expectError bool
303 }{
304 {
305 name: "remove existing secret",
306 setupSecrets: []UnlockedSecret{
307 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
308 },
309 removeSecret: Secret[any]{
310 Key: "API_KEY",
311 Repo: DidSlashRepo("did:plc:test/repo1"),
312 },
313 expectError: false,
314 },
315 {
316 name: "remove non-existent secret",
317 setupSecrets: []UnlockedSecret{},
318 removeSecret: Secret[any]{
319 Key: "API_KEY",
320 Repo: DidSlashRepo("did:plc:test/repo1"),
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
331 // Setup secrets
332 for _, secret := range tt.setupSecrets {
333 err := mock.AddSecret(ctx, secret)
334 assert.NoError(t, err)
335 }
336
337 // Remove secret
338 err := mock.RemoveSecret(ctx, tt.removeSecret)
339
340 if tt.expectError {
341 assert.Equal(t, ErrKeyNotFound, err)
342 } else {
343 assert.NoError(t, err)
344 }
345 })
346 }
347}
348
349func TestMockOpenBaoManager_GetSecretsLocked(t *testing.T) {
350 tests := []struct {
351 name string
352 setupSecrets []UnlockedSecret
353 queryRepo DidSlashRepo
354 expectedCount int
355 expectedKeys []string
356 expectError bool
357 }{
358 {
359 name: "get secrets from repo with secrets",
360 setupSecrets: []UnlockedSecret{
361 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
362 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
363 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"),
364 },
365 queryRepo: DidSlashRepo("did:plc:test/repo1"),
366 expectedCount: 2,
367 expectedKeys: []string{"API_KEY", "DB_PASSWORD"},
368 expectError: false,
369 },
370 {
371 name: "get secrets from empty repo",
372 setupSecrets: []UnlockedSecret{},
373 queryRepo: DidSlashRepo("did:plc:test/empty"),
374 expectedCount: 0,
375 expectedKeys: []string{},
376 expectError: false,
377 },
378 }
379
380 for _, tt := range tests {
381 t.Run(tt.name, func(t *testing.T) {
382 mock := NewMockOpenBaoManager()
383 ctx := context.Background()
384
385 // Setup
386 for _, secret := range tt.setupSecrets {
387 err := mock.AddSecret(ctx, secret)
388 assert.NoError(t, err)
389 }
390
391 // Test
392 secrets, err := mock.GetSecretsLocked(ctx, tt.queryRepo)
393
394 if tt.expectError {
395 assert.Error(t, err)
396 } else {
397 assert.NoError(t, err)
398 assert.Len(t, secrets, tt.expectedCount)
399
400 // Check keys
401 actualKeys := make([]string, len(secrets))
402 for i, secret := range secrets {
403 actualKeys[i] = secret.Key
404 }
405
406 for _, expectedKey := range tt.expectedKeys {
407 assert.Contains(t, actualKeys, expectedKey)
408 }
409 }
410 })
411 }
412}
413
414func TestMockOpenBaoManager_GetSecretsUnlocked(t *testing.T) {
415 tests := []struct {
416 name string
417 setupSecrets []UnlockedSecret
418 queryRepo DidSlashRepo
419 expectedCount int
420 expectedSecrets map[string]string // key -> value
421 expectError bool
422 }{
423 {
424 name: "get unlocked secrets from repo",
425 setupSecrets: []UnlockedSecret{
426 createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator"),
427 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"),
428 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"),
429 },
430 queryRepo: DidSlashRepo("did:plc:test/repo1"),
431 expectedCount: 2,
432 expectedSecrets: map[string]string{
433 "API_KEY": "secret123",
434 "DB_PASSWORD": "dbpass456",
435 },
436 expectError: false,
437 },
438 {
439 name: "get secrets from empty repo",
440 setupSecrets: []UnlockedSecret{},
441 queryRepo: DidSlashRepo("did:plc:test/empty"),
442 expectedCount: 0,
443 expectedSecrets: map[string]string{},
444 expectError: false,
445 },
446 }
447
448 for _, tt := range tests {
449 t.Run(tt.name, func(t *testing.T) {
450 mock := NewMockOpenBaoManager()
451 ctx := context.Background()
452
453 // Setup
454 for _, secret := range tt.setupSecrets {
455 err := mock.AddSecret(ctx, secret)
456 assert.NoError(t, err)
457 }
458
459 // Test
460 secrets, err := mock.GetSecretsUnlocked(ctx, tt.queryRepo)
461
462 if tt.expectError {
463 assert.Error(t, err)
464 } else {
465 assert.NoError(t, err)
466 assert.Len(t, secrets, tt.expectedCount)
467
468 // Check key-value pairs
469 actualSecrets := make(map[string]string)
470 for _, secret := range secrets {
471 actualSecrets[secret.Key] = secret.Value
472 }
473
474 for expectedKey, expectedValue := range tt.expectedSecrets {
475 actualValue, exists := actualSecrets[expectedKey]
476 assert.True(t, exists, "Expected key %s not found", expectedKey)
477 assert.Equal(t, expectedValue, actualValue)
478 }
479 }
480 })
481 }
482}
483
484func TestMockOpenBaoManager_ErrorHandling(t *testing.T) {
485 mock := NewMockOpenBaoManager()
486 ctx := context.Background()
487 testError := assert.AnError
488
489 // Test error injection
490 mock.SetError(testError)
491
492 secret := createTestSecretForOpenBao("did:plc:test/repo1", "API_KEY", "secret123", "did:plc:creator")
493
494 // All operations should return the injected error
495 err := mock.AddSecret(ctx, secret)
496 assert.Equal(t, testError, err)
497
498 _, err = mock.GetSecretsLocked(ctx, "did:plc:test/repo1")
499 assert.Equal(t, testError, err)
500
501 _, err = mock.GetSecretsUnlocked(ctx, "did:plc:test/repo1")
502 assert.Equal(t, testError, err)
503
504 err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: "did:plc:test/repo1"})
505 assert.Equal(t, testError, err)
506
507 // Clear error and test normal operation
508 mock.ClearError()
509 err = mock.AddSecret(ctx, secret)
510 assert.NoError(t, err)
511}
512
513func TestMockOpenBaoManager_Integration(t *testing.T) {
514 tests := []struct {
515 name string
516 scenario func(t *testing.T, mock *MockOpenBaoManager)
517 }{
518 {
519 name: "complete workflow",
520 scenario: func(t *testing.T, mock *MockOpenBaoManager) {
521 ctx := context.Background()
522 repo := DidSlashRepo("did:plc:test/integration")
523
524 // Start with empty repo
525 secrets, err := mock.GetSecretsLocked(ctx, repo)
526 assert.NoError(t, err)
527 assert.Empty(t, secrets)
528
529 // Add some secrets
530 secret1 := createTestSecretForOpenBao(string(repo), "API_KEY", "secret123", "did:plc:creator")
531 secret2 := createTestSecretForOpenBao(string(repo), "DB_PASSWORD", "dbpass456", "did:plc:creator")
532
533 err = mock.AddSecret(ctx, secret1)
534 assert.NoError(t, err)
535
536 err = mock.AddSecret(ctx, secret2)
537 assert.NoError(t, err)
538
539 // Verify secrets exist
540 secrets, err = mock.GetSecretsLocked(ctx, repo)
541 assert.NoError(t, err)
542 assert.Len(t, secrets, 2)
543
544 unlockedSecrets, err := mock.GetSecretsUnlocked(ctx, repo)
545 assert.NoError(t, err)
546 assert.Len(t, unlockedSecrets, 2)
547
548 // Remove one secret
549 err = mock.RemoveSecret(ctx, Secret[any]{Key: "API_KEY", Repo: repo})
550 assert.NoError(t, err)
551
552 // Verify only one secret remains
553 secrets, err = mock.GetSecretsLocked(ctx, repo)
554 assert.NoError(t, err)
555 assert.Len(t, secrets, 1)
556 assert.Equal(t, "DB_PASSWORD", secrets[0].Key)
557 },
558 },
559 }
560
561 for _, tt := range tests {
562 t.Run(tt.name, func(t *testing.T) {
563 mock := NewMockOpenBaoManager()
564 tt.scenario(t, mock)
565 })
566 }
567}
568
569func TestOpenBaoManager_ProxyConfiguration(t *testing.T) {
570 tests := []struct {
571 name string
572 proxyAddr string
573 description string
574 }{
575 {
576 name: "default_localhost",
577 proxyAddr: "http://127.0.0.1:8200",
578 description: "Should connect to default localhost proxy",
579 },
580 {
581 name: "custom_host",
582 proxyAddr: "http://bao-proxy:8200",
583 description: "Should connect to custom proxy host",
584 },
585 {
586 name: "https_proxy",
587 proxyAddr: "https://127.0.0.1:8200",
588 description: "Should connect to HTTPS proxy",
589 },
590 }
591
592 for _, tt := range tests {
593 t.Run(tt.name, func(t *testing.T) {
594 t.Log("Testing scenario:", tt.description)
595 logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
596
597 // All these will fail because no real proxy is running
598 // but we can test that the configuration is properly accepted
599 manager, err := NewOpenBaoManager(tt.proxyAddr, logger)
600 assert.Error(t, err) // Expected because no real proxy
601 assert.Nil(t, manager)
602 assert.Contains(t, err.Error(), "failed to connect to bao proxy")
603 })
604 }
605}