/** * P1-3 fix — analytics time-range filter must respect the requested range. * * Bug from the SRS audit: dashboard sets `range: '7d'` (user picked "Last * 7 days"), but the response comes back with `range: '6m'`. Root cause: * the timeseries endpoint only understood month-granularity ranges and * silently coerced anything else to 6m. * * The fix: the endpoint now honours 7d/30d/90d with daily granularity * and echoes the requested `range` back in the response. This test * pins that contract. */ const request = require('supertest'); const { pointAtDevDb } = require('./setup'); pointAtDevDb(); const app = require('../src/index'); describe('Reports — time-series range filter (P1-3)', () => { let adminToken; beforeAll(async () => { const res = await request(app) .post('/api/auth/login') .send({ email: 'admin@school.com', password: 'admin123' }); adminToken = res.body?.token; }); it('honours range=7d and returns 7 daily points', async () => { const res = await request(app) .get('/api/reports/timeseries?range=7d') .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); expect(res.body.range).toBe('7d'); expect(Array.isArray(res.body.series)).toBe(true); expect(res.body.series).toHaveLength(7); // Daily points carry a YYYY-MM-DD `date` key for (const p of res.body.series) { expect(p.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); } }); it('honours range=30d and returns 30 daily points', async () => { const res = await request(app) .get('/api/reports/timeseries?range=30d') .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); expect(res.body.range).toBe('30d'); expect(res.body.series).toHaveLength(30); }); it('honours range=90d and returns 90 daily points', async () => { const res = await request(app) .get('/api/reports/timeseries?range=90d') .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); expect(res.body.range).toBe('90d'); expect(res.body.series).toHaveLength(90); }); it('still honours range=6m (12 buckets back) for backward compat', async () => { const res = await request(app) .get('/api/reports/timeseries?range=6m') .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); expect(res.body.range).toBe('6m'); expect(res.body.series).toHaveLength(6); // Monthly points carry a yearMonth key for (const p of res.body.series) { expect(p.yearMonth).toMatch(/^\d{4}-\d{2}$/); } }); it('defaults to 6 months when no range is given', async () => { const res = await request(app) .get('/api/reports/timeseries') .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); expect(res.body.range).toBe('6m'); expect(res.body.series).toHaveLength(6); }); it('rejects a teacher token without a student data leak (RBAC unchanged)', async () => { const login = await request(app) .post('/api/auth/login') .send({ email: 'teacher@school.com', password: 'teacher123' }); const teacherToken = login.body?.token; const res = await request(app) .get('/api/reports/timeseries?range=7d') .set('Authorization', `Bearer ${teacherToken}`); // reports.controller.js uses adminOrTeacher on /timeseries. expect(res.status).toBe(200); expect(res.body.range).toBe('7d'); expect(res.body.series).toHaveLength(7); }); });