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
11 changes: 11 additions & 0 deletions src/commands/schedule.create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ describe('runCreate — validation', () => {
).rejects.toMatchObject({ code: 'VALIDATION_ERROR' });
});

it('rejects a whitespace-only cron', async () => {
const { credentialsPath } = makeCreds();
await expect(
runCreate({ ...VALID, cron: ' ' }, { credentialsPath, fetchImpl: noNetwork(), ...sink }),
).rejects.toMatchObject({
code: 'VALIDATION_ERROR',
exitCode: 5,
nextAction: expect.stringContaining('--cron'),
});
});

it('rejects a missing or unknown target type', async () => {
const { credentialsPath } = makeCreds();
const deps = { credentialsPath, fetchImpl: noNetwork(), ...sink };
Expand Down
14 changes: 11 additions & 3 deletions src/commands/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,18 @@ export async function runUpdate(
}
const environment = normalizeScheduleEnvironmentName(opts.env);

// Mirror `schedule create`: a supplied --cron is trimmed and must not be
// blank. Without this guard, `schedule update --cron ""` slipped past the
// check create enforces and PATCHed an empty cadence to the server.
const cron = opts.cron === undefined ? undefined : opts.cron.trim();
if (cron !== undefined && cron.length === 0) {
throw localValidationError('--cron must not be empty or whitespace-only');
}

const body: UpdateScheduleRequest = {
...(opts.name !== undefined ? { name: opts.name } : {}),
...(environment !== undefined ? { environment } : {}),
...(opts.cron !== undefined ? { cron: opts.cron } : {}),
...(cron !== undefined ? { cron } : {}),
...(opts.timezone !== undefined ? { timezone: opts.timezone } : {}),
...(opts.start !== undefined ? { startAt: opts.start } : {}),
...(opts.end !== undefined ? { endAt: opts.end } : {}),
Expand All @@ -420,8 +428,8 @@ export async function runUpdate(
// to `* * * * *` is the same order-of-magnitude mistake as creating one that
// way, on an existing schedule and for the same bill. Printed before the
// request so it is visible even if the update then fails.
if (opts.cron !== undefined) {
stderr(formatScheduleFrequencyAdvisory(opts.cron));
if (cron !== undefined) {
stderr(formatScheduleFrequencyAdvisory(cron));
}

const client = makeClient(opts, deps);
Expand Down
28 changes: 28 additions & 0 deletions src/commands/schedule.write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,34 @@ describe('runUpdate', () => {
});
});

it('blank --cron rejects locally on update, matching create', async () => {
const { credentialsPath } = makeCreds();
for (const cron of ['', ' ']) {
await expect(
runUpdate(
{ ...BASE, output: 'json', scheduleId: 'sch_1', cron },
{ credentialsPath, fetchImpl: noNetwork(), ...sink },
),
).rejects.toMatchObject({
code: 'VALIDATION_ERROR',
exitCode: 5,
nextAction: '--cron must not be empty or whitespace-only',
});
}
});

it('trims --cron before patching, matching create', async () => {
const { credentialsPath } = makeCreds();
const { calls, fetchImpl } = capturing();

await runUpdate(
{ ...BASE, output: 'json', scheduleId: 'sch_1', cron: ' 0 9 * * * ' },
{ credentialsPath, fetchImpl, ...sink },
);

expect(JSON.parse(String(calls[0]!.init.body))).toEqual({ cron: '0 9 * * *' });
});

it('refuses a known old server before changing a pinned schedule', async () => {
const { credentialsPath } = makeCreds();
const calls: Array<{ url: string; init: RequestInit }> = [];
Expand Down
Loading