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