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@coves.social", 34 "atprotoHandle": "gaming.communities.coves.social", 35 "name": "gaming", 36 "createdBy": "did:plc:user123", 37 "hostedBy": "did:web:coves.social", 38 "visibility": "public", 39 "federation": map[string]interface{}{ 40 "allowExternalDiscovery": true, 41 }, 42 "memberCount": 0, 43 "subscriberCount": 0, 44 "createdAt": time.Now().Format(time.RFC3339), 45 }, 46 }, 47 } 48 49 err := consumer.HandleEvent(ctx, event) 50 if err != nil { 51 t.Errorf("V2 community with rkey=self should be accepted, got error: %v", err) 52 } 53 54 // Verify community was indexed 55 community, err := repo.GetByDID(ctx, "did:plc:community123") 56 if err != nil { 57 t.Fatalf("Community should have been indexed: %v", err) 58 } 59 60 // Verify V2 self-ownership 61 if community.OwnerDID != community.DID { 62 t.Errorf("V2 community should be self-owned: expected OwnerDID=%s, got %s", community.DID, community.OwnerDID) 63 } 64 65 // Verify record URI uses "self" 66 expectedURI := "at://did:plc:community123/social.coves.community.profile/self" 67 if community.RecordURI != expectedURI { 68 t.Errorf("Expected RecordURI %s, got %s", expectedURI, community.RecordURI) 69 } 70 }) 71 72 t.Run("rejects V1 community with non-self rkey", func(t *testing.T) { 73 event := &jetstream.JetstreamEvent{ 74 Did: "did:plc:community456", 75 Kind: "commit", 76 Commit: &jetstream.CommitEvent{ 77 Operation: "create", 78 Collection: "social.coves.community.profile", 79 RKey: "3k2j4h5g6f7d", // V1: TID-based rkey (INVALID for V2!) 80 CID: "bafyreiv1community", 81 Record: map[string]interface{}{ 82 "$type": "social.coves.community.profile", 83 "handle": "!v1community@coves.social", 84 "name": "v1community", 85 "createdBy": "did:plc:user456", 86 "hostedBy": "did:web:coves.social", 87 "visibility": "public", 88 "federation": map[string]interface{}{ 89 "allowExternalDiscovery": true, 90 }, 91 "memberCount": 0, 92 "subscriberCount": 0, 93 "createdAt": time.Now().Format(time.RFC3339), 94 }, 95 }, 96 } 97 98 err := consumer.HandleEvent(ctx, event) 99 if err == nil { 100 t.Error("V1 community with TID rkey should be rejected") 101 } 102 103 // Verify error message indicates V1 not supported 104 if err != nil { 105 errMsg := err.Error() 106 if errMsg != "invalid community profile rkey: expected 'self', got '3k2j4h5g6f7d' (V1 communities not supported)" { 107 t.Errorf("Expected V1 rejection error, got: %s", errMsg) 108 } 109 } 110 111 // Verify community was NOT indexed 112 _, err = repo.GetByDID(ctx, "did:plc:community456") 113 if err != communities.ErrCommunityNotFound { 114 t.Errorf("V1 community should not have been indexed, expected ErrCommunityNotFound, got: %v", err) 115 } 116 }) 117 118 t.Run("rejects community with custom rkey", func(t *testing.T) { 119 event := &jetstream.JetstreamEvent{ 120 Did: "did:plc:community789", 121 Kind: "commit", 122 Commit: &jetstream.CommitEvent{ 123 Operation: "create", 124 Collection: "social.coves.community.profile", 125 RKey: "custom-profile-name", // Custom rkey (INVALID!) 126 CID: "bafyreicustom", 127 Record: map[string]interface{}{ 128 "$type": "social.coves.community.profile", 129 "handle": "!custom@coves.social", 130 "name": "custom", 131 "createdBy": "did:plc:user789", 132 "hostedBy": "did:web:coves.social", 133 "visibility": "public", 134 "federation": map[string]interface{}{ 135 "allowExternalDiscovery": true, 136 }, 137 "memberCount": 0, 138 "subscriberCount": 0, 139 "createdAt": time.Now().Format(time.RFC3339), 140 }, 141 }, 142 } 143 144 err := consumer.HandleEvent(ctx, event) 145 if err == nil { 146 t.Error("Community with custom rkey should be rejected") 147 } 148 149 // Verify community was NOT indexed 150 _, err = repo.GetByDID(ctx, "did:plc:community789") 151 if err != communities.ErrCommunityNotFound { 152 t.Error("Community with custom rkey should not have been indexed") 153 } 154 }) 155 156 t.Run("update event also requires rkey=self", func(t *testing.T) { 157 // First create a V2 community 158 createEvent := &jetstream.JetstreamEvent{ 159 Did: "did:plc:updatetest", 160 Kind: "commit", 161 Commit: &jetstream.CommitEvent{ 162 Operation: "create", 163 Collection: "social.coves.community.profile", 164 RKey: "self", 165 CID: "bafyreiupdate1", 166 Record: map[string]interface{}{ 167 "$type": "social.coves.community.profile", 168 "handle": "!updatetest@coves.social", 169 "atprotoHandle": "updatetest.communities.coves.social", 170 "name": "updatetest", 171 "createdBy": "did:plc:userUpdate", 172 "hostedBy": "did:web:coves.social", 173 "visibility": "public", 174 "federation": map[string]interface{}{ 175 "allowExternalDiscovery": true, 176 }, 177 "memberCount": 0, 178 "subscriberCount": 0, 179 "createdAt": time.Now().Format(time.RFC3339), 180 }, 181 }, 182 } 183 184 err := consumer.HandleEvent(ctx, createEvent) 185 if err != nil { 186 t.Fatalf("Failed to create community for update test: %v", err) 187 } 188 189 // Try to update with wrong rkey 190 updateEvent := &jetstream.JetstreamEvent{ 191 Did: "did:plc:updatetest", 192 Kind: "commit", 193 Commit: &jetstream.CommitEvent{ 194 Operation: "update", 195 Collection: "social.coves.community.profile", 196 RKey: "wrong-rkey", // INVALID! 197 CID: "bafyreiupdate2", 198 Record: map[string]interface{}{ 199 "$type": "social.coves.community.profile", 200 "handle": "!updatetest@coves.social", 201 "atprotoHandle": "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_AtprotoHandleField tests the V2 atprotoHandle field 235func TestCommunityConsumer_AtprotoHandleField(t *testing.T) { 236 db := setupTestDB(t) 237 defer db.Close() 238 239 repo := postgres.NewCommunityRepository(db) 240 consumer := jetstream.NewCommunityEventConsumer(repo) 241 ctx := context.Background() 242 243 t.Run("indexes community with atprotoHandle field", func(t *testing.T) { 244 uniqueDID := "did:plc:handletestunique987" 245 event := &jetstream.JetstreamEvent{ 246 Did: uniqueDID, 247 Kind: "commit", 248 Commit: &jetstream.CommitEvent{ 249 Operation: "create", 250 Collection: "social.coves.community.profile", 251 RKey: "self", 252 CID: "bafyreihandle", 253 Record: map[string]interface{}{ 254 "$type": "social.coves.community.profile", 255 "handle": "!gamingtest@coves.social", // Scoped handle 256 "atprotoHandle": "gamingtest.communities.coves.social", // Real atProto handle 257 "name": "gamingtest", 258 "createdBy": "did:plc:user123", 259 "hostedBy": "did:web:coves.social", 260 "visibility": "public", 261 "federation": map[string]interface{}{ 262 "allowExternalDiscovery": true, 263 }, 264 "memberCount": 0, 265 "subscriberCount": 0, 266 "createdAt": time.Now().Format(time.RFC3339), 267 }, 268 }, 269 } 270 271 err := consumer.HandleEvent(ctx, event) 272 if err != nil { 273 t.Errorf("Failed to index community with atprotoHandle: %v", err) 274 } 275 276 community, err := repo.GetByDID(ctx, uniqueDID) 277 if err != nil { 278 t.Fatalf("Community should have been indexed: %v", err) 279 } 280 281 // Verify the scoped handle is stored (this is the primary handle field) 282 if community.Handle != "!gamingtest@coves.social" { 283 t.Errorf("Expected handle !gamingtest@coves.social, got %s", community.Handle) 284 } 285 286 // Note: atprotoHandle is informational in the record but not stored separately 287 // The DID is the authoritative identifier for atProto resolution 288 }) 289}