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