diff --git a/src/commands/project-env.spec.ts b/src/commands/project-env.spec.ts index f56b5dff..f5f775ce 100644 --- a/src/commands/project-env.spec.ts +++ b/src/commands/project-env.spec.ts @@ -2274,3 +2274,115 @@ describe('environment rename validation compatibility', () => { }); }); }); + +describe('project env input validation (empty and whitespace projectId)', () => { + it('rejects empty and whitespace project-id across all commands in live and dry-run modes', async () => { + for (const invalidProjectId of ['', ' ', '\t\n']) { + // list + await expect( + runEnvList( + { ...COMMON, projectId: invalidProjectId }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvList( + { ...COMMON, projectId: invalidProjectId, dryRun: true }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + + // get + await expect( + runEnvGet( + { ...COMMON, projectId: invalidProjectId, name: 'staging' }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvGet( + { ...COMMON, projectId: invalidProjectId, name: 'staging', dryRun: true }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + + // create + await expect( + runEnvCreate( + { + ...COMMON, + projectId: invalidProjectId, + name: 'staging', + url: 'https://staging.example.com', + }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvCreate( + { + ...COMMON, + projectId: invalidProjectId, + name: 'staging', + url: 'https://staging.example.com', + dryRun: true, + }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + + // update + await expect( + runEnvUpdate( + { + ...COMMON, + projectId: invalidProjectId, + name: 'staging', + url: 'https://updated.example.com', + }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvUpdate( + { + ...COMMON, + projectId: invalidProjectId, + name: 'staging', + url: 'https://updated.example.com', + dryRun: true, + }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + + // delete + await expect( + runEnvDelete( + { ...COMMON, projectId: invalidProjectId, name: 'staging', confirm: true }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvDelete( + { ...COMMON, projectId: invalidProjectId, name: 'staging', confirm: true, dryRun: true }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + + // set-default + await expect( + runEnvSetDefault( + { ...COMMON, projectId: invalidProjectId, name: 'staging' }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + await expect( + runEnvSetDefault( + { ...COMMON, projectId: invalidProjectId, name: 'staging', dryRun: true }, + { stdout: () => {}, stderr: () => {} }, + ), + ).rejects.toMatchObject({ code: 'VALIDATION_ERROR', exitCode: 5 }); + } + }); +}); diff --git a/src/commands/project-env.ts b/src/commands/project-env.ts index e5576d03..0e8d32dc 100644 --- a/src/commands/project-env.ts +++ b/src/commands/project-env.ts @@ -148,6 +148,17 @@ function envPath(projectId: string, name?: string): string { return name === undefined ? base : `${base}/${encodeURIComponent(name)}`; } +/** Trim + reject an empty project id before dry-run or network dispatch. */ +function requireProjectId(raw: string | undefined): void { + const id = raw?.trim() ?? ''; + if (id.length === 0) { + throw localValidationError( + ' is required and must not be empty or whitespace-only', + '', + ); + } +} + /** Trim + reject an empty environment name; the server owns every other rule. */ function requireEnvName(raw: string | undefined, flag: string): string { const name = raw?.trim() ?? ''; @@ -499,6 +510,7 @@ export async function runEnvList( opts: EnvListOptions, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); if (opts.dryRun) { emitDryRunBanner(stderrOf(deps)); @@ -523,6 +535,7 @@ export async function runEnvGet( opts: EnvListOptions & { name: string }, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); const name = requireEnvName(opts.name, ''); if (opts.dryRun) { @@ -575,6 +588,7 @@ export async function runEnvCreate( opts: EnvCreateOptions, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); const stderr = stderrOf(deps); assertIdempotencyKey(opts.idempotencyKey); @@ -725,6 +739,7 @@ export async function runEnvUpdate( opts: EnvUpdateOptions, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); const stderr = stderrOf(deps); assertIdempotencyKey(opts.idempotencyKey); @@ -877,6 +892,7 @@ export async function runEnvDelete( opts: EnvDeleteOptions, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); const stderr = stderrOf(deps); assertIdempotencyKey(opts.idempotencyKey); @@ -919,6 +935,7 @@ export async function runEnvSetDefault( opts: EnvSetDefaultOptions, deps: ProjectDeps = {}, ): Promise { + requireProjectId(opts.projectId); const out = makeOutput(opts.output, deps); const stderr = stderrOf(deps); assertIdempotencyKey(opts.idempotencyKey);