Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions apps/backend/src/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import Fastify from 'fastify';
import { analyticsRoutes } from '../routes/analytics.js';
import type { PrismaClient } from '@prisma/client';

const mockPrisma = {
cardView: {
count: vi.fn(),
findMany: vi.fn(),
groupBy: vi.fn(),
},
followLog: {
count: vi.fn(),
},
};

async function buildApp() {
const app = Fastify();
app.decorate('prisma', mockPrisma as unknown as PrismaClient);
app.decorate('authenticate', async (request: any) => {
request.user = { id: 'user-123' };
});
app.register(analyticsRoutes, { prefix: '/api/analytics' });
await app.ready();
return app;
}

describe('Analytics Routes', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('GET /api/analytics/overview', () => {
it('should return overview stats successfully', async () => {
mockPrisma.cardView.count.mockResolvedValueOnce(10);
mockPrisma.cardView.count.mockResolvedValueOnce(2);
mockPrisma.followLog.count.mockResolvedValueOnce(5);
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);
mockPrisma.cardView.groupBy.mockResolvedValueOnce([]);

const app = await buildApp();
const res = await app.inject({
method: 'GET',
url: '/api/analytics/overview',
});

expect(res.statusCode).toBe(200);
const body = res.json();
expect(body.totalViews).toBe(10);
expect(body.totalFollows).toBe(5);
});
});

describe('GET /api/analytics/views', () => {
it('should handle valid page parameter', async () => {
mockPrisma.cardView.count.mockResolvedValueOnce(100);
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);

const app = await buildApp();
const res = await app.inject({
method: 'GET',
url: '/api/analytics/views',
query: { page: '2' },
});

expect(res.statusCode).toBe(200);
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
expect.objectContaining({
skip: 20,
take: 20,
})
);
expect(res.json().meta.page).toBe(2);
});

it('should default to page 1 for invalid non-numeric page parameter', async () => {
mockPrisma.cardView.count.mockResolvedValueOnce(100);
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);

const app = await buildApp();
const res = await app.inject({
method: 'GET',
url: '/api/analytics/views',
query: { page: 'abc' },
});

expect(res.statusCode).toBe(200);
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
expect.objectContaining({
skip: 0,
take: 20,
})
);
expect(res.json().meta.page).toBe(1);
});

it('should default to page 1 for negative page parameter', async () => {
mockPrisma.cardView.count.mockResolvedValueOnce(100);
mockPrisma.cardView.findMany.mockResolvedValueOnce([]);

const app = await buildApp();
const res = await app.inject({
method: 'GET',
url: '/api/analytics/views',
query: { page: '-5' },
});

expect(res.statusCode).toBe(200);
expect(mockPrisma.cardView.findMany).toHaveBeenCalledWith(
expect.objectContaining({
skip: 0,
take: 20,
})
);
expect(res.json().meta.page).toBe(1);
});
});
});
2 changes: 1 addition & 1 deletion apps/backend/src/routes/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function analyticsRoutes(app: FastifyInstance) {
preHandler: [app.authenticate],
}, async (request: FastifyRequest<{ Querystring: { page?: string, cardId?: string } }>, reply: FastifyReply) => {
const userId = (request.user as any).id;
const page = parseInt(request.query.page || '1', 10);
const page = Math.max(1, Number(request.query.page) || 1);
const limit = 20;
const skip = (page - 1) * limit;

Expand Down