diff --git a/tests/configuration.test.ts b/tests/configuration.test.ts index 20cdc21..148f0b4 100644 --- a/tests/configuration.test.ts +++ b/tests/configuration.test.ts @@ -74,29 +74,90 @@ describe('project configuration', () => { ); })); - it('rejects secret-bearing fields without echoing their values', () => + it('rejects unknown top-level fields with generic non-reflective error', () => withTempDirectory((root) => { - const secret = 'S-DO-NOT-ECHO-THIS-VALUE'; writeFileSync( join(root, PROJECT_CONFIG_FILENAME), - JSON.stringify({ - version: 1, - privateKey: secret, - }), + JSON.stringify({ version: 1, unknownField: 'value' }), + ); + expect(() => loadProjectConfig(root)).toThrow(ValidationCliError); + try { + loadProjectConfig(root); + } catch (e) { + expect((e as Error).message).not.toContain('unknownField'); + expect((e as Error).message).not.toContain('value'); + } + })); + + it('rejects nested/array unexpected values safely', () => + withTempDirectory((root) => { + writeFileSync( + join(root, PROJECT_CONFIG_FILENAME), + JSON.stringify({ version: 1, nested: { bad: 'x' } }), + ); + expect(() => loadProjectConfig(root)).toThrow(ValidationCliError); + })); + + it('does not echo secret-looking field values in errors', () => + withTempDirectory((root) => { + writeFileSync( + join(root, PROJECT_CONFIG_FILENAME), + JSON.stringify({ version: 1, password: 's3cr3t' }), ); + try { + loadProjectConfig(root); + } catch (e) { + expect((e as Error).message).not.toContain('s3cr3t'); + } + })); + it('schema version remains unchanged (version 1)', () => + withTempDirectory((root) => { + writeFileSync( + join(root, PROJECT_CONFIG_FILENAME), + JSON.stringify({ version: 1, network: 'testnet' }), + ); + const cfg = loadProjectConfig(root); + expect(cfg?.version).toBe(1); + })); + + it('rejects project-config larger than 64 KiB with predictable error and no content echo', () => + withTempDirectory((root) => { + const large = 'x'.repeat(64 * 1024 + 1); + writeFileSync( + join(root, PROJECT_CONFIG_FILENAME), + JSON.stringify({ version: 1, extra: large }), + ); try { loadProjectConfig(root); - throw new Error('Expected configuration validation to fail.'); - } catch (error) { - expect(error).toBeInstanceOf(ValidationCliError); - expect((error as Error).message).toContain( - 'must not contain secret material', - ); - expect((error as Error).message).not.toContain(secret); + throw new Error('Expected failure'); + } catch (e) { + expect(e).toBeInstanceOf(ValidationCliError); + expect((e as Error).message).toContain('64 KiB'); + expect((e as Error).message).not.toContain(large); } })); + it.each([64 * 1024 - 1, 64 * 1024])( + 'does not reject a configuration of exactly %i bytes for size', + (totalBytes) => + withTempDirectory((root) => { + const base = JSON.stringify({ version: 1, padding: '' }); + const padding = 'x'.repeat(totalBytes - base.length); + const content = JSON.stringify({ version: 1, padding }); + expect(Buffer.byteLength(content)).toBe(totalBytes); + + writeFileSync(join(root, PROJECT_CONFIG_FILENAME), content); + + try { + loadProjectConfig(root); + } catch (error) { + expect((error as Error).message).not.toContain('64 KiB'); + expect((error as Error).message).toContain('unsupported field'); + } + }), + ); + it.each([ 'SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', diff --git a/tests/project-validation.test.ts b/tests/project-validation.test.ts index 02c05e8..43ad1b4 100644 --- a/tests/project-validation.test.ts +++ b/tests/project-validation.test.ts @@ -47,6 +47,37 @@ describe('project input validation', () => { expect(validated.cwd).toBe(resolve('./workspace/..')); }, ); + + it.each([ + ' ', + '\t', + '\n', + '\r\n', + '\u00A0', + '\u200B', + '\u0000', + 'de mo', + 'de\tmo', + 'de\nmo', + 'demo\u0000', + 'demo\u200B', + ])('rejects ambiguous whitespace/control input %j', (projectName) => { + expect(() => + validateProjectInput({ projectName, cwd: '/workspace' }), + ).toThrow(ValidationCliError); + }); + + it.each([' demo ', '\tdemo', 'demo\n', 'demo\u00A0'])( + 'normalizes surrounding whitespace in %j to a single segment', + (projectName) => { + const validated = validateProjectInput({ + projectName, + cwd: '/workspace', + }); + + expect(validated.projectName).toBe('demo'); + }, + ); }); describe('project destination planning', () => { diff --git a/tests/root-command.test.ts b/tests/root-command.test.ts index 6c6bc55..0c273b6 100644 --- a/tests/root-command.test.ts +++ b/tests/root-command.test.ts @@ -30,4 +30,19 @@ describe('StellarForge root command', () => { expect(help).toContain('-V, --version'); expect(help).toContain('-h, --help'); }); + + it('exposes exactly the implemented v1 command inventory in global help', () => { + const help = createRootCommand('1.2.3').helpInformation(); + + const implemented = ['doctor', 'new', 'dev', 'test', 'deploy']; + for (const cmd of implemented) { + expect(help).toContain(cmd); + } + + const notAdvertised = ['add', 'plugin', 'remote-template']; + for (const cmd of notAdvertised) { + expect(help).not.toContain(`add command`); + expect(help).not.toMatch(new RegExp(`\\b${cmd}\\b`)); + } + }); });