From e70c25c94823821da9b88c53c0af72f55b5fd383 Mon Sep 17 00:00:00 2001 From: lxcario Date: Thu, 25 Jun 2026 18:21:02 +0300 Subject: [PATCH] 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. --- src/commands/project.test.ts | 52 ++++++++++++++++++++++++++++++++++++ src/commands/project.ts | 12 +++++++++ 2 files changed, 64 insertions(+) diff --git a/src/commands/project.test.ts b/src/commands/project.test.ts index 0037b8c..4b08838 100644 --- a/src/commands/project.test.ts +++ b/src/commands/project.test.ts @@ -564,6 +564,33 @@ describe('runCreate', () => { ).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' }); expect(fetchImpl).not.toHaveBeenCalled(); }); + + it('rejects a whitespace-only --name with VALIDATION_ERROR (exit 5), no network', async () => { + const { credentialsPath } = makeCreds(); + const fetchImpl = vi.fn(async () => { + throw new Error('should not hit network — validation must fire client-side'); + }); + + await expect( + runCreate( + { + profile: 'default', + output: 'json', + debug: false, + type: 'frontend', + name: ' ', + targetUrl: 'https://example.com', + }, + { + credentialsPath, + fetchImpl: fetchImpl as unknown as typeof fetch, + stdout: () => {}, + stderr: () => {}, + }, + ), + ).rejects.toMatchObject({ exitCode: 5, code: 'VALIDATION_ERROR' }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); }); // --------------------------------------------------------------------------- @@ -641,6 +668,31 @@ describe('runUpdate', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it('rejects a whitespace-only --name with VALIDATION_ERROR (exit 5), no network', async () => { + const { credentialsPath } = makeCreds(); + const fetchImpl = vi.fn(async () => { + throw new Error('should not be called'); + }); + await expect( + runUpdate( + { + profile: 'default', + output: 'json', + debug: false, + projectId: 'proj_abc', + name: ' ', + }, + { + credentialsPath, + fetchImpl: fetchImpl as unknown as typeof fetch, + stdout: () => {}, + stderr: () => {}, + }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + it('P7 — dry-run returns canned shape without network call', async () => { const { credentialsPath } = makeCreds(); const fetchImpl = vi.fn(async () => { diff --git a/src/commands/project.ts b/src/commands/project.ts index 277f3ca..28bcd88 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -140,6 +140,15 @@ export async function runCreate( // (exit 10 UNAVAILABLE) — fail fast with a clear exit 5 instead. assertIdempotencyKey(opts.idempotencyKey); + // Reject empty / whitespace-only names so a junk record never reaches the + // backend — matches the `requireString` whitespace guard `test create` uses + // (dogfood P1 fix #1). Without this, `--name " "` passes the action + // handler's `if (!name)` check (a non-empty string is truthy) and is sent + // verbatim, creating a blank-named project. + if (opts.name !== undefined && opts.name.trim().length === 0) { + throw localValidationError('--name must not be empty or whitespace-only'); + } + // P1-3: client-side length checks matching server limits. if (opts.name !== undefined && opts.name.length > 200) { throw localValidationError('--name must be at most 200 characters'); @@ -247,6 +256,9 @@ export async function runUpdate( assertIdempotencyKey(opts.idempotencyKey); // P1-3: client-side length checks matching server limits. + if (opts.name !== undefined && opts.name.trim().length === 0) { + throw localValidationError('--name must not be empty or whitespace-only'); + } if (opts.name !== undefined && opts.name.length > 200) { throw localValidationError('--name must be at most 200 characters'); }