Skip to content

Commit 35fdaef

Browse files
committed
fix(backend): validate pagination parameters in views endpoint
1 parent f287331 commit 35fdaef

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import Fastify from 'fastify';
3+
import { analyticsRoutes } from '../routes/analytics.js';
4+
import type { PrismaClient } from '@prisma/client';
5+
6+
const mockPrisma = {
7+
cardView: {
8+
count: vi.fn(),
9+
findMany: vi.fn(),
10+
groupBy: vi.fn(),
11+
},
12+
followLog: {
13+
count: vi.fn(),
14+
},
15+
};
16+
17+
async function buildApp() {
18+
const app = Fastify();
19+
app.decorate('prisma', mockPrisma as unknown as PrismaClient);
20+
app.decorate('authenticate', async (request: any) => {
21+
request.user = { id: 'user-123' };
22+
});
23+
app.register(analyticsRoutes, { prefix: '/api/analytics' });
24+
await app.ready();
25+
return app;
26+
}
27+
28+
describe('Analytics Routes', () => {
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
33+
describe('GET /api/analytics/overview', () => {
34+
it('should return overview stats successfully', async () => {
35+
mockPrisma.cardView.count.mockResolvedValueOnce(10);
36+
mockPrisma.cardView.count.mockResolvedValueOnce(2);
37+
mockPrisma.followLog.count.mockResolvedValueOnce(5);
38+
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);
39+
mockPrisma.cardView.groupBy.mockResolvedValueOnce([]);
40+
41+
const app = await buildApp();
42+
const res = await app.inject({
43+
method: 'GET',
44+
url: '/api/analytics/overview',
45+
});
46+
47+
expect(res.statusCode).toBe(200);
48+
const body = res.json();
49+
expect(body.totalViews).toBe(10);
50+
expect(body.totalFollows).toBe(5);
51+
});
52+
});
53+
54+
describe('GET /api/analytics/views', () => {
55+
it('should handle valid page parameter', async () => {
56+
mockPrisma.cardView.count.mockResolvedValueOnce(100);
57+
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);
58+
59+
const app = await buildApp();
60+
const res = await app.inject({
61+
method: 'GET',
62+
url: '/api/analytics/views',
63+
query: { page: '2' },
64+
});
65+
66+
expect(res.statusCode).toBe(200);
67+
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
68+
expect.objectContaining({
69+
skip: 20,
70+
take: 20,
71+
})
72+
);
73+
expect(res.json().meta.page).toBe(2);
74+
});
75+
76+
it('should default to page 1 for invalid non-numeric page parameter', async () => {
77+
mockPrisma.cardView.count.mockResolvedValueOnce(100);
78+
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);
79+
80+
const app = await buildApp();
81+
const res = await app.inject({
82+
method: 'GET',
83+
url: '/api/analytics/views',
84+
query: { page: 'abc' },
85+
});
86+
87+
expect(res.statusCode).toBe(200);
88+
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
89+
expect.objectContaining({
90+
skip: 0,
91+
take: 20,
92+
})
93+
);
94+
expect(res.json().meta.page).toBe(1);
95+
});
96+
97+
it('should default to page 1 for negative page parameter', async () => {
98+
mockPrisma.cardView.count.mockResolvedValueOnce(100);
99+
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);
100+
101+
const app = await buildApp();
102+
const res = await app.inject({
103+
method: 'GET',
104+
url: '/api/analytics/views',
105+
query: { page: '-5' },
106+
});
107+
108+
expect(res.statusCode).toBe(200);
109+
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
110+
expect.objectContaining({
111+
skip: 0,
112+
take: 20,
113+
})
114+
);
115+
expect(res.json().meta.page).toBe(1);
116+
});
117+
});
118+
});

apps/backend/src/routes/analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export async function analyticsRoutes(app: FastifyInstance) {
6161
preHandler: [app.authenticate],
6262
}, async (request: FastifyRequest<{ Querystring: { page?: string, cardId?: string } }>, reply: FastifyReply) => {
6363
const userId = (request.user as any).id;
64-
const page = parseInt(request.query.page || '1', 10);
64+
const page = Math.max(1, Number(request.query.page) || 1);
6565
const limit = 20;
6666
const skip = (page - 1) * limit;
6767

0 commit comments

Comments
 (0)