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