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