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