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}