A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "context"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "testing"
9 "time"
10
11 "Coves/internal/core/communities"
12 "Coves/internal/db/postgres"
13)
14
15// TestTokenRefresh_ExpirationDetection tests the NeedsRefresh function with various token states
16func TestTokenRefresh_ExpirationDetection(t *testing.T) {
17 tests := []struct {
18 name string
19 token string
20 shouldRefresh bool
21 expectError bool
22 }{
23 {
24 name: "Token expiring in 2 minutes (should refresh)",
25 token: createTestJWT(time.Now().Add(2 * time.Minute)),
26 shouldRefresh: true,
27 expectError: false,
28 },
29 {
30 name: "Token expiring in 10 minutes (should not refresh)",
31 token: createTestJWT(time.Now().Add(10 * time.Minute)),
32 shouldRefresh: false,
33 expectError: false,
34 },
35 {
36 name: "Token already expired (should refresh)",
37 token: createTestJWT(time.Now().Add(-1 * time.Minute)),
38 shouldRefresh: true,
39 expectError: false,
40 },
41 {
42 name: "Token expiring in exactly 5 minutes (should not refresh - edge case)",
43 token: createTestJWT(time.Now().Add(6 * time.Minute)),
44 shouldRefresh: false,
45 expectError: false,
46 },
47 {
48 name: "Token expiring in 4 minutes (should refresh)",
49 token: createTestJWT(time.Now().Add(4 * time.Minute)),
50 shouldRefresh: true,
51 expectError: false,
52 },
53 {
54 name: "Invalid JWT format (too many parts)",
55 token: "not.a.valid.jwt.format.extra",
56 shouldRefresh: false,
57 expectError: true,
58 },
59 {
60 name: "Invalid JWT format (too few parts)",
61 token: "invalid.token",
62 shouldRefresh: false,
63 expectError: true,
64 },
65 {
66 name: "Empty token",
67 token: "",
68 shouldRefresh: false,
69 expectError: true,
70 },
71 }
72
73 for _, tt := range tests {
74 t.Run(tt.name, func(t *testing.T) {
75 result, err := communities.NeedsRefresh(tt.token)
76
77 if tt.expectError {
78 if err == nil {
79 t.Errorf("Expected error but got none")
80 }
81 return
82 }
83
84 if err != nil {
85 t.Fatalf("Unexpected error: %v", err)
86 }
87
88 if result != tt.shouldRefresh {
89 t.Errorf("Expected NeedsRefresh=%v, got %v", tt.shouldRefresh, result)
90 }
91 })
92 }
93}
94
95// TestTokenRefresh_UpdateCredentials tests the repository UpdateCredentials method
96func TestTokenRefresh_UpdateCredentials(t *testing.T) {
97 if testing.Short() {
98 t.Skip("skipping integration test in short mode")
99 }
100
101 ctx := context.Background()
102 db := setupTestDB(t)
103 defer func() {
104 if err := db.Close(); err != nil {
105 t.Logf("Failed to close database: %v", err)
106 }
107 }()
108
109 repo := postgres.NewCommunityRepository(db)
110
111 // Create a test community first
112 community := &communities.Community{
113 DID: "did:plc:test123",
114 Handle: "test.community.coves.social",
115 Name: "test",
116 OwnerDID: "did:plc:test123",
117 CreatedByDID: "did:plc:creator",
118 HostedByDID: "did:web:coves.social",
119 PDSEmail: "test@coves.social",
120 PDSPassword: "original-password",
121 PDSAccessToken: "original-access-token",
122 PDSRefreshToken: "original-refresh-token",
123 PDSURL: "http://localhost:3001",
124 Visibility: "public",
125 MemberCount: 0,
126 SubscriberCount: 0,
127 RecordURI: "at://did:plc:test123/social.coves.community.profile/self",
128 RecordCID: "bafytest",
129 }
130
131 created, err := repo.Create(ctx, community)
132 if err != nil {
133 t.Fatalf("Failed to create test community: %v", err)
134 }
135
136 // Update credentials
137 newAccessToken := "new-access-token-12345"
138 newRefreshToken := "new-refresh-token-67890"
139
140 err = repo.UpdateCredentials(ctx, created.DID, newAccessToken, newRefreshToken)
141 if err != nil {
142 t.Fatalf("UpdateCredentials failed: %v", err)
143 }
144
145 // Verify tokens were updated
146 retrieved, err := repo.GetByDID(ctx, created.DID)
147 if err != nil {
148 t.Fatalf("Failed to retrieve community: %v", err)
149 }
150
151 if retrieved.PDSAccessToken != newAccessToken {
152 t.Errorf("Access token not updated: expected %q, got %q", newAccessToken, retrieved.PDSAccessToken)
153 }
154
155 if retrieved.PDSRefreshToken != newRefreshToken {
156 t.Errorf("Refresh token not updated: expected %q, got %q", newRefreshToken, retrieved.PDSRefreshToken)
157 }
158
159 // Verify password unchanged (should not be affected)
160 if retrieved.PDSPassword != "original-password" {
161 t.Errorf("Password should remain unchanged: expected %q, got %q", "original-password", retrieved.PDSPassword)
162 }
163}
164
165// TestTokenRefresh_E2E_UpdateAfterTokenRefresh tests end-to-end token refresh during community update
166func TestTokenRefresh_E2E_UpdateAfterTokenRefresh(t *testing.T) {
167 if testing.Short() {
168 t.Skip("skipping E2E test in short mode")
169 }
170
171 ctx := context.Background()
172 db := setupTestDB(t)
173 defer func() {
174 if err := db.Close(); err != nil {
175 t.Logf("Failed to close database: %v", err)
176 }
177 }()
178
179 // This test requires a real PDS for token refresh
180 // For now, we'll test the token expiration detection logic
181 // Full E2E test with PDS will be added in manual testing phase
182
183 repo := postgres.NewCommunityRepository(db)
184
185 // Create community with expiring token
186 expiringToken := createTestJWT(time.Now().Add(2 * time.Minute)) // Expires in 2 minutes
187
188 community := &communities.Community{
189 DID: "did:plc:expiring123",
190 Handle: "expiring.community.coves.social",
191 Name: "expiring",
192 OwnerDID: "did:plc:expiring123",
193 CreatedByDID: "did:plc:creator",
194 HostedByDID: "did:web:coves.social",
195 PDSEmail: "expiring@coves.social",
196 PDSPassword: "test-password",
197 PDSAccessToken: expiringToken,
198 PDSRefreshToken: "test-refresh-token",
199 PDSURL: "http://localhost:3001",
200 Visibility: "public",
201 RecordURI: "at://did:plc:expiring123/social.coves.community.profile/self",
202 RecordCID: "bafytest",
203 }
204
205 created, err := repo.Create(ctx, community)
206 if err != nil {
207 t.Fatalf("Failed to create community: %v", err)
208 }
209
210 // Verify token is stored
211 if created.PDSAccessToken != expiringToken {
212 t.Errorf("Token not stored correctly")
213 }
214
215 t.Logf("✅ Created community with expiring token (expires in 2 minutes)")
216 t.Logf(" Community DID: %s", created.DID)
217 t.Logf(" NOTE: Full refresh flow requires real PDS - tested in manual/staging tests")
218}
219
220// Helper: Create a test JWT with specific expiration time
221func createTestJWT(expiresAt time.Time) string {
222 // Create JWT header
223 header := map[string]interface{}{
224 "alg": "ES256",
225 "typ": "JWT",
226 }
227 headerJSON, _ := json.Marshal(header)
228 headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
229
230 // Create JWT payload with expiration
231 payload := map[string]interface{}{
232 "sub": "did:plc:test",
233 "iss": "https://pds.example.com",
234 "exp": expiresAt.Unix(),
235 "iat": time.Now().Unix(),
236 }
237 payloadJSON, _ := json.Marshal(payload)
238 payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
239
240 // Fake signature (not verified in our tests)
241 signature := base64.RawURLEncoding.EncodeToString([]byte("fake-signature"))
242
243 return fmt.Sprintf("%s.%s.%s", headerB64, payloadB64, signature)
244}