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}