@@ -20,6 +20,7 @@ const prismaMock = {
2020 followLog : {
2121 count : vi . fn ( ) ,
2222 } ,
23+ $queryRaw : vi . fn ( ) ,
2324} ;
2425
2526// ─── App factory ─────────────────────────────────────────────────────────────
@@ -89,19 +90,16 @@ describe(
8990 ( ) => {
9091 let app : FastifyInstance ;
9192
92- beforeEach (
93- async ( ) => {
94- vi . clearAllMocks ( ) ;
93+ beforeEach ( async ( ) => {
94+ vi . clearAllMocks ( ) ;
9595
96- mockJwtVerify . mockResolvedValue (
97- {
98- id : MOCK_USER_ID ,
99- }
100- ) ;
96+ mockJwtVerify . mockResolvedValue ( { id : MOCK_USER_ID } ) ;
10197
102- app = await buildApp ( ) ;
103- }
104- ) ;
98+ // Default: $queryRaw for uniqueViewers returns 0
99+ prismaMock . $queryRaw . mockResolvedValue ( [ { count : BigInt ( 0 ) } ] ) ;
100+
101+ app = await buildApp ( ) ;
102+ } ) ;
105103
106104 afterEach (
107105 async ( ) => {
@@ -147,21 +145,8 @@ describe(
147145 ]
148146 ) ;
149147
150- prismaMock . cardView . groupBy . mockResolvedValue (
151- [
152- {
153- viewerId :
154- 'u1' ,
155- viewerIp :
156- null ,
157- } ,
158- {
159- viewerId :
160- 'u2' ,
161- viewerIp :
162- null ,
163- } ,
164- ]
148+ prismaMock . $queryRaw . mockResolvedValue (
149+ [ { count : BigInt ( 2 ) } ]
165150 ) ;
166151
167152 const res =
@@ -296,6 +281,58 @@ describe(
296281 ) ;
297282 }
298283 ) ;
284+
285+ it (
286+ 'totalFollows counts rows by followerId (outbound follows), not by targetUsername' ,
287+ async ( ) => {
288+ prismaMock . cardView . count
289+ . mockResolvedValueOnce ( 0 )
290+ . mockResolvedValueOnce ( 0 ) ;
291+
292+ prismaMock . followLog . count . mockResolvedValue ( 3 ) ;
293+ prismaMock . cardView . findMany . mockResolvedValue ( [ ] ) ;
294+
295+ const res = await app . inject ( {
296+ method : 'GET' ,
297+ url : '/api/analytics/overview' ,
298+ headers : authHeader ( ) ,
299+ } ) ;
300+
301+ expect ( res . statusCode ) . toBe ( 200 ) ;
302+ expect ( res . json ( ) . totalFollows ) . toBe ( 3 ) ;
303+
304+ // The query must use followerId, never targetUsername
305+ const followCountCall = prismaMock . followLog . count . mock . calls [ 0 ] [ 0 ] ;
306+ expect ( followCountCall ) . toMatchObject ( {
307+ where : {
308+ followerId : MOCK_USER_ID ,
309+ status : 'success' ,
310+ } ,
311+ } ) ;
312+ expect ( followCountCall . where ) . not . toHaveProperty ( 'targetUsername' ) ;
313+ }
314+ ) ;
315+
316+ it (
317+ 'totalFollows is 0 when user has no successful outbound follows' ,
318+ async ( ) => {
319+ prismaMock . cardView . count
320+ . mockResolvedValueOnce ( 50 )
321+ . mockResolvedValueOnce ( 5 ) ;
322+
323+ prismaMock . followLog . count . mockResolvedValue ( 0 ) ;
324+ prismaMock . cardView . findMany . mockResolvedValue ( [ ] ) ;
325+
326+ const res = await app . inject ( {
327+ method : 'GET' ,
328+ url : '/api/analytics/overview' ,
329+ headers : authHeader ( ) ,
330+ } ) ;
331+
332+ expect ( res . statusCode ) . toBe ( 200 ) ;
333+ expect ( res . json ( ) . totalFollows ) . toBe ( 0 ) ;
334+ }
335+ ) ;
299336 }
300337 ) ;
301338
0 commit comments