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