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