forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 16 kB view raw
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}