From 0f40e5682b079ea0ae8a9aa42974a56866ad8090 Mon Sep 17 00:00:00 2001 From: Rahul Joshi <186129212+crypticsaiyan@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:36:39 +0530 Subject: [PATCH] fix(test,project): replace crashes with VALIDATION_ERROR on --since overflow and --password-file ENOENT --- src/commands/project.test.ts | 54 ++++++++++++++++++++++++ src/commands/project.ts | 12 +++++- src/commands/test.result.history.spec.ts | 18 ++++++++ src/commands/test.ts | 12 +++++- 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/commands/project.test.ts b/src/commands/project.test.ts index 0037b8c..defb206 100644 --- a/src/commands/project.test.ts +++ b/src/commands/project.test.ts @@ -564,6 +564,34 @@ describe('runCreate', () => { ).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' }); expect(fetchImpl).not.toHaveBeenCalled(); }); + + it('--password-file with a missing path throws VALIDATION_ERROR, not raw ENOENT', async () => { + const { credentialsPath } = makeCreds(); + const fetchImpl = vi.fn(async () => { + throw new Error('should not reach network'); + }); + + await expect( + runCreate( + { + profile: 'default', + output: 'json', + debug: false, + type: 'frontend', + name: 'Proj', + targetUrl: 'https://example.com', + passwordFile: '/nonexistent/path/secret.txt', + }, + { + credentialsPath, + fetchImpl: fetchImpl as unknown as typeof fetch, + stdout: () => {}, + stderr: () => {}, + }, + ), + ).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); }); // --------------------------------------------------------------------------- @@ -748,4 +776,30 @@ describe('runUpdate', () => { expect(result.id).toBe('proj_json_no_fields'); expect(result.updatedFields).toBeUndefined(); }); + + it('--password-file with a missing path throws VALIDATION_ERROR, not raw ENOENT', async () => { + const { credentialsPath } = makeCreds(); + const fetchImpl = vi.fn(async () => { + throw new Error('should not reach network'); + }); + + await expect( + runUpdate( + { + profile: 'default', + output: 'json', + debug: false, + projectId: 'proj_abc', + passwordFile: '/nonexistent/path/secret.txt', + }, + { + credentialsPath, + fetchImpl: fetchImpl as unknown as typeof fetch, + stdout: () => {}, + stderr: () => {}, + }, + ), + ).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); }); diff --git a/src/commands/project.ts b/src/commands/project.ts index 277f3ca..7bfc838 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -185,7 +185,11 @@ export async function runCreate( // Resolve password: flag > file > none let password = opts.password; if (password === undefined && opts.passwordFile !== undefined) { - password = readFileSync(opts.passwordFile, 'utf8').trim(); + try { + password = readFileSync(opts.passwordFile, 'utf8').trim(); + } catch { + throw localValidationError('--password-file: file not found or unreadable'); + } } const idempotencyKey = opts.idempotencyKey ?? `cli-proj-create-${randomUUID()}`; @@ -257,7 +261,11 @@ export async function runUpdate( // Resolve password let password = opts.password; if (password === undefined && opts.passwordFile !== undefined) { - password = readFileSync(opts.passwordFile, 'utf8').trim(); + try { + password = readFileSync(opts.passwordFile, 'utf8').trim(); + } catch { + throw localValidationError('--password-file: file not found or unreadable'); + } } // P2-7: guard --url against localhost/RFC1918/non-http(s). diff --git a/src/commands/test.result.history.spec.ts b/src/commands/test.result.history.spec.ts index dea16b6..74f4a88 100644 --- a/src/commands/test.result.history.spec.ts +++ b/src/commands/test.result.history.spec.ts @@ -167,6 +167,24 @@ describe('parseDuration', () => { it('case-insensitive day suffix', () => { expect(parseDuration('7D', NOW)).toBe('2026-05-27T12:00:00.000Z'); }); + + it('overflow hours throws VALIDATION_ERROR instead of crashing', () => { + expect(() => parseDuration('99999999999h', NOW)).toThrow(); + try { + parseDuration('99999999999h', NOW); + } catch (err: unknown) { + expect((err as { code?: string }).code).toBe('VALIDATION_ERROR'); + } + }); + + it('overflow days throws VALIDATION_ERROR instead of crashing', () => { + expect(() => parseDuration('99999999999d', NOW)).toThrow(); + try { + parseDuration('99999999999d', NOW); + } catch (err: unknown) { + expect((err as { code?: string }).code).toBe('VALIDATION_ERROR'); + } + }); }); // --------------------------------------------------------------------------- diff --git a/src/commands/test.ts b/src/commands/test.ts index e254958..b538f4f 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -3698,12 +3698,20 @@ export function parseDuration(raw: string, now: Date = new Date()): string { const hourMatch = /^(\d+)h$/i.exec(raw); if (hourMatch) { const hours = Number(hourMatch[1]); - return new Date(now.getTime() - hours * 60 * 60 * 1000).toISOString(); + const result = new Date(now.getTime() - hours * 60 * 60 * 1000); + if (!Number.isFinite(result.getTime())) { + throw localValidationError('since', 'duration is too large; maximum is ~1141552511h'); + } + return result.toISOString(); } const dayMatch = /^(\d+)d$/i.exec(raw); if (dayMatch) { const days = Number(dayMatch[1]); - return new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString(); + const result = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + if (!Number.isFinite(result.getTime())) { + throw localValidationError('since', 'duration is too large; maximum is ~47564688d'); + } + return result.toISOString(); } // Pass-through: ISO timestamp or epoch value — server validates. return raw;