mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
fix(ci-setup): validate enum flags instead of casting raw strings
--ci/--deploy/--auth/--branch-strategy were cast straight from the raw commander string (`as AuthMode`, etc.) with no validation — only --port was checked. A typo like `--auth toekn` is not 'token', so the templates fell through to the *no-auth* compose, silently producing an unauthenticated, port-published deployment the user believed was token-gated; `--ci githubactions` wrote no CI file and rendered a literal `| undefined |` in GITNEXUS.md. Add a requireEnum() helper (exact, case-sensitive membership; no trim/lowercase normalization so `NONE` exits rather than degrading to the insecure path) called inside each existing option guard, so an omitted flag still resolves via the prompt/fallback while an explicitly-passed invalid value exits non-zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0806ac7c44
commit
e87e3a4122
2 changed files with 74 additions and 5 deletions
|
|
@ -18,6 +18,25 @@ import { generateFiles } from './ci-setup/templates.js';
|
|||
import type { CiSetupOptions, CiSetupResult, GeneratedFile } from './ci-setup/types.js';
|
||||
import type { CiSystem, DeployTarget, AuthMode, BranchStrategy } from './ci-setup/types.js';
|
||||
|
||||
const CI_SYSTEMS: readonly CiSystem[] = ['github-actions', 'azure-devops', 'both'];
|
||||
const DEPLOY_TARGETS: readonly DeployTarget[] = ['docker', 'azure-container-app', 'both'];
|
||||
const AUTH_MODES: readonly AuthMode[] = ['token', 'none'];
|
||||
const BRANCH_STRATEGIES: readonly BranchStrategy[] = ['pr-scoped', 'main-only'];
|
||||
|
||||
/**
|
||||
* Exact, case-sensitive membership check for an enum flag. Exits non-zero on an
|
||||
* unrecognized value rather than letting a typo (e.g. `--auth toekn`) fall
|
||||
* through to a downstream default — which for `--auth` would silently select the
|
||||
* insecure no-auth deployment. No trim/lowercase normalization: `NONE` exits.
|
||||
*/
|
||||
function requireEnum<T extends string>(value: string, allowed: readonly T[], flagName: string): T {
|
||||
if (!(allowed as readonly string[]).includes(value)) {
|
||||
console.log(`✗ Invalid ${flagName}: "${value}". Must be one of: ${allowed.join(', ')}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export const ciSetupCommand = async (options?: {
|
||||
ci?: string;
|
||||
deploy?: string;
|
||||
|
|
@ -54,10 +73,12 @@ export const ciSetupCommand = async (options?: {
|
|||
);
|
||||
console.log(' ⚠ License: PolyForm-Noncommercial — confirm non-commercial use\n');
|
||||
|
||||
// Parse and validate options from commander flags
|
||||
// Parse and validate options from commander flags. An explicitly-passed but
|
||||
// unrecognized value exits here; an omitted flag (undefined) is skipped by the
|
||||
// guard and resolves via resolveOptions' prompt/fallback.
|
||||
const partial: Partial<CiSetupOptions> = {};
|
||||
if (options?.ci) partial.ci = options.ci as CiSystem;
|
||||
if (options?.deploy) partial.deploy = options.deploy as DeployTarget;
|
||||
if (options?.ci) partial.ci = requireEnum(options.ci, CI_SYSTEMS, '--ci');
|
||||
if (options?.deploy) partial.deploy = requireEnum(options.deploy, DEPLOY_TARGETS, '--deploy');
|
||||
if (options?.port) {
|
||||
const portNum = parseInt(options.port as string, 10);
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65534) {
|
||||
|
|
@ -66,8 +87,13 @@ export const ciSetupCommand = async (options?: {
|
|||
}
|
||||
partial.port = portNum;
|
||||
}
|
||||
if (options?.auth) partial.auth = options.auth as AuthMode;
|
||||
if (options?.branchStrategy) partial.branchStrategy = options.branchStrategy as BranchStrategy;
|
||||
if (options?.auth) partial.auth = requireEnum(options.auth, AUTH_MODES, '--auth');
|
||||
if (options?.branchStrategy)
|
||||
partial.branchStrategy = requireEnum(
|
||||
options.branchStrategy,
|
||||
BRANCH_STRATEGIES,
|
||||
'--branch-strategy',
|
||||
);
|
||||
if (options?.dryRun !== undefined) partial.dryRun = options.dryRun;
|
||||
if (options?.apply !== undefined) partial.apply = options.apply;
|
||||
if (options?.yes !== undefined) partial.yes = options.yes;
|
||||
|
|
|
|||
|
|
@ -144,6 +144,49 @@ describe('ciSetupCommand', () => {
|
|||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('rejects an invalid --auth value (no insecure no-auth fallthrough)', async () => {
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code?: number) => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const { ciSetupCommand } = await import('../../src/cli/ci-setup.js');
|
||||
await expect(
|
||||
ciSetupCommand({
|
||||
ci: 'github-actions',
|
||||
deploy: 'docker',
|
||||
auth: 'toekn', // typo
|
||||
apply: true,
|
||||
yes: true,
|
||||
outputDir: tempDir,
|
||||
}),
|
||||
).rejects.toThrow('process.exit called');
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
// Nothing was generated — specifically no no-auth docker-compose.
|
||||
const entries = await fs.readdir(tempDir);
|
||||
expect(entries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects a wrong-case --auth value (no normalization into no-auth)', async () => {
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code?: number) => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const { ciSetupCommand } = await import('../../src/cli/ci-setup.js');
|
||||
await expect(
|
||||
ciSetupCommand({ ci: 'github-actions', deploy: 'docker', auth: 'NONE', outputDir: tempDir }),
|
||||
).rejects.toThrow('process.exit called');
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('rejects an invalid --ci value', async () => {
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code?: number) => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const { ciSetupCommand } = await import('../../src/cli/ci-setup.js');
|
||||
await expect(
|
||||
ciSetupCommand({ ci: 'githubactions', deploy: 'docker', outputDir: tempDir }),
|
||||
).rejects.toThrow('process.exit called');
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('--auth none generates no Caddyfile', async () => {
|
||||
const { ciSetupCommand } = await import('../../src/cli/ci-setup.js');
|
||||
await ciSetupCommand({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue