Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/commands/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
});
});
12 changes: 10 additions & 2 deletions src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`;
Expand Down Expand Up @@ -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).
Expand Down
18 changes: 18 additions & 0 deletions src/commands/test.result.history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});

// ---------------------------------------------------------------------------
Expand Down
12 changes: 10 additions & 2 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down