package integration import ( "Coves/internal/core/communities" "Coves/internal/db/postgres" "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "testing" "time" ) // TestCommunityService_CreateWithRealPDS tests the complete service layer flow // using a REAL local PDS. This verifies: // - Password generation happens in provisioner (not hardcoded test passwords) // - PDS account creation works (real com.atproto.server.createAccount) // - Write-forward to community's own repository succeeds // - Credentials flow correctly: PDS → service → repository // - Complete atProto write-forward architecture // // This test fills the gap between: // - Unit tests (direct DB writes, bypass PDS) // - E2E tests (full HTTP + Jetstream flow) func TestCommunityService_CreateWithRealPDS(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode - requires PDS") } // Check if PDS is running pdsURL := "http://localhost:3001" healthResp, err := http.Get(pdsURL + "/xrpc/_health") if err != nil { t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err) } defer func() { if closeErr := healthResp.Body.Close(); closeErr != nil { t.Logf("Failed to close health response: %v", closeErr) } }() // Setup test database db := setupTestDB(t) defer func() { if err := db.Close(); err != nil { t.Logf("Failed to close database: %v", err) } }() ctx := context.Background() repo := postgres.NewCommunityRepository(db) t.Run("creates community with real PDS provisioning", func(t *testing.T) { // Create provisioner and service (production code path) // Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as .community.coves.social) provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) service := communities.NewCommunityService( repo, pdsURL, "did:web:coves.social", "coves.social", provisioner, ) // Generate unique community name (keep short for DNS label limit) // Must start with letter, can contain alphanumeric and hyphens uniqueName := fmt.Sprintf("svc%d", time.Now().UnixNano()%1000000) // Create community via service (FULL PRODUCTION CODE PATH) t.Logf("Creating community via service.CreateCommunity()...") community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: uniqueName, DisplayName: "Test Community", Description: "Integration test community with real PDS", Visibility: "public", CreatedByDID: "did:plc:testuser123", HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err != nil { t.Fatalf("Failed to create community: %v", err) } t.Logf("✅ Community created: %s", community.DID) // CRITICAL: Verify password was generated by provisioner (not hardcoded) if len(community.PDSPassword) < 32 { t.Errorf("Password too short: expected >= 32 chars from provisioner, got %d", len(community.PDSPassword)) } // Verify password is not empty if community.PDSPassword == "" { t.Error("Password should not be empty") } // Verify password is not a known test password testPasswords := []string{"test-password", "password123", "admin", ""} for _, testPwd := range testPasswords { if community.PDSPassword == testPwd { t.Errorf("Password appears to be hardcoded test password: %s", testPwd) } } t.Logf("✅ Password generated by provisioner: %d chars", len(community.PDSPassword)) // Verify DID is real (did:plc:xxx from PDS) if !strings.HasPrefix(community.DID, "did:plc:") { t.Errorf("Expected real PLC DID from PDS, got: %s", community.DID) } t.Logf("✅ Real DID generated: %s", community.DID) // Verify handle format expectedHandle := fmt.Sprintf("%s.community.coves.social", uniqueName) if community.Handle != expectedHandle { t.Errorf("Expected handle %s, got %s", expectedHandle, community.Handle) } t.Logf("✅ Handle generated correctly: %s", community.Handle) // Verify tokens are present (from PDS) if community.PDSAccessToken == "" { t.Error("Access token should not be empty") } if community.PDSRefreshToken == "" { t.Error("Refresh token should not be empty") } // Verify tokens are JWT format (3 parts separated by dots) accessParts := strings.Split(community.PDSAccessToken, ".") if len(accessParts) != 3 { t.Errorf("Access token should be JWT format (3 parts), got %d parts", len(accessParts)) } t.Logf("✅ JWT tokens received from PDS") // Verify record URI points to community's own repository (V2 architecture) expectedURIPrefix := fmt.Sprintf("at://%s/social.coves.community.profile/self", community.DID) if community.RecordURI != expectedURIPrefix { t.Errorf("Expected record URI %s, got %s", expectedURIPrefix, community.RecordURI) } t.Logf("✅ Record URI points to community's own repo: %s", community.RecordURI) // Verify V2 ownership model (community owns itself) if community.OwnerDID != community.DID { t.Errorf("V2: community should own itself. Expected OwnerDID=%s, got %s", community.DID, community.OwnerDID) } t.Logf("✅ V2 ownership: community owns itself") // CRITICAL: Verify credentials were persisted to database WITH ENCRYPTION retrieved, err := repo.GetByDID(ctx, community.DID) if err != nil { t.Fatalf("Failed to retrieve community from DB: %v", err) } // Verify password roundtrip (encrypted → decrypted) if retrieved.PDSPassword != community.PDSPassword { t.Error("Password not persisted correctly (encryption/decryption failed)") } // Verify tokens roundtrip if retrieved.PDSAccessToken != community.PDSAccessToken { t.Error("Access token not persisted correctly") } if retrieved.PDSRefreshToken != community.PDSRefreshToken { t.Error("Refresh token not persisted correctly") } t.Logf("✅ Credentials persisted to DB with encryption") // Verify password is encrypted at rest in database var encryptedPassword []byte query := ` SELECT pds_password_encrypted FROM communities WHERE did = $1 ` if err := db.QueryRowContext(ctx, query, community.DID).Scan(&encryptedPassword); err != nil { t.Fatalf("Failed to query encrypted password: %v", err) } // Verify NOT stored as plaintext if string(encryptedPassword) == community.PDSPassword { t.Error("CRITICAL: Password stored as plaintext in database!") } // Verify encrypted data exists if len(encryptedPassword) == 0 { t.Error("Encrypted password should have data") } t.Logf("✅ Password encrypted at rest in database") t.Logf("✅ COMPLETE TEST PASSED: Full write-forward architecture verified") }) t.Run("handles PDS errors gracefully", func(t *testing.T) { provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) service := communities.NewCommunityService( repo, pdsURL, "did:web:coves.social", "coves.social", provisioner, ) // Try to create community with invalid name (should fail validation before PDS) _, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: "", // Empty name DisplayName: "Invalid Community", Visibility: "public", CreatedByDID: "did:plc:testuser123", HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err == nil { t.Error("Expected validation error for empty name") } if !strings.Contains(err.Error(), "name") { t.Errorf("Expected 'name' error, got: %v", err) } t.Logf("✅ Validation errors handled correctly") }) t.Run("validates DNS label limits", func(t *testing.T) { provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) service := communities.NewCommunityService( repo, pdsURL, "did:web:coves.social", "coves.social", provisioner, ) // Try 64-char name (exceeds DNS limit of 63) longName := strings.Repeat("a", 64) _, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: longName, DisplayName: "Long Name Test", Visibility: "public", CreatedByDID: "did:plc:testuser123", HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err == nil { t.Error("Expected error for 64-char name (DNS limit is 63)") } if !strings.Contains(err.Error(), "63") { t.Errorf("Expected DNS limit error mentioning '63', got: %v", err) } t.Logf("✅ DNS label limits enforced") }) } // TestCommunityService_UpdateWithRealPDS tests the V2 update flow // This is CRITICAL - currently has ZERO test coverage in unit tests! // // Verifies: // - Updates use community's OWN credentials (not instance credentials) // - Writes to community's repository (at://community_did/...) // - Authorization checks (only creator can update) // - Record rkey is always "self" for V2 func TestCommunityService_UpdateWithRealPDS(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode - requires PDS") } // Check if PDS is running pdsURL := "http://localhost:3001" healthResp, err := http.Get(pdsURL + "/xrpc/_health") if err != nil { t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err) } defer func() { if closeErr := healthResp.Body.Close(); closeErr != nil { t.Logf("Failed to close health response: %v", closeErr) } }() // Setup test database db := setupTestDB(t) defer func() { if err := db.Close(); err != nil { t.Logf("Failed to close database: %v", err) } }() ctx := context.Background() repo := postgres.NewCommunityRepository(db) provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) service := communities.NewCommunityService( repo, pdsURL, "did:web:coves.social", "coves.social", provisioner, ) t.Run("updates community with real PDS", func(t *testing.T) { // First, create a community uniqueName := fmt.Sprintf("upd%d", time.Now().UnixNano()%1000000) creatorDID := "did:plc:updatetestuser" t.Logf("Creating community to update...") community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: uniqueName, DisplayName: "Original Display Name", Description: "Original description", Visibility: "public", CreatedByDID: creatorDID, HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err != nil { t.Fatalf("Failed to create community: %v", err) } t.Logf("✅ Community created: %s", community.DID) // Now update it newDisplayName := "Updated Display Name" newDescription := "Updated description via V2 write-forward" newVisibility := "unlisted" t.Logf("Updating community via service.UpdateCommunity()...") updated, err := service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{ CommunityDID: community.DID, UpdatedByDID: creatorDID, // Same as creator - should be authorized DisplayName: &newDisplayName, Description: &newDescription, Visibility: &newVisibility, AllowExternalDiscovery: nil, // Don't change }) if err != nil { t.Fatalf("Failed to update community: %v", err) } t.Logf("✅ Community updated via PDS") // Verify updates were applied if updated.DisplayName != newDisplayName { t.Errorf("Expected display name %s, got %s", newDisplayName, updated.DisplayName) } if updated.Description != newDescription { t.Errorf("Expected description %s, got %s", newDescription, updated.Description) } if updated.Visibility != newVisibility { t.Errorf("Expected visibility %s, got %s", newVisibility, updated.Visibility) } t.Logf("✅ Updates applied correctly") // Verify record URI still points to community's own repo with rkey "self" expectedURIPrefix := fmt.Sprintf("at://%s/social.coves.community.profile/self", community.DID) if updated.RecordURI != expectedURIPrefix { t.Errorf("Expected record URI %s, got %s", expectedURIPrefix, updated.RecordURI) } t.Logf("✅ Record URI correct (uses community's repo)") // Verify record CID changed (new version) if updated.RecordCID == community.RecordCID { t.Error("Expected record CID to change after update") } t.Logf("✅ Record CID updated (new version)") }) t.Run("rejects unauthorized updates", func(t *testing.T) { // Create a community uniqueName := fmt.Sprintf("auth%d", time.Now().UnixNano()%1000000) creatorDID := "did:plc:creator123" community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: uniqueName, DisplayName: "Auth Test Community", Visibility: "public", CreatedByDID: creatorDID, HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err != nil { t.Fatalf("Failed to create community: %v", err) } // Try to update as different user differentUserDID := "did:plc:nottheowner" newDisplayName := "Hacked Display Name" _, err = service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{ CommunityDID: community.DID, UpdatedByDID: differentUserDID, // NOT the creator DisplayName: &newDisplayName, }) if err == nil { t.Error("Expected authorization error for non-creator update") } if !strings.Contains(strings.ToLower(err.Error()), "unauthorized") { t.Errorf("Expected 'unauthorized' error, got: %v", err) } t.Logf("✅ Unauthorized updates rejected") }) t.Run("handles missing PDS credentials", func(t *testing.T) { // Create a community manually in DB without PDS credentials // (simulating a federated community indexed from another instance) uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) communityDID := generateTestDID(uniqueSuffix) federatedCommunity := &communities.Community{ DID: communityDID, Handle: fmt.Sprintf("federated-%s.external.social", uniqueSuffix), Name: "federated-test", OwnerDID: communityDID, CreatedByDID: "did:plc:externaluser", HostedByDID: "did:web:external.social", Visibility: "public", // No PDS credentials - this is a federated community CreatedAt: time.Now(), UpdatedAt: time.Now(), } _, err := repo.Create(ctx, federatedCommunity) if err != nil { t.Fatalf("Failed to create federated community: %v", err) } // Try to update it - should fail because we don't have credentials newDisplayName := "Cannot Update This" _, err = service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{ CommunityDID: communityDID, UpdatedByDID: "did:plc:externaluser", DisplayName: &newDisplayName, }) if err == nil { t.Error("Expected error when updating community without PDS credentials") } if !strings.Contains(err.Error(), "missing PDS credentials") { t.Logf("Error message: %v", err) } t.Logf("✅ Missing credentials handled gracefully") }) } // TestPasswordAuthentication verifies that generated passwords work for PDS authentication // This is CRITICAL for P0: passwords must be recoverable for session renewal func TestPasswordAuthentication(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode - requires PDS") } // Check if PDS is running pdsURL := "http://localhost:3001" healthResp, err := http.Get(pdsURL + "/xrpc/_health") if err != nil { t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err) } defer func() { if closeErr := healthResp.Body.Close(); closeErr != nil { t.Logf("Failed to close health response: %v", closeErr) } }() // Setup test database db := setupTestDB(t) defer func() { if err := db.Close(); err != nil { t.Logf("Failed to close database: %v", err) } }() ctx := context.Background() repo := postgres.NewCommunityRepository(db) provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) service := communities.NewCommunityService( repo, pdsURL, "did:web:coves.social", "coves.social", provisioner, ) t.Run("generated password works for session creation", func(t *testing.T) { // Create a community with PDS-generated password uniqueName := fmt.Sprintf("pwd%d", time.Now().UnixNano()%1000000) t.Logf("Creating community with generated password...") community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{ Name: uniqueName, DisplayName: "Password Auth Test", Visibility: "public", CreatedByDID: "did:plc:testuser", HostedByDID: "did:web:coves.social", AllowExternalDiscovery: true, }) if err != nil { t.Fatalf("Failed to create community: %v", err) } t.Logf("✅ Community created with password: %d chars", len(community.PDSPassword)) // Retrieve from DB to get decrypted password retrieved, err := repo.GetByDID(ctx, community.DID) if err != nil { t.Fatalf("Failed to retrieve community: %v", err) } t.Logf("✅ Password retrieved from DB (decrypted): %d chars", len(retrieved.PDSPassword)) // Now try to authenticate with the password via com.atproto.server.createSession // This simulates what we'd do for token renewal sessionPayload := map[string]interface{}{ "identifier": retrieved.Handle, // Use handle for login "password": retrieved.PDSPassword, } payloadBytes, err := json.Marshal(sessionPayload) if err != nil { t.Fatalf("Failed to marshal session payload: %v", err) } sessionReq, err := http.NewRequestWithContext(ctx, "POST", pdsURL+"/xrpc/com.atproto.server.createSession", bytes.NewReader(payloadBytes)) if err != nil { t.Fatalf("Failed to create session request: %v", err) } sessionReq.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(sessionReq) if err != nil { t.Fatalf("Failed to create session: %v", err) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { t.Logf("Failed to close response body: %v", closeErr) } }() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Failed to read response body: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Session creation failed with status %d: %s", resp.StatusCode, string(body)) } // Verify we got new tokens var sessionResp struct { AccessJwt string `json:"accessJwt"` RefreshJwt string `json:"refreshJwt"` DID string `json:"did"` } if err := json.Unmarshal(body, &sessionResp); err != nil { t.Fatalf("Failed to parse session response: %v", err) } if sessionResp.AccessJwt == "" { t.Error("Expected new access token from session") } if sessionResp.RefreshJwt == "" { t.Error("Expected new refresh token from session") } if sessionResp.DID != community.DID { t.Errorf("Expected session DID %s, got %s", community.DID, sessionResp.DID) } t.Logf("✅ Password authentication successful!") t.Logf(" - New access token: %d chars", len(sessionResp.AccessJwt)) t.Logf(" - New refresh token: %d chars", len(sessionResp.RefreshJwt)) t.Logf(" - Session DID: %s", sessionResp.DID) t.Logf("✅ CRITICAL TEST PASSED: Password encryption enables session renewal") }) }