geocrop-platform./apps/nextgen/server/tests/reports-timeseries.test.js

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);
});
});