A community based topic aggregation platform built on atproto
1package integration 2 3import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/atproto/identity" 6 "Coves/internal/atproto/jetstream" 7 "Coves/internal/core/communities" 8 "Coves/internal/core/posts" 9 "Coves/internal/core/unfurl" 10 "Coves/internal/core/users" 11 "Coves/internal/db/postgres" 12 "context" 13 "encoding/json" 14 "fmt" 15 "testing" 16 "time" 17 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20) 21 22// TestPostUnfurl_Streamable tests that a post with a Streamable URL gets unfurled 23func TestPostUnfurl_Streamable(t *testing.T) { 24 if testing.Short() { 25 t.Skip("Skipping integration test in short mode") 26 } 27 28 db := setupTestDB(t) 29 defer func() { 30 if err := db.Close(); err != nil { 31 t.Logf("Failed to close database: %v", err) 32 } 33 }() 34 35 ctx := context.Background() 36 37 // Setup repositories and services 38 userRepo := postgres.NewUserRepository(db) 39 communityRepo := postgres.NewCommunityRepository(db) 40 postRepo := postgres.NewPostRepository(db) 41 unfurlRepo := unfurl.NewRepository(db) 42 43 // Setup identity resolver and services 44 identityConfig := identity.DefaultConfig() 45 identityResolver := identity.NewResolver(db, identityConfig) 46 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 47 48 // Setup unfurl service with real oEmbed endpoints 49 unfurlService := unfurl.NewService(unfurlRepo, 50 unfurl.WithTimeout(30*time.Second), // Generous timeout for real network calls 51 unfurl.WithCacheTTL(24*time.Hour), 52 ) 53 54 communityService := communities.NewCommunityService( 55 communityRepo, 56 "http://localhost:3001", 57 "did:web:test.coves.social", 58 "test.coves.social", 59 nil, 60 ) 61 62 postService := posts.NewPostService( 63 postRepo, 64 communityService, 65 nil, // aggregatorService not needed 66 nil, // blobService not needed 67 unfurlService, 68 "http://localhost:3001", 69 ) 70 71 // Cleanup old test data 72 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'") 73 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'") 74 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'") 75 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%streamable.com%'") 76 77 // Create test user 78 testUserDID := generateTestDID("unfurlauthor") 79 testUserHandle := "unfurlauthor.test" 80 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 81 DID: testUserDID, 82 Handle: testUserHandle, 83 PDSURL: "http://localhost:3001", 84 }) 85 require.NoError(t, err, "Failed to create test user") 86 87 // Create test community 88 testCommunity := &communities.Community{ 89 DID: generateTestDID("unfurlcommunity"), 90 Handle: "unfurlcommunity.community.test.coves.social", 91 Name: "unfurlcommunity", 92 DisplayName: "Unfurl Test Community", 93 Description: "A community for testing unfurl", 94 Visibility: "public", 95 CreatedByDID: testUserDID, 96 HostedByDID: "did:web:test.coves.social", 97 PDSURL: "http://localhost:3001", 98 PDSAccessToken: "fake_token_for_test", 99 PDSRefreshToken: "fake_refresh_token", 100 } 101 _, err = communityRepo.Create(ctx, testCommunity) 102 require.NoError(t, err, "Failed to create test community") 103 104 // Test unfurling a Streamable URL 105 streamableURL := "https://streamable.com/7kpdft" 106 title := "Streamable Test Post" 107 content := "Testing Streamable unfurl" 108 109 // Create post with external embed containing only URI 110 createReq := posts.CreatePostRequest{ 111 Community: testCommunity.DID, 112 Title: &title, 113 Content: &content, 114 Embed: map[string]interface{}{ 115 "$type": "social.coves.embed.external", 116 "external": map[string]interface{}{ 117 "uri": streamableURL, 118 }, 119 }, 120 AuthorDID: testUserDID, 121 } 122 123 // Set auth context 124 authCtx := middleware.SetTestUserDID(ctx, testUserDID) 125 126 // Note: This will fail at token refresh, but that's expected for this test 127 // We're testing the unfurl logic, not the full PDS write flow 128 _, err = postService.CreatePost(authCtx, createReq) 129 130 // Expect error at token refresh stage 131 require.Error(t, err, "Expected error due to fake token") 132 assert.Contains(t, err.Error(), "failed to refresh community credentials") 133 134 // However, the unfurl should have been triggered and cached 135 // Let's verify the cache was populated 136 t.Run("Verify unfurl was cached", func(t *testing.T) { 137 // Wait briefly for any async unfurl to complete 138 time.Sleep(1 * time.Second) 139 140 // Check if the URL was cached 141 cached, err := unfurlRepo.Get(ctx, streamableURL) 142 if err != nil { 143 t.Logf("Cache lookup failed: %v", err) 144 t.Skip("Skipping cache verification - unfurl may have failed due to network") 145 return 146 } 147 148 if cached == nil { 149 t.Skip("Unfurl result not cached - may have failed due to network issues") 150 return 151 } 152 153 // Verify unfurl metadata 154 assert.NotEmpty(t, cached.Title, "Expected title from unfurl") 155 assert.Equal(t, "video", cached.Type, "Expected embedType to be video") 156 assert.Equal(t, "streamable", cached.Provider, "Expected provider to be streamable") 157 assert.Equal(t, "streamable.com", cached.Domain, "Expected domain to be streamable.com") 158 159 t.Logf("✓ Unfurl successful:") 160 t.Logf(" Title: %s", cached.Title) 161 t.Logf(" Type: %s", cached.Type) 162 t.Logf(" Provider: %s", cached.Provider) 163 t.Logf(" Description: %s", cached.Description) 164 }) 165} 166 167// TestPostUnfurl_YouTube tests that a post with a YouTube URL gets unfurled 168func TestPostUnfurl_YouTube(t *testing.T) { 169 if testing.Short() { 170 t.Skip("Skipping integration test in short mode") 171 } 172 173 db := setupTestDB(t) 174 defer func() { 175 if err := db.Close(); err != nil { 176 t.Logf("Failed to close database: %v", err) 177 } 178 }() 179 180 ctx := context.Background() 181 182 // Setup unfurl repository and service 183 unfurlRepo := unfurl.NewRepository(db) 184 unfurlService := unfurl.NewService(unfurlRepo, 185 unfurl.WithTimeout(30*time.Second), 186 unfurl.WithCacheTTL(24*time.Hour), 187 ) 188 189 // Cleanup cache 190 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%'") 191 192 // Test YouTube URL 193 youtubeURL := "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 194 195 // Attempt unfurl 196 result, err := unfurlService.UnfurlURL(ctx, youtubeURL) 197 if err != nil { 198 t.Logf("Unfurl failed (may be network issue): %v", err) 199 t.Skip("Skipping test - YouTube unfurl failed") 200 return 201 } 202 203 require.NotNil(t, result, "Expected unfurl result") 204 assert.Equal(t, "video", result.Type, "Expected embedType to be video") 205 assert.Equal(t, "youtube", result.Provider, "Expected provider to be youtube") 206 assert.NotEmpty(t, result.Title, "Expected title from YouTube") 207 208 t.Logf("✓ YouTube unfurl successful:") 209 t.Logf(" Title: %s", result.Title) 210 t.Logf(" Type: %s", result.Type) 211 t.Logf(" Provider: %s", result.Provider) 212} 213 214// TestPostUnfurl_Reddit tests that a post with a Reddit URL gets unfurled 215func TestPostUnfurl_Reddit(t *testing.T) { 216 if testing.Short() { 217 t.Skip("Skipping integration test in short mode") 218 } 219 220 db := setupTestDB(t) 221 defer func() { 222 if err := db.Close(); err != nil { 223 t.Logf("Failed to close database: %v", err) 224 } 225 }() 226 227 ctx := context.Background() 228 229 // Setup unfurl repository and service 230 unfurlRepo := unfurl.NewRepository(db) 231 unfurlService := unfurl.NewService(unfurlRepo, 232 unfurl.WithTimeout(30*time.Second), 233 unfurl.WithCacheTTL(24*time.Hour), 234 ) 235 236 // Cleanup cache 237 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%reddit.com%'") 238 239 // Use a well-known public Reddit post 240 redditURL := "https://www.reddit.com/r/programming/comments/1234/test/" 241 242 // Attempt unfurl 243 result, err := unfurlService.UnfurlURL(ctx, redditURL) 244 if err != nil { 245 t.Logf("Unfurl failed (may be network issue or invalid URL): %v", err) 246 t.Skip("Skipping test - Reddit unfurl failed") 247 return 248 } 249 250 require.NotNil(t, result, "Expected unfurl result") 251 assert.Equal(t, "reddit", result.Provider, "Expected provider to be reddit") 252 assert.NotEmpty(t, result.Domain, "Expected domain to be set") 253 254 t.Logf("✓ Reddit unfurl successful:") 255 t.Logf(" Title: %s", result.Title) 256 t.Logf(" Type: %s", result.Type) 257 t.Logf(" Provider: %s", result.Provider) 258} 259 260// TestPostUnfurl_CacheHit tests that the second post with the same URL uses cache 261func TestPostUnfurl_CacheHit(t *testing.T) { 262 if testing.Short() { 263 t.Skip("Skipping integration test in short mode") 264 } 265 266 db := setupTestDB(t) 267 defer func() { 268 if err := db.Close(); err != nil { 269 t.Logf("Failed to close database: %v", err) 270 } 271 }() 272 273 ctx := context.Background() 274 275 // Setup unfurl repository and service 276 unfurlRepo := unfurl.NewRepository(db) 277 unfurlService := unfurl.NewService(unfurlRepo, 278 unfurl.WithTimeout(30*time.Second), 279 unfurl.WithCacheTTL(24*time.Hour), 280 ) 281 282 // Cleanup cache 283 testURL := "https://streamable.com/test123" 284 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url = $1", testURL) 285 286 // First unfurl - should hit network 287 t.Log("First unfurl - expecting cache miss") 288 result1, err1 := unfurlService.UnfurlURL(ctx, testURL) 289 if err1 != nil { 290 t.Logf("First unfurl failed (may be network issue): %v", err1) 291 t.Skip("Skipping test - network unfurl failed") 292 return 293 } 294 295 require.NotNil(t, result1, "Expected first unfurl result") 296 297 // Second unfurl - should hit cache 298 t.Log("Second unfurl - expecting cache hit") 299 start := time.Now() 300 result2, err2 := unfurlService.UnfurlURL(ctx, testURL) 301 elapsed := time.Since(start) 302 303 require.NoError(t, err2, "Second unfurl should not fail") 304 require.NotNil(t, result2, "Expected second unfurl result") 305 306 // Cache hit should be much faster (< 100ms) 307 assert.Less(t, elapsed.Milliseconds(), int64(100), "Cache hit should be fast") 308 309 // Results should be identical 310 assert.Equal(t, result1.Title, result2.Title, "Cached result should match") 311 assert.Equal(t, result1.Provider, result2.Provider, "Cached provider should match") 312 assert.Equal(t, result1.Type, result2.Type, "Cached type should match") 313 314 // Verify only one entry in cache 315 var count int 316 err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM unfurl_cache WHERE url = $1", testURL).Scan(&count) 317 require.NoError(t, err, "Failed to count cache entries") 318 assert.Equal(t, 1, count, "Should have exactly one cache entry") 319 320 t.Logf("✓ Cache test passed:") 321 t.Logf(" First unfurl: network call") 322 t.Logf(" Second unfurl: cache hit (took %dms)", elapsed.Milliseconds()) 323 t.Logf(" Cache entries: %d", count) 324} 325 326// TestPostUnfurl_UnsupportedURL tests that posts with unsupported URLs still succeed 327func TestPostUnfurl_UnsupportedURL(t *testing.T) { 328 if testing.Short() { 329 t.Skip("Skipping integration test in short mode") 330 } 331 332 db := setupTestDB(t) 333 defer func() { 334 if err := db.Close(); err != nil { 335 t.Logf("Failed to close database: %v", err) 336 } 337 }() 338 339 ctx := context.Background() 340 341 // Setup services 342 userRepo := postgres.NewUserRepository(db) 343 communityRepo := postgres.NewCommunityRepository(db) 344 postRepo := postgres.NewPostRepository(db) 345 346 identityConfig := identity.DefaultConfig() 347 identityResolver := identity.NewResolver(db, identityConfig) 348 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 349 350 communityService := communities.NewCommunityService( 351 communityRepo, 352 "http://localhost:3001", 353 "did:web:test.coves.social", 354 "test.coves.social", 355 nil, 356 ) 357 358 // Create post service WITHOUT unfurl service 359 postService := posts.NewPostService( 360 postRepo, 361 communityService, 362 nil, // aggregatorService 363 nil, // blobService 364 nil, // unfurlService - intentionally nil to test graceful handling 365 "http://localhost:3001", 366 ) 367 368 // Cleanup 369 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:unsupported%'") 370 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:unsupported%'") 371 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:unsupported%'") 372 373 // Create test user 374 testUserDID := generateTestDID("unsupporteduser") 375 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 376 DID: testUserDID, 377 Handle: "unsupporteduser.test", 378 PDSURL: "http://localhost:3001", 379 }) 380 require.NoError(t, err) 381 382 // Create test community 383 testCommunity := &communities.Community{ 384 DID: generateTestDID("unsupportedcommunity"), 385 Handle: "unsupportedcommunity.community.test.coves.social", 386 Name: "unsupportedcommunity", 387 DisplayName: "Unsupported URL Test", 388 Visibility: "public", 389 CreatedByDID: testUserDID, 390 HostedByDID: "did:web:test.coves.social", 391 PDSURL: "http://localhost:3001", 392 PDSAccessToken: "fake_token", 393 PDSRefreshToken: "fake_refresh", 394 } 395 _, err = communityRepo.Create(ctx, testCommunity) 396 require.NoError(t, err) 397 398 // Create post with unsupported URL 399 unsupportedURL := "https://example.com/article/123" 400 title := "Unsupported URL Test" 401 content := "Testing unsupported domain" 402 403 createReq := posts.CreatePostRequest{ 404 Community: testCommunity.DID, 405 Title: &title, 406 Content: &content, 407 Embed: map[string]interface{}{ 408 "$type": "social.coves.embed.external", 409 "external": map[string]interface{}{ 410 "uri": unsupportedURL, 411 }, 412 }, 413 AuthorDID: testUserDID, 414 } 415 416 authCtx := middleware.SetTestUserDID(ctx, testUserDID) 417 _, err = postService.CreatePost(authCtx, createReq) 418 419 // Should still fail at token refresh (expected) 420 require.Error(t, err, "Expected error at token refresh") 421 assert.Contains(t, err.Error(), "failed to refresh community credentials") 422 423 // The point is that it didn't fail earlier due to unsupported URL 424 t.Log("✓ Post creation with unsupported URL proceeded to PDS write stage") 425} 426 427// TestPostUnfurl_UserProvidedMetadata tests that user-provided metadata is preserved 428func TestPostUnfurl_UserProvidedMetadata(t *testing.T) { 429 if testing.Short() { 430 t.Skip("Skipping integration test in short mode") 431 } 432 433 db := setupTestDB(t) 434 defer func() { 435 if err := db.Close(); err != nil { 436 t.Logf("Failed to close database: %v", err) 437 } 438 }() 439 440 ctx := context.Background() 441 442 // Setup 443 userRepo := postgres.NewUserRepository(db) 444 communityRepo := postgres.NewCommunityRepository(db) 445 postRepo := postgres.NewPostRepository(db) 446 unfurlRepo := unfurl.NewRepository(db) 447 448 identityConfig := identity.DefaultConfig() 449 identityResolver := identity.NewResolver(db, identityConfig) 450 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 451 452 unfurlService := unfurl.NewService(unfurlRepo, 453 unfurl.WithTimeout(30*time.Second), 454 unfurl.WithCacheTTL(24*time.Hour), 455 ) 456 457 communityService := communities.NewCommunityService( 458 communityRepo, 459 "http://localhost:3001", 460 "did:web:test.coves.social", 461 "test.coves.social", 462 nil, 463 ) 464 465 postService := posts.NewPostService( 466 postRepo, 467 communityService, 468 nil, 469 nil, 470 unfurlService, 471 "http://localhost:3001", 472 ) 473 474 // Cleanup 475 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:metadata%'") 476 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:metadata%'") 477 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:metadata%'") 478 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%streamable.com%'") 479 480 // Create test user and community 481 testUserDID := generateTestDID("metadatauser") 482 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 483 DID: testUserDID, 484 Handle: "metadatauser.test", 485 PDSURL: "http://localhost:3001", 486 }) 487 require.NoError(t, err) 488 489 testCommunity := &communities.Community{ 490 DID: generateTestDID("metadatacommunity"), 491 Handle: "metadatacommunity.community.test.coves.social", 492 Name: "metadatacommunity", 493 DisplayName: "Metadata Test", 494 Visibility: "public", 495 CreatedByDID: testUserDID, 496 HostedByDID: "did:web:test.coves.social", 497 PDSURL: "http://localhost:3001", 498 PDSAccessToken: "fake_token", 499 PDSRefreshToken: "fake_refresh", 500 } 501 _, err = communityRepo.Create(ctx, testCommunity) 502 require.NoError(t, err) 503 504 // Create post with user-provided metadata 505 streamableURL := "https://streamable.com/abc123" 506 customTitle := "My Custom Title" 507 customDescription := "My Custom Description" 508 title := "Metadata Test Post" 509 content := "Testing metadata preservation" 510 511 createReq := posts.CreatePostRequest{ 512 Community: testCommunity.DID, 513 Title: &title, 514 Content: &content, 515 Embed: map[string]interface{}{ 516 "$type": "social.coves.embed.external", 517 "external": map[string]interface{}{ 518 "uri": streamableURL, 519 "title": customTitle, 520 "description": customDescription, 521 }, 522 }, 523 AuthorDID: testUserDID, 524 } 525 526 authCtx := middleware.SetTestUserDID(ctx, testUserDID) 527 _, err = postService.CreatePost(authCtx, createReq) 528 529 // Expected to fail at token refresh 530 require.Error(t, err) 531 532 // The important check: verify unfurl happened but didn't overwrite user data 533 // In the real flow, this would be checked by examining the record written to PDS 534 // For this test, we just verify the unfurl logic respects user-provided data 535 t.Log("✓ User-provided metadata should be preserved during unfurl enhancement") 536 t.Log(" (Full verification requires E2E test with real PDS)") 537} 538 539// TestPostUnfurl_MissingEmbedType tests posts without external embed type don't trigger unfurling 540func TestPostUnfurl_MissingEmbedType(t *testing.T) { 541 if testing.Short() { 542 t.Skip("Skipping integration test in short mode") 543 } 544 545 db := setupTestDB(t) 546 defer func() { 547 if err := db.Close(); err != nil { 548 t.Logf("Failed to close database: %v", err) 549 } 550 }() 551 552 ctx := context.Background() 553 554 // Setup 555 userRepo := postgres.NewUserRepository(db) 556 communityRepo := postgres.NewCommunityRepository(db) 557 postRepo := postgres.NewPostRepository(db) 558 unfurlRepo := unfurl.NewRepository(db) 559 560 identityConfig := identity.DefaultConfig() 561 identityResolver := identity.NewResolver(db, identityConfig) 562 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 563 564 unfurlService := unfurl.NewService(unfurlRepo, 565 unfurl.WithTimeout(30*time.Second), 566 ) 567 568 communityService := communities.NewCommunityService( 569 communityRepo, 570 "http://localhost:3001", 571 "did:web:test.coves.social", 572 "test.coves.social", 573 nil, 574 ) 575 576 postService := posts.NewPostService( 577 postRepo, 578 communityService, 579 nil, 580 nil, 581 unfurlService, 582 "http://localhost:3001", 583 ) 584 585 // Cleanup 586 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:noembed%'") 587 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:noembed%'") 588 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:noembed%'") 589 590 // Create test user and community 591 testUserDID := generateTestDID("noembeduser") 592 _, err := userService.CreateUser(ctx, users.CreateUserRequest{ 593 DID: testUserDID, 594 Handle: "noembeduser.test", 595 PDSURL: "http://localhost:3001", 596 }) 597 require.NoError(t, err) 598 599 testCommunity := &communities.Community{ 600 DID: generateTestDID("noembedcommunity"), 601 Handle: "noembedcommunity.community.test.coves.social", 602 Name: "noembedcommunity", 603 DisplayName: "No Embed Test", 604 Visibility: "public", 605 CreatedByDID: testUserDID, 606 HostedByDID: "did:web:test.coves.social", 607 PDSURL: "http://localhost:3001", 608 PDSAccessToken: "fake_token", 609 PDSRefreshToken: "fake_refresh", 610 } 611 _, err = communityRepo.Create(ctx, testCommunity) 612 require.NoError(t, err) 613 614 // Test 1: Post with no embed 615 t.Run("Post with no embed", func(t *testing.T) { 616 title := "No Embed Post" 617 content := "Just text content" 618 619 createReq := posts.CreatePostRequest{ 620 Community: testCommunity.DID, 621 Title: &title, 622 Content: &content, 623 AuthorDID: testUserDID, 624 } 625 626 authCtx := middleware.SetTestUserDID(ctx, testUserDID) 627 _, err := postService.CreatePost(authCtx, createReq) 628 629 // Should fail at token refresh (expected) 630 require.Error(t, err) 631 assert.Contains(t, err.Error(), "failed to refresh community credentials") 632 633 t.Log("✓ Post without embed succeeded (no unfurl attempted)") 634 }) 635 636 // Test 2: Post with images embed (different type) 637 t.Run("Post with images embed", func(t *testing.T) { 638 title := "Images Post" 639 content := "Post with images" 640 641 createReq := posts.CreatePostRequest{ 642 Community: testCommunity.DID, 643 Title: &title, 644 Content: &content, 645 Embed: map[string]interface{}{ 646 "$type": "social.coves.embed.images", 647 "images": []interface{}{ 648 map[string]interface{}{ 649 "image": map[string]interface{}{ 650 "ref": "bafytest123", 651 }, 652 "alt": "Test image", 653 }, 654 }, 655 }, 656 AuthorDID: testUserDID, 657 } 658 659 authCtx := middleware.SetTestUserDID(ctx, testUserDID) 660 _, err := postService.CreatePost(authCtx, createReq) 661 662 // Should fail at token refresh (expected) 663 require.Error(t, err) 664 assert.Contains(t, err.Error(), "failed to refresh community credentials") 665 666 t.Log("✓ Post with images embed succeeded (no unfurl attempted)") 667 }) 668} 669 670// TestPostUnfurl_OpenGraph tests that OpenGraph URLs get unfurled 671func TestPostUnfurl_OpenGraph(t *testing.T) { 672 if testing.Short() { 673 t.Skip("Skipping integration test in short mode") 674 } 675 676 db := setupTestDB(t) 677 defer func() { 678 if err := db.Close(); err != nil { 679 t.Logf("Failed to close database: %v", err) 680 } 681 }() 682 683 ctx := context.Background() 684 685 // Setup unfurl repository and service 686 unfurlRepo := unfurl.NewRepository(db) 687 unfurlService := unfurl.NewService(unfurlRepo, 688 unfurl.WithTimeout(30*time.Second), 689 unfurl.WithCacheTTL(24*time.Hour), 690 ) 691 692 // Test with a real website that has OpenGraph tags 693 // Using example.com as it's always available, though it may not have OG tags 694 testURL := "https://www.wikipedia.org/" 695 696 // Check if URL is supported 697 assert.True(t, unfurlService.IsSupported(testURL), "Wikipedia URL should be supported") 698 699 // Attempt unfurl 700 result, err := unfurlService.UnfurlURL(ctx, testURL) 701 if err != nil { 702 t.Logf("Unfurl failed (may be network issue): %v", err) 703 t.Skip("Skipping test - OpenGraph unfurl failed") 704 return 705 } 706 707 require.NotNil(t, result, "Expected unfurl result") 708 assert.Equal(t, "article", result.Type, "Expected type to be article for OpenGraph") 709 assert.Equal(t, "opengraph", result.Provider, "Expected provider to be opengraph") 710 assert.NotEmpty(t, result.Domain, "Expected domain to be set") 711 712 t.Logf("✓ OpenGraph unfurl successful:") 713 t.Logf(" Title: %s", result.Title) 714 t.Logf(" Type: %s", result.Type) 715 t.Logf(" Provider: %s", result.Provider) 716 t.Logf(" Domain: %s", result.Domain) 717 if result.Description != "" { 718 t.Logf(" Description: %s", result.Description) 719 } 720 if result.ThumbnailURL != "" { 721 t.Logf(" Thumbnail: %s", result.ThumbnailURL) 722 } 723} 724 725// TestPostUnfurl_KagiURL tests that Kagi links work with OpenGraph 726func TestPostUnfurl_KagiURL(t *testing.T) { 727 if testing.Short() { 728 t.Skip("Skipping integration test in short mode") 729 } 730 731 db := setupTestDB(t) 732 defer func() { 733 if err := db.Close(); err != nil { 734 t.Logf("Failed to close database: %v", err) 735 } 736 }() 737 738 ctx := context.Background() 739 740 // Setup unfurl repository and service 741 unfurlRepo := unfurl.NewRepository(db) 742 unfurlService := unfurl.NewService(unfurlRepo, 743 unfurl.WithTimeout(30*time.Second), 744 unfurl.WithCacheTTL(24*time.Hour), 745 ) 746 747 // Kagi URL example - note: this will fail if not accessible or no OG tags 748 kagiURL := "https://kite.kagi.com/" 749 750 // Verify it's supported (not an oEmbed provider) 751 assert.True(t, unfurlService.IsSupported(kagiURL), "Kagi URL should be supported") 752 753 // Attempt unfurl 754 result, err := unfurlService.UnfurlURL(ctx, kagiURL) 755 if err != nil { 756 t.Logf("Kagi unfurl failed (expected if site is down or blocked): %v", err) 757 t.Skip("Skipping test - Kagi site may not be accessible") 758 return 759 } 760 761 require.NotNil(t, result, "Expected unfurl result") 762 assert.Equal(t, "kagi", result.Provider, "Expected provider to be kagi (custom parser for Kagi Kite)") 763 assert.Contains(t, result.Domain, "kagi.com", "Expected domain to contain kagi.com") 764 765 t.Logf("✓ Kagi custom parser unfurl successful:") 766 t.Logf(" Title: %s", result.Title) 767 t.Logf(" Provider: %s", result.Provider) 768 t.Logf(" Domain: %s", result.Domain) 769} 770 771// TestPostUnfurl_SmartRouting tests that oEmbed still works while OpenGraph handles others 772func TestPostUnfurl_SmartRouting(t *testing.T) { 773 if testing.Short() { 774 t.Skip("Skipping integration test in short mode") 775 } 776 777 db := setupTestDB(t) 778 defer func() { 779 if err := db.Close(); err != nil { 780 t.Logf("Failed to close database: %v", err) 781 } 782 }() 783 784 ctx := context.Background() 785 786 // Setup unfurl repository and service 787 unfurlRepo := unfurl.NewRepository(db) 788 unfurlService := unfurl.NewService(unfurlRepo, 789 unfurl.WithTimeout(30*time.Second), 790 unfurl.WithCacheTTL(24*time.Hour), 791 ) 792 793 // Clean cache 794 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%' OR url LIKE '%wikipedia.org%'") 795 796 tests := []struct { 797 name string 798 url string 799 expectedProvider string 800 }{ 801 { 802 name: "YouTube (oEmbed)", 803 url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", 804 expectedProvider: "youtube", 805 }, 806 { 807 name: "Generic site (OpenGraph)", 808 url: "https://www.wikipedia.org/", 809 expectedProvider: "opengraph", 810 }, 811 } 812 813 for _, tt := range tests { 814 t.Run(tt.name, func(t *testing.T) { 815 result, err := unfurlService.UnfurlURL(ctx, tt.url) 816 if err != nil { 817 t.Logf("Unfurl failed for %s: %v", tt.url, err) 818 t.Skip("Skipping - network issue") 819 return 820 } 821 822 require.NotNil(t, result) 823 assert.Equal(t, tt.expectedProvider, result.Provider, 824 "URL %s should use %s provider", tt.url, tt.expectedProvider) 825 826 t.Logf("✓ %s correctly routed to %s provider", tt.name, result.Provider) 827 }) 828 } 829} 830 831// TestPostUnfurl_E2E_WithJetstream tests the full unfurl flow with Jetstream consumer 832// This simulates: Create post → unfurl → write to PDS → Jetstream event → index in AppView 833func TestPostUnfurl_E2E_WithJetstream(t *testing.T) { 834 if testing.Short() { 835 t.Skip("Skipping integration test in short mode") 836 } 837 838 db := setupTestDB(t) 839 defer func() { 840 if err := db.Close(); err != nil { 841 t.Logf("Failed to close database: %v", err) 842 } 843 }() 844 845 ctx := context.Background() 846 847 // Setup repositories 848 userRepo := postgres.NewUserRepository(db) 849 communityRepo := postgres.NewCommunityRepository(db) 850 postRepo := postgres.NewPostRepository(db) 851 unfurlRepo := unfurl.NewRepository(db) 852 853 // Setup services 854 identityConfig := identity.DefaultConfig() 855 identityResolver := identity.NewResolver(db, identityConfig) 856 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 857 858 unfurlService := unfurl.NewService(unfurlRepo, 859 unfurl.WithTimeout(30*time.Second), 860 ) 861 862 // Cleanup 863 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:e2eunfurl%'") 864 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:e2eunfurl%'") 865 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:e2eunfurl%'") 866 _, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%streamable.com/e2etest%'") 867 868 // Create test data 869 testUserDID := generateTestDID("e2eunfurluser") 870 author := createTestUser(t, db, "e2eunfurluser.test", testUserDID) 871 872 testCommunityDID := generateTestDID("e2eunfurlcommunity") 873 community := &communities.Community{ 874 DID: testCommunityDID, 875 Handle: "e2eunfurlcommunity.community.test.coves.social", 876 Name: "e2eunfurlcommunity", 877 DisplayName: "E2E Unfurl Test", 878 OwnerDID: testCommunityDID, 879 CreatedByDID: author.DID, 880 HostedByDID: "did:web:coves.test", 881 Visibility: "public", 882 ModerationType: "moderator", 883 RecordURI: fmt.Sprintf("at://%s/social.coves.community.profile/self", testCommunityDID), 884 RecordCID: "fakecid123", 885 PDSAccessToken: "fake_token", 886 PDSRefreshToken: "fake_refresh", 887 } 888 _, err := communityRepo.Create(ctx, community) 889 require.NoError(t, err) 890 891 // Simulate creating a post with external embed that gets unfurled 892 streamableURL := "https://streamable.com/e2etest" 893 rkey := generateTID() 894 895 // First, trigger unfurl (simulating what would happen in post service) 896 // Use a real unfurl if possible, otherwise create mock data 897 var unfurlResult *unfurl.UnfurlResult 898 unfurlResult, err = unfurlService.UnfurlURL(ctx, streamableURL) 899 if err != nil { 900 t.Logf("Real unfurl failed, using mock data: %v", err) 901 // Create mock unfurl result 902 unfurlResult = &unfurl.UnfurlResult{ 903 Type: "video", 904 URI: streamableURL, 905 Title: "E2E Test Video", 906 Description: "Test video for E2E unfurl", 907 ThumbnailURL: "https://example.com/thumb.jpg", 908 Provider: "streamable", 909 Domain: "streamable.com", 910 Width: 1920, 911 Height: 1080, 912 } 913 // Manually cache it 914 _ = unfurlRepo.Set(ctx, streamableURL, unfurlResult, 24*time.Hour) 915 } 916 917 // Build the embed that would be written to PDS (with unfurl enhancement) 918 enhancedEmbed := map[string]interface{}{ 919 "$type": "social.coves.embed.external", 920 "external": map[string]interface{}{ 921 "uri": streamableURL, 922 "title": unfurlResult.Title, 923 "description": unfurlResult.Description, 924 "embedType": unfurlResult.Type, 925 "provider": unfurlResult.Provider, 926 "domain": unfurlResult.Domain, 927 "thumbnailUrl": unfurlResult.ThumbnailURL, 928 }, 929 } 930 931 // Simulate Jetstream event with enhanced embed 932 jetstreamEvent := jetstream.JetstreamEvent{ 933 Did: community.DID, 934 Kind: "commit", 935 Commit: &jetstream.CommitEvent{ 936 Operation: "create", 937 Collection: "social.coves.community.post", 938 RKey: rkey, 939 CID: "bafy2bzaceunfurle2e", 940 Record: map[string]interface{}{ 941 "$type": "social.coves.community.post", 942 "community": community.DID, 943 "author": author.DID, 944 "title": "E2E Unfurl Test Post", 945 "content": "Testing unfurl E2E flow", 946 "embed": enhancedEmbed, 947 "createdAt": time.Now().Format(time.RFC3339), 948 }, 949 }, 950 } 951 952 // Process through Jetstream consumer 953 consumer := jetstream.NewPostEventConsumer(postRepo, communityRepo, userService, db) 954 err = consumer.HandleEvent(ctx, &jetstreamEvent) 955 require.NoError(t, err, "Failed to process Jetstream event") 956 957 // Verify post was indexed with unfurl metadata 958 uri := fmt.Sprintf("at://%s/social.coves.community.post/%s", community.DID, rkey) 959 indexedPost, err := postRepo.GetByURI(ctx, uri) 960 require.NoError(t, err, "Post should be indexed") 961 962 // Verify embed was stored 963 require.NotNil(t, indexedPost.Embed, "Post should have embed") 964 965 // Parse embed JSON 966 var embedData map[string]interface{} 967 err = json.Unmarshal([]byte(*indexedPost.Embed), &embedData) 968 require.NoError(t, err, "Embed should be valid JSON") 969 970 // Verify unfurl enhancement fields are present 971 external, ok := embedData["external"].(map[string]interface{}) 972 require.True(t, ok, "Embed should have external field") 973 974 assert.Equal(t, streamableURL, external["uri"], "URI should match") 975 assert.Equal(t, unfurlResult.Title, external["title"], "Title should match unfurl") 976 assert.Equal(t, unfurlResult.Type, external["embedType"], "EmbedType should be set") 977 assert.Equal(t, unfurlResult.Provider, external["provider"], "Provider should be set") 978 assert.Equal(t, unfurlResult.Domain, external["domain"], "Domain should be set") 979 980 t.Logf("✓ E2E unfurl test complete:") 981 t.Logf(" Post URI: %s", uri) 982 t.Logf(" Unfurl Title: %s", unfurlResult.Title) 983 t.Logf(" Unfurl Type: %s", unfurlResult.Type) 984 t.Logf(" Unfurl Provider: %s", unfurlResult.Provider) 985} 986 987// TestPostUnfurl_KagiKite tests that Kagi Kite URLs get unfurled with story images 988func TestPostUnfurl_KagiKite(t *testing.T) { 989 if testing.Short() { 990 t.Skip("Skipping integration test in short mode") 991 } 992 993 db := setupTestDB(t) 994 defer func() { 995 if err := db.Close(); err != nil { 996 t.Logf("Failed to close database: %v", err) 997 } 998 }() 999 1000 // Note: This test requires network access to kite.kagi.com 1001 // It will be skipped if the URL is not reachable 1002 1003 kagiURL := "https://kite.kagi.com/96cf948f-8a1b-4281-9ba4-8a9e1ad7b3c6/world/11" 1004 1005 // Test unfurl service 1006 ctx := context.Background() 1007 unfurlRepo := unfurl.NewRepository(db) 1008 unfurlService := unfurl.NewService(unfurlRepo, 1009 unfurl.WithTimeout(30*time.Second), 1010 unfurl.WithCacheTTL(1*time.Hour), 1011 ) 1012 1013 result, err := unfurlService.UnfurlURL(ctx, kagiURL) 1014 if err != nil { 1015 t.Skipf("Skipping Kagi test (URL not reachable): %v", err) 1016 return 1017 } 1018 1019 require.NoError(t, err) 1020 assert.Equal(t, "article", result.Type) 1021 assert.Equal(t, "kagi", result.Provider) 1022 assert.NotEmpty(t, result.Title, "Should extract story title") 1023 assert.NotEmpty(t, result.ThumbnailURL, "Should extract story image") 1024 assert.Contains(t, result.ThumbnailURL, "kagiproxy.com", "Should be Kagi proxy URL") 1025 1026 t.Logf("✓ Kagi unfurl successful:") 1027 t.Logf(" Title: %s", result.Title) 1028 t.Logf(" Image: %s", result.ThumbnailURL) 1029 t.Logf(" Description: %s", result.Description) 1030}