1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

chore: add tests for new traffic usage functions (#9208)

This PR adds tests to all the TODOs created in
https://github.com/Unleash/unleash/pull/9191.

Additionally it finally manages to refactor the `toChartData` function.
This commit is contained in:
Thomas Heartman 2025-02-05 09:47:36 +01:00 committed by GitHub
parent 419d189ddc
commit bd6a90ffd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 362 additions and 100 deletions

View File

@ -233,7 +233,7 @@ const NewNetworkTrafficUsage: FC = () => {
const traffic = useInstanceTrafficMetrics2( const traffic = useInstanceTrafficMetrics2(
chartDataSelection.grouping, chartDataSelection.grouping,
toDateRange(chartDataSelection), toDateRange(chartDataSelection, currentDate),
); );
const data = newToChartData(traffic.usage); const data = newToChartData(traffic.usage);

View File

@ -0,0 +1,30 @@
import { type ChartDataSelection, toDateRange } from './chart-data-selection';
test('daily conversion', () => {
const input: ChartDataSelection = {
grouping: 'daily',
month: '2021-03',
};
const expectedOutput = {
from: '2021-03-01',
to: '2021-03-31',
};
expect(toDateRange(input)).toStrictEqual(expectedOutput);
});
test('monthly conversion', () => {
const now = new Date('2023-06-15');
const input: ChartDataSelection = {
grouping: 'monthly',
monthsBack: 3,
};
const expectedOutput = {
from: '2023-03-01',
to: '2023-06-30',
};
expect(toDateRange(input, now)).toStrictEqual(expectedOutput);
});

View File

@ -10,9 +10,9 @@ export type ChartDataSelection =
monthsBack: number; monthsBack: number;
}; };
// todo: write test
export const toDateRange = ( export const toDateRange = (
selection: ChartDataSelection, selection: ChartDataSelection,
now = new Date(),
): { from: string; to: string } => { ): { from: string; to: string } => {
const fmt = (date: Date) => format(date, 'yyyy-MM-dd'); const fmt = (date: Date) => format(date, 'yyyy-MM-dd');
if (selection.grouping === 'daily') { if (selection.grouping === 'daily') {
@ -21,7 +21,6 @@ export const toDateRange = (
const to = fmt(endOfMonth(month)); const to = fmt(endOfMonth(month));
return { from, to }; return { from, to };
} else { } else {
const now = new Date();
const from = fmt(startOfMonth(subMonths(now, selection.monthsBack))); const from = fmt(startOfMonth(subMonths(now, selection.monthsBack)));
const to = fmt(endOfMonth(now)); const to = fmt(endOfMonth(now));
return { from, to }; return { from, to };

View File

@ -0,0 +1,158 @@
import type { TrafficUsageDataSegmentedCombinedSchema } from 'openapi';
import { toChartData } from './chart-functions';
import { endpointsInfo } from './endpoint-info';
describe('toChartData', () => {
const dataPoint = (period: string, count: number) => ({
period,
trafficTypes: [{ count, group: 'successful-requests' }],
});
const fromEndpointInfo = (endpoint: keyof typeof endpointsInfo) => {
const info = endpointsInfo[endpoint];
return {
backgroundColor: info.color,
hoverBackgroundColor: info.color,
label: info.label,
};
};
test('monthly data conversion', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly',
dateRange: {
from: '2025-01-01',
to: '2025-06-30',
},
apiData: [
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-06', 5),
dataPoint('2025-05', 4),
dataPoint('2025-02', 6),
dataPoint('2025-04', 2),
],
},
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-06', 10),
dataPoint('2025-01', 7),
dataPoint('2025-03', 11),
dataPoint('2025-04', 13),
],
},
],
};
const expectedOutput = {
datasets: [
{
data: [0, 6, 0, 2, 4, 5],
...fromEndpointInfo('/api/admin'),
},
{
data: [7, 0, 11, 13, 0, 10],
...fromEndpointInfo('/api/client'),
},
],
labels: [
'2025-01',
'2025-02',
'2025-03',
'2025-04',
'2025-05',
'Current month',
],
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('daily data conversion', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: {
from: '2025-01-01',
to: '2025-01-31',
},
apiData: [
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-01-01', 5),
dataPoint('2025-01-15', 4),
dataPoint('2025-01-14', 6),
dataPoint('2025-01-06', 2),
],
},
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-01-02', 2),
dataPoint('2025-01-17', 6),
dataPoint('2025-01-19', 4),
dataPoint('2025-01-06', 8),
],
},
],
};
const expectedOutput = {
datasets: [
{
data: [
5, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 6, 4, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
...fromEndpointInfo('/api/admin'),
},
{
data: [
0, 2, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 4,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
...fromEndpointInfo('/api/client'),
},
],
labels: Array.from({ length: 31 }).map((_, index) =>
(index + 1).toString(),
),
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('sorts endpoints according to endpoint data spec', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: {
from: '2025-01-01',
to: '2025-01-31',
},
apiData: [
{ apiPath: '/api/frontend', dataPoints: [] },
{ apiPath: '/api/client', dataPoints: [] },
{ apiPath: '/api/admin', dataPoints: [] },
],
};
const expectedOutput = {
datasets: [
{ label: 'Admin' },
{ label: 'Frontend' },
{ label: 'Server' },
],
};
expect(toChartData(input)).toMatchObject(expectedOutput);
});
test('returns empty data if traffic is undefined', () => {
expect(toChartData(undefined)).toStrictEqual({
labels: [],
datasets: [],
});
});
});

View File

@ -11,113 +11,74 @@ import { formatDay, formatMonth } from './dates';
import type { ChartDataSelection } from './chart-data-selection'; import type { ChartDataSelection } from './chart-data-selection';
export type ChartDatasetType = ChartDataset<'bar'>; export type ChartDatasetType = ChartDataset<'bar'>;
// todo: test
export const toChartData = ( export const toChartData = (
traffic?: TrafficUsageDataSegmentedCombinedSchema, traffic?: TrafficUsageDataSegmentedCombinedSchema,
): { datasets: ChartDatasetType[]; labels: (string | number)[] } => { ): { datasets: ChartDatasetType[]; labels: string[] } => {
if (!traffic) { if (!traffic) {
return { labels: [], datasets: [] }; return { labels: [], datasets: [] };
} }
if (traffic.grouping === 'monthly') { const { newRecord, labels } = getLabelsAndRecords(traffic);
return toMonthlyChartData(traffic); const datasets = traffic.apiData
} else {
return toDailyChartData(traffic);
}
};
type SegmentedSchemaApiData =
TrafficUsageDataSegmentedCombinedSchema['apiData'][0];
// todo: integrate filtering `filterData` frontend/src/component/admin/network/NetworkTrafficUsage/util.ts
const prepareApiData = (
apiData: TrafficUsageDataSegmentedCombinedSchema['apiData'],
) =>
apiData
.filter((item) => item.apiPath in endpointsInfo)
.sort( .sort(
(item1: SegmentedSchemaApiData, item2: SegmentedSchemaApiData) => (item1, item2) =>
endpointsInfo[item1.apiPath].order - endpointsInfo[item1.apiPath].order -
endpointsInfo[item2.apiPath].order, endpointsInfo[item2.apiPath].order,
); )
.map((item) => {
const toMonthlyChartData = ( const record = newRecord();
traffic: TrafficUsageDataSegmentedCombinedSchema, for (const dataPoint of Object.values(item.dataPoints)) {
): { datasets: ChartDatasetType[]; labels: string[] } => { record[dataPoint.period] = dataPoint.trafficTypes[0].count;
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1;
const datasets = prepareApiData(traffic.apiData).map(
(item: SegmentedSchemaApiData) => {
const monthsRec: { [month: string]: number } = {};
for (let i = 0; i < numMonths; i++) {
monthsRec[formatMonth(addMonths(from, i))] = 0;
}
for (const month of Object.values(item.dataPoints)) {
monthsRec[month.period] = month.trafficTypes[0].count;
} }
const epInfo = endpointsInfo[item.apiPath]; const epInfo = endpointsInfo[item.apiPath];
return { return {
label: epInfo.label, label: epInfo.label,
data: Object.values(monthsRec), data: Object.values(record),
backgroundColor: epInfo.color, backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color, hoverBackgroundColor: epInfo.color,
}; };
}, });
);
return { datasets, labels };
};
const getLabelsAndRecords = (
traffic: TrafficUsageDataSegmentedCombinedSchema,
) => {
if (traffic.grouping === 'monthly') {
const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to);
const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1;
const monthsRec: { [month: string]: number } = {};
for (let i = 0; i < numMonths; i++) {
monthsRec[formatMonth(addMonths(from, i))] = 0;
}
const labels = Array.from({ length: numMonths }).map((_, index) => const labels = Array.from({ length: numMonths }).map((_, index) =>
index === numMonths - 1 index === numMonths - 1
? 'Current month' ? 'Current month'
: formatMonth(addMonths(from, index)), : formatMonth(addMonths(from, index)),
); );
return { newRecord: () => ({ ...monthsRec }), labels };
return { datasets, labels }; } else {
};
const toDailyChartData = (
traffic: TrafficUsageDataSegmentedCombinedSchema,
): { datasets: ChartDatasetType[]; labels: number[] } => {
const from = new Date(traffic.dateRange.from); const from = new Date(traffic.dateRange.from);
const to = new Date(traffic.dateRange.to); const to = new Date(traffic.dateRange.to);
const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1; const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1;
const daysRec: { [day: string]: number } = {}; const daysRec: { [day: string]: number } = {};
for (let i = 0; i < numDays; i++) { for (let i = 0; i < numDays; i++) {
daysRec[formatDay(addDays(from, i))] = 0; daysRec[formatDay(addDays(from, i))] = 0;
} }
const getDaysRec = () => ({ // simplification: the chart only allows for single, full-month views
...daysRec, // when you use a daily chart, so just use the day of the month as the label
}); const labels = Array.from({ length: numDays }).map((_, index) =>
(index + 1).toString(),
const datasets = prepareApiData(traffic.apiData).map(
(item: SegmentedSchemaApiData) => {
const daysRec = getDaysRec();
for (const day of Object.values(item.dataPoints)) {
daysRec[day.period] = day.trafficTypes[0].count;
}
const epInfo = endpointsInfo[item.apiPath];
return {
label: epInfo.label,
data: Object.values(daysRec),
backgroundColor: epInfo.color,
hoverBackgroundColor: epInfo.color,
};
},
); );
// simplification: assuming days run in a single month from the 1st onwards return { newRecord: () => ({ ...daysRec }), labels };
const labels = Array.from({ length: numDays }).map((_, index) => index + 1); }
return { datasets, labels };
}; };
const [lastLabel, ...otherLabels] = Object.values(endpointsInfo) const [lastLabel, ...otherLabels] = Object.values(endpointsInfo)

View File

@ -0,0 +1,31 @@
import { generateSelectablePeriodsFromDate } from './selectable-periods';
test('marks months before May 2024 as unselectable', () => {
const now = new Date('2025-01-01');
const selectablePeriods = generateSelectablePeriodsFromDate(now);
expect(
selectablePeriods.map(({ key, selectable }) => ({ key, selectable })),
).toEqual([
{ key: '2025-01', selectable: true },
{ key: '2024-12', selectable: true },
{ key: '2024-11', selectable: true },
{ key: '2024-10', selectable: true },
{ key: '2024-09', selectable: true },
{ key: '2024-08', selectable: true },
{ key: '2024-07', selectable: true },
{ key: '2024-06', selectable: true },
{ key: '2024-05', selectable: true },
{ key: '2024-04', selectable: false },
{ key: '2024-03', selectable: false },
{ key: '2024-02', selectable: false },
]);
});
test('generates 12 months, including the current month', () => {
const now = new Date('2025-01-01');
const selectablePeriods = generateSelectablePeriodsFromDate(now);
expect(selectablePeriods.length).toBe(12);
expect(selectablePeriods[0].label).toBe('Current month');
});

View File

@ -1,4 +1,4 @@
import { getDaysInMonth, subMonths } from 'date-fns'; import { getDaysInMonth } from 'date-fns';
import { currentDate, formatMonth } from './dates'; import { currentDate, formatMonth } from './dates';
import { TRAFFIC_MEASUREMENT_START_DATE } from 'utils/traffic-calculations'; import { TRAFFIC_MEASUREMENT_START_DATE } from 'utils/traffic-calculations';
@ -36,20 +36,23 @@ export const toSelectablePeriod = (
}; };
}; };
// todo: test export const generateSelectablePeriodsFromDate = (now: Date) => {
const generateSelectablePeriodsFromDate = (now: Date) => {
const selectablePeriods = [toSelectablePeriod(now, 'Current month')]; const selectablePeriods = [toSelectablePeriod(now, 'Current month')];
for ( for (
let subtractMonthCount = 1; let subtractMonthCount = 1;
subtractMonthCount < 12; subtractMonthCount < 12;
subtractMonthCount++ subtractMonthCount++
) { ) {
const date = subMonths(now, subtractMonthCount); // this complicated calc avoids DST issues
const utcYear = now.getUTCFullYear();
const utcMonth = now.getUTCMonth();
const targetMonth = utcMonth - subtractMonthCount;
const targetDate = new Date(Date.UTC(utcYear, targetMonth, 1, 0, 0, 0));
selectablePeriods.push( selectablePeriods.push(
toSelectablePeriod( toSelectablePeriod(
date, targetDate,
undefined, undefined,
date >= TRAFFIC_MEASUREMENT_START_DATE, targetDate >= TRAFFIC_MEASUREMENT_START_DATE,
), ),
); );
} }

View File

@ -3,6 +3,7 @@ import {
calculateEstimatedMonthlyCost, calculateEstimatedMonthlyCost,
calculateOverageCost, calculateOverageCost,
calculateProjectedUsage, calculateProjectedUsage,
calculateTotalUsage,
cleanTrafficData, cleanTrafficData,
} from './traffic-calculations'; } from './traffic-calculations';
import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods'; import { toSelectablePeriod } from '../component/admin/network/NetworkTrafficUsage/selectable-periods';
@ -146,7 +147,7 @@ describe('traffic overage calculation', () => {
}); });
describe('filtering out unwanted data', () => { describe('filtering out unwanted data', () => {
test('it removes the /edge endpoint data', () => { it('removes the /edge endpoint data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = { const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily', grouping: 'daily',
dateRange: { from: '2025-02-01', to: '2025-02-28' }, dateRange: { from: '2025-02-01', to: '2025-02-28' },
@ -171,7 +172,7 @@ describe('filtering out unwanted data', () => {
expect(cleanTrafficData(input)).toStrictEqual(expected); expect(cleanTrafficData(input)).toStrictEqual(expected);
}); });
test('it removes any data from before the traffic measuring was put in place', () => { it('removes any data from before the traffic measuring was put in place', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = { const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly', grouping: 'monthly',
dateRange: { dateRange: {
@ -212,3 +213,79 @@ describe('filtering out unwanted data', () => {
expect(cleanTrafficData(input)).toStrictEqual(expected); expect(cleanTrafficData(input)).toStrictEqual(expected);
}); });
}); });
describe('calculateTotalUsage', () => {
const dataPoint = (period: string, count: number) => ({
period,
trafficTypes: [{ count, group: 'successful-requests' }],
});
it('calculates total from daily data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'daily',
dateRange: { from: '2025-02-01', to: '2025-02-28' },
apiData: [
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2024-02-01', 1),
dataPoint('2024-02-15', 2),
dataPoint('2024-02-07', 3),
],
},
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2024-02-01', 4),
dataPoint('2024-02-15', 5),
dataPoint('2024-02-07', 6),
],
},
{
apiPath: '/api/frontend',
dataPoints: [
dataPoint('2024-02-01', 7),
dataPoint('2024-02-15', 8),
dataPoint('2024-02-07', 9),
],
},
],
};
expect(calculateTotalUsage(input)).toBe(45);
});
it('calculates total for the most recent month in monthly data', () => {
const input: TrafficUsageDataSegmentedCombinedSchema = {
grouping: 'monthly',
dateRange: { from: '2024-10-01', to: '2025-01-31' },
apiData: [
{
apiPath: '/api/client',
dataPoints: [
dataPoint('2025-01', 1),
dataPoint('2024-12', 2),
dataPoint('2024-10', 3),
],
},
{
apiPath: '/api/admin',
dataPoints: [
dataPoint('2025-01', 4),
dataPoint('2024-11', 5),
dataPoint('2024-10', 6),
],
},
{
apiPath: '/api/frontend',
dataPoints: [
dataPoint('2024-11', 7),
dataPoint('2024-12', 8),
dataPoint('2024-10', 9),
],
},
],
};
expect(calculateTotalUsage(input)).toBe(5);
});
});

View File

@ -7,6 +7,7 @@ import {
daysInCurrentMonth, daysInCurrentMonth,
} from '../component/admin/network/NetworkTrafficUsage/dates'; } from '../component/admin/network/NetworkTrafficUsage/dates';
import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions'; import type { ChartDatasetType } from '../component/admin/network/NetworkTrafficUsage/chart-functions';
import { format } from 'date-fns';
const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5;
const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000;
@ -39,13 +40,13 @@ export const cleanTrafficData = (
return { apiData: cleanedApiData, ...rest }; return { apiData: cleanedApiData, ...rest };
}; };
// todo: extract "currentMonth" into a function argument instead
const monthlyTrafficDataToCurrentUsage = ( const monthlyTrafficDataToCurrentUsage = (
apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[], apiData: TrafficUsageDataSegmentedCombinedSchemaApiDataItem[],
latestMonth: string,
) => { ) => {
return apiData.reduce((acc, current) => { return apiData.reduce((acc, current) => {
const currentPoint = current.dataPoints.find( const currentPoint = current.dataPoints.find(
({ period }) => period === currentMonth, ({ period }) => period === latestMonth,
); );
const pointUsage = const pointUsage =
currentPoint?.trafficTypes.reduce( currentPoint?.trafficTypes.reduce(
@ -68,9 +69,8 @@ const dailyTrafficDataToCurrentUsage = (
.reduce((acc, count) => acc + count, 0); .reduce((acc, count) => acc + count, 0);
}; };
// todo: test
// Return the total number of requests for the selected month if showing daily // Return the total number of requests for the selected month if showing daily
// data, or the current month if showing monthly data // data, or the total for the most recent month if showing monthly data
export const calculateTotalUsage = ( export const calculateTotalUsage = (
data?: TrafficUsageDataSegmentedCombinedSchema, data?: TrafficUsageDataSegmentedCombinedSchema,
): number => { ): number => {
@ -78,9 +78,12 @@ export const calculateTotalUsage = (
return 0; return 0;
} }
const { grouping, apiData } = data; const { grouping, apiData } = data;
return grouping === 'monthly' if (grouping === 'monthly') {
? monthlyTrafficDataToCurrentUsage(apiData) const latestMonth = format(new Date(data.dateRange.to), 'yyyy-MM');
: dailyTrafficDataToCurrentUsage(apiData); return monthlyTrafficDataToCurrentUsage(apiData, latestMonth);
} else {
return dailyTrafficDataToCurrentUsage(apiData);
}
}; };
const calculateTrafficDataCost = ( const calculateTrafficDataCost = (