Signed-off-by: oppiliappan me@oppi.li
+171
spindle/secrets/sqlite.go
+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
+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
+
}