Skip to content

Commit 0f40e56

Browse files
committed
fix(test,project): replace crashes with VALIDATION_ERROR on --since overflow and --password-file ENOENT
1 parent 18f6e6e commit 0f40e56

4 files changed

Lines changed: 92 additions & 4 deletions

File tree

src/commands/project.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,34 @@ describe('runCreate', () => {
564564
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
565565
expect(fetchImpl).not.toHaveBeenCalled();
566566
});
567+
568+
it('--password-file with a missing path throws VALIDATION_ERROR, not raw ENOENT', async () => {
569+
const { credentialsPath } = makeCreds();
570+
const fetchImpl = vi.fn(async () => {
571+
throw new Error('should not reach network');
572+
});
573+
574+
await expect(
575+
runCreate(
576+
{
577+
profile: 'default',
578+
output: 'json',
579+
debug: false,
580+
type: 'frontend',
581+
name: 'Proj',
582+
targetUrl: 'https://example.com',
583+
passwordFile: '/nonexistent/path/secret.txt',
584+
},
585+
{
586+
credentialsPath,
587+
fetchImpl: fetchImpl as unknown as typeof fetch,
588+
stdout: () => {},
589+
stderr: () => {},
590+
},
591+
),
592+
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
593+
expect(fetchImpl).not.toHaveBeenCalled();
594+
});
567595
});
568596

569597
// ---------------------------------------------------------------------------
@@ -748,4 +776,30 @@ describe('runUpdate', () => {
748776
expect(result.id).toBe('proj_json_no_fields');
749777
expect(result.updatedFields).toBeUndefined();
750778
});
779+
780+
it('--password-file with a missing path throws VALIDATION_ERROR, not raw ENOENT', async () => {
781+
const { credentialsPath } = makeCreds();
782+
const fetchImpl = vi.fn(async () => {
783+
throw new Error('should not reach network');
784+
});
785+
786+
await expect(
787+
runUpdate(
788+
{
789+
profile: 'default',
790+
output: 'json',
791+
debug: false,
792+
projectId: 'proj_abc',
793+
passwordFile: '/nonexistent/path/secret.txt',
794+
},
795+
{
796+
credentialsPath,
797+
fetchImpl: fetchImpl as unknown as typeof fetch,
798+
stdout: () => {},
799+
stderr: () => {},
800+
},
801+
),
802+
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
803+
expect(fetchImpl).not.toHaveBeenCalled();
804+
});
751805
});

src/commands/project.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,11 @@ export async function runCreate(
185185
// Resolve password: flag > file > none
186186
let password = opts.password;
187187
if (password === undefined && opts.passwordFile !== undefined) {
188-
password = readFileSync(opts.passwordFile, 'utf8').trim();
188+
try {
189+
password = readFileSync(opts.passwordFile, 'utf8').trim();
190+
} catch {
191+
throw localValidationError('--password-file: file not found or unreadable');
192+
}
189193
}
190194

191195
const idempotencyKey = opts.idempotencyKey ?? `cli-proj-create-${randomUUID()}`;
@@ -257,7 +261,11 @@ export async function runUpdate(
257261
// Resolve password
258262
let password = opts.password;
259263
if (password === undefined && opts.passwordFile !== undefined) {
260-
password = readFileSync(opts.passwordFile, 'utf8').trim();
264+
try {
265+
password = readFileSync(opts.passwordFile, 'utf8').trim();
266+
} catch {
267+
throw localValidationError('--password-file: file not found or unreadable');
268+
}
261269
}
262270

263271
// P2-7: guard --url against localhost/RFC1918/non-http(s).

src/commands/test.result.history.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ describe('parseDuration', () => {
167167
it('case-insensitive day suffix', () => {
168168
expect(parseDuration('7D', NOW)).toBe('2026-05-27T12:00:00.000Z');
169169
});
170+
171+
it('overflow hours throws VALIDATION_ERROR instead of crashing', () => {
172+
expect(() => parseDuration('99999999999h', NOW)).toThrow();
173+
try {
174+
parseDuration('99999999999h', NOW);
175+
} catch (err: unknown) {
176+
expect((err as { code?: string }).code).toBe('VALIDATION_ERROR');
177+
}
178+
});
179+
180+
it('overflow days throws VALIDATION_ERROR instead of crashing', () => {
181+
expect(() => parseDuration('99999999999d', NOW)).toThrow();
182+
try {
183+
parseDuration('99999999999d', NOW);
184+
} catch (err: unknown) {
185+
expect((err as { code?: string }).code).toBe('VALIDATION_ERROR');
186+
}
187+
});
170188
});
171189

172190
// ---------------------------------------------------------------------------

src/commands/test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3698,12 +3698,20 @@ export function parseDuration(raw: string, now: Date = new Date()): string {
36983698
const hourMatch = /^(\d+)h$/i.exec(raw);
36993699
if (hourMatch) {
37003700
const hours = Number(hourMatch[1]);
3701-
return new Date(now.getTime() - hours * 60 * 60 * 1000).toISOString();
3701+
const result = new Date(now.getTime() - hours * 60 * 60 * 1000);
3702+
if (!Number.isFinite(result.getTime())) {
3703+
throw localValidationError('since', 'duration is too large; maximum is ~1141552511h');
3704+
}
3705+
return result.toISOString();
37023706
}
37033707
const dayMatch = /^(\d+)d$/i.exec(raw);
37043708
if (dayMatch) {
37053709
const days = Number(dayMatch[1]);
3706-
return new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString();
3710+
const result = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
3711+
if (!Number.isFinite(result.getTime())) {
3712+
throw localValidationError('since', 'duration is too large; maximum is ~47564688d');
3713+
}
3714+
return result.toISOString();
37073715
}
37083716
// Pass-through: ISO timestamp or epoch value — server validates.
37093717
return raw;

0 commit comments

Comments
 (0)