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/jetstream" 10 "Coves/internal/core/communities" 11 "Coves/internal/db/postgres" 12) 13 14// TestCommunityConsumer_V2RKeyValidation tests that only V2 communities (rkey="self") are accepted 15func TestCommunityConsumer_V2RKeyValidation(t *testing.T) { 16 db := setupTestDB(t) 17 defer func() { 18 if err := db.Close(); err != nil { 19 t.Logf("Failed to close database: %v", err) 20 } 21 }() 22 23 repo := postgres.NewCommunityRepository(db) 24 // Skip verification in tests 25 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 26 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.local", true, nil) 27 ctx := context.Background() 28 29 t.Run("accepts V2 community with rkey=self", func(t *testing.T) { 30 // Use unique DID and handle to avoid conflicts with other test runs 31 timestamp := time.Now().UnixNano() 32 testDID := fmt.Sprintf("did:plc:testv2rkey%d", timestamp) 33 testHandle := fmt.Sprintf("testv2rkey%d.community.coves.social", timestamp) 34 35 event := &jetstream.JetstreamEvent{ 36 Did: testDID, 37 Kind: "commit", 38 Commit: &jetstream.CommitEvent{ 39 Operation: "create", 40 Collection: "social.coves.community.profile", 41 RKey: "self", // V2: correct rkey 42 CID: "bafyreigaming123", 43 Record: map[string]interface{}{ 44 "$type": "social.coves.community.profile", 45 "handle": testHandle, 46 "name": "testv2rkey", 47 "createdBy": "did:plc:user123", 48 "hostedBy": "did:web:coves.social", 49 "visibility": "public", 50 "federation": map[string]interface{}{ 51 "allowExternalDiscovery": true, 52 }, 53 "memberCount": 0, 54 "subscriberCount": 0, 55 "createdAt": time.Now().Format(time.RFC3339), 56 }, 57 }, 58 } 59 60 err := consumer.HandleEvent(ctx, event) 61 if err != nil { 62 t.Errorf("V2 community with rkey=self should be accepted, got error: %v", err) 63 } 64 65 // Verify community was indexed 66 community, err := repo.GetByDID(ctx, testDID) 67 if err != nil { 68 t.Fatalf("Community should have been indexed: %v", err) 69 } 70 71 // Verify V2 self-ownership 72 if community.OwnerDID != community.DID { 73 t.Errorf("V2 community should be self-owned: expected OwnerDID=%s, got %s", community.DID, community.OwnerDID) 74 } 75 76 // Verify record URI uses "self" 77 expectedURI := fmt.Sprintf("at://%s/social.coves.community.profile/self", testDID) 78 if community.RecordURI != expectedURI { 79 t.Errorf("Expected RecordURI %s, got %s", expectedURI, community.RecordURI) 80 } 81 }) 82 83 t.Run("rejects V1 community with non-self rkey", func(t *testing.T) { 84 event := &jetstream.JetstreamEvent{ 85 Did: "did:plc:community456", 86 Kind: "commit", 87 Commit: &jetstream.CommitEvent{ 88 Operation: "create", 89 Collection: "social.coves.community.profile", 90 RKey: "3k2j4h5g6f7d", // V1: TID-based rkey (INVALID for V2!) 91 CID: "bafyreiv1community", 92 Record: map[string]interface{}{ 93 "$type": "social.coves.community.profile", 94 "handle": "v1community.community.coves.social", 95 "name": "v1community", 96 "createdBy": "did:plc:user456", 97 "hostedBy": "did:web:coves.social", 98 "visibility": "public", 99 "federation": map[string]interface{}{ 100 "allowExternalDiscovery": true, 101 }, 102 "memberCount": 0, 103 "subscriberCount": 0, 104 "createdAt": time.Now().Format(time.RFC3339), 105 }, 106 }, 107 } 108 109 err := consumer.HandleEvent(ctx, event) 110 if err == nil { 111 t.Error("V1 community with TID rkey should be rejected") 112 } 113 114 // Verify error message indicates V1 not supported 115 if err != nil { 116 errMsg := err.Error() 117 if errMsg != "invalid community profile rkey: expected 'self', got '3k2j4h5g6f7d' (V1 communities not supported)" { 118 t.Errorf("Expected V1 rejection error, got: %s", errMsg) 119 } 120 } 121 122 // Verify community was NOT indexed 123 _, err = repo.GetByDID(ctx, "did:plc:community456") 124 if err != communities.ErrCommunityNotFound { 125 t.Errorf("V1 community should not have been indexed, expected ErrCommunityNotFound, got: %v", err) 126 } 127 }) 128 129 t.Run("rejects community with custom rkey", func(t *testing.T) { 130 event := &jetstream.JetstreamEvent{ 131 Did: "did:plc:community789", 132 Kind: "commit", 133 Commit: &jetstream.CommitEvent{ 134 Operation: "create", 135 Collection: "social.coves.community.profile", 136 RKey: "custom-profile-name", // Custom rkey (INVALID!) 137 CID: "bafyreicustom", 138 Record: map[string]interface{}{ 139 "$type": "social.coves.community.profile", 140 "handle": "custom.community.coves.social", 141 "name": "custom", 142 "createdBy": "did:plc:user789", 143 "hostedBy": "did:web:coves.social", 144 "visibility": "public", 145 "federation": map[string]interface{}{ 146 "allowExternalDiscovery": true, 147 }, 148 "memberCount": 0, 149 "subscriberCount": 0, 150 "createdAt": time.Now().Format(time.RFC3339), 151 }, 152 }, 153 } 154 155 err := consumer.HandleEvent(ctx, event) 156 if err == nil { 157 t.Error("Community with custom rkey should be rejected") 158 } 159 160 // Verify community was NOT indexed 161 _, err = repo.GetByDID(ctx, "did:plc:community789") 162 if err != communities.ErrCommunityNotFound { 163 t.Error("Community with custom rkey should not have been indexed") 164 } 165 }) 166 167 t.Run("update event also requires rkey=self", func(t *testing.T) { 168 // First create a V2 community 169 createEvent := &jetstream.JetstreamEvent{ 170 Did: "did:plc:updatetest", 171 Kind: "commit", 172 Commit: &jetstream.CommitEvent{ 173 Operation: "create", 174 Collection: "social.coves.community.profile", 175 RKey: "self", 176 CID: "bafyreiupdate1", 177 Record: map[string]interface{}{ 178 "$type": "social.coves.community.profile", 179 "handle": "updatetest.community.coves.social", 180 "name": "updatetest", 181 "createdBy": "did:plc:userUpdate", 182 "hostedBy": "did:web:coves.social", 183 "visibility": "public", 184 "federation": map[string]interface{}{ 185 "allowExternalDiscovery": true, 186 }, 187 "memberCount": 0, 188 "subscriberCount": 0, 189 "createdAt": time.Now().Format(time.RFC3339), 190 }, 191 }, 192 } 193 194 err := consumer.HandleEvent(ctx, createEvent) 195 if err != nil { 196 t.Fatalf("Failed to create community for update test: %v", err) 197 } 198 199 // Try to update with wrong rkey 200 updateEvent := &jetstream.JetstreamEvent{ 201 Did: "did:plc:updatetest", 202 Kind: "commit", 203 Commit: &jetstream.CommitEvent{ 204 Operation: "update", 205 Collection: "social.coves.community.profile", 206 RKey: "wrong-rkey", // INVALID! 207 CID: "bafyreiupdate2", 208 Record: map[string]interface{}{ 209 "$type": "social.coves.community.profile", 210 "handle": "updatetest.community.coves.social", 211 "name": "updatetest", 212 "displayName": "Updated Name", 213 "createdBy": "did:plc:userUpdate", 214 "hostedBy": "did:web:coves.social", 215 "visibility": "public", 216 "federation": map[string]interface{}{ 217 "allowExternalDiscovery": true, 218 }, 219 "memberCount": 0, 220 "subscriberCount": 0, 221 "createdAt": time.Now().Format(time.RFC3339), 222 }, 223 }, 224 } 225 226 err = consumer.HandleEvent(ctx, updateEvent) 227 if err == nil { 228 t.Error("Update event with wrong rkey should be rejected") 229 } 230 231 // Verify original community still exists unchanged 232 community, err := repo.GetByDID(ctx, "did:plc:updatetest") 233 if err != nil { 234 t.Fatalf("Original community should still exist: %v", err) 235 } 236 237 if community.DisplayName == "Updated Name" { 238 t.Error("Community should not have been updated with invalid rkey") 239 } 240 }) 241} 242 243// TestCommunityConsumer_HandleField tests the V2 handle field 244func TestCommunityConsumer_HandleField(t *testing.T) { 245 db := setupTestDB(t) 246 defer func() { 247 if err := db.Close(); err != nil { 248 t.Logf("Failed to close database: %v", err) 249 } 250 }() 251 252 repo := postgres.NewCommunityRepository(db) 253 // Skip verification in tests 254 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 255 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.local", true, nil) 256 ctx := context.Background() 257 258 t.Run("indexes community with atProto handle", func(t *testing.T) { 259 uniqueDID := "did:plc:handletestunique987" 260 event := &jetstream.JetstreamEvent{ 261 Did: uniqueDID, 262 Kind: "commit", 263 Commit: &jetstream.CommitEvent{ 264 Operation: "create", 265 Collection: "social.coves.community.profile", 266 RKey: "self", 267 CID: "bafyreihandle", 268 Record: map[string]interface{}{ 269 "$type": "social.coves.community.profile", 270 "handle": "gamingtest.community.coves.social", // atProto handle (DNS-resolvable) 271 "name": "gamingtest", // Short name for !mentions 272 "createdBy": "did:plc:user123", 273 "hostedBy": "did:web:coves.social", 274 "visibility": "public", 275 "federation": map[string]interface{}{ 276 "allowExternalDiscovery": true, 277 }, 278 "memberCount": 0, 279 "subscriberCount": 0, 280 "createdAt": time.Now().Format(time.RFC3339), 281 }, 282 }, 283 } 284 285 err := consumer.HandleEvent(ctx, event) 286 if err != nil { 287 t.Errorf("Failed to index community with handle: %v", err) 288 } 289 290 community, err := repo.GetByDID(ctx, uniqueDID) 291 if err != nil { 292 t.Fatalf("Community should have been indexed: %v", err) 293 } 294 295 // Verify the atProto handle is stored 296 if community.Handle != "gamingtest.community.coves.social" { 297 t.Errorf("Expected handle gamingtest.community.coves.social, got %s", community.Handle) 298 } 299 300 // Note: The DID is the authoritative identifier for atProto resolution 301 // The handle is DNS-resolvable via .well-known/atproto-did 302 }) 303}