openvidu/openvidu-dashboard/dashboard-ce/backend/test/unit/time-series.test.ts

106 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { bucketStarts, densePoints, resolveGranularity } from '../../src/api/time-series.js';
const day = 86_400_000;
describe('time series', () => {
it('chooses the finest granularity with at most 1 000 buckets', () => {
const to = new Date('2026-10-13T00:00:00Z');
expect(resolveGranularity('auto', new Date(to.getTime() - 7 * day), to)).toBe('15m');
expect(resolveGranularity('auto', new Date(to.getTime() - day), to)).toBe('5m');
expect(resolveGranularity('auto', new Date(to.getTime() - 15 * 60_000), to)).toBe('1m');
expect(resolveGranularity('auto', new Date(to.getTime() - 400 * day), to)).toBe('1d');
});
it('rejects an explicit granularity above the bucket limit', () => {
const to = new Date('2026-10-13T00:00:00Z');
expect(resolveGranularity('1m', new Date(to.getTime() - 30 * day), to)).toEqual({
error: 'gives more than 1000 buckets for this range; choose a coarser granularity'
});
expect(resolveGranularity('1h', new Date(to.getTime() - 30 * day), to)).toBe('1h');
});
it('starts daily buckets at local midnight across a DST change', () => {
const starts = bucketStarts(
new Date('2026-10-31T12:00:00Z'),
new Date('2026-11-03T12:00:00Z'),
'1d',
'America/New_York'
);
expect(starts.map((start) => start.toISOString())).toEqual([
'2026-10-31T04:00:00.000Z',
'2026-11-01T04:00:00.000Z',
'2026-11-02T05:00:00.000Z',
'2026-11-03T05:00:00.000Z'
]);
});
it('aligns six-hour and quarter-hour buckets on the zone wall clock', () => {
expect(
bucketStarts(new Date('2026-10-13T00:10:00Z'), new Date('2026-10-13T13:00:00Z'), '6h', 'Asia/Kolkata').map(
(d) => d.toISOString()
)
).toEqual([
'2026-10-12T18:30:00.000Z',
'2026-10-13T00:30:00.000Z',
'2026-10-13T06:30:00.000Z',
'2026-10-13T12:30:00.000Z'
]);
expect(
bucketStarts(new Date('2026-10-13T10:07:00Z'), new Date('2026-10-13T10:40:00Z'), '15m', 'UTC').map((d) =>
d.toISOString()
)
).toEqual(['2026-10-13T10:00:00.000Z', '2026-10-13T10:15:00.000Z', '2026-10-13T10:30:00.000Z']);
});
it('steps six-hour buckets on the wall clock across both DST changes', () => {
const starts = (from: string, to: string) =>
bucketStarts(new Date(from), new Date(to), '6h', 'Europe/Madrid').map((d) => d.toISOString());
expect(starts('2026-10-24T22:00:00Z', '2026-10-25T23:00:00Z')).toEqual([
'2026-10-24T22:00:00.000Z',
'2026-10-25T05:00:00.000Z',
'2026-10-25T11:00:00.000Z',
'2026-10-25T17:00:00.000Z'
]);
expect(starts('2026-03-28T23:00:00Z', '2026-03-29T22:00:00Z')).toEqual([
'2026-03-28T23:00:00.000Z',
'2026-03-29T04:00:00.000Z',
'2026-03-29T10:00:00.000Z',
'2026-03-29T16:00:00.000Z'
]);
});
it.each(['Europe/Madrid', 'America/New_York', 'America/Santiago', 'Australia/Lord_Howe', 'Asia/Kolkata'])(
'advances through a year of local days and six-hour buckets in %s',
(zone) => {
const from = new Date('2026-01-01T12:00:00Z');
const to = new Date('2027-01-01T12:00:00Z');
for (const [granularity, perDay] of [
['1d', 1],
['6h', 4]
] as const) {
const starts = bucketStarts(from, to, granularity, zone).map((d) => d.getTime());
expect(starts.every((start, index) => index === 0 || start > (starts[index - 1] ?? 0))).toBe(true);
expect(Math.abs(starts.length - 365 * perDay)).toBeLessThanOrEqual(perDay);
}
}
);
it('fills empty buckets with 0 for counters and null for averages', () => {
const buckets = bucketStarts(new Date('2026-10-13T00:00:00Z'), new Date('2026-10-14T00:00:00Z'), '1h', 'UTC');
const values = new Map([[buckets[3]?.getTime() ?? 0, 7]]);
expect(buckets).toHaveLength(24);
expect(densePoints(buckets, values, 0).filter(([, value]) => value === 0)).toHaveLength(23);
expect(densePoints(buckets, values, null)[3]).toEqual(['2026-10-13T03:00:00.000Z', 7]);
expect(densePoints(buckets, values, null)[4]).toEqual(['2026-10-13T04:00:00.000Z', null]);
});
});