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}