From cb27d82e8f4522b9085795f3f934c7e627ef7c0c Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 13 Aug 2026 09:07:49 +0000 Subject: [PATCH] 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. --- .../report-export-request.util.spec.ts | 62 +++++++++++++++++++ .../reports/report-export-request.util.ts | 29 +++++++++ .../src/modules/reports/reports.controller.ts | 18 ++---- 3 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/reports/report-export-request.util.spec.ts create mode 100644 apps/edr-freight-api/src/modules/reports/report-export-request.util.ts diff --git a/apps/edr-freight-api/src/modules/reports/report-export-request.util.spec.ts b/apps/edr-freight-api/src/modules/reports/report-export-request.util.spec.ts new file mode 100644 index 000000000..dc6fa6230 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/report-export-request.util.spec.ts @@ -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); + }); +}); diff --git a/apps/edr-freight-api/src/modules/reports/report-export-request.util.ts b/apps/edr-freight-api/src/modules/reports/report-export-request.util.ts new file mode 100644 index 000000000..18f2fa322 --- /dev/null +++ b/apps/edr-freight-api/src/modules/reports/report-export-request.util.ts @@ -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, + 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; +} diff --git a/apps/edr-freight-api/src/modules/reports/reports.controller.ts b/apps/edr-freight-api/src/modules/reports/reports.controller.ts index 7941c7583..4d213bc2b 100644 --- a/apps/edr-freight-api/src/modules/reports/reports.controller.ts +++ b/apps/edr-freight-api/src/modules/reports/reports.controller.ts @@ -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 { 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 =