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