Skip to content

Commit e70c25c

Browse files
committed
fix(project): reject empty/whitespace-only --name in create and update
project create/update validated --name with the action handler's if (!name) check, which a whitespace-only string passes (a non-empty string is truthy). The blank name was then sent verbatim, creating a junk-named project. The sibling est create already rejects this via the requireString whitespace guard (dogfood P1 fix #1); this aligns project create/update with that behavior. Adds 2 regression tests.
1 parent 18f6e6e commit e70c25c

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

src/commands/project.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,33 @@ describe('runCreate', () => {
564564
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
565565
expect(fetchImpl).not.toHaveBeenCalled();
566566
});
567+
568+
it('rejects a whitespace-only --name with VALIDATION_ERROR (exit 5), no network', async () => {
569+
const { credentialsPath } = makeCreds();
570+
const fetchImpl = vi.fn(async () => {
571+
throw new Error('should not hit network — validation must fire client-side');
572+
});
573+
574+
await expect(
575+
runCreate(
576+
{
577+
profile: 'default',
578+
output: 'json',
579+
debug: false,
580+
type: 'frontend',
581+
name: ' ',
582+
targetUrl: 'https://example.com',
583+
},
584+
{
585+
credentialsPath,
586+
fetchImpl: fetchImpl as unknown as typeof fetch,
587+
stdout: () => {},
588+
stderr: () => {},
589+
},
590+
),
591+
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
592+
expect(fetchImpl).not.toHaveBeenCalled();
593+
});
567594
});
568595

569596
// ---------------------------------------------------------------------------
@@ -641,6 +668,31 @@ describe('runUpdate', () => {
641668
expect(fetchImpl).not.toHaveBeenCalled();
642669
});
643670

671+
it('rejects a whitespace-only --name with VALIDATION_ERROR (exit 5), no network', async () => {
672+
const { credentialsPath } = makeCreds();
673+
const fetchImpl = vi.fn(async () => {
674+
throw new Error('should not be called');
675+
});
676+
await expect(
677+
runUpdate(
678+
{
679+
profile: 'default',
680+
output: 'json',
681+
debug: false,
682+
projectId: 'proj_abc',
683+
name: ' ',
684+
},
685+
{
686+
credentialsPath,
687+
fetchImpl: fetchImpl as unknown as typeof fetch,
688+
stdout: () => {},
689+
stderr: () => {},
690+
},
691+
),
692+
).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 });
693+
expect(fetchImpl).not.toHaveBeenCalled();
694+
});
695+
644696
it('P7 — dry-run returns canned shape without network call', async () => {
645697
const { credentialsPath } = makeCreds();
646698
const fetchImpl = vi.fn(async () => {

src/commands/project.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ export async function runCreate(
140140
// (exit 10 UNAVAILABLE) — fail fast with a clear exit 5 instead.
141141
assertIdempotencyKey(opts.idempotencyKey);
142142

143+
// Reject empty / whitespace-only names so a junk record never reaches the
144+
// backend — matches the `requireString` whitespace guard `test create` uses
145+
// (dogfood P1 fix #1). Without this, `--name " "` passes the action
146+
// handler's `if (!name)` check (a non-empty string is truthy) and is sent
147+
// verbatim, creating a blank-named project.
148+
if (opts.name !== undefined && opts.name.trim().length === 0) {
149+
throw localValidationError('--name must not be empty or whitespace-only');
150+
}
151+
143152
// P1-3: client-side length checks matching server limits.
144153
if (opts.name !== undefined && opts.name.length > 200) {
145154
throw localValidationError('--name must be at most 200 characters');
@@ -247,6 +256,9 @@ export async function runUpdate(
247256
assertIdempotencyKey(opts.idempotencyKey);
248257

249258
// P1-3: client-side length checks matching server limits.
259+
if (opts.name !== undefined && opts.name.trim().length === 0) {
260+
throw localValidationError('--name must not be empty or whitespace-only');
261+
}
250262
if (opts.name !== undefined && opts.name.length > 200) {
251263
throw localValidationError('--name must be at most 200 characters');
252264
}

0 commit comments

Comments
 (0)