···
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"
18
+
"github.com/stretchr/testify/assert"
19
+
"github.com/stretchr/testify/require"
22
+
// TestPostUnfurl_Streamable tests that a post with a Streamable URL gets unfurled
23
+
func TestPostUnfurl_Streamable(t *testing.T) {
24
+
if testing.Short() {
25
+
t.Skip("Skipping integration test in short mode")
28
+
db := setupTestDB(t)
30
+
if err := db.Close(); err != nil {
31
+
t.Logf("Failed to close database: %v", err)
35
+
ctx := context.Background()
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)
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")
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),
54
+
communityService := communities.NewCommunityService(
56
+
"http://localhost:3001",
57
+
"did:web:test.coves.social",
58
+
"test.coves.social",
62
+
postService := posts.NewPostService(
65
+
nil, // aggregatorService not needed
66
+
nil, // blobService not needed
68
+
"http://localhost:3001",
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%'")
78
+
testUserDID := generateTestDID("unfurlauthor")
79
+
testUserHandle := "unfurlauthor.test"
80
+
_, err := userService.CreateUser(ctx, users.CreateUserRequest{
82
+
Handle: testUserHandle,
83
+
PDSURL: "http://localhost:3001",
85
+
require.NoError(t, err, "Failed to create test user")
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",
101
+
_, err = communityRepo.Create(ctx, testCommunity)
102
+
require.NoError(t, err, "Failed to create test community")
104
+
// Test unfurling a Streamable URL
105
+
streamableURL := "https://streamable.com/7kpdft"
106
+
title := "Streamable Test Post"
107
+
content := "Testing Streamable unfurl"
109
+
// Create post with external embed containing only URI
110
+
createReq := posts.CreatePostRequest{
111
+
Community: testCommunity.DID,
114
+
Embed: map[string]interface{}{
115
+
"$type": "social.coves.embed.external",
116
+
"external": map[string]interface{}{
117
+
"uri": streamableURL,
120
+
AuthorDID: testUserDID,
123
+
// Set auth context
124
+
authCtx := middleware.SetTestUserDID(ctx, testUserDID)
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)
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")
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)
140
+
// Check if the URL was cached
141
+
cached, err := unfurlRepo.Get(ctx, streamableURL)
143
+
t.Logf("Cache lookup failed: %v", err)
144
+
t.Skip("Skipping cache verification - unfurl may have failed due to network")
149
+
t.Skip("Unfurl result not cached - may have failed due to network issues")
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")
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)
167
+
// TestPostUnfurl_YouTube tests that a post with a YouTube URL gets unfurled
168
+
func TestPostUnfurl_YouTube(t *testing.T) {
169
+
if testing.Short() {
170
+
t.Skip("Skipping integration test in short mode")
173
+
db := setupTestDB(t)
175
+
if err := db.Close(); err != nil {
176
+
t.Logf("Failed to close database: %v", err)
180
+
ctx := context.Background()
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),
190
+
_, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%'")
192
+
// Test YouTube URL
193
+
youtubeURL := "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
196
+
result, err := unfurlService.UnfurlURL(ctx, youtubeURL)
198
+
t.Logf("Unfurl failed (may be network issue): %v", err)
199
+
t.Skip("Skipping test - YouTube unfurl failed")
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")
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)
214
+
// TestPostUnfurl_Reddit tests that a post with a Reddit URL gets unfurled
215
+
func TestPostUnfurl_Reddit(t *testing.T) {
216
+
if testing.Short() {
217
+
t.Skip("Skipping integration test in short mode")
220
+
db := setupTestDB(t)
222
+
if err := db.Close(); err != nil {
223
+
t.Logf("Failed to close database: %v", err)
227
+
ctx := context.Background()
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),
237
+
_, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%reddit.com%'")
239
+
// Use a well-known public Reddit post
240
+
redditURL := "https://www.reddit.com/r/programming/comments/1234/test/"
243
+
result, err := unfurlService.UnfurlURL(ctx, redditURL)
245
+
t.Logf("Unfurl failed (may be network issue or invalid URL): %v", err)
246
+
t.Skip("Skipping test - Reddit unfurl failed")
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")
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)
260
+
// TestPostUnfurl_CacheHit tests that the second post with the same URL uses cache
261
+
func TestPostUnfurl_CacheHit(t *testing.T) {
262
+
if testing.Short() {
263
+
t.Skip("Skipping integration test in short mode")
266
+
db := setupTestDB(t)
268
+
if err := db.Close(); err != nil {
269
+
t.Logf("Failed to close database: %v", err)
273
+
ctx := context.Background()
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),
283
+
testURL := "https://streamable.com/test123"
284
+
_, _ = db.Exec("DELETE FROM unfurl_cache WHERE url = $1", testURL)
286
+
// First unfurl - should hit network
287
+
t.Log("First unfurl - expecting cache miss")
288
+
result1, err1 := unfurlService.UnfurlURL(ctx, testURL)
290
+
t.Logf("First unfurl failed (may be network issue): %v", err1)
291
+
t.Skip("Skipping test - network unfurl failed")
295
+
require.NotNil(t, result1, "Expected first unfurl result")
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)
303
+
require.NoError(t, err2, "Second unfurl should not fail")
304
+
require.NotNil(t, result2, "Expected second unfurl result")
306
+
// Cache hit should be much faster (< 100ms)
307
+
assert.Less(t, elapsed.Milliseconds(), int64(100), "Cache hit should be fast")
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")
314
+
// Verify only one entry in cache
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")
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)
326
+
// TestPostUnfurl_UnsupportedURL tests that posts with unsupported URLs still succeed
327
+
func TestPostUnfurl_UnsupportedURL(t *testing.T) {
328
+
if testing.Short() {
329
+
t.Skip("Skipping integration test in short mode")
332
+
db := setupTestDB(t)
334
+
if err := db.Close(); err != nil {
335
+
t.Logf("Failed to close database: %v", err)
339
+
ctx := context.Background()
342
+
userRepo := postgres.NewUserRepository(db)
343
+
communityRepo := postgres.NewCommunityRepository(db)
344
+
postRepo := postgres.NewPostRepository(db)
346
+
identityConfig := identity.DefaultConfig()
347
+
identityResolver := identity.NewResolver(db, identityConfig)
348
+
userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001")
350
+
communityService := communities.NewCommunityService(
352
+
"http://localhost:3001",
353
+
"did:web:test.coves.social",
354
+
"test.coves.social",
358
+
// Create post service WITHOUT unfurl service
359
+
postService := posts.NewPostService(
362
+
nil, // aggregatorService
363
+
nil, // blobService
364
+
nil, // unfurlService - intentionally nil to test graceful handling
365
+
"http://localhost:3001",
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%'")
373
+
// Create test user
374
+
testUserDID := generateTestDID("unsupporteduser")
375
+
_, err := userService.CreateUser(ctx, users.CreateUserRequest{
377
+
Handle: "unsupporteduser.test",
378
+
PDSURL: "http://localhost:3001",
380
+
require.NoError(t, err)
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",
395
+
_, err = communityRepo.Create(ctx, testCommunity)
396
+
require.NoError(t, err)
398
+
// Create post with unsupported URL
399
+
unsupportedURL := "https://example.com/article/123"
400
+
title := "Unsupported URL Test"
401
+
content := "Testing unsupported domain"
403
+
createReq := posts.CreatePostRequest{
404
+
Community: testCommunity.DID,
407
+
Embed: map[string]interface{}{
408
+
"$type": "social.coves.embed.external",
409
+
"external": map[string]interface{}{
410
+
"uri": unsupportedURL,
413
+
AuthorDID: testUserDID,
416
+
authCtx := middleware.SetTestUserDID(ctx, testUserDID)
417
+
_, err = postService.CreatePost(authCtx, createReq)
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")
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")
427
+
// TestPostUnfurl_UserProvidedMetadata tests that user-provided metadata is preserved
428
+
func TestPostUnfurl_UserProvidedMetadata(t *testing.T) {
429
+
if testing.Short() {
430
+
t.Skip("Skipping integration test in short mode")
433
+
db := setupTestDB(t)
435
+
if err := db.Close(); err != nil {
436
+
t.Logf("Failed to close database: %v", err)
440
+
ctx := context.Background()
443
+
userRepo := postgres.NewUserRepository(db)
444
+
communityRepo := postgres.NewCommunityRepository(db)
445
+
postRepo := postgres.NewPostRepository(db)
446
+
unfurlRepo := unfurl.NewRepository(db)
448
+
identityConfig := identity.DefaultConfig()
449
+
identityResolver := identity.NewResolver(db, identityConfig)
450
+
userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001")
452
+
unfurlService := unfurl.NewService(unfurlRepo,
453
+
unfurl.WithTimeout(30*time.Second),
454
+
unfurl.WithCacheTTL(24*time.Hour),
457
+
communityService := communities.NewCommunityService(
459
+
"http://localhost:3001",
460
+
"did:web:test.coves.social",
461
+
"test.coves.social",
465
+
postService := posts.NewPostService(
471
+
"http://localhost:3001",
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%'")
480
+
// Create test user and community
481
+
testUserDID := generateTestDID("metadatauser")
482
+
_, err := userService.CreateUser(ctx, users.CreateUserRequest{
484
+
Handle: "metadatauser.test",
485
+
PDSURL: "http://localhost:3001",
487
+
require.NoError(t, err)
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",
501
+
_, err = communityRepo.Create(ctx, testCommunity)
502
+
require.NoError(t, err)
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"
511
+
createReq := posts.CreatePostRequest{
512
+
Community: testCommunity.DID,
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,
523
+
AuthorDID: testUserDID,
526
+
authCtx := middleware.SetTestUserDID(ctx, testUserDID)
527
+
_, err = postService.CreatePost(authCtx, createReq)
529
+
// Expected to fail at token refresh
530
+
require.Error(t, err)
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)")
539
+
// TestPostUnfurl_MissingEmbedType tests posts without external embed type don't trigger unfurling
540
+
func TestPostUnfurl_MissingEmbedType(t *testing.T) {
541
+
if testing.Short() {
542
+
t.Skip("Skipping integration test in short mode")
545
+
db := setupTestDB(t)
547
+
if err := db.Close(); err != nil {
548
+
t.Logf("Failed to close database: %v", err)
552
+
ctx := context.Background()
555
+
userRepo := postgres.NewUserRepository(db)
556
+
communityRepo := postgres.NewCommunityRepository(db)
557
+
postRepo := postgres.NewPostRepository(db)
558
+
unfurlRepo := unfurl.NewRepository(db)
560
+
identityConfig := identity.DefaultConfig()
561
+
identityResolver := identity.NewResolver(db, identityConfig)
562
+
userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001")
564
+
unfurlService := unfurl.NewService(unfurlRepo,
565
+
unfurl.WithTimeout(30*time.Second),
568
+
communityService := communities.NewCommunityService(
570
+
"http://localhost:3001",
571
+
"did:web:test.coves.social",
572
+
"test.coves.social",
576
+
postService := posts.NewPostService(
582
+
"http://localhost:3001",
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%'")
590
+
// Create test user and community
591
+
testUserDID := generateTestDID("noembeduser")
592
+
_, err := userService.CreateUser(ctx, users.CreateUserRequest{
594
+
Handle: "noembeduser.test",
595
+
PDSURL: "http://localhost:3001",
597
+
require.NoError(t, err)
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",
611
+
_, err = communityRepo.Create(ctx, testCommunity)
612
+
require.NoError(t, err)
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"
619
+
createReq := posts.CreatePostRequest{
620
+
Community: testCommunity.DID,
623
+
AuthorDID: testUserDID,
626
+
authCtx := middleware.SetTestUserDID(ctx, testUserDID)
627
+
_, err := postService.CreatePost(authCtx, createReq)
629
+
// Should fail at token refresh (expected)
630
+
require.Error(t, err)
631
+
assert.Contains(t, err.Error(), "failed to refresh community credentials")
633
+
t.Log("✓ Post without embed succeeded (no unfurl attempted)")
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"
641
+
createReq := posts.CreatePostRequest{
642
+
Community: testCommunity.DID,
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",
652
+
"alt": "Test image",
656
+
AuthorDID: testUserDID,
659
+
authCtx := middleware.SetTestUserDID(ctx, testUserDID)
660
+
_, err := postService.CreatePost(authCtx, createReq)
662
+
// Should fail at token refresh (expected)
663
+
require.Error(t, err)
664
+
assert.Contains(t, err.Error(), "failed to refresh community credentials")
666
+
t.Log("✓ Post with images embed succeeded (no unfurl attempted)")
670
+
// TestPostUnfurl_OpenGraph tests that OpenGraph URLs get unfurled
671
+
func TestPostUnfurl_OpenGraph(t *testing.T) {
672
+
if testing.Short() {
673
+
t.Skip("Skipping integration test in short mode")
676
+
db := setupTestDB(t)
678
+
if err := db.Close(); err != nil {
679
+
t.Logf("Failed to close database: %v", err)
683
+
ctx := context.Background()
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),
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/"
696
+
// Check if URL is supported
697
+
assert.True(t, unfurlService.IsSupported(testURL), "Wikipedia URL should be supported")
700
+
result, err := unfurlService.UnfurlURL(ctx, testURL)
702
+
t.Logf("Unfurl failed (may be network issue): %v", err)
703
+
t.Skip("Skipping test - OpenGraph unfurl failed")
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")
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)
720
+
if result.ThumbnailURL != "" {
721
+
t.Logf(" Thumbnail: %s", result.ThumbnailURL)
725
+
// TestPostUnfurl_KagiURL tests that Kagi links work with OpenGraph
726
+
func TestPostUnfurl_KagiURL(t *testing.T) {
727
+
if testing.Short() {
728
+
t.Skip("Skipping integration test in short mode")
731
+
db := setupTestDB(t)
733
+
if err := db.Close(); err != nil {
734
+
t.Logf("Failed to close database: %v", err)
738
+
ctx := context.Background()
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),
747
+
// Kagi URL example - note: this will fail if not accessible or no OG tags
748
+
kagiURL := "https://kite.kagi.com/"
750
+
// Verify it's supported (not an oEmbed provider)
751
+
assert.True(t, unfurlService.IsSupported(kagiURL), "Kagi URL should be supported")
754
+
result, err := unfurlService.UnfurlURL(ctx, kagiURL)
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")
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")
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)
771
+
// TestPostUnfurl_SmartRouting tests that oEmbed still works while OpenGraph handles others
772
+
func TestPostUnfurl_SmartRouting(t *testing.T) {
773
+
if testing.Short() {
774
+
t.Skip("Skipping integration test in short mode")
777
+
db := setupTestDB(t)
779
+
if err := db.Close(); err != nil {
780
+
t.Logf("Failed to close database: %v", err)
784
+
ctx := context.Background()
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),
794
+
_, _ = db.Exec("DELETE FROM unfurl_cache WHERE url LIKE '%youtube.com%' OR url LIKE '%wikipedia.org%'")
796
+
tests := []struct {
799
+
expectedProvider string
802
+
name: "YouTube (oEmbed)",
803
+
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
804
+
expectedProvider: "youtube",
807
+
name: "Generic site (OpenGraph)",
808
+
url: "https://www.wikipedia.org/",
809
+
expectedProvider: "opengraph",
813
+
for _, tt := range tests {
814
+
t.Run(tt.name, func(t *testing.T) {
815
+
result, err := unfurlService.UnfurlURL(ctx, tt.url)
817
+
t.Logf("Unfurl failed for %s: %v", tt.url, err)
818
+
t.Skip("Skipping - network issue")
822
+
require.NotNil(t, result)
823
+
assert.Equal(t, tt.expectedProvider, result.Provider,
824
+
"URL %s should use %s provider", tt.url, tt.expectedProvider)
826
+
t.Logf("✓ %s correctly routed to %s provider", tt.name, result.Provider)
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
833
+
func TestPostUnfurl_E2E_WithJetstream(t *testing.T) {
834
+
if testing.Short() {
835
+
t.Skip("Skipping integration test in short mode")
838
+
db := setupTestDB(t)
840
+
if err := db.Close(); err != nil {
841
+
t.Logf("Failed to close database: %v", err)
845
+
ctx := context.Background()
847
+
// Setup repositories
848
+
userRepo := postgres.NewUserRepository(db)
849
+
communityRepo := postgres.NewCommunityRepository(db)
850
+
postRepo := postgres.NewPostRepository(db)
851
+
unfurlRepo := unfurl.NewRepository(db)
854
+
identityConfig := identity.DefaultConfig()
855
+
identityResolver := identity.NewResolver(db, identityConfig)
856
+
userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001")
858
+
unfurlService := unfurl.NewService(unfurlRepo,
859
+
unfurl.WithTimeout(30*time.Second),
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%'")
868
+
// Create test data
869
+
testUserDID := generateTestDID("e2eunfurluser")
870
+
author := createTestUser(t, db, "e2eunfurluser.test", testUserDID)
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",
888
+
_, err := communityRepo.Create(ctx, community)
889
+
require.NoError(t, err)
891
+
// Simulate creating a post with external embed that gets unfurled
892
+
streamableURL := "https://streamable.com/e2etest"
893
+
rkey := generateTID()
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)
900
+
t.Logf("Real unfurl failed, using mock data: %v", err)
901
+
// Create mock unfurl result
902
+
unfurlResult = &unfurl.UnfurlResult{
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",
913
+
// Manually cache it
914
+
_ = unfurlRepo.Set(ctx, streamableURL, unfurlResult, 24*time.Hour)
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,
931
+
// Simulate Jetstream event with enhanced embed
932
+
jetstreamEvent := jetstream.JetstreamEvent{
933
+
Did: community.DID,
935
+
Commit: &jetstream.CommitEvent{
936
+
Operation: "create",
937
+
Collection: "social.coves.community.post",
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),
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")
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")
962
+
// Verify embed was stored
963
+
require.NotNil(t, indexedPost.Embed, "Post should have embed")
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")
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")
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")
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)
987
+
// TestPostUnfurl_KagiKite tests that Kagi Kite URLs get unfurled with story images
988
+
func TestPostUnfurl_KagiKite(t *testing.T) {
989
+
if testing.Short() {
990
+
t.Skip("Skipping integration test in short mode")
993
+
db := setupTestDB(t)
995
+
if err := db.Close(); err != nil {
996
+
t.Logf("Failed to close database: %v", err)
1000
+
// Note: This test requires network access to kite.kagi.com
1001
+
// It will be skipped if the URL is not reachable
1003
+
kagiURL := "https://kite.kagi.com/96cf948f-8a1b-4281-9ba4-8a9e1ad7b3c6/world/11"
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),
1013
+
result, err := unfurlService.UnfurlURL(ctx, kagiURL)
1015
+
t.Skipf("Skipping Kagi test (URL not reachable): %v", err)
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")
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)