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 "fmt" 8 "testing" 9 "time" 10) 11 12// TestCommunityRepository_CredentialPersistence tests that PDS credentials are properly persisted 13func TestCommunityRepository_CredentialPersistence(t *testing.T) { 14 db := setupTestDB(t) 15 defer func() { 16 if err := db.Close(); err != nil { 17 t.Logf("Failed to close database: %v", err) 18 } 19 }() 20 21 repo := postgres.NewCommunityRepository(db) 22 ctx := context.Background() 23 24 t.Run("persists PDS credentials on create", func(t *testing.T) { 25 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 26 communityDID := generateTestDID(uniqueSuffix) 27 28 community := &communities.Community{ 29 DID: communityDID, 30 Handle: fmt.Sprintf("!cred-test-%s@coves.local", uniqueSuffix), 31 Name: "cred-test", 32 OwnerDID: communityDID, // V2: self-owned 33 CreatedByDID: "did:plc:user123", 34 HostedByDID: "did:web:coves.local", 35 Visibility: "public", 36 // V2: PDS credentials 37 PDSEmail: "community-test@communities.coves.local", 38 PDSPassword: "cleartext-password-encrypted-by-repo", // V2: Cleartext (encrypted by repository) 39 PDSAccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token", 40 PDSRefreshToken: "refresh_token_xyz123", 41 PDSURL: "http://localhost:2583", 42 CreatedAt: time.Now(), 43 UpdatedAt: time.Now(), 44 } 45 46 created, err := repo.Create(ctx, community) 47 if err != nil { 48 t.Fatalf("Failed to create community with credentials: %v", err) 49 } 50 51 if created.ID == 0 { 52 t.Error("Expected non-zero ID") 53 } 54 55 // Retrieve and verify credentials were persisted 56 retrieved, err := repo.GetByDID(ctx, communityDID) 57 if err != nil { 58 t.Fatalf("Failed to retrieve community: %v", err) 59 } 60 61 if retrieved.PDSEmail != community.PDSEmail { 62 t.Errorf("Expected PDSEmail %s, got %s", community.PDSEmail, retrieved.PDSEmail) 63 } 64 if retrieved.PDSPassword != community.PDSPassword { 65 t.Errorf("Expected PDSPassword to be persisted and encrypted/decrypted") 66 } 67 if retrieved.PDSAccessToken != community.PDSAccessToken { 68 t.Errorf("Expected PDSAccessToken to be persisted and decrypted correctly") 69 } 70 if retrieved.PDSRefreshToken != community.PDSRefreshToken { 71 t.Errorf("Expected PDSRefreshToken to be persisted and decrypted correctly") 72 } 73 if retrieved.PDSURL != community.PDSURL { 74 t.Errorf("Expected PDSURL %s, got %s", community.PDSURL, retrieved.PDSURL) 75 } 76 }) 77 78 t.Run("handles empty credentials gracefully", func(t *testing.T) { 79 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 80 communityDID := generateTestDID(uniqueSuffix) 81 82 // Community without PDS credentials (e.g., from Jetstream consumer) 83 community := &communities.Community{ 84 DID: communityDID, 85 Handle: fmt.Sprintf("!nocred-test-%s@coves.local", uniqueSuffix), 86 Name: "nocred-test", 87 OwnerDID: communityDID, 88 CreatedByDID: "did:plc:user123", 89 HostedByDID: "did:web:coves.local", 90 Visibility: "public", 91 // No PDS credentials 92 CreatedAt: time.Now(), 93 UpdatedAt: time.Now(), 94 } 95 96 created, err := repo.Create(ctx, community) 97 if err != nil { 98 t.Fatalf("Failed to create community without credentials: %v", err) 99 } 100 101 retrieved, err := repo.GetByDID(ctx, communityDID) 102 if err != nil { 103 t.Fatalf("Failed to retrieve community: %v", err) 104 } 105 106 if retrieved.PDSEmail != "" { 107 t.Errorf("Expected empty PDSEmail, got %s", retrieved.PDSEmail) 108 } 109 if retrieved.PDSAccessToken != "" { 110 t.Errorf("Expected empty PDSAccessToken, got %s", retrieved.PDSAccessToken) 111 } 112 if retrieved.PDSRefreshToken != "" { 113 t.Errorf("Expected empty PDSRefreshToken, got %s", retrieved.PDSRefreshToken) 114 } 115 116 // Verify community is still functional 117 if created.ID == 0 { 118 t.Error("Expected non-zero ID even without credentials") 119 } 120 }) 121} 122 123// TestCommunityRepository_EncryptedCredentials tests encryption at rest 124func TestCommunityRepository_EncryptedCredentials(t *testing.T) { 125 db := setupTestDB(t) 126 defer func() { 127 if err := db.Close(); err != nil { 128 t.Logf("Failed to close database: %v", err) 129 } 130 }() 131 132 repo := postgres.NewCommunityRepository(db) 133 ctx := context.Background() 134 135 t.Run("credentials are encrypted in database", func(t *testing.T) { 136 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 137 communityDID := generateTestDID(uniqueSuffix) 138 139 accessToken := "sensitive_access_token_xyz123" 140 refreshToken := "sensitive_refresh_token_abc456" 141 142 community := &communities.Community{ 143 DID: communityDID, 144 Handle: fmt.Sprintf("!encrypt-test-%s@coves.local", uniqueSuffix), 145 Name: "encrypt-test", 146 OwnerDID: communityDID, 147 CreatedByDID: "did:plc:user123", 148 HostedByDID: "did:web:coves.local", 149 Visibility: "public", 150 PDSEmail: "encrypted@communities.coves.local", 151 PDSPassword: "cleartext-password-for-encryption", // V2: Cleartext (encrypted by repository) 152 PDSAccessToken: accessToken, 153 PDSRefreshToken: refreshToken, 154 PDSURL: "http://localhost:2583", 155 CreatedAt: time.Now(), 156 UpdatedAt: time.Now(), 157 } 158 159 if _, err := repo.Create(ctx, community); err != nil { 160 t.Fatalf("Failed to create community: %v", err) 161 } 162 163 // Query database directly to verify encryption 164 var encryptedAccess, encryptedRefresh []byte 165 query := ` 166 SELECT pds_access_token_encrypted, pds_refresh_token_encrypted 167 FROM communities 168 WHERE did = $1 169 ` 170 if err := db.QueryRowContext(ctx, query, communityDID).Scan(&encryptedAccess, &encryptedRefresh); err != nil { 171 t.Fatalf("Failed to query encrypted data: %v", err) 172 } 173 174 // Verify encrypted data is NOT the same as plaintext 175 if string(encryptedAccess) == accessToken { 176 t.Error("Access token should be encrypted, but found plaintext in database") 177 } 178 if string(encryptedRefresh) == refreshToken { 179 t.Error("Refresh token should be encrypted, but found plaintext in database") 180 } 181 182 // Verify encrypted data is not empty 183 if len(encryptedAccess) == 0 { 184 t.Error("Expected encrypted access token to have data") 185 } 186 if len(encryptedRefresh) == 0 { 187 t.Error("Expected encrypted refresh token to have data") 188 } 189 190 // Verify repository decrypts correctly 191 retrieved, err := repo.GetByDID(ctx, communityDID) 192 if err != nil { 193 t.Fatalf("Failed to retrieve community: %v", err) 194 } 195 196 if retrieved.PDSAccessToken != accessToken { 197 t.Errorf("Decrypted access token mismatch: expected %s, got %s", accessToken, retrieved.PDSAccessToken) 198 } 199 if retrieved.PDSRefreshToken != refreshToken { 200 t.Errorf("Decrypted refresh token mismatch: expected %s, got %s", refreshToken, retrieved.PDSRefreshToken) 201 } 202 }) 203 204 t.Run("encryption handles special characters", func(t *testing.T) { 205 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 206 communityDID := generateTestDID(uniqueSuffix) 207 208 // Token with special characters 209 specialToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2NvdmVzLnNvY2lhbCIsInN1YiI6ImRpZDpwbGM6YWJjMTIzIiwiaWF0IjoxNzA5MjQwMDAwfQ.special/chars+here==" 210 211 community := &communities.Community{ 212 DID: communityDID, 213 Handle: fmt.Sprintf("!special-test-%s@coves.local", uniqueSuffix), 214 Name: "special-test", 215 OwnerDID: communityDID, 216 CreatedByDID: "did:plc:user123", 217 HostedByDID: "did:web:coves.local", 218 Visibility: "public", 219 PDSAccessToken: specialToken, 220 PDSRefreshToken: "refresh+with/special=chars", 221 CreatedAt: time.Now(), 222 UpdatedAt: time.Now(), 223 } 224 225 if _, err := repo.Create(ctx, community); err != nil { 226 t.Fatalf("Failed to create community with special chars: %v", err) 227 } 228 229 retrieved, err := repo.GetByDID(ctx, communityDID) 230 if err != nil { 231 t.Fatalf("Failed to retrieve community: %v", err) 232 } 233 234 if retrieved.PDSAccessToken != specialToken { 235 t.Errorf("Special characters not preserved during encryption/decryption: expected %s, got %s", specialToken, retrieved.PDSAccessToken) 236 } 237 }) 238} 239 240// TestCommunityRepository_V2OwnershipModel tests that communities are self-owned 241func TestCommunityRepository_V2OwnershipModel(t *testing.T) { 242 db := setupTestDB(t) 243 defer func() { 244 if err := db.Close(); err != nil { 245 t.Logf("Failed to close database: %v", err) 246 } 247 }() 248 249 repo := postgres.NewCommunityRepository(db) 250 ctx := context.Background() 251 252 t.Run("V2 communities are self-owned", func(t *testing.T) { 253 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 254 communityDID := generateTestDID(uniqueSuffix) 255 256 community := &communities.Community{ 257 DID: communityDID, 258 Handle: fmt.Sprintf("!v2-test-%s@coves.local", uniqueSuffix), 259 Name: "v2-test", 260 OwnerDID: communityDID, // V2: owner == community DID 261 CreatedByDID: "did:plc:user123", 262 HostedByDID: "did:web:coves.local", 263 Visibility: "public", 264 CreatedAt: time.Now(), 265 UpdatedAt: time.Now(), 266 } 267 268 created, err := repo.Create(ctx, community) 269 if err != nil { 270 t.Fatalf("Failed to create V2 community: %v", err) 271 } 272 273 // Verify self-ownership 274 if created.OwnerDID != created.DID { 275 t.Errorf("V2 community should be self-owned: expected OwnerDID=%s, got %s", created.DID, created.OwnerDID) 276 } 277 278 retrieved, err := repo.GetByDID(ctx, communityDID) 279 if err != nil { 280 t.Fatalf("Failed to retrieve community: %v", err) 281 } 282 283 if retrieved.OwnerDID != retrieved.DID { 284 t.Errorf("V2 community should be self-owned after retrieval: expected OwnerDID=%s, got %s", retrieved.DID, retrieved.OwnerDID) 285 } 286 }) 287}