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}