UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

405 lines 17.7 kB
// SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai'; import { describe, it, beforeEach, afterEach } from 'mocha'; import { Container } from '../../../src/core/dependency-injection/container-init.js'; import { InjectTokens } from '../../../src/core/dependency-injection/inject-tokens.js'; import { container } from 'tsyringe-neo'; import * as constants from '../../../src/core/constants.js'; import { ArgumentProcessor } from '../../../src/argument-processor.js'; describe('ArgumentProcessor', () => { let originalExit; let originalExitCode; let consoleOutput; let originalConsoleLog; beforeEach(() => { // Initialize container Container.getInstance().init(constants.SOLO_HOME_DIR, constants.SOLO_CACHE_DIR, constants.SOLO_LOG_LEVEL); void container.resolve(InjectTokens.SoloLogger); // Capture console output consoleOutput = []; originalConsoleLog = console.log; console.log = (...arguments_) => { consoleOutput.push(arguments_.map(String).join(' ')); }; // Mock process.exit to prevent test from exiting originalExit = process.exit; originalExitCode = process.exitCode; process.exit = (() => { throw new Error('process.exit called'); }); process.exitCode = undefined; }); afterEach(() => { // Restore original functions console.log = originalConsoleLog; process.exit = originalExit; process.exitCode = originalExitCode; }); describe('Missing Subcommands - Level 1 (Command Groups)', () => { it('should show help when running command without subcommand', async () => { const argv = ['node', 'solo.ts', 'consensus']; try { await ArgumentProcessor.process(argv); } catch (error) { // Should throw SilentBreak expect(error.constructor.name).to.equal('SilentBreak'); expect(error.message).to.include('No subcommand provided'); } // Verify help was shown const output = consoleOutput.join('\n'); expect(output).to.include('consensus'); expect(output).to.include('Commands:'); expect(output).to.include('consensus network'); expect(output).to.include('consensus node'); }); it('should exit cleanly and show subcommands/options for consensus', async () => { const argv = ['node', 'solo.ts', 'consensus']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus'); expect(output).to.include('Commands:'); expect(output).to.include('Options:'); expect(process.exitCode).to.not.equal(1); }); }); describe('Missing Subcommands - Level 2 (Command Subgroups)', () => { it('should show help when running subgroup without action', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus network'); expect(output).to.include('Commands:'); expect(output).to.include('deploy'); expect(output).to.include('destroy'); expect(output).to.include('freeze'); expect(output).to.include('upgrade'); }); it('should exit cleanly and show subgroup commands/options for consensus network', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus network'); expect(output).to.include('Commands:'); expect(output).to.include('Options:'); expect(process.exitCode).to.not.equal(1); }); }); describe('Invalid Commands', () => { it('should show error and help for unknown top-level command', async () => { const argv = ['node', 'solo.ts', 'invalid-command']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Unknown'); } const output = consoleOutput.join('\n'); expect(output).to.include('Unknown'); }); it('should show error for unknown second-level command', async () => { const argv = ['node', 'solo.ts', 'consensus', 'invalid-subcommand']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Unknown'); } const output = consoleOutput.join('\n'); expect(output).to.include('Unknown'); }); it('should show error for unknown third-level command', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'invalid-action']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Unknown'); } const output = consoleOutput.join('\n'); expect(output).to.include('Unknown'); }); }); describe('Missing Required Arguments - Level 3 (Actions)', () => { it('should show error when missing required argument', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'deploy']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('deployment'); } const output = consoleOutput.join('\n'); expect(output).to.include('Missing required argument'); expect(output).to.include('deployment'); }); it('should fail for destroy without deployment and include exact message', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'destroy']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); expect.fail('Expected SoloError to be thrown'); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Missing required argument: deployment'); } const output = consoleOutput.join('\n'); expect(output).to.include('Missing required argument: deployment'); expect(process.exitCode).to.equal(1); }); }); describe('Unknown Arguments', () => { it('should show error for unknown flag at action level', async () => { const argv = [ 'node', 'solo.ts', 'consensus', 'network', 'deploy', '--deployment', 'test', '--unknown-flag', ]; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Unknown'); } const output = consoleOutput.join('\n'); expect(output).to.include('Unknown'); }); }); describe('Help Flag Behavior', () => { it('should show help when --help flag is used', async () => { const argv = ['node', 'solo.ts', 'consensus', '--help']; try { await ArgumentProcessor.process(argv); } catch { // Should throw SilentBreak or Error (due to process.exit mock) // Just verify help was shown } const output = consoleOutput.join('\n'); expect(output).to.include('consensus'); expect(output).to.include('Commands:'); }); it('should show clean help for trailing help shorthand on action command', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'destroy', 'help']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus network destroy'); expect(output).to.not.include('Missing required argument'); expect(process.exitCode).to.not.equal(1); }); it('should show clean help for --help on action command with required args', async () => { const argv = ['node', 'solo.ts', 'block', 'node', 'add', '--help']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('block node add'); expect(output).to.include('Options:'); expect(output).to.not.include('Missing required argument'); expect(process.exitCode).to.not.equal(1); }); it('should show clean help for --help on consensus action command', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'destroy', '--help']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus network destroy'); expect(output).to.include('Options:'); expect(output).to.not.include('Missing required argument'); expect(process.exitCode).to.not.equal(1); }); it('should exit cleanly and show subgroup commands/options for consensus network help', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'help']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus network'); expect(output).to.include('Commands:'); expect(output).to.include('Options:'); expect(output).to.not.include('Missing required argument'); expect(process.exitCode).to.not.equal(1); }); it('should exit cleanly and show subcommands/options for consensus help', async () => { const argv = ['node', 'solo.ts', 'consensus', 'help']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('consensus'); expect(output).to.include('Commands:'); expect(output).to.include('Options:'); expect(output).to.not.include('Missing required argument'); expect(process.exitCode).to.not.equal(1); }); }); describe('No Command Provided', () => { it('should show help when no command is provided', async () => { const argv = ['node', 'solo.ts']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); expect(output).to.include('Usage:'); expect(output).to.include('solo <command>'); expect(output).to.include('Commands:'); }); }); describe('Error Message Quality', () => { it('should provide clear error message for missing required argument', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'deploy']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.message).to.include('Missing required argument'); expect(error.message).to.include('deployment'); } const output = consoleOutput.join('\n'); // Should show help with available options expect(output).to.include('Options:'); expect(output).to.include('--deployment'); }); it('should provide clear error message for unknown command', async () => { const argv = ['node', 'solo.ts', 'invalid-command']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.message).to.include('Unknown'); } const output = consoleOutput.join('\n'); // Should show available commands expect(output).to.include('Commands:'); }); it('should not show ERROR banner when displaying help for missing subcommand', async () => { const argv = ['node', 'solo.ts', 'consensus']; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } const output = consoleOutput.join('\n'); // Should NOT contain error banner expect(output).not.to.include('*********************************** ERROR'); // Should contain help information expect(output).to.include('Commands:'); }); it('should throw SoloError for actual errors', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'deploy']; try { await ArgumentProcessor.process(argv); // Should not reach here expect.fail('Expected error to be thrown'); } catch (error) { // Should throw SoloError for missing required arguments expect(error.constructor.name).to.equal('SoloError'); expect(error.message).to.include('Missing required argument'); } }); }); describe('Exit Code Behavior', () => { it('should not set error exit code when showing help for missing subcommand', async () => { const argv = ['node', 'solo.ts', 'consensus']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SilentBreak'); } // Exit code should not be set to error (1) for help display expect(process.exitCode).not.to.equal(1); }); it('should set error exit code for missing required arguments', async () => { const argv = ['node', 'solo.ts', 'consensus', 'network', 'deploy']; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); } // Exit code should be set to error (1) for actual errors expect(process.exitCode).to.equal(1); }); it('should set error exit code for unknown arguments', async () => { const argv = [ 'node', 'solo.ts', 'consensus', 'network', 'deploy', '--deployment', 'test', '--unknown-flag', ]; process.exitCode = undefined; try { await ArgumentProcessor.process(argv); } catch (error) { expect(error.constructor.name).to.equal('SoloError'); } // Exit code should be set to error (1) for unknown arguments expect(process.exitCode).to.equal(1); }); }); }); //# sourceMappingURL=argument-processor.test.js.map