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}