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}