mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
test(freight-api): cover export field/limit/format resolution
Extracted the export route's format/cap/column-whitelist branching out of the controller into pure functions (resolveExportFormat, resolveExportCap, resolveExportColumns) and added a spec: unknown format falls back to xlsx, limit clamps to the format cap and ignores non-positive/NaN input, unknown field keys are dropped and an all-unknown fields list falls back to every column instead of shipping a blank sheet. Was untested branching logic before this.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { PDF_ROW_CAP, XLSX_ROW_CAP } from './report-export.service';
|
||||
import { resolveExportCap, resolveExportColumns, resolveExportFormat } from './report-export-request.util';
|
||||
import { ReportColumn } from './report.types';
|
||||
|
||||
describe('resolveExportFormat', () => {
|
||||
it('only \'pdf\' exports as pdf', () => {
|
||||
expect(resolveExportFormat('pdf')).toBe('pdf');
|
||||
});
|
||||
|
||||
it.each([undefined, 'xlsx', 'csv', ''])('%p falls back to xlsx', (raw) => {
|
||||
expect(resolveExportFormat(raw)).toBe('xlsx');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveExportCap', () => {
|
||||
it('missing limit uses the full format cap', () => {
|
||||
expect(resolveExportCap('xlsx', undefined)).toBe(XLSX_ROW_CAP);
|
||||
expect(resolveExportCap('pdf', undefined)).toBe(PDF_ROW_CAP);
|
||||
});
|
||||
|
||||
it('a limit under the cap is used as-is', () => {
|
||||
expect(resolveExportCap('pdf', '100')).toBe(100);
|
||||
});
|
||||
|
||||
it('a limit over the cap is clamped down', () => {
|
||||
expect(resolveExportCap('pdf', String(PDF_ROW_CAP + 1000))).toBe(PDF_ROW_CAP);
|
||||
expect(resolveExportCap('xlsx', String(XLSX_ROW_CAP + 1))).toBe(XLSX_ROW_CAP);
|
||||
});
|
||||
|
||||
it.each(['0', '-5', 'not-a-number', ''])('non-positive/invalid limit %p falls back to the cap', (raw) => {
|
||||
expect(resolveExportCap('xlsx', raw)).toBe(XLSX_ROW_CAP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveExportColumns', () => {
|
||||
const columns: ReportColumn[] = [
|
||||
{ key: 'a', label: 'A', type: 'string' },
|
||||
{ key: 'b', label: 'B', type: 'number' },
|
||||
{ key: 'c', label: 'C', type: 'money' },
|
||||
];
|
||||
const def = { columns };
|
||||
|
||||
it('missing fields returns every column', () => {
|
||||
expect(resolveExportColumns(def, undefined)).toEqual(columns);
|
||||
});
|
||||
|
||||
it('empty fields string returns every column', () => {
|
||||
expect(resolveExportColumns(def, '')).toEqual(columns);
|
||||
});
|
||||
|
||||
it('a known subset filters to just those columns, in the report\'s own order', () => {
|
||||
expect(resolveExportColumns(def, 'c,a')).toEqual([columns[0], columns[2]]);
|
||||
});
|
||||
|
||||
it('unknown keys are dropped, not passed through', () => {
|
||||
expect(resolveExportColumns(def, 'a,ghost')).toEqual([columns[0]]);
|
||||
});
|
||||
|
||||
it('all-unknown keys falls back to every column instead of a blank sheet', () => {
|
||||
expect(resolveExportColumns(def, 'ghost,also-ghost')).toEqual(columns);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PDF_ROW_CAP, XLSX_ROW_CAP } from './report-export.service';
|
||||
import { ReportColumn, ReportDefinition } from './report.types';
|
||||
|
||||
export type ExportFormat = 'xlsx' | 'pdf';
|
||||
|
||||
/** Anything but the literal string 'pdf' exports as xlsx. */
|
||||
export function resolveExportFormat(raw: string | undefined): ExportFormat {
|
||||
return raw === 'pdf' ? 'pdf' : 'xlsx';
|
||||
}
|
||||
|
||||
/** Caller's requested row limit, clamped to the format's hard cap. A
|
||||
* missing/non-positive/non-numeric limit means "as many as the format allows". */
|
||||
export function resolveExportCap(format: ExportFormat, rawLimit: string | undefined): number {
|
||||
const formatCap = format === 'pdf' ? PDF_ROW_CAP : XLSX_ROW_CAP;
|
||||
const requested = Number(rawLimit);
|
||||
return requested > 0 ? Math.min(requested, formatCap) : formatCap;
|
||||
}
|
||||
|
||||
/** Caller's requested column subset, whitelisted against the report's own
|
||||
* columns. Missing, empty, or all-unknown `rawFields` falls back to every
|
||||
* column rather than shipping a blank sheet. */
|
||||
export function resolveExportColumns(
|
||||
def: Pick<ReportDefinition, 'columns'>,
|
||||
rawFields: string | undefined,
|
||||
): ReportColumn[] {
|
||||
const requested = rawFields?.split(',').filter(Boolean);
|
||||
const filtered = requested?.length ? def.columns.filter((c) => requested.includes(c.key)) : def.columns;
|
||||
return filtered.length ? filtered : def.columns;
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import { BookingStaff } from '../../common/booking-guards';
|
||||
import { assertFreightPermission, hasFreightPermission } from '../../common/freight-permission.util';
|
||||
import { FREIGHT_PERMS, reportPermissionKey } from '../../seed/freight-permissions.registry';
|
||||
import { UserTradeAccessService } from '../user-trade-access/user-trade-access.service';
|
||||
import { PDF_ROW_CAP, ReportExportService, XLSX_ROW_CAP } from './report-export.service';
|
||||
import { ReportExportService } from './report-export.service';
|
||||
import { resolveExportCap, resolveExportColumns, resolveExportFormat } from './report-export-request.util';
|
||||
import { RawReportQuery, ReportRunnerService } from './report-runner.service';
|
||||
import { REPORTS, getReport } from './report.registry';
|
||||
import { ReportCatalogEntry, ReportDefinition } from './report.types';
|
||||
@@ -59,18 +60,9 @@ export class ReportsController {
|
||||
): Promise<void> {
|
||||
const def = this.resolve(key, user);
|
||||
const directions = await this.userTradeAccessService.resolveAllowedDirections(user);
|
||||
const format = query.format === 'pdf' ? 'pdf' : 'xlsx';
|
||||
const formatCap = format === 'pdf' ? PDF_ROW_CAP : XLSX_ROW_CAP;
|
||||
const requestedLimit = Number(query.limit);
|
||||
const cap = requestedLimit > 0 ? Math.min(requestedLimit, formatCap) : formatCap;
|
||||
|
||||
// Whitelist against the report's own columns — an unknown/empty `fields`
|
||||
// value falls back to every column rather than shipping a blank sheet.
|
||||
const requestedFields = query.fields?.split(',').filter(Boolean);
|
||||
const columns = requestedFields?.length
|
||||
? def.columns.filter((c) => requestedFields.includes(c.key))
|
||||
: def.columns;
|
||||
const exportColumns = columns.length ? columns : def.columns;
|
||||
const format = resolveExportFormat(query.format);
|
||||
const cap = resolveExportCap(format, query.limit);
|
||||
const exportColumns = resolveExportColumns(def, query.fields);
|
||||
|
||||
const { items, kpis } = await this.runner.runAll(def, query, directions, cap);
|
||||
const buffer =
|
||||
|
||||
Reference in New Issue
Block a user