forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package secrets 2 3import ( 4 "testing" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8) 9 10func createInMemoryDB(t *testing.T) *SqliteManager { 11 t.Helper() 12 manager, err := NewSQLiteManager(":memory:") 13 if err != nil { 14 t.Fatalf("Failed to create in-memory manager: %v", err) 15 } 16 return manager 17} 18 19func createTestSecret(repo, key, value, createdBy string) UnlockedSecret { 20 return UnlockedSecret{ 21 Key: key, 22 Value: value, 23 Repo: DidSlashRepo(repo), 24 CreatedAt: time.Now(), 25 CreatedBy: syntax.DID(createdBy), 26 } 27} 28 29// ensure that interface is satisfied 30func TestManagerInterface(t *testing.T) { 31 var _ Manager = (*SqliteManager)(nil) 32} 33 34func TestNewSQLiteManager(t *testing.T) { 35 tests := []struct { 36 name string 37 dbPath string 38 opts []SqliteManagerOpt 39 expectError bool 40 expectTable string 41 }{ 42 { 43 name: "default table name", 44 dbPath: ":memory:", 45 opts: nil, 46 expectError: false, 47 expectTable: "secrets", 48 }, 49 { 50 name: "custom table name", 51 dbPath: ":memory:", 52 opts: []SqliteManagerOpt{WithTableName("custom_secrets")}, 53 expectError: false, 54 expectTable: "custom_secrets", 55 }, 56 { 57 name: "invalid database path", 58 dbPath: "/invalid/path/to/database.db", 59 opts: nil, 60 expectError: true, 61 expectTable: "", 62 }, 63 } 64 65 for _, tt := range tests { 66 t.Run(tt.name, func(t *testing.T) { 67 manager, err := NewSQLiteManager(tt.dbPath, tt.opts...) 68 if tt.expectError { 69 if err == nil { 70 t.Error("Expected error but got none") 71 } 72 return 73 } 74 75 if err != nil { 76 t.Fatalf("Unexpected error: %v", err) 77 } 78 defer manager.db.Close() 79 80 if manager.tableName != tt.expectTable { 81 t.Errorf("Expected table name %s, got %s", tt.expectTable, manager.tableName) 82 } 83 }) 84 } 85} 86 87func TestSqliteManager_AddSecret(t *testing.T) { 88 tests := []struct { 89 name string 90 secrets []UnlockedSecret 91 expectError []error 92 }{ 93 { 94 name: "add single secret", 95 secrets: []UnlockedSecret{ 96 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 97 }, 98 expectError: []error{nil}, 99 }, 100 { 101 name: "add multiple unique secrets", 102 secrets: []UnlockedSecret{ 103 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 104 createTestSecret("did:plc:foo/repo", "db_password", "password_456", "did:plc:example123"), 105 createTestSecret("other.com/repo", "api_key", "other_secret", "did:plc:other"), 106 }, 107 expectError: []error{nil, nil, nil}, 108 }, 109 { 110 name: "add duplicate secret", 111 secrets: []UnlockedSecret{ 112 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 113 createTestSecret("did:plc:foo/repo", "api_key", "different_value", "did:plc:example123"), 114 }, 115 expectError: []error{nil, ErrKeyAlreadyPresent}, 116 }, 117 } 118 119 for _, tt := range tests { 120 t.Run(tt.name, func(t *testing.T) { 121 manager := createInMemoryDB(t) 122 defer manager.db.Close() 123 124 for i, secret := range tt.secrets { 125 err := manager.AddSecret(secret) 126 if err != tt.expectError[i] { 127 t.Errorf("Secret %d: expected error %v, got %v", i, tt.expectError[i], err) 128 } 129 } 130 }) 131 } 132} 133 134func TestSqliteManager_RemoveSecret(t *testing.T) { 135 tests := []struct { 136 name string 137 setupSecrets []UnlockedSecret 138 removeSecret Secret[any] 139 expectError error 140 }{ 141 { 142 name: "remove existing secret", 143 setupSecrets: []UnlockedSecret{ 144 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 145 }, 146 removeSecret: Secret[any]{ 147 Key: "api_key", 148 Repo: DidSlashRepo("did:plc:foo/repo"), 149 }, 150 expectError: nil, 151 }, 152 { 153 name: "remove non-existent secret", 154 setupSecrets: []UnlockedSecret{ 155 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 156 }, 157 removeSecret: Secret[any]{ 158 Key: "non_existent_key", 159 Repo: DidSlashRepo("did:plc:foo/repo"), 160 }, 161 expectError: ErrKeyNotFound, 162 }, 163 { 164 name: "remove from empty database", 165 setupSecrets: []UnlockedSecret{}, 166 removeSecret: Secret[any]{ 167 Key: "any_key", 168 Repo: DidSlashRepo("did:plc:foo/repo"), 169 }, 170 expectError: ErrKeyNotFound, 171 }, 172 { 173 name: "remove secret from wrong repo", 174 setupSecrets: []UnlockedSecret{ 175 createTestSecret("did:plc:foo/repo", "api_key", "secret_value_123", "did:plc:example123"), 176 }, 177 removeSecret: Secret[any]{ 178 Key: "api_key", 179 Repo: DidSlashRepo("other.com/repo"), 180 }, 181 expectError: ErrKeyNotFound, 182 }, 183 } 184 185 for _, tt := range tests { 186 t.Run(tt.name, func(t *testing.T) { 187 manager := createInMemoryDB(t) 188 defer manager.db.Close() 189 190 // Setup secrets 191 for _, secret := range tt.setupSecrets { 192 if err := manager.AddSecret(secret); err != nil { 193 t.Fatalf("Failed to setup secret: %v", err) 194 } 195 } 196 197 // Test removal 198 err := manager.RemoveSecret(tt.removeSecret) 199 if err != tt.expectError { 200 t.Errorf("Expected error %v, got %v", tt.expectError, err) 201 } 202 }) 203 } 204} 205 206func TestSqliteManager_GetSecretsLocked(t *testing.T) { 207 tests := []struct { 208 name string 209 setupSecrets []UnlockedSecret 210 queryRepo DidSlashRepo 211 expectedCount int 212 expectedKeys []string 213 expectError bool 214 }{ 215 { 216 name: "get secrets for repo with multiple secrets", 217 setupSecrets: []UnlockedSecret{ 218 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 219 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 220 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 221 }, 222 queryRepo: DidSlashRepo("did:plc:foo/repo"), 223 expectedCount: 2, 224 expectedKeys: []string{"key1", "key2"}, 225 expectError: false, 226 }, 227 { 228 name: "get secrets for repo with single secret", 229 setupSecrets: []UnlockedSecret{ 230 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 231 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 232 }, 233 queryRepo: DidSlashRepo("did:plc:foo/repo"), 234 expectedCount: 1, 235 expectedKeys: []string{"single_key"}, 236 expectError: false, 237 }, 238 { 239 name: "get secrets for non-existent repo", 240 setupSecrets: []UnlockedSecret{ 241 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 242 }, 243 queryRepo: DidSlashRepo("nonexistent.com/repo"), 244 expectedCount: 0, 245 expectedKeys: []string{}, 246 expectError: false, 247 }, 248 { 249 name: "get secrets from empty database", 250 setupSecrets: []UnlockedSecret{}, 251 queryRepo: DidSlashRepo("did:plc:foo/repo"), 252 expectedCount: 0, 253 expectedKeys: []string{}, 254 expectError: false, 255 }, 256 } 257 258 for _, tt := range tests { 259 t.Run(tt.name, func(t *testing.T) { 260 manager := createInMemoryDB(t) 261 defer manager.db.Close() 262 263 // Setup secrets 264 for _, secret := range tt.setupSecrets { 265 if err := manager.AddSecret(secret); err != nil { 266 t.Fatalf("Failed to setup secret: %v", err) 267 } 268 } 269 270 // Test getting locked secrets 271 lockedSecrets, err := manager.GetSecretsLocked(tt.queryRepo) 272 if tt.expectError && err == nil { 273 t.Error("Expected error but got none") 274 return 275 } 276 if !tt.expectError && err != nil { 277 t.Fatalf("Unexpected error: %v", err) 278 } 279 280 if len(lockedSecrets) != tt.expectedCount { 281 t.Errorf("Expected %d secrets, got %d", tt.expectedCount, len(lockedSecrets)) 282 } 283 284 // Verify keys and that values are not present (locked) 285 foundKeys := make(map[string]bool) 286 for _, ls := range lockedSecrets { 287 foundKeys[ls.Key] = true 288 if ls.Repo != tt.queryRepo { 289 t.Errorf("Expected repo %s, got %s", tt.queryRepo, ls.Repo) 290 } 291 if ls.CreatedBy == "" { 292 t.Error("Expected CreatedBy to be present") 293 } 294 if ls.CreatedAt.IsZero() { 295 t.Error("Expected CreatedAt to be set") 296 } 297 } 298 299 for _, expectedKey := range tt.expectedKeys { 300 if !foundKeys[expectedKey] { 301 t.Errorf("Expected key %s not found", expectedKey) 302 } 303 } 304 }) 305 } 306} 307 308func TestSqliteManager_GetSecretsUnlocked(t *testing.T) { 309 tests := []struct { 310 name string 311 setupSecrets []UnlockedSecret 312 queryRepo DidSlashRepo 313 expectedCount int 314 expectedSecrets map[string]string // key -> value 315 expectError bool 316 }{ 317 { 318 name: "get unlocked secrets for repo with multiple secrets", 319 setupSecrets: []UnlockedSecret{ 320 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 321 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 322 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 323 }, 324 queryRepo: DidSlashRepo("did:plc:foo/repo"), 325 expectedCount: 2, 326 expectedSecrets: map[string]string{ 327 "key1": "value1", 328 "key2": "value2", 329 }, 330 expectError: false, 331 }, 332 { 333 name: "get unlocked secrets for repo with single secret", 334 setupSecrets: []UnlockedSecret{ 335 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 336 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 337 }, 338 queryRepo: DidSlashRepo("did:plc:foo/repo"), 339 expectedCount: 1, 340 expectedSecrets: map[string]string{ 341 "single_key": "single_value", 342 }, 343 expectError: false, 344 }, 345 { 346 name: "get unlocked secrets for non-existent repo", 347 setupSecrets: []UnlockedSecret{ 348 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 349 }, 350 queryRepo: DidSlashRepo("nonexistent.com/repo"), 351 expectedCount: 0, 352 expectedSecrets: map[string]string{}, 353 expectError: false, 354 }, 355 { 356 name: "get unlocked secrets from empty database", 357 setupSecrets: []UnlockedSecret{}, 358 queryRepo: DidSlashRepo("did:plc:foo/repo"), 359 expectedCount: 0, 360 expectedSecrets: map[string]string{}, 361 expectError: false, 362 }, 363 } 364 365 for _, tt := range tests { 366 t.Run(tt.name, func(t *testing.T) { 367 manager := createInMemoryDB(t) 368 defer manager.db.Close() 369 370 // Setup secrets 371 for _, secret := range tt.setupSecrets { 372 if err := manager.AddSecret(secret); err != nil { 373 t.Fatalf("Failed to setup secret: %v", err) 374 } 375 } 376 377 // Test getting unlocked secrets 378 unlockedSecrets, err := manager.GetSecretsUnlocked(tt.queryRepo) 379 if tt.expectError && err == nil { 380 t.Error("Expected error but got none") 381 return 382 } 383 if !tt.expectError && err != nil { 384 t.Fatalf("Unexpected error: %v", err) 385 } 386 387 if len(unlockedSecrets) != tt.expectedCount { 388 t.Errorf("Expected %d secrets, got %d", tt.expectedCount, len(unlockedSecrets)) 389 } 390 391 // Verify keys, values, and metadata 392 for _, us := range unlockedSecrets { 393 expectedValue, exists := tt.expectedSecrets[us.Key] 394 if !exists { 395 t.Errorf("Unexpected key: %s", us.Key) 396 continue 397 } 398 if us.Value != expectedValue { 399 t.Errorf("Expected value %s for key %s, got %s", expectedValue, us.Key, us.Value) 400 } 401 if us.Repo != tt.queryRepo { 402 t.Errorf("Expected repo %s, got %s", tt.queryRepo, us.Repo) 403 } 404 if us.CreatedBy == "" { 405 t.Error("Expected CreatedBy to be present") 406 } 407 if us.CreatedAt.IsZero() { 408 t.Error("Expected CreatedAt to be set") 409 } 410 } 411 }) 412 } 413} 414 415// Test that demonstrates interface usage with table-driven tests 416func TestManagerInterface_Usage(t *testing.T) { 417 tests := []struct { 418 name string 419 operations []func(Manager) error 420 expectError bool 421 }{ 422 { 423 name: "successful workflow", 424 operations: []func(Manager) error{ 425 func(m Manager) error { 426 secret := createTestSecret("interface.test/repo", "test_key", "test_value", "did:plc:user") 427 return m.AddSecret(secret) 428 }, 429 func(m Manager) error { 430 _, err := m.GetSecretsLocked(DidSlashRepo("interface.test/repo")) 431 return err 432 }, 433 func(m Manager) error { 434 _, err := m.GetSecretsUnlocked(DidSlashRepo("interface.test/repo")) 435 return err 436 }, 437 func(m Manager) error { 438 secret := Secret[any]{ 439 Key: "test_key", 440 Repo: DidSlashRepo("interface.test/repo"), 441 } 442 return m.RemoveSecret(secret) 443 }, 444 }, 445 expectError: false, 446 }, 447 { 448 name: "error on duplicate key", 449 operations: []func(Manager) error{ 450 func(m Manager) error { 451 secret := createTestSecret("interface.test/repo", "dup_key", "value1", "did:plc:user") 452 return m.AddSecret(secret) 453 }, 454 func(m Manager) error { 455 secret := createTestSecret("interface.test/repo", "dup_key", "value2", "did:plc:user") 456 return m.AddSecret(secret) // Should return ErrKeyAlreadyPresent 457 }, 458 }, 459 expectError: true, 460 }, 461 } 462 463 for _, tt := range tests { 464 t.Run(tt.name, func(t *testing.T) { 465 var manager Manager = createInMemoryDB(t) 466 defer func() { 467 if sqliteManager, ok := manager.(*SqliteManager); ok { 468 sqliteManager.db.Close() 469 } 470 }() 471 472 var finalErr error 473 for i, operation := range tt.operations { 474 if err := operation(manager); err != nil { 475 finalErr = err 476 t.Logf("Operation %d returned error: %v", i, err) 477 } 478 } 479 480 if tt.expectError && finalErr == nil { 481 t.Error("Expected error but got none") 482 } 483 if !tt.expectError && finalErr != nil { 484 t.Errorf("Unexpected error: %v", finalErr) 485 } 486 }) 487 } 488} 489 490// Integration test with table-driven scenarios 491func TestSqliteManager_Integration(t *testing.T) { 492 tests := []struct { 493 name string 494 scenario func(*testing.T, *SqliteManager) 495 }{ 496 { 497 name: "multi-repo secret management", 498 scenario: func(t *testing.T, manager *SqliteManager) { 499 repo1 := DidSlashRepo("example1.com/repo") 500 repo2 := DidSlashRepo("example2.com/repo") 501 502 secrets := []UnlockedSecret{ 503 createTestSecret(string(repo1), "db_password", "super_secret_123", "did:plc:admin"), 504 createTestSecret(string(repo1), "api_key", "api_key_456", "did:plc:user1"), 505 createTestSecret(string(repo2), "token", "bearer_token_789", "did:plc:user2"), 506 } 507 508 // Add all secrets 509 for _, secret := range secrets { 510 if err := manager.AddSecret(secret); err != nil { 511 t.Fatalf("Failed to add secret %s: %v", secret.Key, err) 512 } 513 } 514 515 // Verify counts 516 locked1, _ := manager.GetSecretsLocked(repo1) 517 locked2, _ := manager.GetSecretsLocked(repo2) 518 519 if len(locked1) != 2 { 520 t.Errorf("Expected 2 secrets for repo1, got %d", len(locked1)) 521 } 522 if len(locked2) != 1 { 523 t.Errorf("Expected 1 secret for repo2, got %d", len(locked2)) 524 } 525 526 // Remove and verify 527 secretToRemove := Secret[any]{Key: "db_password", Repo: repo1} 528 if err := manager.RemoveSecret(secretToRemove); err != nil { 529 t.Fatalf("Failed to remove secret: %v", err) 530 } 531 532 locked1After, _ := manager.GetSecretsLocked(repo1) 533 if len(locked1After) != 1 { 534 t.Errorf("Expected 1 secret for repo1 after removal, got %d", len(locked1After)) 535 } 536 if locked1After[0].Key != "api_key" { 537 t.Errorf("Expected remaining secret to be 'api_key', got %s", locked1After[0].Key) 538 } 539 }, 540 }, 541 { 542 name: "empty database operations", 543 scenario: func(t *testing.T, manager *SqliteManager) { 544 repo := DidSlashRepo("empty.test/repo") 545 546 // Operations on empty database should not error 547 locked, err := manager.GetSecretsLocked(repo) 548 if err != nil { 549 t.Errorf("GetSecretsLocked on empty DB failed: %v", err) 550 } 551 if len(locked) != 0 { 552 t.Errorf("Expected 0 secrets, got %d", len(locked)) 553 } 554 555 unlocked, err := manager.GetSecretsUnlocked(repo) 556 if err != nil { 557 t.Errorf("GetSecretsUnlocked on empty DB failed: %v", err) 558 } 559 if len(unlocked) != 0 { 560 t.Errorf("Expected 0 secrets, got %d", len(unlocked)) 561 } 562 563 // Remove from empty should return ErrKeyNotFound 564 nonExistent := Secret[any]{Key: "none", Repo: repo} 565 err = manager.RemoveSecret(nonExistent) 566 if err != ErrKeyNotFound { 567 t.Errorf("Expected ErrKeyNotFound, got %v", err) 568 } 569 }, 570 }, 571 } 572 573 for _, tt := range tests { 574 t.Run(tt.name, func(t *testing.T) { 575 manager := createInMemoryDB(t) 576 defer manager.db.Close() 577 tt.scenario(t, manager) 578 }) 579 } 580}