···
"Coves/internal/api/handlers/discover"
5
+
"Coves/internal/api/middleware"
6
+
"Coves/internal/core/votes"
"Coves/internal/db/postgres"
···
discoverCore "Coves/internal/core/discover"
18
+
oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
19
+
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
24
+
// mockVoteService implements votes.Service for testing viewer vote state
25
+
type mockVoteService struct {
26
+
cachedVotes map[string]*votes.CachedVote // userDID:subjectURI -> vote
29
+
func newMockVoteService() *mockVoteService {
30
+
return &mockVoteService{
31
+
cachedVotes: make(map[string]*votes.CachedVote),
35
+
func (m *mockVoteService) AddVote(userDID, subjectURI, direction, voteURI string) {
36
+
key := userDID + ":" + subjectURI
37
+
m.cachedVotes[key] = &votes.CachedVote{
38
+
Direction: direction,
43
+
func (m *mockVoteService) CreateVote(_ context.Context, _ *oauthlib.ClientSessionData, _ votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
44
+
return &votes.CreateVoteResponse{}, nil
47
+
func (m *mockVoteService) DeleteVote(_ context.Context, _ *oauthlib.ClientSessionData, _ votes.DeleteVoteRequest) error {
51
+
func (m *mockVoteService) EnsureCachePopulated(_ context.Context, _ *oauthlib.ClientSessionData) error {
52
+
return nil // Mock always succeeds - votes pre-populated via AddVote
55
+
func (m *mockVoteService) GetViewerVote(userDID, subjectURI string) *votes.CachedVote {
56
+
key := userDID + ":" + subjectURI
57
+
return m.cachedVotes[key]
60
+
func (m *mockVoteService) GetViewerVotesForSubjects(userDID string, subjectURIs []string) map[string]*votes.CachedVote {
61
+
result := make(map[string]*votes.CachedVote)
62
+
for _, uri := range subjectURIs {
63
+
key := userDID + ":" + uri
64
+
if vote, exists := m.cachedVotes[key]; exists {
// TestGetDiscover_ShowsAllCommunities tests discover feed shows posts from ALL communities
func TestGetDiscover_ShowsAllCommunities(t *testing.T) {
···
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
discoverService := discoverCore.NewDiscoverService(discoverRepo)
32
-
handler := discover.NewGetDiscoverHandler(discoverService)
83
+
handler := discover.NewGetDiscoverHandler(discoverService, nil) // nil vote service - tests don't need vote state
ctx := context.Background()
testID := time.Now().UnixNano()
···
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
discoverService := discoverCore.NewDiscoverService(discoverRepo)
103
-
handler := discover.NewGetDiscoverHandler(discoverService)
154
+
handler := discover.NewGetDiscoverHandler(discoverService, nil) // nil vote service - tests don't need vote state
ctx := context.Background()
testID := time.Now().UnixNano()
···
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
discoverService := discoverCore.NewDiscoverService(discoverRepo)
150
-
handler := discover.NewGetDiscoverHandler(discoverService)
201
+
handler := discover.NewGetDiscoverHandler(discoverService, nil)
ctx := context.Background()
testID := time.Now().UnixNano()
···
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
discoverService := discoverCore.NewDiscoverService(discoverRepo)
200
-
handler := discover.NewGetDiscoverHandler(discoverService)
251
+
handler := discover.NewGetDiscoverHandler(discoverService, nil)
ctx := context.Background()
testID := time.Now().UnixNano()
···
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
discoverService := discoverCore.NewDiscoverService(discoverRepo)
257
-
handler := discover.NewGetDiscoverHandler(discoverService)
308
+
handler := discover.NewGetDiscoverHandler(discoverService, nil)
t.Run("Limit exceeds maximum", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=100", nil)
···
assert.Contains(t, errorResp["message"], "limit")
326
+
// TestGetDiscover_ViewerVoteState tests that authenticated users see their vote state on posts
327
+
func TestGetDiscover_ViewerVoteState(t *testing.T) {
328
+
if testing.Short() {
329
+
t.Skip("Skipping integration test in short mode")
332
+
db := setupTestDB(t)
333
+
t.Cleanup(func() { _ = db.Close() })
335
+
ctx := context.Background()
336
+
testID := time.Now().UnixNano()
338
+
// Create community and posts
339
+
communityDID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("votes-%d", testID), fmt.Sprintf("alice-%d.test", testID))
340
+
require.NoError(t, err)
342
+
post1URI := createTestPost(t, db, communityDID, "did:plc:author1", "Post with upvote", 10, time.Now().Add(-1*time.Hour))
343
+
post2URI := createTestPost(t, db, communityDID, "did:plc:author2", "Post with downvote", 5, time.Now().Add(-2*time.Hour))
344
+
_ = createTestPost(t, db, communityDID, "did:plc:author3", "Post without vote", 3, time.Now().Add(-3*time.Hour))
346
+
// Setup mock vote service with pre-populated votes
347
+
viewerDID := "did:plc:viewer123"
348
+
mockVotes := newMockVoteService()
349
+
mockVotes.AddVote(viewerDID, post1URI, "up", "at://"+viewerDID+"/social.coves.vote/vote1")
350
+
mockVotes.AddVote(viewerDID, post2URI, "down", "at://"+viewerDID+"/social.coves.vote/vote2")
352
+
// Setup handler with mock vote service
353
+
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
354
+
discoverService := discoverCore.NewDiscoverService(discoverRepo)
355
+
handler := discover.NewGetDiscoverHandler(discoverService, mockVotes)
357
+
// Create request with authenticated user context
358
+
req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=50", nil)
360
+
// Inject OAuth session into context (simulates OptionalAuth middleware)
361
+
did, _ := syntax.ParseDID(viewerDID)
362
+
session := &oauthlib.ClientSessionData{
364
+
AccessToken: "test_token",
366
+
reqCtx := context.WithValue(req.Context(), middleware.UserDIDKey, viewerDID)
367
+
reqCtx = context.WithValue(reqCtx, middleware.OAuthSessionKey, session)
368
+
req = req.WithContext(reqCtx)
370
+
rec := httptest.NewRecorder()
371
+
handler.HandleGetDiscover(rec, req)
374
+
assert.Equal(t, http.StatusOK, rec.Code)
376
+
var response discoverCore.DiscoverResponse
377
+
err = json.Unmarshal(rec.Body.Bytes(), &response)
378
+
require.NoError(t, err)
380
+
// Find our test posts and verify vote state
381
+
var foundPost1, foundPost2, foundPost3 bool
382
+
for _, feedPost := range response.Feed {
383
+
switch feedPost.Post.URI {
386
+
require.NotNil(t, feedPost.Post.Viewer, "Post1 should have viewer state")
387
+
require.NotNil(t, feedPost.Post.Viewer.Vote, "Post1 should have vote direction")
388
+
assert.Equal(t, "up", *feedPost.Post.Viewer.Vote, "Post1 should show upvote")
389
+
require.NotNil(t, feedPost.Post.Viewer.VoteURI, "Post1 should have vote URI")
390
+
assert.Contains(t, *feedPost.Post.Viewer.VoteURI, "vote1", "Post1 should have correct vote URI")
394
+
require.NotNil(t, feedPost.Post.Viewer, "Post2 should have viewer state")
395
+
require.NotNil(t, feedPost.Post.Viewer.Vote, "Post2 should have vote direction")
396
+
assert.Equal(t, "down", *feedPost.Post.Viewer.Vote, "Post2 should show downvote")
397
+
require.NotNil(t, feedPost.Post.Viewer.VoteURI, "Post2 should have vote URI")
400
+
// Posts without votes should have nil Viewer or nil Vote
401
+
if feedPost.Post.Viewer != nil && feedPost.Post.Viewer.Vote != nil {
402
+
// This post has a vote from our viewer - it's not post3
409
+
assert.True(t, foundPost1, "Should find post1 with upvote")
410
+
assert.True(t, foundPost2, "Should find post2 with downvote")
411
+
assert.True(t, foundPost3, "Should find post3 without vote")
414
+
// TestGetDiscover_NoViewerStateWithoutAuth tests that unauthenticated users don't get viewer state
415
+
func TestGetDiscover_NoViewerStateWithoutAuth(t *testing.T) {
416
+
if testing.Short() {
417
+
t.Skip("Skipping integration test in short mode")
420
+
db := setupTestDB(t)
421
+
t.Cleanup(func() { _ = db.Close() })
423
+
ctx := context.Background()
424
+
testID := time.Now().UnixNano()
426
+
// Create community and post
427
+
communityDID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("noauth-%d", testID), fmt.Sprintf("alice-%d.test", testID))
428
+
require.NoError(t, err)
430
+
postURI := createTestPost(t, db, communityDID, "did:plc:author", "Some post", 10, time.Now())
432
+
// Setup mock vote service with a vote (but request will be unauthenticated)
433
+
mockVotes := newMockVoteService()
434
+
mockVotes.AddVote("did:plc:someuser", postURI, "up", "at://did:plc:someuser/social.coves.vote/vote1")
436
+
// Setup handler with mock vote service
437
+
discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
438
+
discoverService := discoverCore.NewDiscoverService(discoverRepo)
439
+
handler := discover.NewGetDiscoverHandler(discoverService, mockVotes)
441
+
// Create request WITHOUT auth context
442
+
req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=50", nil)
443
+
rec := httptest.NewRecorder()
444
+
handler.HandleGetDiscover(rec, req)
447
+
assert.Equal(t, http.StatusOK, rec.Code)
449
+
var response discoverCore.DiscoverResponse
450
+
err = json.Unmarshal(rec.Body.Bytes(), &response)
451
+
require.NoError(t, err)
453
+
// Find our post and verify NO viewer state (unauthenticated)
454
+
for _, feedPost := range response.Feed {
455
+
if feedPost.Post.URI == postURI {
456
+
assert.Nil(t, feedPost.Post.Viewer, "Unauthenticated request should not have viewer state")
460
+
t.Fatal("Test post not found in response")