A community based topic aggregation platform built on atproto
1package repository_test
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "os"
8 "testing"
9
10 "Coves/internal/atproto/carstore"
11 "Coves/internal/core/repository"
12 "Coves/internal/db/postgres"
13
14 "github.com/ipfs/go-cid"
15 _ "github.com/lib/pq"
16 "github.com/pressly/goose/v3"
17 postgresDriver "gorm.io/driver/postgres"
18 "gorm.io/gorm"
19)
20
21// Mock signing key for testing
22type mockSigningKey struct{}
23
24// Test database connection
25func setupTestDB(t *testing.T) (*sql.DB, *gorm.DB, func()) {
26 // Use test database URL from environment or default
27 dbURL := os.Getenv("TEST_DATABASE_URL")
28 if dbURL == "" {
29 // Skip test if no database configured
30 t.Skip("TEST_DATABASE_URL not set, skipping database tests")
31 }
32
33 // Connect with sql.DB for migrations
34 sqlDB, err := sql.Open("postgres", dbURL)
35 if err != nil {
36 t.Fatalf("Failed to connect to test database: %v", err)
37 }
38
39 // Run migrations
40 if err := goose.Up(sqlDB, "../../db/migrations"); err != nil {
41 t.Fatalf("Failed to run migrations: %v", err)
42 }
43
44 // Connect with GORM using a fresh connection
45 gormDB, err := gorm.Open(postgresDriver.Open(dbURL), &gorm.Config{
46 DisableForeignKeyConstraintWhenMigrating: true,
47 PrepareStmt: false,
48 })
49 if err != nil {
50 t.Fatalf("Failed to create GORM connection: %v", err)
51 }
52
53 // Cleanup function
54 cleanup := func() {
55 // Clean up test data
56 gormDB.Exec("DELETE FROM repositories")
57 gormDB.Exec("DELETE FROM commits")
58 gormDB.Exec("DELETE FROM records")
59 gormDB.Exec("DELETE FROM user_maps")
60 gormDB.Exec("DELETE FROM car_shards")
61 gormDB.Exec("DELETE FROM block_refs")
62
63 // Close GORM connection
64 if sqlGormDB, err := gormDB.DB(); err == nil {
65 sqlGormDB.Close()
66 }
67
68 // Close original SQL connection
69 sqlDB.Close()
70 }
71
72 return sqlDB, gormDB, cleanup
73}
74
75func TestRepositoryService_CreateRepository(t *testing.T) {
76 sqlDB, gormDB, cleanup := setupTestDB(t)
77 defer cleanup()
78
79 // Create temporary directory for carstore
80 tempDir, err := os.MkdirTemp("", "carstore_test")
81 if err != nil {
82 t.Fatalf("Failed to create temp dir: %v", err)
83 }
84 defer os.RemoveAll(tempDir)
85
86 // Initialize carstore
87 carDirs := []string{tempDir}
88 repoStore, err := carstore.NewRepoStore(gormDB, carDirs)
89 if err != nil {
90 t.Fatalf("Failed to create repo store: %v", err)
91 }
92
93 // Initialize repository service
94 repoRepo := postgres.NewRepositoryRepo(sqlDB)
95 service := repository.NewService(repoRepo, repoStore)
96
97 // Test DID
98 testDID := "did:plc:testuser123"
99
100 // Set signing key
101 service.SetSigningKey(testDID, &mockSigningKey{})
102
103 // Create repository
104 repo, err := service.CreateRepository(testDID)
105 if err != nil {
106 t.Fatalf("Failed to create repository: %v", err)
107 }
108
109 // Verify repository was created
110 if repo.DID != testDID {
111 t.Errorf("Expected DID %s, got %s", testDID, repo.DID)
112 }
113 if !repo.HeadCID.Defined() {
114 t.Error("Expected HeadCID to be defined")
115 }
116 if repo.RecordCount != 0 {
117 t.Errorf("Expected RecordCount 0, got %d", repo.RecordCount)
118 }
119
120 // Verify repository exists in database
121 fetchedRepo, err := service.GetRepository(testDID)
122 if err != nil {
123 t.Fatalf("Failed to get repository: %v", err)
124 }
125 if fetchedRepo.DID != testDID {
126 t.Errorf("Expected fetched DID %s, got %s", testDID, fetchedRepo.DID)
127 }
128
129 // Test duplicate creation should fail
130 _, err = service.CreateRepository(testDID)
131 if err == nil {
132 t.Error("Expected error creating duplicate repository")
133 }
134}
135
136func TestRepositoryService_ImportExport(t *testing.T) {
137 sqlDB, gormDB, cleanup := setupTestDB(t)
138 defer cleanup()
139
140 // Create temporary directory for carstore
141 tempDir, err := os.MkdirTemp("", "carstore_test")
142 if err != nil {
143 t.Fatalf("Failed to create temp dir: %v", err)
144 }
145 defer os.RemoveAll(tempDir)
146
147 // Log the temp directory for debugging
148 t.Logf("Using carstore directory: %s", tempDir)
149
150 // Initialize carstore
151 carDirs := []string{tempDir}
152 repoStore, err := carstore.NewRepoStore(gormDB, carDirs)
153 if err != nil {
154 t.Fatalf("Failed to create repo store: %v", err)
155 }
156
157 // Initialize repository service
158 repoRepo := postgres.NewRepositoryRepo(sqlDB)
159 service := repository.NewService(repoRepo, repoStore)
160
161 // Create first repository
162 did1 := "did:plc:user1"
163 service.SetSigningKey(did1, &mockSigningKey{})
164 repo1, err := service.CreateRepository(did1)
165 if err != nil {
166 t.Fatalf("Failed to create repository 1: %v", err)
167 }
168 t.Logf("Created repository with HeadCID: %s", repo1.HeadCID)
169
170 // Check what's in the database
171 var userMapCount int
172 gormDB.Raw("SELECT COUNT(*) FROM user_maps").Scan(&userMapCount)
173 t.Logf("User maps count: %d", userMapCount)
174
175 var carShardCount int
176 gormDB.Raw("SELECT COUNT(*) FROM car_shards").Scan(&carShardCount)
177 t.Logf("Car shards count: %d", carShardCount)
178
179 // Check block_refs too
180 var blockRefCount int
181 gormDB.Raw("SELECT COUNT(*) FROM block_refs").Scan(&blockRefCount)
182 t.Logf("Block refs count: %d", blockRefCount)
183
184 // Export repository
185 carData, err := service.ExportRepository(did1)
186 if err != nil {
187 t.Fatalf("Failed to export repository: %v", err)
188 }
189 // For now, empty repositories return empty CAR data
190 t.Logf("Exported CAR data size: %d bytes", len(carData))
191
192 // Import to new DID
193 did2 := "did:plc:user2"
194 err = service.ImportRepository(did2, carData)
195 if err != nil {
196 t.Fatalf("Failed to import repository: %v", err)
197 }
198
199 // Verify imported repository
200 repo2, err := service.GetRepository(did2)
201 if err != nil {
202 t.Fatalf("Failed to get imported repository: %v", err)
203 }
204 if repo2.DID != did2 {
205 t.Errorf("Expected DID %s, got %s", did2, repo2.DID)
206 }
207 // Note: HeadCID might differ due to new import
208}
209
210func TestRepositoryService_DeleteRepository(t *testing.T) {
211 sqlDB, gormDB, cleanup := setupTestDB(t)
212 defer cleanup()
213
214 // Create temporary directory for carstore
215 tempDir, err := os.MkdirTemp("", "carstore_test")
216 if err != nil {
217 t.Fatalf("Failed to create temp dir: %v", err)
218 }
219 defer os.RemoveAll(tempDir)
220
221 // Initialize carstore
222 carDirs := []string{tempDir}
223 repoStore, err := carstore.NewRepoStore(gormDB, carDirs)
224 if err != nil {
225 t.Fatalf("Failed to create repo store: %v", err)
226 }
227
228 // Initialize repository service
229 repoRepo := postgres.NewRepositoryRepo(sqlDB)
230 service := repository.NewService(repoRepo, repoStore)
231
232 // Create repository
233 testDID := "did:plc:deletetest"
234 service.SetSigningKey(testDID, &mockSigningKey{})
235 _, err = service.CreateRepository(testDID)
236 if err != nil {
237 t.Fatalf("Failed to create repository: %v", err)
238 }
239
240 // Delete repository
241 err = service.DeleteRepository(testDID)
242 if err != nil {
243 t.Fatalf("Failed to delete repository: %v", err)
244 }
245
246 // Verify repository is deleted
247 _, err = service.GetRepository(testDID)
248 if err == nil {
249 t.Error("Expected error getting deleted repository")
250 }
251}
252
253func TestRepositoryService_CompactRepository(t *testing.T) {
254 sqlDB, gormDB, cleanup := setupTestDB(t)
255 defer cleanup()
256
257 // Create temporary directory for carstore
258 tempDir, err := os.MkdirTemp("", "carstore_test")
259 if err != nil {
260 t.Fatalf("Failed to create temp dir: %v", err)
261 }
262 defer os.RemoveAll(tempDir)
263
264 // Initialize carstore
265 carDirs := []string{tempDir}
266 repoStore, err := carstore.NewRepoStore(gormDB, carDirs)
267 if err != nil {
268 t.Fatalf("Failed to create repo store: %v", err)
269 }
270
271 // Initialize repository service
272 repoRepo := postgres.NewRepositoryRepo(sqlDB)
273 service := repository.NewService(repoRepo, repoStore)
274
275 // Create repository
276 testDID := "did:plc:compacttest"
277 service.SetSigningKey(testDID, &mockSigningKey{})
278 _, err = service.CreateRepository(testDID)
279 if err != nil {
280 t.Fatalf("Failed to create repository: %v", err)
281 }
282
283 // Run compaction (should not error even with minimal data)
284 err = service.CompactRepository(testDID)
285 if err != nil {
286 t.Errorf("Failed to compact repository: %v", err)
287 }
288}
289
290// Test UserMapping functionality
291func TestUserMapping(t *testing.T) {
292 _, gormDB, cleanup := setupTestDB(t)
293 defer cleanup()
294
295 // Create user mapping
296 mapping, err := carstore.NewUserMapping(gormDB)
297 if err != nil {
298 t.Fatalf("Failed to create user mapping: %v", err)
299 }
300
301 // Test creating new mapping
302 did1 := "did:plc:mapping1"
303 uid1, err := mapping.GetOrCreateUID(context.Background(), did1)
304 if err != nil {
305 t.Fatalf("Failed to create UID for %s: %v", did1, err)
306 }
307 if uid1 == 0 {
308 t.Error("Expected non-zero UID")
309 }
310
311 // Test getting existing mapping
312 uid1Again, err := mapping.GetOrCreateUID(context.Background(), did1)
313 if err != nil {
314 t.Fatalf("Failed to get UID for %s: %v", did1, err)
315 }
316 if uid1 != uid1Again {
317 t.Errorf("Expected same UID, got %d and %d", uid1, uid1Again)
318 }
319
320 // Test reverse lookup
321 didLookup, err := mapping.GetDID(uid1)
322 if err != nil {
323 t.Fatalf("Failed to get DID for UID %d: %v", uid1, err)
324 }
325 if didLookup != did1 {
326 t.Errorf("Expected DID %s, got %s", did1, didLookup)
327 }
328
329 // Test second user gets different UID
330 did2 := "did:plc:mapping2"
331 uid2, err := mapping.GetOrCreateUID(context.Background(), did2)
332 if err != nil {
333 t.Fatalf("Failed to create UID for %s: %v", did2, err)
334 }
335 if uid2 == uid1 {
336 t.Error("Expected different UIDs for different DIDs")
337 }
338}
339
340// Test with mock repository and carstore
341func TestRepositoryService_MockedComponents(t *testing.T) {
342 // Use the existing mock repository from the old test file
343 _ = NewMockRepositoryRepository()
344
345 // For unit testing without real carstore, we would need to mock RepoStore
346 // For now, this demonstrates the structure
347 t.Skip("Mocked carstore tests would require creating mock RepoStore interface")
348}
349
350// Benchmark repository creation
351func BenchmarkRepositoryCreation(b *testing.B) {
352 sqlDB, gormDB, cleanup := setupTestDB(&testing.T{})
353 defer cleanup()
354
355 tempDir, _ := os.MkdirTemp("", "carstore_bench")
356 defer os.RemoveAll(tempDir)
357
358 carDirs := []string{tempDir}
359 repoStore, _ := carstore.NewRepoStore(gormDB, carDirs)
360 repoRepo := postgres.NewRepositoryRepo(sqlDB)
361 service := repository.NewService(repoRepo, repoStore)
362
363 b.ResetTimer()
364 for i := 0; i < b.N; i++ {
365 did := fmt.Sprintf("did:plc:bench%d", i)
366 service.SetSigningKey(did, &mockSigningKey{})
367 _, _ = service.CreateRepository(did)
368 }
369}
370
371// MockRepositoryRepository is a mock implementation of repository.RepositoryRepository
372type MockRepositoryRepository struct {
373 repositories map[string]*repository.Repository
374 commits map[string][]*repository.Commit
375 records map[string]*repository.Record
376}
377
378func NewMockRepositoryRepository() *MockRepositoryRepository {
379 return &MockRepositoryRepository{
380 repositories: make(map[string]*repository.Repository),
381 commits: make(map[string][]*repository.Commit),
382 records: make(map[string]*repository.Record),
383 }
384}
385
386// Repository operations
387func (m *MockRepositoryRepository) Create(repo *repository.Repository) error {
388 m.repositories[repo.DID] = repo
389 return nil
390}
391
392func (m *MockRepositoryRepository) GetByDID(did string) (*repository.Repository, error) {
393 repo, exists := m.repositories[did]
394 if !exists {
395 return nil, nil
396 }
397 return repo, nil
398}
399
400func (m *MockRepositoryRepository) Update(repo *repository.Repository) error {
401 if _, exists := m.repositories[repo.DID]; !exists {
402 return nil
403 }
404 m.repositories[repo.DID] = repo
405 return nil
406}
407
408func (m *MockRepositoryRepository) Delete(did string) error {
409 delete(m.repositories, did)
410 return nil
411}
412
413// Commit operations
414func (m *MockRepositoryRepository) CreateCommit(commit *repository.Commit) error {
415 m.commits[commit.DID] = append(m.commits[commit.DID], commit)
416 return nil
417}
418
419func (m *MockRepositoryRepository) GetCommit(did string, commitCID cid.Cid) (*repository.Commit, error) {
420 commits, exists := m.commits[did]
421 if !exists {
422 return nil, nil
423 }
424
425 for _, c := range commits {
426 if c.CID.Equals(commitCID) {
427 return c, nil
428 }
429 }
430 return nil, nil
431}
432
433func (m *MockRepositoryRepository) GetLatestCommit(did string) (*repository.Commit, error) {
434 commits, exists := m.commits[did]
435 if !exists || len(commits) == 0 {
436 return nil, nil
437 }
438 return commits[len(commits)-1], nil
439}
440
441func (m *MockRepositoryRepository) ListCommits(did string, limit int, offset int) ([]*repository.Commit, error) {
442 commits, exists := m.commits[did]
443 if !exists {
444 return []*repository.Commit{}, nil
445 }
446
447 start := offset
448 if start >= len(commits) {
449 return []*repository.Commit{}, nil
450 }
451
452 end := start + limit
453 if end > len(commits) {
454 end = len(commits)
455 }
456
457 return commits[start:end], nil
458}
459
460// Record operations
461func (m *MockRepositoryRepository) CreateRecord(record *repository.Record) error {
462 key := record.URI
463 m.records[key] = record
464 return nil
465}
466
467func (m *MockRepositoryRepository) GetRecord(did string, collection string, recordKey string) (*repository.Record, error) {
468 uri := "at://" + did + "/" + collection + "/" + recordKey
469 record, exists := m.records[uri]
470 if !exists {
471 return nil, nil
472 }
473 return record, nil
474}
475
476func (m *MockRepositoryRepository) UpdateRecord(record *repository.Record) error {
477 key := record.URI
478 if _, exists := m.records[key]; !exists {
479 return nil
480 }
481 m.records[key] = record
482 return nil
483}
484
485func (m *MockRepositoryRepository) DeleteRecord(did string, collection string, recordKey string) error {
486 uri := "at://" + did + "/" + collection + "/" + recordKey
487 delete(m.records, uri)
488 return nil
489}
490
491func (m *MockRepositoryRepository) ListRecords(did string, collection string, limit int, offset int) ([]*repository.Record, error) {
492 var records []*repository.Record
493 prefix := "at://" + did + "/" + collection + "/"
494
495 for uri, record := range m.records {
496 if len(uri) > len(prefix) && uri[:len(prefix)] == prefix {
497 records = append(records, record)
498 }
499 }
500
501 // Simple pagination
502 start := offset
503 if start >= len(records) {
504 return []*repository.Record{}, nil
505 }
506
507 end := start + limit
508 if end > len(records) {
509 end = len(records)
510 }
511
512 return records[start:end], nil
513}