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}