99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|