Skip to content
Closed
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
112 changes: 112 additions & 0 deletions src/commands/project-env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
});
});
17 changes: 17 additions & 0 deletions src/commands/project-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<project-id> is required and must not be empty or whitespace-only',
'<project-id>',
);
}
}

/** Trim + reject an empty environment name; the server owns every other rule. */
function requireEnvName(raw: string | undefined, flag: string): string {
const name = raw?.trim() ?? '';
Expand Down Expand Up @@ -499,6 +510,7 @@ export async function runEnvList(
opts: EnvListOptions,
deps: ProjectDeps = {},
): Promise<CliProjectEnvListResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
if (opts.dryRun) {
emitDryRunBanner(stderrOf(deps));
Expand All @@ -523,6 +535,7 @@ export async function runEnvGet(
opts: EnvListOptions & { name: string },
deps: ProjectDeps = {},
): Promise<CliProjectEnvUpdateResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
const name = requireEnvName(opts.name, '<name>');
if (opts.dryRun) {
Expand Down Expand Up @@ -575,6 +588,7 @@ export async function runEnvCreate(
opts: EnvCreateOptions,
deps: ProjectDeps = {},
): Promise<CliProjectEnvCreateResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
const stderr = stderrOf(deps);
assertIdempotencyKey(opts.idempotencyKey);
Expand Down Expand Up @@ -725,6 +739,7 @@ export async function runEnvUpdate(
opts: EnvUpdateOptions,
deps: ProjectDeps = {},
): Promise<CliProjectEnvUpdateResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
const stderr = stderrOf(deps);
assertIdempotencyKey(opts.idempotencyKey);
Expand Down Expand Up @@ -877,6 +892,7 @@ export async function runEnvDelete(
opts: EnvDeleteOptions,
deps: ProjectDeps = {},
): Promise<CliProjectEnvDeleteResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
const stderr = stderrOf(deps);
assertIdempotencyKey(opts.idempotencyKey);
Expand Down Expand Up @@ -919,6 +935,7 @@ export async function runEnvSetDefault(
opts: EnvSetDefaultOptions,
deps: ProjectDeps = {},
): Promise<CliProjectEnvUpdateResponse> {
requireProjectId(opts.projectId);
const out = makeOutput(opts.output, deps);
const stderr = stderrOf(deps);
assertIdempotencyKey(opts.idempotencyKey);
Expand Down
Loading