Monorepo for Tangled tangled.org

spindle: add sqlite backed secret manager implementation

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li cecf5558 b6c8564d

verified
Changed files
+751
spindle
+171
spindle/secrets/sqlite.go
··· 1 + // an sqlite3 backed secret manager 2 + package secrets 3 + 4 + import ( 5 + "database/sql" 6 + "fmt" 7 + "time" 8 + 9 + _ "github.com/mattn/go-sqlite3" 10 + ) 11 + 12 + type SqliteManager struct { 13 + db *sql.DB 14 + tableName string 15 + } 16 + 17 + type SqliteManagerOpt func(*SqliteManager) 18 + 19 + func WithTableName(name string) SqliteManagerOpt { 20 + return func(s *SqliteManager) { 21 + s.tableName = name 22 + } 23 + } 24 + 25 + func NewSQLiteManager(dbPath string, opts ...SqliteManagerOpt) (*SqliteManager, error) { 26 + db, err := sql.Open("sqlite3", dbPath) 27 + if err != nil { 28 + return nil, fmt.Errorf("failed to open sqlite database: %w", err) 29 + } 30 + 31 + manager := &SqliteManager{ 32 + db: db, 33 + tableName: "secrets", 34 + } 35 + 36 + for _, o := range opts { 37 + o(manager) 38 + } 39 + 40 + if err := manager.init(); err != nil { 41 + return nil, err 42 + } 43 + 44 + return manager, nil 45 + } 46 + 47 + // creates a table and sets up the schema, migrations if any can go here 48 + func (s *SqliteManager) init() error { 49 + createTable := 50 + `create table if not exists ` + s.tableName + `( 51 + id integer primary key autoincrement, 52 + repo text not null, 53 + key text not null, 54 + value text not null, 55 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 56 + created_by text not null, 57 + 58 + unique(repo, key) 59 + );` 60 + _, err := s.db.Exec(createTable) 61 + return err 62 + } 63 + 64 + func (s *SqliteManager) AddSecret(secret UnlockedSecret) error { 65 + query := fmt.Sprintf(` 66 + insert or ignore into %s (repo, key, value, created_by) 67 + values (?, ?, ?, ?); 68 + `, s.tableName) 69 + 70 + res, err := s.db.Exec(query, secret.Repo, secret.Key, secret.Value, secret.CreatedBy) 71 + if err != nil { 72 + return err 73 + } 74 + 75 + num, err := res.RowsAffected() 76 + if err != nil { 77 + return err 78 + } 79 + 80 + if num == 0 { 81 + return ErrKeyAlreadyPresent 82 + } 83 + 84 + return nil 85 + } 86 + 87 + func (s *SqliteManager) RemoveSecret(secret Secret[any]) error { 88 + query := fmt.Sprintf(` 89 + delete from %s where repo = ? and key = ?; 90 + `, s.tableName) 91 + 92 + res, err := s.db.Exec(query, secret.Repo, secret.Key) 93 + if err != nil { 94 + return err 95 + } 96 + 97 + num, err := res.RowsAffected() 98 + if err != nil { 99 + return err 100 + } 101 + 102 + if num == 0 { 103 + return ErrKeyNotFound 104 + } 105 + 106 + return nil 107 + } 108 + 109 + func (s *SqliteManager) GetSecretsLocked(didSlashRepo DidSlashRepo) ([]LockedSecret, error) { 110 + query := fmt.Sprintf(` 111 + select repo, key, created_at, created_by from %s where repo = ?; 112 + `, s.tableName) 113 + 114 + rows, err := s.db.Query(query, didSlashRepo) 115 + if err != nil { 116 + return nil, err 117 + } 118 + 119 + var ls []LockedSecret 120 + for rows.Next() { 121 + var l LockedSecret 122 + var createdAt string 123 + if err = rows.Scan(&l.Repo, &l.Key, &createdAt, &l.CreatedBy); err != nil { 124 + return nil, err 125 + } 126 + 127 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 128 + l.CreatedAt = t 129 + } 130 + 131 + ls = append(ls, l) 132 + } 133 + 134 + if err = rows.Err(); err != nil { 135 + return nil, err 136 + } 137 + 138 + return ls, nil 139 + } 140 + 141 + func (s *SqliteManager) GetSecretsUnlocked(didSlashRepo DidSlashRepo) ([]UnlockedSecret, error) { 142 + query := fmt.Sprintf(` 143 + select repo, key, value, created_at, created_by from %s where repo = ?; 144 + `, s.tableName) 145 + 146 + rows, err := s.db.Query(query, didSlashRepo) 147 + if err != nil { 148 + return nil, err 149 + } 150 + 151 + var ls []UnlockedSecret 152 + for rows.Next() { 153 + var l UnlockedSecret 154 + var createdAt string 155 + if err = rows.Scan(&l.Repo, &l.Key, &l.Value, &createdAt, &l.CreatedBy); err != nil { 156 + return nil, err 157 + } 158 + 159 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 160 + l.CreatedAt = t 161 + } 162 + 163 + ls = append(ls, l) 164 + } 165 + 166 + if err = rows.Err(); err != nil { 167 + return nil, err 168 + } 169 + 170 + return ls, nil 171 + }
+580
spindle/secrets/sqlite_test.go
··· 1 + package secrets 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + func 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 + 19 + func 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 30 + func TestManagerInterface(t *testing.T) { 31 + var _ Manager = (*SqliteManager)(nil) 32 + } 33 + 34 + func 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 + 87 + func 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 + 134 + func 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 + 206 + func 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 + 308 + func 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 416 + func 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 491 + func 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 + }