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