mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Stop two test suites from depending on the network and the scheduler
The second flake recorded as 'costs' is not a timing flake. It is a live HTTPS call: budget.service.ts settlement() reaches getRates() unconditionally, unlike the two guarded call sites above it, and ExchangeRatesService fetches api.frankfurter.dev with a 10 s abort. tests/integration/budget.test.ts stubbed fetch nowhere at all, so the suite talked to the internet - fast when the API answers, ten seconds per call when it does not. The stub fails closed, which changes no assertion: every settlement case here is single-currency, so rates never enter the arithmetic. tools-budget-advanced had the stub inside a single case; hoisted so a second settlement case cannot quietly reintroduce the call. The five ReDoS budgets in maps.service.test.ts measured wall-clock time, which on a loaded machine reports how long the test waited for a core. MAPS-024 has gone red at 523 ms against an unchanged file for exactly that reason. They now measure CPU time, which is what catastrophic backtracking actually burns. This does not soften them: CPU time is always <= wall-clock time, so every budget got stricter, and pool:'forks' keeps the measurement to this file.
This commit is contained in:
@@ -63,9 +63,17 @@ beforeAll(async () => {
|
||||
beforeEach(() => {
|
||||
resetTestDb(testDb);
|
||||
resetRateLimits(nestApp);
|
||||
// GET /budget/settlement reaches ExchangeRatesService.getRates unconditionally
|
||||
// (budget.service.ts settlement()), and that is a real fetch to
|
||||
// api.frankfurter.dev with a 10 s abort. Without this stub the suite talks to
|
||||
// the internet: slow, offline-dependent, and it makes the run take minutes
|
||||
// longer on a machine that cannot reach it. Fail closed — every assertion here
|
||||
// is single-currency, so rates never enter the arithmetic.
|
||||
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
await nestApp.close();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
@@ -49,9 +49,14 @@ beforeEach(() => {
|
||||
resetTestDb(testDb);
|
||||
broadcastMock.mockClear();
|
||||
delete process.env.DEMO_MODE;
|
||||
// get_settlement_summary calls getRates(), which is a real fetch to
|
||||
// api.frankfurter.dev with a 10 s abort. One case used to stub this inline;
|
||||
// hoisted so a second settlement case cannot quietly reintroduce the call.
|
||||
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
@@ -328,8 +333,6 @@ describe('Settlement tools', () => {
|
||||
});
|
||||
|
||||
it('get_settlement_summary returns balances and flows', async () => {
|
||||
// Avoid a real exchange-rate network call: force getRates() to fail closed.
|
||||
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
|
||||
try {
|
||||
const { user, other, trip } = tripWithTwo();
|
||||
// user paid 100 for an item split between both → other owes user 50.
|
||||
|
||||
@@ -141,6 +141,23 @@ afterEach(() => {
|
||||
|
||||
// ── parseOpeningHours ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* CPU milliseconds burned by `fn`.
|
||||
*
|
||||
* The ReDoS budgets below used to measure wall-clock time, which on a loaded
|
||||
* machine reports how long the test waited for a core rather than how long the
|
||||
* regex ran — MAPS-024 has gone red at 523 ms on an unchanged file for exactly
|
||||
* that reason. Catastrophic backtracking burns CPU, so CPU time is both the
|
||||
* honest signal and a STRICTER one: cpu <= wall always, so no budget here got
|
||||
* looser. vitest.config.ts uses pool:'forks', so this counts only this file.
|
||||
*/
|
||||
function cpuMillis(fn: () => void): number {
|
||||
const before = process.cpuUsage();
|
||||
fn();
|
||||
const d = process.cpuUsage(before);
|
||||
return (d.user + d.system) / 1000;
|
||||
}
|
||||
|
||||
describe('parseOpeningHours', () => {
|
||||
it('MAPS-001: returns 7 weekday descriptions and openNow', () => {
|
||||
const result = parseOpeningHours('Mo-Fr 09:00-18:00');
|
||||
@@ -214,12 +231,9 @@ describe('parseOpeningHours', () => {
|
||||
expect(result.weekdayDescriptions[1]).toContain('?');
|
||||
});
|
||||
|
||||
it('MAPS-007 (ReDoS): opening hours regex on adversarial input < 100ms', () => {
|
||||
it('MAPS-007 (ReDoS): opening hours regex on adversarial input < 100ms of CPU', () => {
|
||||
const adversarial = 'Mo' + ',Mo'.repeat(500) + ' closed';
|
||||
const start = Date.now();
|
||||
parseOpeningHours(adversarial);
|
||||
const elapsed = Date.now() - start;
|
||||
expect(elapsed).toBeLessThan(100);
|
||||
expect(cpuMillis(() => { parseOpeningHours(adversarial); })).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('MAPS-007b: emits machine-readable periods in Google day numbering (Sunday = 0)', () => {
|
||||
@@ -545,32 +559,24 @@ describe('resolveGoogleMapsUrl coordinate extraction (ReDoS guards)', () => {
|
||||
expect(result.lng).toBeCloseTo(-74.0445, 3);
|
||||
});
|
||||
|
||||
it('MAPS-024 (ReDoS): /@(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/ on adversarial input < 500ms', () => {
|
||||
it('MAPS-024 (ReDoS): /@(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/ on adversarial input < 500ms of CPU', () => {
|
||||
const adversarial = '/@' + '1'.repeat(10000) + '.';
|
||||
const start = Date.now();
|
||||
adversarial.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*)/);
|
||||
expect(Date.now() - start).toBeLessThan(500);
|
||||
expect(cpuMillis(() => { adversarial.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*)/); })).toBeLessThan(500);
|
||||
});
|
||||
|
||||
it('MAPS-025 (ReDoS): /!3d(-?\\d+\\.?\\d*)!4d/ on adversarial input < 500ms', () => {
|
||||
it('MAPS-025 (ReDoS): /!3d(-?\\d+\\.?\\d*)!4d/ on adversarial input < 500ms of CPU', () => {
|
||||
const adversarial = '!3d' + '1'.repeat(10000) + '.';
|
||||
const start = Date.now();
|
||||
adversarial.match(/!3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)/);
|
||||
expect(Date.now() - start).toBeLessThan(500);
|
||||
expect(cpuMillis(() => { adversarial.match(/!3d(-?\d+\.?\d*)!4d(-?\d+\.?\d*)/); })).toBeLessThan(500);
|
||||
});
|
||||
|
||||
it('MAPS-026 (ReDoS): /[?&]q=(-?\\d+\\.?\\d*)/ on adversarial input < 500ms', () => {
|
||||
it('MAPS-026 (ReDoS): /[?&]q=(-?\\d+\\.?\\d*)/ on adversarial input < 500ms of CPU', () => {
|
||||
const adversarial = '?q=' + '1'.repeat(10000) + '.';
|
||||
const start = Date.now();
|
||||
adversarial.match(/[?&]q=(-?\d+\.?\d*),(-?\d+\.?\d*)/);
|
||||
expect(Date.now() - start).toBeLessThan(500);
|
||||
expect(cpuMillis(() => { adversarial.match(/[?&]q=(-?\d+\.?\d*),(-?\d+\.?\d*)/); })).toBeLessThan(500);
|
||||
});
|
||||
|
||||
it('MAPS-027 (ReDoS): /<[^>]+>/ HTML strip on adversarial input < 100ms', () => {
|
||||
it('MAPS-027 (ReDoS): /<[^>]+>/ HTML strip on adversarial input < 100ms of CPU', () => {
|
||||
const adversarial = '<' + 'a'.repeat(10000);
|
||||
const start = Date.now();
|
||||
adversarial.replace(/<[^>]+>/g, '');
|
||||
expect(Date.now() - start).toBeLessThan(100);
|
||||
expect(cpuMillis(() => { adversarial.replace(/<[^>]+>/g, ''); })).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('MAPS-028: throws when no coordinates found in URL', async () => {
|
||||
|
||||
Reference in New Issue
Block a user