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.