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