forked from tangled.org/core
this repo has no description
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 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}