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