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}