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}