@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
405 lines • 17.7 kB
JavaScript
// 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