A community based topic aggregation platform built on atproto

feat(communities): migrate handle format from .community. subdomain to c- prefix

Change community handle format to simplify DNS/Caddy configuration:
- Old: gaming.community.coves.social
- New: c-gaming.coves.social

This works with single-level wildcard certificates (*.coves.social)
while keeping the same user-facing display format (!gaming@coves.social).

Changes:
- Add migration 022 to update existing handles in database
- Update handle generation in pds_provisioning.go
- Update GetDisplayHandle() parsing in community.go
- Update scoped identifier resolution in service.go
- Update PDS_SERVICE_HANDLE_DOMAINS in docker-compose.dev.yml

Also addresses PR review feedback:
- Fix LRU cache error handling (panic on critical failure)
- Add logging for JSON marshal failures in facets
- Add DEBUG logging for domain extraction fallbacks
- Add DEBUG logging for GetDisplayHandle parse failures
- Add WARNING log for non-did:web hostedBy
- Add edge case tests for malformed handle inputs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+2 -2
docker-compose.dev.yml
··· 85 85 PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: ${PDS_PLC_ROTATION_KEY:-af514fb84c4356241deed29feb392d1ee359f99c05a7b8f7bff2e5f2614f64b2} 86 86 87 87 # Service endpoints 88 - # Allow both user handles (.local.coves.dev) and community handles (.community.coves.social) 89 - PDS_SERVICE_HANDLE_DOMAINS: ${PDS_SERVICE_HANDLE_DOMAINS:-.local.coves.dev,.community.coves.social} 88 + # Allow both user handles (.local.coves.dev) and community handles (c-*.coves.social) 89 + PDS_SERVICE_HANDLE_DOMAINS: ${PDS_SERVICE_HANDLE_DOMAINS:-.local.coves.dev,.coves.social} 90 90 91 91 # Dev mode settings (allows HTTP instead of HTTPS) 92 92 PDS_DEV_MODE: "true"
+1 -1
internal/api/handlers/community/create_test.go
··· 23 23 } 24 24 return &communities.Community{ 25 25 DID: "did:plc:test123", 26 - Handle: "test.community.coves.social", 26 + Handle: "c-test.coves.social", 27 27 RecordURI: "at://did:plc:test123/social.coves.community.profile/self", 28 28 RecordCID: "bafytest123", 29 29 DisplayName: req.DisplayName,
+43 -12
internal/atproto/jetstream/community_consumer.go
··· 50 50 cache, err := lru.New[string, cachedDIDDoc](1000) 51 51 if err != nil { 52 52 // This should never happen with a valid size, but handle gracefully 53 - log.Printf("WARNING: Failed to create DID cache, verification will be slower: %v", err) 53 + log.Printf("WARNING: Failed to create DID cache (size=1000), verification will be slower: %v", err) 54 54 // Create minimal cache to avoid nil pointer 55 - cache, _ = lru.New[string, cachedDIDDoc](1) 55 + cache, fallbackErr := lru.New[string, cachedDIDDoc](1) 56 + if fallbackErr != nil { 57 + // Both attempts failed - this indicates a serious issue with the LRU library 58 + log.Printf("CRITICAL: Failed to create fallback DID cache (size=1): %v", fallbackErr) 59 + panic(fmt.Sprintf("cannot create LRU cache: primary error=%v, fallback error=%v", err, fallbackErr)) 60 + } 61 + return &CommunityEventConsumer{ 62 + repo: repo, 63 + identityResolver: identityResolver, 64 + instanceDID: instanceDID, 65 + skipVerification: skipVerification, 66 + httpClient: &http.Client{ 67 + Timeout: 10 * time.Second, 68 + Transport: &http.Transport{ 69 + MaxIdleConns: 100, 70 + MaxIdleConnsPerHost: 10, 71 + IdleConnTimeout: 90 * time.Second, 72 + }, 73 + }, 74 + didCache: cache, 75 + wellKnownLimiter: rate.NewLimiter(10, 20), 76 + } 56 77 } 57 78 58 79 return &CommunityEventConsumer{ ··· 219 240 // Handle description facets (rich text) 220 241 if profile.DescriptionFacets != nil { 221 242 facetsJSON, marshalErr := json.Marshal(profile.DescriptionFacets) 222 - if marshalErr == nil { 243 + if marshalErr != nil { 244 + log.Printf("WARNING: Failed to marshal description facets for community %s: %v (facets will be omitted)", did, marshalErr) 245 + } else { 223 246 community.DescriptionFacets = facetsJSON 224 247 } 225 248 } ··· 508 531 // extractDomainFromHandle extracts the registrable domain from a community handle 509 532 // Handles both formats: 510 533 // - Bluesky-style: "!gaming@coves.social" → "coves.social" 511 - // - DNS-style: "gaming.community.coves.social" → "coves.social" 534 + // - DNS-style: "c-gaming.coves.social" → "coves.social" 512 535 // 513 536 // Uses golang.org/x/net/publicsuffix to correctly handle multi-part TLDs: 514 - // - "gaming.community.coves.co.uk" → "coves.co.uk" (not "co.uk") 515 - // - "gaming.community.example.com.au" → "example.com.au" (not "com.au") 537 + // - "c-gaming.coves.co.uk" → "coves.co.uk" (not "co.uk") 538 + // - "c-gaming.example.com.au" → "example.com.au" (not "com.au") 516 539 func extractDomainFromHandle(handle string) string { 517 540 // Remove leading ! if present 518 541 handle = strings.TrimPrefix(handle, "!") ··· 527 550 if err != nil { 528 551 // If publicsuffix fails, fall back to returning the full domain part 529 552 // This handles edge cases like localhost, IP addresses, etc. 553 + log.Printf("DEBUG: publicsuffix failed for @-format handle domain %q, using raw domain: %v", domain, err) 530 554 return domain 531 555 } 532 556 return registrable ··· 534 558 return "" 535 559 } 536 560 537 - // For DNS-style handles (e.g., "gaming.community.coves.social") 561 + // For DNS-style handles (e.g., "c-gaming.coves.social") 538 562 // Extract the registrable domain (eTLD+1) using publicsuffix 539 563 // This correctly handles multi-part TLDs like .co.uk, .com.au, etc. 540 564 registrable, err := publicsuffix.EffectiveTLDPlusOne(handle) 541 565 if err != nil { 542 566 // If publicsuffix fails (e.g., invalid TLD, localhost, IP address) 543 567 // fall back to naive extraction (last 2 parts) 544 - // This maintains backward compatibility for edge cases 568 + // WARNING: This is incorrect for multi-part TLDs (.co.uk -> would return "co.uk") 569 + // but maintains compatibility for localhost/dev environments 545 570 parts := strings.Split(handle, ".") 546 571 if len(parts) < 2 { 572 + log.Printf("DEBUG: Invalid handle format (no dots): %q", handle) 547 573 return "" // Invalid handle 548 574 } 549 - return strings.Join(parts[len(parts)-2:], ".") 575 + fallbackDomain := strings.Join(parts[len(parts)-2:], ".") 576 + log.Printf("DEBUG: publicsuffix failed for handle %q, using naive fallback: %q (error: %v)", handle, fallbackDomain, err) 577 + return fallbackDomain 550 578 } 551 579 552 580 return registrable ··· 788 816 } 789 817 790 818 // constructHandleFromProfile constructs a deterministic handle from profile data 791 - // Format: {name}.community.{instanceDomain} 792 - // Example: gaming.community.coves.social 819 + // Format: c-{name}.{instanceDomain} 820 + // Example: c-gaming.coves.social 793 821 // This is ONLY used in test mode (when identity resolver is nil) 794 822 // Production MUST resolve handles from PLC (source of truth) 795 823 // Returns empty string if hostedBy is not did:web format (caller will fail validation) 796 824 func constructHandleFromProfile(profile *CommunityProfile) string { 797 825 if !strings.HasPrefix(profile.HostedBy, "did:web:") { 798 826 // hostedBy must be did:web format for handle construction 827 + // Log warning since this indicates invalid community data 828 + log.Printf("WARNING: constructHandleFromProfile: hostedBy %q is not did:web format, cannot construct handle for community %q", 829 + profile.HostedBy, profile.Name) 799 830 // Return empty to trigger validation error in repository 800 831 return "" 801 832 } 802 833 instanceDomain := strings.TrimPrefix(profile.HostedBy, "did:web:") 803 - return fmt.Sprintf("%s.community.%s", profile.Name, instanceDomain) 834 + return fmt.Sprintf("c-%s.%s", profile.Name, instanceDomain) 804 835 } 805 836 806 837 // extractContentVisibility extracts contentVisibility from subscription record with clamping
+6 -6
internal/core/comments/comment_service_test.go
··· 458 458 author := createTestUser(authorDID, "author.test") 459 459 _, _ = userRepo.Create(context.Background(), author) 460 460 461 - community := createTestCommunity(communityDID, "test.community.coves.social") 461 + community := createTestCommunity(communityDID, "c-test.coves.social") 462 462 _, _ = communityRepo.Create(context.Background(), community) 463 463 464 464 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0) ··· 579 579 author := createTestUser(authorDID, "author.test") 580 580 _, _ = userRepo.Create(context.Background(), author) 581 581 582 - community := createTestCommunity(communityDID, "test.community.coves.social") 582 + community := createTestCommunity(communityDID, "c-test.coves.social") 583 583 _, _ = communityRepo.Create(context.Background(), community) 584 584 585 585 commentRepo.listByParentWithHotRankFunc = func(ctx context.Context, parentURI, sort, timeframe string, limit int, cursor *string) ([]*Comment, *string, error) { ··· 625 625 author := createTestUser(authorDID, "author.test") 626 626 _, _ = userRepo.Create(context.Background(), author) 627 627 628 - community := createTestCommunity(communityDID, "test.community.coves.social") 628 + community := createTestCommunity(communityDID, "c-test.coves.social") 629 629 _, _ = communityRepo.Create(context.Background(), community) 630 630 631 631 comment1URI := "at://did:plc:commenter123/comment/1" ··· 694 694 author := createTestUser(authorDID, "author.test") 695 695 _, _ = userRepo.Create(context.Background(), author) 696 696 697 - community := createTestCommunity(communityDID, "test.community.coves.social") 697 + community := createTestCommunity(communityDID, "c-test.coves.social") 698 698 _, _ = communityRepo.Create(context.Background(), community) 699 699 700 700 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0) ··· 762 762 author := createTestUser(authorDID, "author.test") 763 763 _, _ = userRepo.Create(context.Background(), author) 764 764 765 - community := createTestCommunity(communityDID, "test.community.coves.social") 765 + community := createTestCommunity(communityDID, "c-test.coves.social") 766 766 _, _ = communityRepo.Create(context.Background(), community) 767 767 768 768 comment1 := createTestComment("at://did:plc:commenter123/comment/1", commenterDID, "commenter.test", postURI, postURI, 0) ··· 813 813 author := createTestUser(authorDID, "author.test") 814 814 _, _ = userRepo.Create(context.Background(), author) 815 815 816 - community := createTestCommunity(communityDID, "test.community.coves.social") 816 + community := createTestCommunity(communityDID, "c-test.coves.social") 817 817 _, _ = communityRepo.Create(context.Background(), community) 818 818 819 819 // Mock repository error
+23 -13
internal/core/communities/community.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "log" 5 6 "strings" 6 7 "time" 7 8 ) ··· 141 142 142 143 // GetDisplayHandle returns the user-facing display format for a community handle 143 144 // Following Bluesky's pattern where client adds @ prefix for users, but for communities we use ! prefix 144 - // Example: "gardening.community.coves.social" -> "!gardening@coves.social" 145 + // Example: "c-gardening.coves.social" -> "!gardening@coves.social" 145 146 // 146 147 // Handles various domain formats correctly: 147 - // - "gaming.community.coves.social" -> "!gaming@coves.social" 148 - // - "gaming.community.coves.co.uk" -> "!gaming@coves.co.uk" 149 - // - "test.community.dev.coves.social" -> "!test@dev.coves.social" 148 + // - "c-gaming.coves.social" -> "!gaming@coves.social" 149 + // - "c-gaming.coves.co.uk" -> "!gaming@coves.co.uk" 150 + // - "c-test.dev.coves.social" -> "!test@dev.coves.social" 150 151 func (c *Community) GetDisplayHandle() string { 151 - // Find the ".community." substring in the handle 152 - communityIndex := strings.Index(c.Handle, ".community.") 153 - if communityIndex == -1 { 154 - // Fallback if format doesn't match expected pattern 152 + // Handle format: c-{name}.{instance} 153 + if !strings.HasPrefix(c.Handle, "c-") { 154 + log.Printf("DEBUG: GetDisplayHandle: handle %q missing c- prefix, returning raw handle", c.Handle) 155 + return c.Handle // Fallback for invalid format 156 + } 157 + 158 + // Remove "c-" prefix and find first dot 159 + afterPrefix := c.Handle[2:] 160 + dotIndex := strings.Index(afterPrefix, ".") 161 + if dotIndex == -1 { 162 + log.Printf("DEBUG: GetDisplayHandle: handle %q has no dot after c- prefix, returning raw handle", c.Handle) 155 163 return c.Handle 156 164 } 157 165 158 - // Extract name (everything before ".community.") 159 - name := c.Handle[:communityIndex] 166 + // Edge case: "c-." would result in empty name 167 + if dotIndex == 0 { 168 + log.Printf("DEBUG: GetDisplayHandle: handle %q has empty name after c- prefix, returning raw handle", c.Handle) 169 + return c.Handle 170 + } 160 171 161 - // Extract instance domain (everything after ".community.") 162 - communitySegment := ".community." 163 - instanceDomain := c.Handle[communityIndex+len(communitySegment):] 172 + name := afterPrefix[:dotIndex] 173 + instanceDomain := afterPrefix[dotIndex+1:] 164 174 165 175 return fmt.Sprintf("!%s@%s", name, instanceDomain) 166 176 }
+5 -6
internal/core/communities/pds_provisioning.go
··· 67 67 } 68 68 69 69 // 1. Generate unique handle for the community 70 - // Format: {name}.community.{instance-domain} 71 - // Example: "gaming.community.coves.social" 72 - // NOTE: Using SINGULAR "community" to follow atProto lexicon conventions 73 - // (all record types use singular: app.bsky.feed.post, app.bsky.graph.follow, etc.) 74 - handle := fmt.Sprintf("%s.community.%s", strings.ToLower(communityName), p.instanceDomain) 70 + // Format: c-{name}.{instance-domain} 71 + // Example: "c-gaming.coves.social" 72 + // Uses c- prefix to distinguish from user handles while keeping single-level subdomain 73 + handle := fmt.Sprintf("c-%s.%s", strings.ToLower(communityName), p.instanceDomain) 75 74 76 75 // 2. Generate system email for PDS account management 77 76 // This email is used for account operations, not for user communication 78 - email := fmt.Sprintf("community-%s@community.%s", strings.ToLower(communityName), p.instanceDomain) 77 + email := fmt.Sprintf("c-%s@%s", strings.ToLower(communityName), p.instanceDomain) 79 78 80 79 // 3. Generate secure random password (32 characters) 81 80 // This password is never shown to users - it's for Coves to authenticate as the community
+4 -4
internal/core/communities/service.go
··· 891 891 // resolveScopedIdentifier handles Coves-specific !name@instance format 892 892 // Formats accepted: 893 893 // 894 - // !gardening@coves.social -> gardening.community.coves.social 894 + // !gardening@coves.social -> c-gardening.coves.social 895 895 func (s *communityService) resolveScopedIdentifier(ctx context.Context, scoped string) (string, error) { 896 896 // Remove ! prefix 897 897 scoped = strings.TrimPrefix(scoped, "!") ··· 934 934 fmt.Sprintf("community is not hosted on this instance (expected @%s)", s.instanceDomain)) 935 935 } 936 936 937 - // Construct canonical handle: {name}.community.{instanceDomain} 937 + // Construct canonical handle: c-{name}.{instanceDomain} 938 938 // Both name and instanceDomain are normalized to lowercase for consistent DB lookup 939 - canonicalHandle := fmt.Sprintf("%s.community.%s", 939 + canonicalHandle := fmt.Sprintf("c-%s.%s", 940 940 strings.ToLower(name), 941 - instanceDomain) // Already normalized to lowercase on line 923 941 + instanceDomain) // Already normalized to lowercase above 942 942 943 943 // Look up by canonical handle 944 944 community, err := s.repo.GetByHandle(ctx, canonicalHandle)
+49
internal/db/migrations/022_migrate_community_handles_to_c_prefix.sql
··· 1 + -- +goose Up 2 + -- +goose StatementBegin 3 + 4 + -- Migration: Change community handles from .community. subdomain to c- prefix 5 + -- This simplifies DNS/Caddy configuration (works with *.coves.social wildcard) 6 + -- 7 + -- Examples: 8 + -- gardening.community.coves.social -> c-gardening.coves.social 9 + -- gaming.community.coves.social -> c-gaming.coves.social 10 + -- 11 + -- Also updates the system email format: 12 + -- community-gardening@community.coves.social -> c-gardening@coves.social 13 + 14 + -- Update community handles in the communities table 15 + UPDATE communities 16 + SET handle = 'c-' || SPLIT_PART(handle, '.community.', 1) || '.' || SPLIT_PART(handle, '.community.', 2) 17 + WHERE handle LIKE '%.community.%'; 18 + 19 + -- Update email addresses to match new format 20 + -- Old: community-{name}@community.{instance} 21 + -- New: c-{name}@{instance} 22 + UPDATE communities 23 + SET pds_email = 'c-' || SUBSTRING(pds_email FROM 11 FOR POSITION('@' IN pds_email) - 11) || '@' || SUBSTRING(pds_email FROM POSITION('@community.' IN pds_email) + 11) 24 + WHERE pds_email LIKE 'community-%@community.%'; 25 + 26 + -- +goose StatementEnd 27 + 28 + -- +goose Down 29 + -- +goose StatementBegin 30 + 31 + -- Rollback: Revert handles from c- prefix back to .community. subdomain 32 + -- Parse: c-{name}.{instance} -> {name}.community.{instance} 33 + 34 + UPDATE communities 35 + SET handle = SUBSTRING(handle FROM 3 FOR POSITION('.' IN SUBSTRING(handle FROM 3)) - 1) 36 + || '.community.' 37 + || SUBSTRING(handle FROM POSITION('.' IN SUBSTRING(handle FROM 3)) + 3) 38 + WHERE handle LIKE 'c-%' AND handle NOT LIKE '%.community.%'; 39 + 40 + -- Revert email addresses 41 + -- New: c-{name}@{instance} 42 + -- Old: community-{name}@community.{instance} 43 + UPDATE communities 44 + SET pds_email = 'community-' || SUBSTRING(pds_email FROM 3 FOR POSITION('@' IN pds_email) - 3) 45 + || '@community.' 46 + || SUBSTRING(pds_email FROM POSITION('@' IN pds_email) + 1) 47 + WHERE pds_email LIKE 'c-%@%' AND pds_email NOT LIKE 'community-%@community.%'; 48 + 49 + -- +goose StatementEnd
+2 -2
tests/integration/aggregator_e2e_test.go
··· 211 211 t.Log("\n🔐 Part 2: Create community account and authorize aggregator...") 212 212 213 213 // STEP 1: Create community account on real PDS 214 - // Use PDS configured domain (.community.coves.social for communities) 214 + // Use PDS configured domain (c-{name}.coves.social for communities) 215 215 // Keep handle short to avoid PDS "handle too long" error 216 216 timestamp := time.Now().Unix() % 100000 // Last 5 digits 217 - communityHandle := fmt.Sprintf("e2e-%d.community.coves.social", timestamp) 217 + communityHandle := fmt.Sprintf("c-e2e-%d.coves.social", timestamp) 218 218 communityEmail := fmt.Sprintf("comm-%d@test.com", timestamp) 219 219 communityPassword := "community-test-password-123" 220 220
+2 -2
tests/integration/block_handle_resolution_test.go
··· 231 231 // To properly test invalid handle → 404, we'd need to add auth middleware context 232 232 // For now, we just verify that the resolution code doesn't crash 233 233 reqBody := map[string]string{ 234 - "community": "nonexistent.community.coves.social", 234 + "community": "c-nonexistent.coves.social", 235 235 } 236 236 reqJSON, _ := json.Marshal(reqBody) 237 237 ··· 317 317 t.Run("Unblock with invalid handle", func(t *testing.T) { 318 318 // Note: Without auth context, returns 401 before reaching resolution 319 319 reqBody := map[string]string{ 320 - "community": "fake.community.coves.social", 320 + "community": "c-fake.coves.social", 321 321 } 322 322 reqJSON, _ := json.Marshal(reqBody) 323 323
+5 -5
tests/integration/community_consumer_test.go
··· 27 27 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 28 28 communityDID := generateTestDID(uniqueSuffix) 29 29 communityName := fmt.Sprintf("test-community-%s", uniqueSuffix) 30 - expectedHandle := fmt.Sprintf("%s.community.coves.local", communityName) 30 + expectedHandle := fmt.Sprintf("c-%s.coves.local", communityName) 31 31 32 32 // Set up mock resolver for this test DID 33 33 mockResolver := newMockIdentityResolver() ··· 89 89 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 90 90 communityDID := generateTestDID(uniqueSuffix) 91 91 communityName := fmt.Sprintf("update-test-%s", uniqueSuffix) 92 - expectedHandle := fmt.Sprintf("%s.community.coves.local", communityName) 92 + expectedHandle := fmt.Sprintf("c-%s.coves.local", communityName) 93 93 94 94 // Set up mock resolver for this test DID 95 95 mockResolver := newMockIdentityResolver() ··· 174 174 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 175 175 communityDID := generateTestDID(uniqueSuffix) 176 176 communityName := fmt.Sprintf("delete-test-%s", uniqueSuffix) 177 - expectedHandle := fmt.Sprintf("%s.community.coves.local", communityName) 177 + expectedHandle := fmt.Sprintf("c-%s.coves.local", communityName) 178 178 179 179 // Set up mock resolver for this test DID 180 180 mockResolver := newMockIdentityResolver() ··· 239 239 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 240 240 communityDID := generateTestDID(uniqueSuffix) 241 241 communityName := fmt.Sprintf("sub-test-%s", uniqueSuffix) 242 - expectedHandle := fmt.Sprintf("%s.community.coves.local", communityName) 242 + expectedHandle := fmt.Sprintf("c-%s.coves.local", communityName) 243 243 244 244 // Set up mock resolver for this test DID 245 245 mockResolver := newMockIdentityResolver() ··· 418 418 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 419 419 communityDID := generateTestDID(uniqueSuffix) 420 420 communityName := fmt.Sprintf("test-plc-%s", uniqueSuffix) 421 - expectedHandle := fmt.Sprintf("%s.community.coves.social", communityName) 421 + expectedHandle := fmt.Sprintf("c-%s.coves.social", communityName) 422 422 423 423 // Create mock resolver 424 424 mockResolver := newMockIdentityResolver()
+5 -5
tests/integration/community_e2e_test.go
··· 109 109 110 110 // Initialize OAuth auth middleware for E2E testing 111 111 e2eAuth := NewE2EOAuthMiddleware() 112 - // Register the instance user for OAuth authentication 113 - token := e2eAuth.AddUser(instanceDID) 112 + // Register the instance user with their REAL PDS access token for write-forward operations 113 + token := e2eAuth.AddUserWithPDSToken(instanceDID, accessToken, pdsURL) 114 114 115 115 // V2.0: Extract instance domain for community provisioning 116 116 var instanceDomain string ··· 159 159 // ==================================================================================== 160 160 t.Run("1. Write-Forward to PDS", func(t *testing.T) { 161 161 // Use shorter names to avoid "Handle too long" errors 162 - // atProto handles max: 63 chars, format: name.community.coves.social 162 + // atProto handles max: 63 chars, format: c-name.coves.social 163 163 communityName := fmt.Sprintf("e2e-%d", time.Now().Unix()) 164 164 165 165 createReq := communities.CreateCommunityRequest{ ··· 191 191 192 192 // V2: Verify PDS account was created for the community 193 193 t.Logf("\n🔍 V2: Verifying community PDS account exists...") 194 - expectedHandle := fmt.Sprintf("%s.community.%s", communityName, instanceDomain) 194 + expectedHandle := fmt.Sprintf("c-%s.%s", communityName, instanceDomain) 195 195 t.Logf(" Expected handle: %s", expectedHandle) 196 - t.Logf(" (Using subdomain: *.community.%s)", instanceDomain) 196 + t.Logf(" (Using subdomain: c-*.%s)", instanceDomain) 197 197 198 198 accountDID, accountHandle, err := queryPDSAccount(pdsURL, expectedHandle) 199 199 if err != nil {
+14 -14
tests/integration/community_hostedby_security_test.go
··· 31 31 32 32 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 33 33 communityDID := generateTestDID(uniqueSuffix) 34 - uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 34 + uniqueHandle := fmt.Sprintf("c-gaming%s.coves.social", uniqueSuffix) 35 35 36 36 // Attempt to create community claiming to be hosted by nintendo.com 37 37 // but with a coves.social handle (ATTACK!) ··· 91 91 92 92 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 93 93 communityDID := generateTestDID(uniqueSuffix) 94 - uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 94 + uniqueHandle := fmt.Sprintf("c-gaming%s.coves.social", uniqueSuffix) 95 95 96 96 // Create community with matching hostedBy and handle domains 97 97 event := &jetstream.JetstreamEvent{ ··· 145 145 146 146 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 147 147 communityDID := generateTestDID(uniqueSuffix) 148 - uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 148 + uniqueHandle := fmt.Sprintf("c-gaming%s.coves.social", uniqueSuffix) 149 149 150 150 // Attempt to use did:plc for hostedBy (not allowed) 151 151 event := &jetstream.JetstreamEvent{ ··· 191 191 192 192 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 193 193 communityDID := generateTestDID(uniqueSuffix) 194 - uniqueHandle := fmt.Sprintf("gaming%s.community.example.com", uniqueSuffix) 194 + uniqueHandle := fmt.Sprintf("c-gaming%s.example.com", uniqueSuffix) 195 195 196 196 // Even with mismatched domain, this should succeed with skipVerification=true 197 197 event := &jetstream.JetstreamEvent{ ··· 278 278 279 279 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 280 280 communityDID := generateTestDID(uniqueSuffix) 281 - uniqueHandle := fmt.Sprintf("gaming%s.community.%s", uniqueSuffix, mockDomain) 281 + uniqueHandle := fmt.Sprintf("c-gaming%s.%s", uniqueSuffix, mockDomain) 282 282 283 283 event := &jetstream.JetstreamEvent{ 284 284 Did: communityDID, ··· 351 351 352 352 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 353 353 communityDID := generateTestDID(uniqueSuffix) 354 - uniqueHandle := fmt.Sprintf("gaming%s.community.%s", uniqueSuffix, mockDomain) 354 + uniqueHandle := fmt.Sprintf("c-gaming%s.%s", uniqueSuffix, mockDomain) 355 355 356 356 event := &jetstream.JetstreamEvent{ 357 357 Did: communityDID, ··· 411 411 }{ 412 412 { 413 413 name: "DNS-style handle with subdomain", 414 - handle: "gaming.community.coves.social", 414 + handle: "c-gaming.coves.social", 415 415 hostedByDID: "did:web:coves.social", 416 416 shouldSucceed: true, 417 417 }, ··· 423 423 }, 424 424 { 425 425 name: "Multi-part subdomain", 426 - handle: "gaming.test.community.example.com", 426 + handle: "c-gaming.test.example.com", 427 427 hostedByDID: "did:web:example.com", 428 428 shouldSucceed: true, 429 429 }, 430 430 { 431 431 name: "Mismatched domain", 432 - handle: "gaming.community.coves.social", 432 + handle: "c-gaming.coves.social", 433 433 hostedByDID: "did:web:example.com", 434 434 shouldSucceed: false, 435 435 }, 436 436 // CRITICAL: Multi-part TLD tests (PR review feedback) 437 437 { 438 438 name: "Multi-part TLD: .co.uk", 439 - handle: "gaming.community.coves.co.uk", 439 + handle: "c-gaming.coves.co.uk", 440 440 hostedByDID: "did:web:coves.co.uk", 441 441 shouldSucceed: true, 442 442 }, 443 443 { 444 444 name: "Multi-part TLD: .com.au", 445 - handle: "gaming.community.example.com.au", 445 + handle: "c-gaming.example.com.au", 446 446 hostedByDID: "did:web:example.com.au", 447 447 shouldSucceed: true, 448 448 }, 449 449 { 450 450 name: "Multi-part TLD: Reject incorrect .co.uk extraction", 451 - handle: "gaming.community.coves.co.uk", 451 + handle: "c-gaming.coves.co.uk", 452 452 hostedByDID: "did:web:co.uk", // Wrong! Should be coves.co.uk 453 453 shouldSucceed: false, 454 454 }, 455 455 { 456 456 name: "Multi-part TLD: .org.uk", 457 - handle: "gaming.community.myinstance.org.uk", 457 + handle: "c-gaming.myinstance.org.uk", 458 458 hostedByDID: "did:web:myinstance.org.uk", 459 459 shouldSucceed: true, 460 460 }, 461 461 { 462 462 name: "Multi-part TLD: .ac.uk", 463 - handle: "gaming.community.university.ac.uk", 463 + handle: "c-gaming.university.ac.uk", 464 464 hostedByDID: "did:web:university.ac.uk", 465 465 shouldSucceed: true, 466 466 },
+17 -12
tests/integration/community_identifier_resolution_test.go
··· 102 102 103 103 t.Run("resolves uppercase canonical handle (case-insensitive)", func(t *testing.T) { 104 104 // Use actual community handle in uppercase 105 - upperHandle := fmt.Sprintf("%s.COMMUNITY.%s", uniqueName, strings.ToUpper(instanceDomain)) 105 + upperHandle := fmt.Sprintf("c-%s.%s", uniqueName, strings.ToUpper(instanceDomain)) 106 106 did, err := service.ResolveCommunityIdentifier(ctx, upperHandle) 107 107 require.NoError(t, err) 108 108 assert.Equal(t, community.DID, did) 109 109 }) 110 110 111 111 t.Run("rejects non-existent canonical handle", func(t *testing.T) { 112 - _, err := service.ResolveCommunityIdentifier(ctx, fmt.Sprintf("nonexistent.community.%s", instanceDomain)) 112 + _, err := service.ResolveCommunityIdentifier(ctx, fmt.Sprintf("c-nonexistent.%s", instanceDomain)) 113 113 require.Error(t, err) 114 114 assert.Contains(t, err.Error(), "community not found") 115 115 }) ··· 124 124 }) 125 125 126 126 t.Run("resolves @-prefixed handle with uppercase (case-insensitive)", func(t *testing.T) { 127 - atHandle := "@" + fmt.Sprintf("%s.COMMUNITY.%s", uniqueName, strings.ToUpper(instanceDomain)) 127 + atHandle := "@" + fmt.Sprintf("c-%s.%s", uniqueName, strings.ToUpper(instanceDomain)) 128 128 did, err := service.ResolveCommunityIdentifier(ctx, atHandle) 129 129 require.NoError(t, err) 130 130 assert.Equal(t, community.DID, did) ··· 330 330 }{ 331 331 { 332 332 name: "standard two-part domain", 333 - handle: "gardening.community.coves.social", 333 + handle: "c-gardening.coves.social", 334 334 expectedDisplay: "!gardening@coves.social", 335 335 }, 336 336 { 337 337 name: "multi-part TLD", 338 - handle: "gaming.community.coves.co.uk", 338 + handle: "c-gaming.coves.co.uk", 339 339 expectedDisplay: "!gaming@coves.co.uk", 340 340 }, 341 341 { 342 342 name: "subdomain instance", 343 - handle: "test.community.dev.coves.social", 343 + handle: "c-test.dev.coves.social", 344 344 expectedDisplay: "!test@dev.coves.social", 345 345 }, 346 346 { 347 347 name: "single part name", 348 - handle: "a.community.coves.social", 348 + handle: "c-a.coves.social", 349 349 expectedDisplay: "!a@coves.social", 350 350 }, 351 351 } ··· 368 368 testCases := []struct { 369 369 handle string 370 370 fallback string 371 + desc string 371 372 }{ 372 - {"nodots", "nodots"}, // No dots - should return as-is 373 - {"single.dot", "single.dot"}, // Single dot - should return as-is 374 - {"", ""}, // Empty - should return as-is 373 + {"nodots", "nodots", "No dots - should return as-is"}, 374 + {"single.dot", "single.dot", "Single dot without c- prefix - should return as-is"}, 375 + {"", "", "Empty - should return as-is"}, 376 + {"c-", "c-", "Prefix only, no name - should return as-is"}, 377 + {"c-.", "c-.", "Prefix with empty name - should return as-is"}, 378 + {"c-.coves.social", "c-.coves.social", "Prefix with dot but no name - should return as-is"}, 379 + {"c-nodot", "c-nodot", "Prefix but no dot after name - should return as-is"}, 375 380 } 376 381 377 382 for _, tc := range testCases { ··· 379 384 Handle: tc.handle, 380 385 } 381 386 result := community.GetDisplayHandle() 382 - assert.Equal(t, tc.fallback, result, "Should fallback to original handle for: %s", tc.handle) 387 + assert.Equal(t, tc.fallback, result, "Should fallback to original handle for: %s (%s)", tc.handle, tc.desc) 383 388 } 384 389 }) 385 390 } ··· 433 438 }) 434 439 435 440 t.Run("handle error includes identifier", func(t *testing.T) { 436 - testHandle := fmt.Sprintf("nonexistent.community.%s", instanceDomain) 441 + testHandle := fmt.Sprintf("c-nonexistent.%s", instanceDomain) 437 442 _, err := service.ResolveCommunityIdentifier(ctx, testHandle) 438 443 require.Error(t, err) 439 444 assert.Contains(t, err.Error(), "community not found")
+9 -9
tests/integration/community_provisioning_test.go
··· 29 29 30 30 community := &communities.Community{ 31 31 DID: generateTestDID(uniqueSuffix), 32 - Handle: fmt.Sprintf("test-encryption-%s.community.test.local", uniqueSuffix), 32 + Handle: fmt.Sprintf("c-test-encryption-%s.test.local", uniqueSuffix), 33 33 Name: "test-encryption", 34 34 DisplayName: "Test Encryption", 35 35 Description: "Testing password encryption", ··· 100 100 101 101 community := &communities.Community{ 102 102 DID: generateTestDID(uniqueSuffix), 103 - Handle: fmt.Sprintf("test-empty-pass-%s.community.test.local", uniqueSuffix), 103 + Handle: fmt.Sprintf("c-test-empty-pass-%s.test.local", uniqueSuffix), 104 104 Name: "test-empty-pass", 105 105 DisplayName: "Test Empty Password", 106 106 Description: "Testing empty password handling", ··· 332 332 333 333 community := &communities.Community{ 334 334 DID: generateTestDID(uniqueSuffix), 335 - Handle: fmt.Sprintf("pwd-unique-%s.community.test.local", uniqueSuffix), 335 + Handle: fmt.Sprintf("c-pwd-unique-%s.test.local", uniqueSuffix), 336 336 Name: fmt.Sprintf("pwd-unique-%s", uniqueSuffix), 337 337 DisplayName: fmt.Sprintf("Password Unique Test %d", i), 338 338 Description: "Testing password uniqueness", ··· 402 402 403 403 community := &communities.Community{ 404 404 DID: generateTestDID(uniqueSuffix), 405 - Handle: fmt.Sprintf("test-pwd-len-%s.community.test.local", uniqueSuffix), 405 + Handle: fmt.Sprintf("c-test-pwd-len-%s.test.local", uniqueSuffix), 406 406 Name: "test-pwd-len", 407 407 DisplayName: "Test Password Length", 408 408 Description: "Testing password length requirements", ··· 470 470 uniqueSuffix := fmt.Sprintf("%d-%d", time.Now().UnixNano(), idx) 471 471 community := &communities.Community{ 472 472 DID: generateTestDID(uniqueSuffix), 473 - Handle: fmt.Sprintf("%s.community.test.local", sameName), 473 + Handle: fmt.Sprintf("c-%s.test.local", sameName), 474 474 Name: sameName, 475 475 DisplayName: "Concurrent Test", 476 476 Description: "Testing concurrent creation", ··· 530 530 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 531 531 community := &communities.Community{ 532 532 DID: generateTestDID(uniqueSuffix), 533 - Handle: fmt.Sprintf("read-test-%s.community.test.local", uniqueSuffix), 533 + Handle: fmt.Sprintf("c-read-test-%s.test.local", uniqueSuffix), 534 534 Name: "read-test", 535 535 DisplayName: "Read Test", 536 536 Description: "Testing concurrent reads", ··· 719 719 720 720 community := &communities.Community{ 721 721 DID: generateTestDID(uniqueSuffix), 722 - Handle: fmt.Sprintf("token-test-%s.community.test.local", uniqueSuffix), 722 + Handle: fmt.Sprintf("c-token-test-%s.test.local", uniqueSuffix), 723 723 Name: "token-test", 724 724 DisplayName: "Token Test", 725 725 Description: "Testing token storage", ··· 784 784 785 785 community := &communities.Community{ 786 786 DID: generateTestDID(uniqueSuffix), 787 - Handle: fmt.Sprintf("empty-token-%s.community.test.local", uniqueSuffix), 787 + Handle: fmt.Sprintf("c-empty-token-%s.test.local", uniqueSuffix), 788 788 Name: "empty-token", 789 789 DisplayName: "Empty Token Test", 790 790 Description: "Testing empty token handling", ··· 832 832 833 833 community := &communities.Community{ 834 834 DID: generateTestDID(uniqueSuffix), 835 - Handle: fmt.Sprintf("encrypted-token-%s.community.test.local", uniqueSuffix), 835 + Handle: fmt.Sprintf("c-encrypted-token-%s.test.local", uniqueSuffix), 836 836 Name: "encrypted-token", 837 837 DisplayName: "Encrypted Token Test", 838 838 Description: "Testing token encryption",
+2 -2
tests/integration/community_service_integration_test.go
··· 55 55 56 56 t.Run("creates community with real PDS provisioning", func(t *testing.T) { 57 57 // Create provisioner and service (production code path) 58 - // Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as .community.coves.social) 58 + // Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as c-{name}.coves.social) 59 59 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 60 60 service := communities.NewCommunityService( 61 61 repo, ··· 114 114 t.Logf("✅ Real DID generated: %s", community.DID) 115 115 116 116 // Verify handle format 117 - expectedHandle := fmt.Sprintf("%s.community.coves.social", uniqueName) 117 + expectedHandle := fmt.Sprintf("c-%s.coves.social", uniqueName) 118 118 if community.Handle != expectedHandle { 119 119 t.Errorf("Expected handle %s, got %s", expectedHandle, community.Handle) 120 120 }
+8 -8
tests/integration/community_v2_validation_test.go
··· 29 29 // Use unique DID and handle to avoid conflicts with other test runs 30 30 timestamp := time.Now().UnixNano() 31 31 testDID := fmt.Sprintf("did:plc:testv2rkey%d", timestamp) 32 - testHandle := fmt.Sprintf("testv2rkey%d.community.coves.social", timestamp) 32 + testHandle := fmt.Sprintf("c-testv2rkey%d.coves.social", timestamp) 33 33 34 34 event := &jetstream.JetstreamEvent{ 35 35 Did: testDID, ··· 90 90 CID: "bafyreiv1community", 91 91 Record: map[string]interface{}{ 92 92 "$type": "social.coves.community.profile", 93 - "handle": "v1community.community.coves.social", 93 + "handle": "c-v1community.coves.social", 94 94 "name": "v1community", 95 95 "createdBy": "did:plc:user456", 96 96 "hostedBy": "did:web:coves.social", ··· 136 136 CID: "bafyreicustom", 137 137 Record: map[string]interface{}{ 138 138 "$type": "social.coves.community.profile", 139 - "handle": "custom.community.coves.social", 139 + "handle": "c-custom.coves.social", 140 140 "name": "custom", 141 141 "createdBy": "did:plc:user789", 142 142 "hostedBy": "did:web:coves.social", ··· 175 175 CID: "bafyreiupdate1", 176 176 Record: map[string]interface{}{ 177 177 "$type": "social.coves.community.profile", 178 - "handle": "updatetest.community.coves.social", 178 + "handle": "c-updatetest.coves.social", 179 179 "name": "updatetest", 180 180 "createdBy": "did:plc:userUpdate", 181 181 "hostedBy": "did:web:coves.social", ··· 206 206 CID: "bafyreiupdate2", 207 207 Record: map[string]interface{}{ 208 208 "$type": "social.coves.community.profile", 209 - "handle": "updatetest.community.coves.social", 209 + "handle": "c-updatetest.coves.social", 210 210 "name": "updatetest", 211 211 "displayName": "Updated Name", 212 212 "createdBy": "did:plc:userUpdate", ··· 266 266 CID: "bafyreihandle", 267 267 Record: map[string]interface{}{ 268 268 "$type": "social.coves.community.profile", 269 - "handle": "gamingtest.community.coves.social", // atProto handle (DNS-resolvable) 269 + "handle": "c-gamingtest.coves.social", // atProto handle (DNS-resolvable) 270 270 "name": "gamingtest", // Short name for !mentions 271 271 "createdBy": "did:plc:user123", 272 272 "hostedBy": "did:web:coves.social", ··· 292 292 } 293 293 294 294 // Verify the atProto handle is stored 295 - if community.Handle != "gamingtest.community.coves.social" { 296 - t.Errorf("Expected handle gamingtest.community.coves.social, got %s", community.Handle) 295 + if community.Handle != "c-gamingtest.coves.social" { 296 + t.Errorf("Expected handle c-gamingtest.coves.social, got %s", community.Handle) 297 297 } 298 298 299 299 // Note: The DID is the authoritative identifier for atProto resolution
+8 -7
tests/integration/post_creation_test.go
··· 67 67 // Setup: Create test community (insert directly to DB for speed) 68 68 testCommunity := &communities.Community{ 69 69 DID: generateTestDID("testcommunity"), 70 - Handle: "testcommunity.community.test.coves.social", // Canonical atProto handle (no ! prefix, .community. format) 70 + Handle: "c-testcommunity.test.coves.social", // Canonical atProto handle (no ! prefix, c- format) 71 71 Name: "testcommunity", 72 72 DisplayName: "Test Community", 73 73 Description: "A community for testing posts", ··· 134 134 135 135 t.Run("Create text post with ! prefix handle", func(t *testing.T) { 136 136 // Test that we can also use ! prefix with scoped format: !name@instance 137 - // This is Coves-specific UX shorthand for name.community.instance 137 + // This is Coves-specific UX shorthand for c-name.instance 138 138 139 139 content := "Post using !-prefixed handle" 140 140 title := "Prefixed Handle Test" 141 141 142 - // Extract name from handle: "gardening.community.coves.social" -> "gardening" 142 + // Extract name from handle: "c-gardening.coves.social" -> "gardening" 143 143 // Scoped format: !gardening@coves.social 144 144 handleParts := strings.Split(testCommunity.Handle, ".") 145 - communityName := handleParts[0] 146 - instanceDomain := strings.Join(handleParts[2:], ".") // Skip ".community." 145 + communityNameWithPrefix := handleParts[0] // "c-gardening" 146 + communityName := strings.TrimPrefix(communityNameWithPrefix, "c-") // "gardening" 147 + instanceDomain := strings.Join(handleParts[1:], ".") // "coves.social" 147 148 scopedHandle := fmt.Sprintf("!%s@%s", communityName, instanceDomain) 148 149 149 150 req := posts.CreatePostRequest{ ··· 181 182 content := "Post with non-existent handle" 182 183 183 184 req := posts.CreatePostRequest{ 184 - Community: "nonexistent.community.test.coves.social", // Valid canonical handle format, but doesn't exist 185 + Community: "c-nonexistent.test.coves.social", // Valid canonical handle format, but doesn't exist 185 186 Content: &content, 186 187 AuthorDID: testUserDID, 187 188 } ··· 320 321 testCommunityDID := generateTestDID("testcommunity2") 321 322 _, err = communityRepo.Create(ctx, &communities.Community{ 322 323 DID: testCommunityDID, 323 - Handle: "testcommunity2.community.test.coves.social", // Canonical format (no ! prefix) 324 + Handle: "c-testcommunity2.test.coves.social", // Canonical format (no ! prefix) 324 325 Name: "testcommunity2", 325 326 Visibility: "public", 326 327 CreatedByDID: testUserDID,
+1 -1
tests/integration/post_e2e_test.go
··· 71 71 // In real E2E, this would be a real community provisioned on PDS 72 72 community := &communities.Community{ 73 73 DID: "did:plc:gaming123", 74 - Handle: "gaming.community.test.coves.social", 74 + Handle: "c-gaming.test.coves.social", 75 75 Name: "gaming", 76 76 DisplayName: "Gaming Community", 77 77 OwnerDID: "did:plc:gaming123",
+7 -7
tests/integration/post_handler_test.go
··· 255 255 }) 256 256 257 257 t.Run("Accept valid scoped handle format", func(t *testing.T) { 258 - // Scoped format: !name@instance (gets converted to name.community.instance internally) 258 + // Scoped format: !name@instance (gets converted to c-name.instance internally) 259 259 validScopedHandles := []string{ 260 260 "!mycommunity@bsky.social", // Scoped format 261 261 "!gaming@test.coves.social", // Scoped format ··· 293 293 }) 294 294 295 295 t.Run("Accept valid canonical handle format", func(t *testing.T) { 296 - // Canonical format: name.community.instance (DNS-resolvable atProto handle) 296 + // Canonical format: c-name.instance (DNS-resolvable atProto handle) 297 297 validCanonicalHandles := []string{ 298 - "gaming.community.test.coves.social", 299 - "books.community.bsky.social", 298 + "c-gaming.test.coves.social", 299 + "c-books.bsky.social", 300 300 } 301 301 302 302 for _, validHandle := range validCanonicalHandles { ··· 331 331 }) 332 332 333 333 t.Run("Accept valid @-prefixed handle format", func(t *testing.T) { 334 - // @-prefixed format: @name.community.instance (atProto standard, @ gets stripped) 334 + // @-prefixed format: @c-name.instance (atProto standard, @ gets stripped) 335 335 validAtHandles := []string{ 336 - "@gaming.community.test.coves.social", 337 - "@books.community.bsky.social", 336 + "@c-gaming.test.coves.social", 337 + "@c-books.bsky.social", 338 338 } 339 339 340 340 for _, validHandle := range validAtHandles {
+5 -5
tests/integration/post_unfurl_test.go
··· 87 87 // Create test community 88 88 testCommunity := &communities.Community{ 89 89 DID: generateTestDID("unfurlcommunity"), 90 - Handle: "unfurlcommunity.community.test.coves.social", 90 + Handle: "c-unfurlcommunity.test.coves.social", 91 91 Name: "unfurlcommunity", 92 92 DisplayName: "Unfurl Test Community", 93 93 Description: "A community for testing unfurl", ··· 382 382 // Create test community 383 383 testCommunity := &communities.Community{ 384 384 DID: generateTestDID("unsupportedcommunity"), 385 - Handle: "unsupportedcommunity.community.test.coves.social", 385 + Handle: "c-unsupportedcommunity.test.coves.social", 386 386 Name: "unsupportedcommunity", 387 387 DisplayName: "Unsupported URL Test", 388 388 Visibility: "public", ··· 488 488 489 489 testCommunity := &communities.Community{ 490 490 DID: generateTestDID("metadatacommunity"), 491 - Handle: "metadatacommunity.community.test.coves.social", 491 + Handle: "c-metadatacommunity.test.coves.social", 492 492 Name: "metadatacommunity", 493 493 DisplayName: "Metadata Test", 494 494 Visibility: "public", ··· 598 598 599 599 testCommunity := &communities.Community{ 600 600 DID: generateTestDID("noembedcommunity"), 601 - Handle: "noembedcommunity.community.test.coves.social", 601 + Handle: "c-noembedcommunity.test.coves.social", 602 602 Name: "noembedcommunity", 603 603 DisplayName: "No Embed Test", 604 604 Visibility: "public", ··· 872 872 testCommunityDID := generateTestDID("e2eunfurlcommunity") 873 873 community := &communities.Community{ 874 874 DID: testCommunityDID, 875 - Handle: "e2eunfurlcommunity.community.test.coves.social", 875 + Handle: "c-e2eunfurlcommunity.test.coves.social", 876 876 Name: "e2eunfurlcommunity", 877 877 DisplayName: "E2E Unfurl Test", 878 878 OwnerDID: testCommunityDID,
+2 -2
tests/integration/token_refresh_test.go
··· 110 110 // Create a test community first 111 111 community := &communities.Community{ 112 112 DID: "did:plc:test123", 113 - Handle: "test.community.coves.social", 113 + Handle: "c-test.coves.social", 114 114 Name: "test", 115 115 OwnerDID: "did:plc:test123", 116 116 CreatedByDID: "did:plc:creator", ··· 186 186 187 187 community := &communities.Community{ 188 188 DID: "did:plc:expiring123", 189 - Handle: "expiring.community.coves.social", 189 + Handle: "c-expiring.coves.social", 190 190 Name: "expiring", 191 191 OwnerDID: "did:plc:expiring123", 192 192 CreatedByDID: "did:plc:creator",
+2 -2
tests/integration/user_journey_e2e_test.go
··· 115 115 userService := users.NewUserService(userRepo, identityResolver, pdsURL) 116 116 117 117 // Extract instance domain and DID 118 - // IMPORTANT: Instance domain must match PDS_SERVICE_HANDLE_DOMAINS config (.community.coves.social) 118 + // IMPORTANT: Instance domain must match PDS_SERVICE_HANDLE_DOMAINS config (c-{name}.coves.social) 119 119 instanceDID := os.Getenv("INSTANCE_DID") 120 120 if instanceDID == "" { 121 121 instanceDID = "did:web:coves.social" // Must match PDS handle domain config ··· 226 226 t.Run("2. User A - Create Community", func(t *testing.T) { 227 227 t.Log("\n🏘️ Part 2: User A creates a community...") 228 228 229 - // Community handle will be {name}.community.coves.social 229 + // Community handle will be {name}c-{name}.coves.social 230 230 // Max 34 chars total, so name must be short (34 - 23 = 11 chars max) 231 231 shortTS := timestamp % 10000 232 232 communityName := fmt.Sprintf("gj%d", shortTS) // "gj9261" = 6 chars -> handle = 29 chars