A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "Coves/internal/api/handlers/aggregator"
5 "Coves/internal/atproto/identity"
6 "Coves/internal/core/users"
7 "Coves/internal/db/postgres"
8 "bytes"
9 "context"
10 "crypto/tls"
11 "database/sql"
12 "encoding/json"
13 "fmt"
14 "net/http"
15 "net/http/httptest"
16 "testing"
17 "time"
18
19 "github.com/stretchr/testify/assert"
20 "github.com/stretchr/testify/require"
21)
22
23// mockAggregatorIdentityResolver is a mock implementation of identity.Resolver for aggregator registration testing
24type mockAggregatorIdentityResolver struct {
25 resolveFunc func(ctx context.Context, identifier string) (*identity.Identity, error)
26 resolveHandleFunc func(ctx context.Context, handle string) (did, pdsURL string, err error)
27 resolveDIDFunc func(ctx context.Context, did string) (*identity.DIDDocument, error)
28 purgeFunc func(ctx context.Context, identifier string) error
29}
30
31func (m *mockAggregatorIdentityResolver) Resolve(ctx context.Context, identifier string) (*identity.Identity, error) {
32 if m.resolveFunc != nil {
33 return m.resolveFunc(ctx, identifier)
34 }
35 return &identity.Identity{
36 DID: identifier,
37 Handle: "test.bsky.social",
38 PDSURL: "https://bsky.social",
39 ResolvedAt: time.Now(),
40 Method: identity.MethodHTTPS,
41 }, nil
42}
43
44func (m *mockAggregatorIdentityResolver) ResolveHandle(ctx context.Context, handle string) (did, pdsURL string, err error) {
45 if m.resolveHandleFunc != nil {
46 return m.resolveHandleFunc(ctx, handle)
47 }
48 return "did:plc:test", "https://bsky.social", nil
49}
50
51func (m *mockAggregatorIdentityResolver) ResolveDID(ctx context.Context, did string) (*identity.DIDDocument, error) {
52 if m.resolveDIDFunc != nil {
53 return m.resolveDIDFunc(ctx, did)
54 }
55 return &identity.DIDDocument{DID: did}, nil
56}
57
58func (m *mockAggregatorIdentityResolver) Purge(ctx context.Context, identifier string) error {
59 if m.purgeFunc != nil {
60 return m.purgeFunc(ctx, identifier)
61 }
62 return nil
63}
64
65func TestAggregatorRegistration_Success(t *testing.T) {
66 if testing.Short() {
67 t.Skip("Skipping integration test in short mode")
68 }
69
70 // Setup test database
71 db := setupTestDB(t)
72 defer func() { _ = db.Close() }()
73
74 testDID := "did:plc:test123"
75 testHandle := "aggregator.bsky.social"
76
77 // Setup test server with .well-known endpoint
78 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
79 if r.URL.Path == "/.well-known/atproto-did" {
80 w.Header().Set("Content-Type", "text/plain")
81 _, _ = w.Write([]byte(testDID))
82 } else {
83 w.WriteHeader(http.StatusNotFound)
84 }
85 }))
86 defer wellKnownServer.Close()
87
88 // Extract domain from test server URL (remove https:// prefix)
89 domain := wellKnownServer.URL[8:] // Remove "https://"
90
91 // Create mock identity resolver
92 mockResolver := &mockAggregatorIdentityResolver{
93 resolveFunc: func(ctx context.Context, identifier string) (*identity.Identity, error) {
94 if identifier == testDID {
95 return &identity.Identity{
96 DID: testDID,
97 Handle: testHandle,
98 PDSURL: "https://bsky.social",
99 ResolvedAt: time.Now(),
100 Method: identity.MethodHTTPS,
101 }, nil
102 }
103 return nil, fmt.Errorf("DID not found")
104 },
105 }
106
107 // Create services and handler
108 userRepo := postgres.NewUserRepository(db)
109 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
110 handler := aggregator.NewRegisterHandler(userService, mockResolver)
111
112 // Create HTTP client that accepts self-signed certs for test server
113 testClient := &http.Client{
114 Transport: &http.Transport{
115 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
116 },
117 Timeout: 10 * time.Second,
118 }
119
120 // Set test client on handler for .well-known verification
121 handler.SetHTTPClient(testClient)
122
123 // Test registration request
124 reqBody := map[string]string{
125 "did": testDID,
126 "domain": domain,
127 }
128
129 reqJSON, err := json.Marshal(reqBody)
130 require.NoError(t, err)
131
132 // Create HTTP request
133 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
134 req.Header.Set("Content-Type", "application/json")
135
136 // Create response recorder
137 rr := httptest.NewRecorder()
138
139 // Call handler
140 handler.HandleRegister(rr, req)
141
142 // Assert response
143 assert.Equal(t, http.StatusOK, rr.Code, "Response body: %s", rr.Body.String())
144
145 var resp map[string]interface{}
146 err = json.Unmarshal(rr.Body.Bytes(), &resp)
147 require.NoError(t, err)
148
149 assert.Equal(t, testDID, resp["did"])
150 assert.Equal(t, testHandle, resp["handle"])
151 assert.Contains(t, resp["message"], "registered successfully")
152
153 // Verify user exists in database
154 assertUserExists(t, db, testDID)
155}
156
157func TestAggregatorRegistration_DomainVerificationFailed(t *testing.T) {
158 if testing.Short() {
159 t.Skip("Skipping integration test in short mode")
160 }
161
162 // Setup test database
163 db := setupTestDB(t)
164 defer func() { _ = db.Close() }()
165
166 // Setup test server that returns wrong DID
167 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168 if r.URL.Path == "/.well-known/atproto-did" {
169 w.Header().Set("Content-Type", "text/plain")
170 _, _ = w.Write([]byte("did:plc:wrongdid"))
171 } else {
172 w.WriteHeader(http.StatusNotFound)
173 }
174 }))
175 defer wellKnownServer.Close()
176
177 domain := wellKnownServer.URL[8:]
178
179 // Create mock identity resolver
180 mockResolver := &mockAggregatorIdentityResolver{}
181
182 // Create services and handler
183 userRepo := postgres.NewUserRepository(db)
184 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
185 handler := aggregator.NewRegisterHandler(userService, mockResolver)
186
187 // Create HTTP client that accepts self-signed certs
188 testClient := &http.Client{
189 Transport: &http.Transport{
190 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
191 },
192 Timeout: 10 * time.Second,
193 }
194 handler.SetHTTPClient(testClient)
195
196 reqBody := map[string]string{
197 "did": "did:plc:correctdid",
198 "domain": domain,
199 }
200
201 reqJSON, err := json.Marshal(reqBody)
202 require.NoError(t, err)
203
204 // Create HTTP request
205 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
206 req.Header.Set("Content-Type", "application/json")
207
208 // Create response recorder
209 rr := httptest.NewRecorder()
210
211 // Call handler
212 handler.HandleRegister(rr, req)
213
214 // Assert response
215 assert.Equal(t, http.StatusUnauthorized, rr.Code)
216
217 var errResp map[string]interface{}
218 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
219 require.NoError(t, err)
220
221 assert.Equal(t, "DomainVerificationFailed", errResp["error"])
222 assert.Contains(t, errResp["message"], "domain ownership")
223}
224
225func TestAggregatorRegistration_InvalidDID(t *testing.T) {
226 if testing.Short() {
227 t.Skip("Skipping integration test in short mode")
228 }
229
230 db := setupTestDB(t)
231 defer func() { _ = db.Close() }()
232
233 tests := []struct {
234 name string
235 did string
236 domain string
237 }{
238 {"empty DID", "", "example.com"},
239 {"invalid format", "not-a-did", "example.com"},
240 {"missing prefix", "plc:test123", "example.com"},
241 {"unsupported method", "did:key:test123", "example.com"},
242 {"empty domain", "did:plc:test123", ""},
243 {"whitespace domain", "did:plc:test123", " "},
244 {"https only", "did:plc:test123", "https://"},
245 }
246
247 for _, tt := range tests {
248 t.Run(tt.name, func(t *testing.T) {
249 // Create mock identity resolver
250 mockResolver := &mockAggregatorIdentityResolver{}
251
252 // Create services and handler
253 userRepo := postgres.NewUserRepository(db)
254 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
255 handler := aggregator.NewRegisterHandler(userService, mockResolver)
256
257 reqBody := map[string]string{
258 "did": tt.did,
259 "domain": tt.domain,
260 }
261
262 reqJSON, err := json.Marshal(reqBody)
263 require.NoError(t, err)
264
265 // Create HTTP request
266 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
267 req.Header.Set("Content-Type", "application/json")
268
269 // Create response recorder
270 rr := httptest.NewRecorder()
271
272 // Call handler
273 handler.HandleRegister(rr, req)
274
275 // Assert response
276 assert.Equal(t, http.StatusBadRequest, rr.Code, "Response body: %s", rr.Body.String())
277
278 var errResp map[string]interface{}
279 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
280 require.NoError(t, err)
281
282 assert.Equal(t, "InvalidDID", errResp["error"], "Expected InvalidDID error for: %s", tt.name)
283 })
284 }
285}
286
287func TestAggregatorRegistration_AlreadyRegistered(t *testing.T) {
288 if testing.Short() {
289 t.Skip("Skipping integration test in short mode")
290 }
291
292 db := setupTestDB(t)
293 defer func() { _ = db.Close() }()
294
295 // Pre-create user with same DID
296 existingDID := "did:plc:existing123"
297 createTestUser(t, db, "existing.bsky.social", existingDID)
298
299 // Setup test server with .well-known
300 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
301 if r.URL.Path == "/.well-known/atproto-did" {
302 w.Header().Set("Content-Type", "text/plain")
303 _, _ = w.Write([]byte(existingDID))
304 } else {
305 w.WriteHeader(http.StatusNotFound)
306 }
307 }))
308 defer wellKnownServer.Close()
309
310 domain := wellKnownServer.URL[8:]
311
312 // Create mock identity resolver
313 mockResolver := &mockAggregatorIdentityResolver{
314 resolveFunc: func(ctx context.Context, identifier string) (*identity.Identity, error) {
315 if identifier == existingDID {
316 return &identity.Identity{
317 DID: existingDID,
318 Handle: "existing.bsky.social",
319 PDSURL: "https://bsky.social",
320 ResolvedAt: time.Now(),
321 Method: identity.MethodHTTPS,
322 }, nil
323 }
324 return nil, fmt.Errorf("DID not found")
325 },
326 }
327
328 // Create services and handler
329 userRepo := postgres.NewUserRepository(db)
330 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
331 handler := aggregator.NewRegisterHandler(userService, mockResolver)
332
333 // Create HTTP client that accepts self-signed certs
334 testClient := &http.Client{
335 Transport: &http.Transport{
336 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
337 },
338 Timeout: 10 * time.Second,
339 }
340 handler.SetHTTPClient(testClient)
341
342 reqBody := map[string]string{
343 "did": existingDID,
344 "domain": domain,
345 }
346
347 reqJSON, err := json.Marshal(reqBody)
348 require.NoError(t, err)
349
350 // Create HTTP request
351 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
352 req.Header.Set("Content-Type", "application/json")
353
354 // Create response recorder
355 rr := httptest.NewRecorder()
356
357 // Call handler
358 handler.HandleRegister(rr, req)
359
360 // Assert response
361 assert.Equal(t, http.StatusConflict, rr.Code)
362
363 var errResp map[string]interface{}
364 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
365 require.NoError(t, err)
366
367 assert.Equal(t, "AlreadyRegistered", errResp["error"])
368 assert.Contains(t, errResp["message"], "already registered")
369}
370
371func TestAggregatorRegistration_WellKnownNotAccessible(t *testing.T) {
372 if testing.Short() {
373 t.Skip("Skipping integration test in short mode")
374 }
375
376 db := setupTestDB(t)
377 defer func() { _ = db.Close() }()
378
379 // Setup test server that returns 404 for .well-known
380 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
381 w.WriteHeader(http.StatusNotFound)
382 }))
383 defer wellKnownServer.Close()
384
385 domain := wellKnownServer.URL[8:]
386
387 // Create mock identity resolver
388 mockResolver := &mockAggregatorIdentityResolver{}
389
390 // Create services and handler
391 userRepo := postgres.NewUserRepository(db)
392 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
393 handler := aggregator.NewRegisterHandler(userService, mockResolver)
394
395 // Create HTTP client that accepts self-signed certs
396 testClient := &http.Client{
397 Transport: &http.Transport{
398 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
399 },
400 Timeout: 10 * time.Second,
401 }
402 handler.SetHTTPClient(testClient)
403
404 reqBody := map[string]string{
405 "did": "did:plc:test123",
406 "domain": domain,
407 }
408
409 reqJSON, err := json.Marshal(reqBody)
410 require.NoError(t, err)
411
412 // Create HTTP request
413 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
414 req.Header.Set("Content-Type", "application/json")
415
416 // Create response recorder
417 rr := httptest.NewRecorder()
418
419 // Call handler
420 handler.HandleRegister(rr, req)
421
422 // Assert response
423 assert.Equal(t, http.StatusUnauthorized, rr.Code)
424
425 var errResp map[string]interface{}
426 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
427 require.NoError(t, err)
428
429 assert.Equal(t, "DomainVerificationFailed", errResp["error"])
430 assert.Contains(t, errResp["message"], "domain ownership")
431}
432
433func TestAggregatorRegistration_WellKnownTooLarge(t *testing.T) {
434 if testing.Short() {
435 t.Skip("Skipping integration test in short mode")
436 }
437
438 db := setupTestDB(t)
439 defer func() { _ = db.Close() }()
440
441 testDID := "did:plc:toolarge"
442
443 // Setup test server that streams a very large .well-known response
444 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
445 if r.URL.Path == "/.well-known/atproto-did" {
446 w.Header().Set("Content-Type", "text/plain")
447 if _, err := w.Write(bytes.Repeat([]byte("A"), 10*1024)); err != nil {
448 t.Fatalf("Failed to write fake response: %v", err)
449 }
450 return
451 }
452 w.WriteHeader(http.StatusNotFound)
453 }))
454 defer wellKnownServer.Close()
455
456 domain := wellKnownServer.URL[8:]
457
458 mockResolver := &mockAggregatorIdentityResolver{}
459
460 userRepo := postgres.NewUserRepository(db)
461 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
462 handler := aggregator.NewRegisterHandler(userService, mockResolver)
463
464 testClient := &http.Client{
465 Transport: &http.Transport{
466 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
467 },
468 Timeout: 10 * time.Second,
469 }
470 handler.SetHTTPClient(testClient)
471
472 reqBody := map[string]string{
473 "did": testDID,
474 "domain": domain,
475 }
476
477 reqJSON, err := json.Marshal(reqBody)
478 require.NoError(t, err)
479
480 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
481 req.Header.Set("Content-Type", "application/json")
482
483 rr := httptest.NewRecorder()
484 handler.HandleRegister(rr, req)
485
486 assert.Equal(t, http.StatusUnauthorized, rr.Code, "Response body: %s", rr.Body.String())
487
488 var errResp map[string]interface{}
489 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
490 require.NoError(t, err)
491
492 assert.Equal(t, "DomainVerificationFailed", errResp["error"])
493 assert.Contains(t, errResp["message"], "domain ownership")
494
495 assertUserDoesNotExist(t, db, testDID)
496}
497
498func TestAggregatorRegistration_DIDResolutionFailed(t *testing.T) {
499 if testing.Short() {
500 t.Skip("Skipping integration test in short mode")
501 }
502
503 db := setupTestDB(t)
504 defer func() { _ = db.Close() }()
505
506 testDID := "did:plc:nonexistent"
507
508 // Setup test server with .well-known
509 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
510 if r.URL.Path == "/.well-known/atproto-did" {
511 w.Header().Set("Content-Type", "text/plain")
512 _, _ = w.Write([]byte(testDID))
513 } else {
514 w.WriteHeader(http.StatusNotFound)
515 }
516 }))
517 defer wellKnownServer.Close()
518
519 domain := wellKnownServer.URL[8:]
520
521 // Create mock identity resolver that fails for this DID
522 mockResolver := &mockAggregatorIdentityResolver{
523 resolveFunc: func(ctx context.Context, identifier string) (*identity.Identity, error) {
524 return nil, fmt.Errorf("DID not found in PLC directory")
525 },
526 }
527
528 // Create services and handler
529 userRepo := postgres.NewUserRepository(db)
530 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
531 handler := aggregator.NewRegisterHandler(userService, mockResolver)
532
533 // Create HTTP client that accepts self-signed certs
534 testClient := &http.Client{
535 Transport: &http.Transport{
536 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
537 },
538 Timeout: 10 * time.Second,
539 }
540 handler.SetHTTPClient(testClient)
541
542 reqBody := map[string]string{
543 "did": testDID,
544 "domain": domain,
545 }
546
547 reqJSON, err := json.Marshal(reqBody)
548 require.NoError(t, err)
549
550 // Create HTTP request
551 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
552 req.Header.Set("Content-Type", "application/json")
553
554 // Create response recorder
555 rr := httptest.NewRecorder()
556
557 // Call handler
558 handler.HandleRegister(rr, req)
559
560 // Assert response
561 assert.Equal(t, http.StatusBadRequest, rr.Code)
562
563 var errResp map[string]interface{}
564 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
565 require.NoError(t, err)
566
567 assert.Equal(t, "DIDResolutionFailed", errResp["error"])
568 assert.Contains(t, errResp["message"], "resolve DID")
569
570 // Verify user was NOT created in database
571 assertUserDoesNotExist(t, db, testDID)
572}
573
574func TestAggregatorRegistration_LargeWellKnownResponse(t *testing.T) {
575 if testing.Short() {
576 t.Skip("Skipping integration test in short mode")
577 }
578
579 db := setupTestDB(t)
580 defer func() { _ = db.Close() }()
581
582 testDID := "did:plc:largedos123"
583
584 // Setup server that streams a large response to attempt DoS
585 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
586 if r.URL.Path == "/.well-known/atproto-did" {
587 w.Header().Set("Content-Type", "text/plain")
588 // Attempt to stream 10MB of data (should be capped at 1KB by io.LimitReader)
589 // This simulates a malicious server trying to DoS the AppView
590 for i := 0; i < 10*1024*1024; i++ {
591 if _, err := w.Write([]byte("A")); err != nil {
592 // Client disconnected (expected when limit is reached)
593 return
594 }
595 }
596 } else {
597 w.WriteHeader(http.StatusNotFound)
598 }
599 }))
600 defer wellKnownServer.Close()
601
602 domain := wellKnownServer.URL[8:]
603
604 // Create mock identity resolver
605 mockResolver := &mockAggregatorIdentityResolver{}
606
607 // Create services and handler
608 userRepo := postgres.NewUserRepository(db)
609 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
610 handler := aggregator.NewRegisterHandler(userService, mockResolver)
611
612 // Create HTTP client that accepts self-signed certs
613 testClient := &http.Client{
614 Transport: &http.Transport{
615 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
616 },
617 Timeout: 10 * time.Second,
618 }
619 handler.SetHTTPClient(testClient)
620
621 reqBody := map[string]string{
622 "did": testDID,
623 "domain": domain,
624 }
625
626 reqJSON, err := json.Marshal(reqBody)
627 require.NoError(t, err)
628
629 // Create HTTP request
630 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
631 req.Header.Set("Content-Type", "application/json")
632
633 // Create response recorder
634 rr := httptest.NewRecorder()
635
636 // Record start time to ensure the test completes quickly
637 startTime := time.Now()
638
639 // Call handler - should fail gracefully, not hang or DoS
640 handler.HandleRegister(rr, req)
641
642 elapsed := time.Since(startTime)
643
644 // Assert the handler completed quickly (not trying to read 10MB)
645 // Should complete in well under 1 second. Using 5 seconds as generous upper bound.
646 assert.Less(t, elapsed, 5*time.Second, "Handler should complete quickly even with large response")
647
648 // Should fail with domain verification error (DID mismatch: got "AAAA..." instead of expected DID)
649 assert.Equal(t, http.StatusUnauthorized, rr.Code, "Should reject due to DID mismatch")
650
651 var errResp map[string]interface{}
652 err = json.Unmarshal(rr.Body.Bytes(), &errResp)
653 require.NoError(t, err)
654
655 assert.Equal(t, "DomainVerificationFailed", errResp["error"])
656 assert.Contains(t, errResp["message"], "domain ownership")
657
658 // Verify user was NOT created
659 assertUserDoesNotExist(t, db, testDID)
660
661 t.Logf("✓ DoS protection test completed in %v (prevented reading 10MB payload)", elapsed)
662}
663
664func TestAggregatorRegistration_E2E_WithRealInfrastructure(t *testing.T) {
665 if testing.Short() {
666 t.Skip("Skipping E2E test in short mode")
667 }
668
669 // This test requires docker-compose infrastructure to be running:
670 // docker-compose -f docker-compose.dev.yml --profile test up postgres-test
671 //
672 // This is a TRUE E2E test that validates the full registration flow
673 // with real .well-known server and real identity resolution
674
675 db := setupTestDB(t)
676 defer func() { _ = db.Close() }()
677
678 testDID := "did:plc:e2etest123"
679 testHandle := "e2ebot.bsky.social"
680 testPDSURL := "https://bsky.social"
681
682 // Setup .well-known server (simulates aggregator's domain)
683 wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
684 if r.URL.Path == "/.well-known/atproto-did" {
685 w.Header().Set("Content-Type", "text/plain")
686 _, _ = w.Write([]byte(testDID))
687 } else {
688 w.WriteHeader(http.StatusNotFound)
689 }
690 }))
691 defer wellKnownServer.Close()
692
693 domain := wellKnownServer.URL[8:] // Remove "https://"
694
695 // Create mock identity resolver (for E2E, this simulates PLC directory response)
696 mockResolver := &mockAggregatorIdentityResolver{
697 resolveFunc: func(ctx context.Context, identifier string) (*identity.Identity, error) {
698 if identifier == testDID {
699 return &identity.Identity{
700 DID: testDID,
701 Handle: testHandle,
702 PDSURL: testPDSURL,
703 ResolvedAt: time.Now(),
704 Method: identity.MethodHTTPS,
705 }, nil
706 }
707 return nil, fmt.Errorf("DID not found")
708 },
709 }
710
711 // Create services and handler
712 userRepo := postgres.NewUserRepository(db)
713 userService := users.NewUserService(userRepo, mockResolver, "https://bsky.social")
714 handler := aggregator.NewRegisterHandler(userService, mockResolver)
715
716 // Create HTTP client for self-signed test server certs
717 testClient := &http.Client{
718 Transport: &http.Transport{
719 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
720 },
721 Timeout: 10 * time.Second,
722 }
723 handler.SetHTTPClient(testClient)
724
725 // Build registration request
726 reqBody := map[string]string{
727 "did": testDID,
728 "domain": domain,
729 }
730 reqJSON, err := json.Marshal(reqBody)
731 require.NoError(t, err)
732
733 // Create HTTP request
734 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.aggregator.register", bytes.NewBuffer(reqJSON))
735 req.Header.Set("Content-Type", "application/json")
736
737 // Create response recorder
738 rr := httptest.NewRecorder()
739
740 // Execute registration
741 handler.HandleRegister(rr, req)
742
743 // Assert HTTP 200 response
744 assert.Equal(t, http.StatusOK, rr.Code, "Response body: %s", rr.Body.String())
745
746 // Parse response
747 var resp map[string]interface{}
748 err = json.Unmarshal(rr.Body.Bytes(), &resp)
749 require.NoError(t, err)
750
751 // Assert response contains correct data
752 assert.Equal(t, testDID, resp["did"], "DID should match request")
753 assert.Equal(t, testHandle, resp["handle"], "Handle should be resolved from DID")
754 assert.Contains(t, resp["message"], "registered successfully", "Success message should be present")
755 assert.Contains(t, resp["message"], "service declaration", "Message should mention next steps")
756
757 // Verify user was created in database
758 user := assertUserExists(t, db, testDID)
759 assert.Equal(t, testHandle, user.Handle, "User handle should match resolved identity")
760 assert.Equal(t, testPDSURL, user.PDSURL, "User PDS URL should match resolved identity")
761
762 t.Logf("✓ E2E test completed successfully")
763 t.Logf(" DID: %s", testDID)
764 t.Logf(" Handle: %s", testHandle)
765 t.Logf(" Domain: %s", domain)
766}
767
768// Helper to verify user exists in database
769func assertUserExists(t *testing.T, db *sql.DB, did string) *users.User {
770 t.Helper()
771
772 var user users.User
773 err := db.QueryRow(`
774 SELECT did, handle, pds_url
775 FROM users
776 WHERE did = $1
777 `, did).Scan(&user.DID, &user.Handle, &user.PDSURL)
778
779 require.NoError(t, err, "User should exist in database")
780 return &user
781}
782
783// Helper to verify user does not exist
784func assertUserDoesNotExist(t *testing.T, db *sql.DB, did string) {
785 t.Helper()
786
787 var count int
788 err := db.QueryRow("SELECT COUNT(*) FROM users WHERE did = $1", did).Scan(&count)
789 require.NoError(t, err)
790 assert.Equal(t, 0, count, "User should not exist in database")
791}
792
793// TODO: Implement full E2E tests with actual HTTP server and handler
794// This requires:
795// 1. Setting up test HTTP server with all routes
796// 2. Mocking the identity resolver to avoid external calls
797// 3. Setting up test database
798// 4. Making actual HTTP requests and asserting responses
799//
800// For now, these tests serve as placeholders and documentation
801// of the expected behavior.