feat(11-05): GET/PUT /modules/tender-radar/triage routes + favOnly filter
Adds the batch-triage read/write routes (declared before @Get(':id') per
the route-order pitfall, T-11-13) and wires them through
TenderTriageService with userId/tenantId always derived from the request
context, never the body (T-11-10 / V4 IDOR). Extends TenderQueryDto/
buildTenderWhere with favOnly (UI-04): the controller resolves the
current user's favorited tenderIds server-side before building the
where-clause, and an empty favorites list yields zero matches instead of
the unfiltered catalog. Both batch-ids and favIds in-lists are bounded
(T-11-11 DoS). tenders.controller.spec.ts constructor calls updated for
the new TenderTriageService dependency (Rule 3 — required to keep the
existing suite compiling/passing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,56 @@ describe('buildTenderWhere', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTenderWhere — favOnly (UI-04, D-10, T-11-10/11)', () => {
|
||||
it('favOnly=true with favIds supplied: adds an id-in constraint containing exactly those ids', () => {
|
||||
const where = buildTenderWhere(dto({ favOnly: true }), ['t1', 't2']);
|
||||
|
||||
expect(where.AND).toEqual(
|
||||
expect.arrayContaining([{ id: { in: ['t1', 't2'] } }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('favOnly=true with empty favIds: yields a guaranteed-empty match, never "all tenders"', () => {
|
||||
const where = buildTenderWhere(dto({ favOnly: true }), []);
|
||||
|
||||
expect(where.AND).toEqual(
|
||||
expect.arrayContaining([{ id: { in: ['__none__'] } }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('favOnly=true with favIds omitted entirely: also yields a guaranteed-empty match', () => {
|
||||
const where = buildTenderWhere(dto({ favOnly: true }));
|
||||
|
||||
expect(where.AND).toEqual(
|
||||
expect.arrayContaining([{ id: { in: ['__none__'] } }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('favOnly unset: never adds an id-in constraint, regardless of favIds', () => {
|
||||
const where = buildTenderWhere(dto(), ['t1', 't2']);
|
||||
|
||||
const and = (where.AND as unknown[]) ?? [];
|
||||
const hasFavClause = and.some(
|
||||
(clause) =>
|
||||
typeof clause === 'object' &&
|
||||
clause !== null &&
|
||||
'id' in (clause as Record<string, unknown>),
|
||||
);
|
||||
expect(hasFavClause).toBe(false);
|
||||
});
|
||||
|
||||
it('favOnly=true with more favIds than MAX_FAV_IDS: the in-list is capped (T-11-11 DoS)', () => {
|
||||
const manyIds = Array.from({ length: 600 }, (_, i) => `t${i}`);
|
||||
const where = buildTenderWhere(dto({ favOnly: true }), manyIds);
|
||||
|
||||
const and = (where.AND as Array<Record<string, unknown>>) ?? [];
|
||||
const favClause = and.find((c) => 'id' in c) as
|
||||
| { id: { in: string[] } }
|
||||
| undefined;
|
||||
expect(favClause?.id.in.length).toBeLessThanOrEqual(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildOrderBy', () => {
|
||||
it('sort=deadline maps to { deadlineAt: asc }', () => {
|
||||
expect(buildOrderBy('deadline')).toEqual({ deadlineAt: 'asc' });
|
||||
|
||||
Reference in New Issue
Block a user