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}