From 5264dba2575bfcac38ccc431d20f4aa3705296b5 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Fri, 8 May 2026 21:36:19 +0200 Subject: [PATCH] fix(social): compute postsCount + followingCount live (were hardcoded 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Endpoint /api/social/profile/[userId] returned (profile as any).postsCount ?? 0 und (profile as any).followingCount ?? 0 — Profile-schema hat aber weder postsCount noch followingCount columns. Daher zeigte UI immer 0 obwohl User Posts hatte. Fix: 2 zusätzliche COUNT-queries in Promise.all: - usePrisma().communityPost.count({ userId, isModerated: false }) → postsCount - usePrisma().userFollow.count({ followerId: userId }) → followingCount followersCount bleibt unverändert (wird via trigger denormalisiert in profile-row). Tests: backend/tests/social/profile-counts.test.ts — 4 Cases (posts>0, posts=0, following count, followers passthrough). 4/4 grün. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../server/api/social/profile/[userId].get.ts | 12 +- backend/tests/social/profile-counts.test.ts | 136 ++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 backend/tests/social/profile-counts.test.ts diff --git a/backend/server/api/social/profile/[userId].get.ts b/backend/server/api/social/profile/[userId].get.ts index 63d66f0..e1602a8 100644 --- a/backend/server/api/social/profile/[userId].get.ts +++ b/backend/server/api/social/profile/[userId].get.ts @@ -17,7 +17,7 @@ export default defineEventHandler(async (event) => { currentUserId = u.id; } catch {} - const [profile, score, followRelation, recentPosts, metaMap] = + const [profile, score, followRelation, recentPosts, metaMap, postsCount, followingCount] = await Promise.all([ getProfile(targetUserId), getUserScore(targetUserId), @@ -38,6 +38,12 @@ export default defineEventHandler(async (event) => { }, }), getUsersMeta([targetUserId]), + usePrisma().communityPost.count({ + where: { userId: targetUserId, isModerated: false }, + }), + usePrisma().userFollow.count({ + where: { followerId: targetUserId }, + }), ]); if (!profile) @@ -52,8 +58,8 @@ export default defineEventHandler(async (event) => { avatar: meta.avatar, bio: (profile as any).bio ?? null, followersCount: profile.followersCount ?? 0, - followingCount: (profile as any).followingCount ?? 0, - postsCount: (profile as any).postsCount ?? 0, + followingCount, + postsCount, tier: score?.tier ?? "beginner", totalPoints: score?.totalPoints ?? 0, isFollowing: !!followRelation, diff --git a/backend/tests/social/profile-counts.test.ts b/backend/tests/social/profile-counts.test.ts new file mode 100644 index 0000000..5c25ddd --- /dev/null +++ b/backend/tests/social/profile-counts.test.ts @@ -0,0 +1,136 @@ +/** + * Tests for GET /api/social/profile/[userId] — postsCount + followingCount + * + * Covers: + * - postsCount reflects live communityPost.count (not 0 when posts exist) + * - followingCount reflects live userFollow.count + * - followersCount passes through from profile row (denormalized) + */ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const mocks = vi.hoisted(() => ({ + communityPost: { + findMany: vi.fn(), + count: vi.fn(), + }, + userFollow: { + findUnique: vi.fn(), + count: vi.fn(), + }, + profile: { + findUnique: vi.fn(), + }, + userScore: { + findUnique: vi.fn(), + }, +})); + +vi.mock("../../server/utils/prisma", () => ({ + usePrisma: () => ({ + communityPost: mocks.communityPost, + userFollow: mocks.userFollow, + profile: mocks.profile, + userScore: mocks.userScore, + }), +})); + +vi.mock("../../server/utils/auth", () => ({ + requireUser: vi.fn().mockRejectedValue( + Object.assign(new Error("Unauthorized"), { statusCode: 401 }), + ), +})); + +vi.mock("../../server/db/profile", () => ({ + getProfile: vi.fn().mockResolvedValue({ + id: "user-1", + username: "testuser", + followersCount: 3, + createdAt: new Date("2026-01-01T00:00:00Z"), + }), +})); + +vi.mock("../../server/db/scores", () => ({ + getUserScore: vi.fn().mockResolvedValue({ tier: "beginner", totalPoints: 0 }), +})); + +vi.mock("../../server/db/social", () => ({ + getFollowRelation: vi.fn().mockResolvedValue(null), +})); + +vi.mock("../../server/utils/getUsersMeta", () => ({ + getUsersMeta: vi.fn().mockResolvedValue({ + "user-1": { nickname: "Tester", avatar: null }, + }), +})); + +// Stub Nitro globals needed by the endpoint file +const g = globalThis as Record; +if (typeof g.getRouterParam === "undefined") { + g.getRouterParam = (_event: unknown, key: string) => + key === "userId" ? "user-1" : undefined; +} + +beforeEach(() => { + vi.clearAllMocks(); + mocks.communityPost.findMany.mockResolvedValue([]); + mocks.communityPost.count.mockResolvedValue(0); + mocks.userFollow.count.mockResolvedValue(0); + mocks.userFollow.findUnique.mockResolvedValue(null); +}); + +async function callHandler() { + const mod = await import("../../server/api/social/profile/[userId].get"); + const handler = + typeof mod.default === "function" + ? mod.default + : (mod.default as { handler?: unknown }).handler; + return (handler as (e: unknown) => Promise)({ body: null }); +} + +describe("postsCount", () => { + it("returns postsCount > 0 when user has unmoderated posts", async () => { + mocks.communityPost.count.mockResolvedValueOnce(5); + mocks.userFollow.count.mockResolvedValueOnce(2); + + const result = (await callHandler()) as Record; + + expect(result.postsCount).toBe(5); + expect(mocks.communityPost.count).toHaveBeenCalledWith({ + where: { userId: "user-1", isModerated: false }, + }); + }); + + it("returns postsCount = 0 when user has no posts", async () => { + mocks.communityPost.count.mockResolvedValueOnce(0); + mocks.userFollow.count.mockResolvedValueOnce(0); + + const result = (await callHandler()) as Record; + + expect(result.postsCount).toBe(0); + }); +}); + +describe("followingCount", () => { + it("returns followingCount from userFollow.count", async () => { + mocks.communityPost.count.mockResolvedValueOnce(2); + mocks.userFollow.count.mockResolvedValueOnce(7); + + const result = (await callHandler()) as Record; + + expect(result.followingCount).toBe(7); + expect(mocks.userFollow.count).toHaveBeenCalledWith({ + where: { followerId: "user-1" }, + }); + }); +}); + +describe("followersCount", () => { + it("passes through denormalized followersCount from profile row", async () => { + mocks.communityPost.count.mockResolvedValueOnce(0); + mocks.userFollow.count.mockResolvedValueOnce(0); + + const result = (await callHandler()) as Record; + + expect(result.followersCount).toBe(3); + }); +});