···
userService := users.NewUserService(userRepo, identityResolver, pdsURL)
// Extract instance domain and DID
119
+
// IMPORTANT: Instance domain must match PDS_SERVICE_HANDLE_DOMAINS config (.community.coves.social)
instanceDID := os.Getenv("INSTANCE_DID")
121
-
instanceDID = "did:web:test.coves.social"
122
+
instanceDID = "did:web:coves.social" // Must match PDS handle domain config
var instanceDomain string
if strings.HasPrefix(instanceDID, "did:web:") {
···
voteConsumer := jetstream.NewVoteEventConsumer(voteRepo, userService, db)
// Setup HTTP server with all routes
142
-
authMiddleware := middleware.NewAtProtoAuthMiddleware(nil, true) // Skip JWT verification for testing
143
+
// IMPORTANT: skipVerify=true because PDS password auth returns Bearer tokens (not DPoP-bound).
144
+
// E2E tests use Bearer tokens with DPoP scheme header, which only works with skipVerify=true.
145
+
// In production, skipVerify=false requires proper DPoP-bound tokens from OAuth flow.
146
+
authMiddleware := middleware.NewAtProtoAuthMiddleware(nil, true)
147
+
defer authMiddleware.Stop() // Clean up DPoP replay cache goroutine
routes.RegisterCommunityRoutes(r, communityService, authMiddleware, nil) // nil = allow all community creators
routes.RegisterPostRoutes(r, postService, authMiddleware)
···
// Cleanup test data from previous runs (clean up ALL journey test data)
timestamp := time.Now().Unix()
152
-
// Clean up previous test runs - use pattern that matches ANY journey test data
153
-
_, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE '%alice-journey-%' OR voter_did LIKE '%bob-journey-%'")
154
-
_, _ = db.Exec("DELETE FROM comments WHERE author_did LIKE '%alice-journey-%' OR author_did LIKE '%bob-journey-%'")
155
-
_, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE '%gaming-journey-%'")
156
-
_, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE '%alice-journey-%' OR user_did LIKE '%bob-journey-%'")
157
-
_, _ = db.Exec("DELETE FROM communities WHERE handle LIKE 'gaming-journey-%'")
158
-
_, _ = db.Exec("DELETE FROM users WHERE handle LIKE '%alice-journey-%' OR handle LIKE '%bob-journey-%'")
157
+
// Clean up previous test runs - use pattern that matches journey test data
158
+
// Handles are now shorter: alice{4-digit}.local.coves.dev, bob{4-digit}.local.coves.dev
159
+
_, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE '%alice%.local.coves.dev%' OR voter_did LIKE '%bob%.local.coves.dev%'")
160
+
_, _ = db.Exec("DELETE FROM comments WHERE author_did LIKE '%alice%.local.coves.dev%' OR author_did LIKE '%bob%.local.coves.dev%'")
161
+
_, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE '%gj%'")
162
+
_, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE '%alice%.local.coves.dev%' OR user_did LIKE '%bob%.local.coves.dev%'")
163
+
_, _ = db.Exec("DELETE FROM communities WHERE handle LIKE 'gj%'")
164
+
_, _ = db.Exec("DELETE FROM users WHERE handle LIKE 'alice%.local.coves.dev' OR handle LIKE 'bob%.local.coves.dev'")
// Defer cleanup for current test run using specific timestamp pattern
162
-
pattern := fmt.Sprintf("%%journey-%d%%", timestamp)
163
-
_, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE $1", pattern)
164
-
_, _ = db.Exec("DELETE FROM comments WHERE author_did LIKE $1", pattern)
165
-
_, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE $1", pattern)
166
-
_, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE $1", pattern)
167
-
_, _ = db.Exec("DELETE FROM communities WHERE did LIKE $1 OR handle LIKE $1", pattern, pattern)
168
-
_, _ = db.Exec("DELETE FROM users WHERE did LIKE $1 OR handle LIKE $1", pattern, pattern)
168
+
shortTS := timestamp % 10000
169
+
alicePattern := fmt.Sprintf("%%alice%d%%", shortTS)
170
+
bobPattern := fmt.Sprintf("%%bob%d%%", shortTS)
171
+
gjPattern := fmt.Sprintf("%%gj%d%%", shortTS)
172
+
_, _ = db.Exec("DELETE FROM votes WHERE voter_did LIKE $1 OR voter_did LIKE $2", alicePattern, bobPattern)
173
+
_, _ = db.Exec("DELETE FROM comments WHERE author_did LIKE $1 OR author_did LIKE $2", alicePattern, bobPattern)
174
+
_, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE $1", gjPattern)
175
+
_, _ = db.Exec("DELETE FROM community_subscriptions WHERE user_did LIKE $1 OR user_did LIKE $2", alicePattern, bobPattern)
176
+
_, _ = db.Exec("DELETE FROM communities WHERE handle LIKE $1", gjPattern)
177
+
_, _ = db.Exec("DELETE FROM users WHERE handle LIKE $1 OR handle LIKE $2", alicePattern, bobPattern)
// Test variables to track state across steps
···
t.Run("1. User A - Signup and Authenticate", func(t *testing.T) {
t.Log("\n👤 Part 1: User A creates account and authenticates...")
193
-
userAHandle = fmt.Sprintf("alice-journey-%d.local.coves.dev", timestamp)
194
-
email := fmt.Sprintf("alice-journey-%d@test.com", timestamp)
202
+
// Use short handle format to stay under PDS 34-char limit
203
+
shortTS := timestamp % 10000 // Use last 4 digits
204
+
userAHandle = fmt.Sprintf("alice%d.local.coves.dev", shortTS)
205
+
email := fmt.Sprintf("alice%d@test.com", shortTS)
password := "test-password-alice-123"
···
t.Run("2. User A - Create Community", func(t *testing.T) {
t.Log("\n🏘️ Part 2: User A creates a community...")
218
-
communityName := fmt.Sprintf("gaming-journey-%d", timestamp%10000) // Keep name short
229
+
// Community handle will be {name}.community.coves.social
230
+
// Max 34 chars total, so name must be short (34 - 23 = 11 chars max)
231
+
shortTS := timestamp % 10000
232
+
communityName := fmt.Sprintf("gj%d", shortTS) // "gj9261" = 6 chars -> handle = 29 chars
createReq := map[string]interface{}{
···
httpServer.URL+"/xrpc/social.coves.community.create",
bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
233
-
req.Header.Set("Authorization", "Bearer "+userAToken)
247
+
req.Header.Set("Authorization", "DPoP "+userAToken)
resp, err := http.DefaultClient.Do(req)
···
httpServer.URL+"/xrpc/social.coves.community.post.create",
bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
317
-
req.Header.Set("Authorization", "Bearer "+userAToken)
331
+
req.Header.Set("Authorization", "DPoP "+userAToken)
resp, err := http.DefaultClient.Do(req)
···
t.Run("4. User B - Signup and Authenticate", func(t *testing.T) {
t.Log("\n👤 Part 4: User B creates account and authenticates...")
384
-
userBHandle = fmt.Sprintf("bob-journey-%d.local.coves.dev", timestamp)
385
-
email := fmt.Sprintf("bob-journey-%d@test.com", timestamp)
398
+
// Use short handle format to stay under PDS 34-char limit
399
+
shortTS := timestamp % 10000 // Use last 4 digits
400
+
userBHandle = fmt.Sprintf("bob%d.local.coves.dev", shortTS)
401
+
email := fmt.Sprintf("bob%d@test.com", shortTS)
password := "test-password-bob-123"
···
httpServer.URL+"/xrpc/social.coves.community.subscribe",
bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
424
-
req.Header.Set("Authorization", "Bearer "+userBToken)
440
+
req.Header.Set("Authorization", "DPoP "+userBToken)
resp, err := http.DefaultClient.Do(req)
···
t.Run("9. User B - Verify Timeline Feed Shows Subscribed Community Posts", func(t *testing.T) {
t.Log("\n📰 Part 9: User B checks timeline feed...")
656
-
req := httptest.NewRequest(http.MethodGet,
657
-
"/xrpc/social.coves.feed.getTimeline?sort=new&limit=10", nil)
658
-
req = req.WithContext(middleware.SetTestUserDID(req.Context(), userBDID))
659
-
rec := httptest.NewRecorder()
672
+
// Use HTTP client to properly go through auth middleware with DPoP token
673
+
req, _ := http.NewRequest(http.MethodGet,
674
+
httpServer.URL+"/xrpc/social.coves.feed.getTimeline?sort=new&limit=10", nil)
675
+
req.Header.Set("Authorization", "DPoP "+userBToken)
661
-
// Call timeline handler directly
662
-
timelineHandler := httpServer.Config.Handler
663
-
timelineHandler.ServeHTTP(rec, req)
677
+
resp, err := http.DefaultClient.Do(req)
678
+
require.NoError(t, err)
679
+
defer func() { _ = resp.Body.Close() }()
665
-
require.Equal(t, http.StatusOK, rec.Code, "Timeline request should succeed")
681
+
require.Equal(t, http.StatusOK, resp.StatusCode, "Timeline request should succeed")
var response timelineCore.TimelineResponse
668
-
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &response))
684
+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&response))
// User B should see the post from the community they subscribed to
require.NotEmpty(t, response.Feed, "Timeline should contain posts")
···
"Post author should be User A")
assert.Equal(t, communityDID, feedPost.Post.Community.DID,
"Post community should match")
682
-
assert.Equal(t, 1, feedPost.Post.UpvoteCount,
698
+
// Check stats (counts are in Stats struct, not direct fields)
699
+
require.NotNil(t, feedPost.Post.Stats, "Post should have stats")
700
+
assert.Equal(t, 1, feedPost.Post.Stats.Upvotes,
"Post should show 1 upvote from User B")
684
-
assert.Equal(t, 1, feedPost.Post.CommentCount,
702
+
assert.Equal(t, 1, feedPost.Post.Stats.CommentCount,
"Post should show 1 comment from User B")
···
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
ON CONFLICT (did) DO NOTHING
`, did, handle, strings.Split(handle, ".")[0], "Test Community", did, ownerDID,
791
-
"did:web:test.coves.social", "public", "moderator",
809
+
"did:web:coves.social", "public", "moderator",
fmt.Sprintf("at://%s/social.coves.community.profile/self", did), "fakecid")
require.NoError(t, err, "Failed to simulate community indexing")