Skip to content

Commit 9aa7ba9

Browse files
authored
fix(project): reject empty/whitespace-only --name in create and update (#36)
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 497bf62 commit 9aa7ba9

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
@@ -569,6 +569,33 @@ describe('runCreate', () => {
569569
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
570570
expect(fetchImpl).not.toHaveBeenCalled();
571571
});
572+
573+
it('rejects a whitespace-only --name with VALIDATION_ERROR (exit 5), no network', async () => {
574+
const { credentialsPath } = makeCreds();
575+
const fetchImpl = vi.fn(async () => {
576+
throw new Error('should not hit network — validation must fire client-side');
577+
});
578+
579+
await expect(
580+
runCreate(
581+
{
582+
profile: 'default',
583+
output: 'json',
584+
debug: false,
585+
type: 'frontend',
586+
name: ' ',
587+
targetUrl: 'https://example.com',
588+
},
589+
{
590+
credentialsPath,
591+
fetchImpl: fetchImpl as unknown as typeof fetch,
592+
stdout: () => {},
593+
stderr: () => {},
594+
},
595+
),
596+
).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' });
597+
expect(fetchImpl).not.toHaveBeenCalled();
598+
});
572599
});
573600

574601
// ---------------------------------------------------------------------------
@@ -646,6 +673,31 @@ describe('runUpdate', () => {
646673
expect(fetchImpl).not.toHaveBeenCalled();
647674
});
648675

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

src/commands/project.ts

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

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

253262
// P1-3: client-side length checks matching server limits.
263+
if (opts.name !== undefined && opts.name.trim().length === 0) {
264+
throw localValidationError('--name must not be empty or whitespace-only');
265+
}
254266
if (opts.name !== undefined && opts.name.length > 200) {
255267
throw localValidationError('--name must be at most 200 characters');
256268
}

0 commit comments

Comments
 (0)