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}