mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 02:04:40 +10:00
324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { AppErrorCode } from '../errors/app-error';
|
|
import { resolveTeamAnalyticsRange } from './team-analytics-range';
|
|
|
|
const invalidRequest = expect.objectContaining({ code: AppErrorCode.INVALID_REQUEST });
|
|
|
|
describe('resolveTeamAnalyticsRange', () => {
|
|
describe('presets', () => {
|
|
it('ends at the start of tomorrow so today is included', () => {
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: '7d',
|
|
timezone: 'UTC',
|
|
now: new Date('2025-01-15T12:34:56.000Z'),
|
|
});
|
|
|
|
expect(resolved).toEqual({
|
|
range: '7d',
|
|
from: '2025-01-09',
|
|
to: '2025-01-15',
|
|
timezone: 'UTC',
|
|
start: new Date('2025-01-09T00:00:00.000Z'),
|
|
end: new Date('2025-01-16T00:00:00.000Z'),
|
|
previousStart: new Date('2025-01-02T00:00:00.000Z'),
|
|
previousEnd: new Date('2025-01-09T00:00:00.000Z'),
|
|
bucket: 'day',
|
|
});
|
|
});
|
|
|
|
it('keeps boundaries on local midnight across the March DST change in America/New_York', () => {
|
|
// DST began on 2024-03-10 in New York (EST -05:00 -> EDT -04:00).
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: '7d',
|
|
timezone: 'America/New_York',
|
|
now: new Date('2024-03-12T18:00:00.000Z'),
|
|
});
|
|
|
|
// 2024-03-13T00:00 EDT
|
|
expect(resolved.end).toEqual(new Date('2024-03-13T04:00:00.000Z'));
|
|
// 2024-03-06T00:00 EST (before the change), still local midnight.
|
|
expect(resolved.start).toEqual(new Date('2024-03-06T05:00:00.000Z'));
|
|
// 2024-02-28T00:00 EST
|
|
expect(resolved.previousStart).toEqual(new Date('2024-02-28T05:00:00.000Z'));
|
|
});
|
|
|
|
it('resolves 30d and 90d as calendar-day windows', () => {
|
|
const now = new Date('2025-06-30T23:59:59.000Z');
|
|
|
|
const thirty = resolveTeamAnalyticsRange({ range: '30d', timezone: 'UTC', now });
|
|
|
|
expect(thirty.end).toEqual(new Date('2025-07-01T00:00:00.000Z'));
|
|
expect(thirty.start).toEqual(new Date('2025-06-01T00:00:00.000Z'));
|
|
expect(thirty.previousStart).toEqual(new Date('2025-05-02T00:00:00.000Z'));
|
|
expect(thirty.previousEnd).toEqual(thirty.start);
|
|
expect(thirty.from).toBe('2025-06-01');
|
|
expect(thirty.to).toBe('2025-06-30');
|
|
|
|
const ninety = resolveTeamAnalyticsRange({ range: '90d', timezone: 'UTC', now });
|
|
|
|
expect(ninety.end).toEqual(new Date('2025-07-01T00:00:00.000Z'));
|
|
expect(ninety.start).toEqual(new Date('2025-04-02T00:00:00.000Z'));
|
|
expect(ninety.previousStart).toEqual(new Date('2025-01-02T00:00:00.000Z'));
|
|
expect(ninety.from).toBe('2025-04-02');
|
|
expect(ninety.to).toBe('2025-06-30');
|
|
});
|
|
|
|
it('aligns 12m to the start of the month 11 months ago with monthly buckets', () => {
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: '12m',
|
|
timezone: 'UTC',
|
|
now: new Date('2025-03-20T10:00:00.000Z'),
|
|
});
|
|
|
|
expect(resolved).toEqual({
|
|
range: '12m',
|
|
from: '2024-04-01',
|
|
to: '2025-03-20',
|
|
timezone: 'UTC',
|
|
start: new Date('2024-04-01T00:00:00.000Z'),
|
|
end: new Date('2025-03-21T00:00:00.000Z'),
|
|
previousStart: new Date('2023-04-01T00:00:00.000Z'),
|
|
previousEnd: new Date('2024-04-01T00:00:00.000Z'),
|
|
bucket: 'month',
|
|
});
|
|
});
|
|
|
|
it('aligns 12m month boundaries to the request timezone', () => {
|
|
// 2025-01-01T03:00Z is still 2024-12-31 in Los Angeles.
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: '12m',
|
|
timezone: 'America/Los_Angeles',
|
|
now: new Date('2025-01-01T03:00:00.000Z'),
|
|
});
|
|
|
|
// 2024-01-01T00:00 PST
|
|
expect(resolved.start).toEqual(new Date('2024-01-01T08:00:00.000Z'));
|
|
// 2025-01-01T00:00 PST
|
|
expect(resolved.end).toEqual(new Date('2025-01-01T08:00:00.000Z'));
|
|
// 2023-01-01T00:00 PST
|
|
expect(resolved.previousStart).toEqual(new Date('2023-01-01T08:00:00.000Z'));
|
|
expect(resolved.from).toBe('2024-01-01');
|
|
expect(resolved.to).toBe('2024-12-31');
|
|
});
|
|
|
|
it('ignores from/to for presets', () => {
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: '7d',
|
|
from: '2020-01-01',
|
|
to: '2020-01-02',
|
|
timezone: 'UTC',
|
|
now: new Date('2025-01-15T12:34:56.000Z'),
|
|
});
|
|
|
|
expect(resolved.from).toBe('2025-01-09');
|
|
expect(resolved.to).toBe('2025-01-15');
|
|
});
|
|
|
|
it('rejects invalid timezones', () => {
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: '30d',
|
|
timezone: 'Not/A_Zone',
|
|
now: new Date('2025-01-15T12:00:00.000Z'),
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
});
|
|
});
|
|
|
|
describe('custom', () => {
|
|
const now = new Date('2025-01-15T12:00:00.000Z');
|
|
|
|
it('resolves an inclusive calendar window with an equally sized previous window', () => {
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-02-01',
|
|
to: '2024-02-29',
|
|
timezone: 'UTC',
|
|
now,
|
|
});
|
|
|
|
// 29 days (leap February), previous window is the 29 days ending Jan 31.
|
|
expect(resolved).toEqual({
|
|
range: 'custom',
|
|
from: '2024-02-01',
|
|
to: '2024-02-29',
|
|
timezone: 'UTC',
|
|
start: new Date('2024-02-01T00:00:00.000Z'),
|
|
end: new Date('2024-03-01T00:00:00.000Z'),
|
|
previousStart: new Date('2024-01-03T00:00:00.000Z'),
|
|
previousEnd: new Date('2024-02-01T00:00:00.000Z'),
|
|
bucket: 'day',
|
|
});
|
|
});
|
|
|
|
it('uses day buckets up to 92 days and month buckets beyond', () => {
|
|
// Apr 1 .. Jul 1 2024 inclusive is 92 days.
|
|
const ninetyTwo = resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-04-01',
|
|
to: '2024-07-01',
|
|
timezone: 'UTC',
|
|
now,
|
|
});
|
|
|
|
expect(ninetyTwo.bucket).toBe('day');
|
|
|
|
const ninetyThree = resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-04-01',
|
|
to: '2024-07-02',
|
|
timezone: 'UTC',
|
|
now,
|
|
});
|
|
|
|
expect(ninetyThree.bucket).toBe('month');
|
|
expect(ninetyThree.start).toEqual(new Date('2024-04-01T00:00:00.000Z'));
|
|
expect(ninetyThree.end).toEqual(new Date('2024-07-03T00:00:00.000Z'));
|
|
expect(ninetyThree.previousStart).toEqual(new Date('2023-12-30T00:00:00.000Z'));
|
|
});
|
|
|
|
it('allows a single-day range and a range ending today', () => {
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2025-01-15',
|
|
to: '2025-01-15',
|
|
timezone: 'UTC',
|
|
now,
|
|
});
|
|
|
|
expect(resolved.start).toEqual(new Date('2025-01-15T00:00:00.000Z'));
|
|
expect(resolved.end).toEqual(new Date('2025-01-16T00:00:00.000Z'));
|
|
expect(resolved.previousStart).toEqual(new Date('2025-01-14T00:00:00.000Z'));
|
|
});
|
|
|
|
it('keeps local midnight bounds across the March DST change in America/New_York', () => {
|
|
// DST began on 2024-03-10 in New York (EST -05:00 -> EDT -04:00).
|
|
const resolved = resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-03-06',
|
|
to: '2024-03-12',
|
|
timezone: 'America/New_York',
|
|
now,
|
|
});
|
|
|
|
// 2024-03-06T00:00 EST
|
|
expect(resolved.start).toEqual(new Date('2024-03-06T05:00:00.000Z'));
|
|
// 2024-03-13T00:00 EDT
|
|
expect(resolved.end).toEqual(new Date('2024-03-13T04:00:00.000Z'));
|
|
// 7 calendar days, not 7 * 24h: 2024-02-28T00:00 EST
|
|
expect(resolved.previousStart).toEqual(new Date('2024-02-28T05:00:00.000Z'));
|
|
expect(resolved.from).toBe('2024-03-06');
|
|
expect(resolved.to).toBe('2024-03-12');
|
|
expect(resolved.bucket).toBe('day');
|
|
});
|
|
|
|
it('evaluates "today" in the request timezone', () => {
|
|
// 2025-01-15T03:00Z is still 2025-01-14 in Los Angeles, so 2025-01-15 is in the future there.
|
|
const lateNow = new Date('2025-01-15T03:00:00.000Z');
|
|
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2025-01-10',
|
|
to: '2025-01-15',
|
|
timezone: 'America/Los_Angeles',
|
|
now: lateNow,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2025-01-10',
|
|
to: '2025-01-15',
|
|
timezone: 'UTC',
|
|
now: lateNow,
|
|
}),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('rejects from after to', () => {
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-02-10',
|
|
to: '2024-02-01',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
});
|
|
|
|
it('rejects a missing bound', () => {
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-02-01',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
to: '2024-02-01',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
});
|
|
|
|
it('rejects a to in the future', () => {
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2025-01-01',
|
|
to: '2025-01-16',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
});
|
|
|
|
it('rejects ranges starting more than a year and a day ago', () => {
|
|
// now is 2025-01-15, so the earliest allowed start is 2024-01-14.
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-01-13',
|
|
to: '2024-03-10',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
|
|
// Exactly a year and a day ago is allowed, up to today.
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-01-14',
|
|
to: '2025-01-15',
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('rejects malformed or non-existent dates', () => {
|
|
for (const to of ['2024-02-30', '2024-13-01', '2024-02-1', '2024-02-01T00:00:00Z', 'yesterday']) {
|
|
expect(() =>
|
|
resolveTeamAnalyticsRange({
|
|
range: 'custom',
|
|
from: '2024-06-01',
|
|
to,
|
|
timezone: 'UTC',
|
|
now,
|
|
}),
|
|
).toThrow(invalidRequest);
|
|
}
|
|
});
|
|
});
|
|
});
|