A community based topic aggregation platform built on atproto
1package integration 2 3import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "strings" 12 "testing" 13 14 "github.com/go-chi/chi/v5" 15 _ "github.com/lib/pq" 16 "github.com/pressly/goose/v3" 17 18 "Coves/internal/api/routes" 19 "Coves/internal/core/users" 20 "Coves/internal/db/postgres" 21) 22 23func setupTestDB(t *testing.T) *sql.DB { 24 // Build connection string from environment variables (set by .env.dev) 25 testUser := os.Getenv("POSTGRES_TEST_USER") 26 testPassword := os.Getenv("POSTGRES_TEST_PASSWORD") 27 testPort := os.Getenv("POSTGRES_TEST_PORT") 28 testDB := os.Getenv("POSTGRES_TEST_DB") 29 30 // Fallback to defaults if not set 31 if testUser == "" { 32 testUser = "test_user" 33 } 34 if testPassword == "" { 35 testPassword = "test_password" 36 } 37 if testPort == "" { 38 testPort = "5434" 39 } 40 if testDB == "" { 41 testDB = "coves_test" 42 } 43 44 dbURL := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=disable", 45 testUser, testPassword, testPort, testDB) 46 47 db, err := sql.Open("postgres", dbURL) 48 if err != nil { 49 t.Fatalf("Failed to connect to test database: %v", err) 50 } 51 52 if err := db.Ping(); err != nil { 53 t.Fatalf("Failed to ping test database: %v", err) 54 } 55 56 if err := goose.SetDialect("postgres"); err != nil { 57 t.Fatalf("Failed to set goose dialect: %v", err) 58 } 59 60 if err := goose.Up(db, "../../internal/db/migrations"); err != nil { 61 t.Fatalf("Failed to run migrations: %v", err) 62 } 63 64 // Clean up any existing test data 65 _, err = db.Exec("DELETE FROM users WHERE handle LIKE '%.test'") 66 if err != nil { 67 t.Logf("Warning: Failed to clean up test data: %v", err) 68 } 69 70 return db 71} 72 73func TestUserCreationAndRetrieval(t *testing.T) { 74 db := setupTestDB(t) 75 defer db.Close() 76 77 // Wire up dependencies 78 userRepo := postgres.NewUserRepository(db) 79 userService := users.NewUserService(userRepo, "http://localhost:3001") 80 81 ctx := context.Background() 82 83 // Test 1: Create a user 84 t.Run("Create User", func(t *testing.T) { 85 req := users.CreateUserRequest{ 86 DID: "did:plc:test123456", 87 Handle: "alice.test", 88 } 89 90 user, err := userService.CreateUser(ctx, req) 91 if err != nil { 92 t.Fatalf("Failed to create user: %v", err) 93 } 94 95 if user.DID != req.DID { 96 t.Errorf("Expected DID %s, got %s", req.DID, user.DID) 97 } 98 99 if user.Handle != req.Handle { 100 t.Errorf("Expected handle %s, got %s", req.Handle, user.Handle) 101 } 102 103 if user.CreatedAt.IsZero() { 104 t.Error("CreatedAt should not be zero") 105 } 106 }) 107 108 // Test 2: Retrieve user by DID 109 t.Run("Get User By DID", func(t *testing.T) { 110 user, err := userService.GetUserByDID(ctx, "did:plc:test123456") 111 if err != nil { 112 t.Fatalf("Failed to get user by DID: %v", err) 113 } 114 115 if user.Handle != "alice.test" { 116 t.Errorf("Expected handle alice.test, got %s", user.Handle) 117 } 118 }) 119 120 // Test 3: Retrieve user by handle 121 t.Run("Get User By Handle", func(t *testing.T) { 122 user, err := userService.GetUserByHandle(ctx, "alice.test") 123 if err != nil { 124 t.Fatalf("Failed to get user by handle: %v", err) 125 } 126 127 if user.DID != "did:plc:test123456" { 128 t.Errorf("Expected DID did:plc:test123456, got %s", user.DID) 129 } 130 }) 131 132 // Test 4: Resolve handle to DID 133 t.Run("Resolve Handle to DID", func(t *testing.T) { 134 did, err := userService.ResolveHandleToDID(ctx, "alice.test") 135 if err != nil { 136 t.Fatalf("Failed to resolve handle: %v", err) 137 } 138 139 if did != "did:plc:test123456" { 140 t.Errorf("Expected DID did:plc:test123456, got %s", did) 141 } 142 }) 143} 144 145func TestGetProfileEndpoint(t *testing.T) { 146 db := setupTestDB(t) 147 defer db.Close() 148 149 // Wire up dependencies 150 userRepo := postgres.NewUserRepository(db) 151 userService := users.NewUserService(userRepo, "http://localhost:3001") 152 153 // Create test user directly in service 154 ctx := context.Background() 155 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 156 DID: "did:plc:endpoint123", 157 Handle: "bob.test", 158 }) 159 if err != nil { 160 t.Fatalf("Failed to create test user: %v", err) 161 } 162 163 // Set up HTTP router 164 r := chi.NewRouter() 165 r.Mount("/xrpc/social.coves.actor", routes.UserRoutes(userService)) 166 167 // Test 1: Get profile by DID 168 t.Run("Get Profile By DID", func(t *testing.T) { 169 req := httptest.NewRequest("GET", "/xrpc/social.coves.actor/profile?actor=did:plc:endpoint123", nil) 170 w := httptest.NewRecorder() 171 r.ServeHTTP(w, req) 172 173 if w.Code != http.StatusOK { 174 t.Errorf("Expected status %d, got %d. Response: %s", http.StatusOK, w.Code, w.Body.String()) 175 return 176 } 177 178 var response map[string]interface{} 179 if err := json.NewDecoder(w.Body).Decode(&response); err != nil { 180 t.Fatalf("Failed to decode response: %v", err) 181 } 182 183 if response["did"] != "did:plc:endpoint123" { 184 t.Errorf("Expected DID did:plc:endpoint123, got %v", response["did"]) 185 } 186 }) 187 188 // Test 2: Get profile by handle 189 t.Run("Get Profile By Handle", func(t *testing.T) { 190 req := httptest.NewRequest("GET", "/xrpc/social.coves.actor/profile?actor=bob.test", nil) 191 w := httptest.NewRecorder() 192 r.ServeHTTP(w, req) 193 194 if w.Code != http.StatusOK { 195 t.Errorf("Expected status %d, got %d. Response: %s", http.StatusOK, w.Code, w.Body.String()) 196 return 197 } 198 199 var response map[string]interface{} 200 if err := json.NewDecoder(w.Body).Decode(&response); err != nil { 201 t.Fatalf("Failed to decode response: %v", err) 202 } 203 204 profile := response["profile"].(map[string]interface{}) 205 if profile["handle"] != "bob.test" { 206 t.Errorf("Expected handle bob.test, got %v", profile["handle"]) 207 } 208 }) 209 210 // Test 3: Missing actor parameter 211 t.Run("Missing Actor Parameter", func(t *testing.T) { 212 req := httptest.NewRequest("GET", "/xrpc/social.coves.actor/profile", nil) 213 w := httptest.NewRecorder() 214 r.ServeHTTP(w, req) 215 216 if w.Code != http.StatusBadRequest { 217 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) 218 } 219 }) 220 221 // Test 4: User not found 222 t.Run("User Not Found", func(t *testing.T) { 223 req := httptest.NewRequest("GET", "/xrpc/social.coves.actor/profile?actor=nonexistent.test", nil) 224 w := httptest.NewRecorder() 225 r.ServeHTTP(w, req) 226 227 if w.Code != http.StatusNotFound { 228 t.Errorf("Expected status %d, got %d", http.StatusNotFound, w.Code) 229 } 230 }) 231} 232 233// TestDuplicateCreation tests that duplicate DID/handle creation fails properly 234func TestDuplicateCreation(t *testing.T) { 235 db := setupTestDB(t) 236 defer db.Close() 237 238 userRepo := postgres.NewUserRepository(db) 239 userService := users.NewUserService(userRepo, "http://localhost:3001") 240 ctx := context.Background() 241 242 // Create first user 243 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 244 DID: "did:plc:duplicate123", 245 Handle: "duplicate.test", 246 }) 247 if err != nil { 248 t.Fatalf("Failed to create first user: %v", err) 249 } 250 251 // Test duplicate DID 252 t.Run("Duplicate DID", func(t *testing.T) { 253 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 254 DID: "did:plc:duplicate123", 255 Handle: "different.test", 256 }) 257 258 if err == nil { 259 t.Error("Expected error for duplicate DID, got nil") 260 } 261 262 if !strings.Contains(err.Error(), "DID already exists") { 263 t.Errorf("Expected 'DID already exists' error, got: %v", err) 264 } 265 }) 266 267 // Test duplicate handle 268 t.Run("Duplicate Handle", func(t *testing.T) { 269 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 270 DID: "did:plc:different456", 271 Handle: "duplicate.test", 272 }) 273 274 if err == nil { 275 t.Error("Expected error for duplicate handle, got nil") 276 } 277 278 if !strings.Contains(err.Error(), "handle already taken") { 279 t.Errorf("Expected 'handle already taken' error, got: %v", err) 280 } 281 }) 282} 283 284// TestHandleValidation tests atProto handle validation rules 285func TestHandleValidation(t *testing.T) { 286 db := setupTestDB(t) 287 defer db.Close() 288 289 userRepo := postgres.NewUserRepository(db) 290 userService := users.NewUserService(userRepo, "http://localhost:3001") 291 ctx := context.Background() 292 293 testCases := []struct { 294 name string 295 did string 296 handle string 297 shouldError bool 298 errorMsg string 299 }{ 300 { 301 name: "Valid handle with hyphen", 302 did: "did:plc:valid1", 303 handle: "alice-bob.test", 304 shouldError: false, 305 }, 306 { 307 name: "Valid handle with dots", 308 did: "did:plc:valid2", 309 handle: "alice.bob.test", 310 shouldError: false, 311 }, 312 { 313 name: "Invalid: consecutive hyphens", 314 did: "did:plc:invalid1", 315 handle: "alice--bob.test", 316 shouldError: true, 317 errorMsg: "consecutive hyphens", 318 }, 319 { 320 name: "Invalid: starts with hyphen", 321 did: "did:plc:invalid2", 322 handle: "-alice.test", 323 shouldError: true, 324 errorMsg: "invalid handle format", 325 }, 326 { 327 name: "Invalid: ends with hyphen", 328 did: "did:plc:invalid3", 329 handle: "alice-.test", 330 shouldError: true, 331 errorMsg: "invalid handle format", 332 }, 333 { 334 name: "Invalid: special characters", 335 did: "did:plc:invalid4", 336 handle: "alice!bob.test", 337 shouldError: true, 338 errorMsg: "invalid handle format", 339 }, 340 { 341 name: "Invalid: spaces", 342 did: "did:plc:invalid5", 343 handle: "alice bob.test", 344 shouldError: true, 345 errorMsg: "invalid handle format", 346 }, 347 { 348 name: "Invalid: too long", 349 did: "did:plc:invalid6", 350 handle: strings.Repeat("a", 254) + ".test", 351 shouldError: true, 352 errorMsg: "must be between 1 and 253 characters", 353 }, 354 { 355 name: "Invalid: missing DID prefix", 356 did: "plc:invalid7", 357 handle: "valid.test", 358 shouldError: true, 359 errorMsg: "must start with 'did:'", 360 }, 361 } 362 363 for _, tc := range testCases { 364 t.Run(tc.name, func(t *testing.T) { 365 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 366 DID: tc.did, 367 Handle: tc.handle, 368 }) 369 370 if tc.shouldError { 371 if err == nil { 372 t.Errorf("Expected error, got nil") 373 } else if !strings.Contains(err.Error(), tc.errorMsg) { 374 t.Errorf("Expected error containing '%s', got: %v", tc.errorMsg, err) 375 } 376 } else { 377 if err != nil { 378 t.Errorf("Expected no error, got: %v", err) 379 } 380 } 381 }) 382 } 383}