@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
338 lines • 14.9 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import Sinon from 'sinon';
import { EventEmitter } from 'node:events';
import { KindExecutionException } from '../../../../../src/integration/kind/errors/kind-execution-exception.js';
import { KindParserException } from '../../../../../src/integration/kind/errors/kind-parser-exception.js';
// Create a mock implementation that mimics KindExecution behavior
class MockKindExecution {
output = [];
errOutput = [];
exitCodeValue = null;
mockProcess;
constructor(_command, _workingDirectory, _environmentVariables) {
// eslint-disable-next-line unicorn/prefer-event-target
this.mockProcess = new EventEmitter();
// @ts-expect-error TS2339: Property stdout does not exist on type EventEmitter<DefaultEventMap>
// eslint-disable-next-line unicorn/prefer-event-target
this.mockProcess.stdout = new EventEmitter();
// @ts-expect-error TS2339: Property stderr does not exist on type EventEmitter<DefaultEventMap>
// eslint-disable-next-line unicorn/prefer-event-target
this.mockProcess.stderr = new EventEmitter();
// @ts-expect-error TS2339: Property stdin does not exist on type EventEmitter<DefaultEventMap>
this.mockProcess.stdin = {
end: Sinon.stub(),
};
// Listen for events from our mock process
// @ts-expect-error TS2339: Property stdout does not exist on type EventEmitter<DefaultEventMap>
this.mockProcess.stdout.on('data', data => this.output.push(data.toString()));
// @ts-expect-error TS2339: Property stderr does not exist on type EventEmitter<DefaultEventMap>
this.mockProcess.stderr.on('data', data => this.errOutput.push(data.toString()));
this.mockProcess.on('exit', code => {
this.exitCodeValue = code ?? 0;
});
}
// Expose methods to control the mock process
emitStdout(data) {
// @ts-expect-error TS2339: Property stdout does not exist on type EventEmitter<DefaultEventMap>
this.mockProcess.stdout.emit('data', data);
}
emitStderr(data) {
// @ts-expect-error TS2339: Property stderr does not exist on type EventEmitter<DefaultEventMap>
this.mockProcess.stderr.emit('data', data);
}
emitExit(code) {
this.mockProcess.emit('exit', code);
}
emitError(error) {
this.mockProcess.emit('error', error);
}
// Implement the methods being tested
async waitForCompletion(timeout) {
return new Promise((resolve, reject) => {
if (timeout) {
setTimeout(() => {
reject(new Error('Timed out waiting for the process to complete'));
}, timeout);
}
this.mockProcess.on('exit', code => {
if (code === 0) {
resolve();
}
else {
reject(new KindExecutionException(code, this.output.join(''), this.errOutput.join('')));
}
});
this.mockProcess.on('error', error => {
reject(error);
});
});
}
async responseAs(clazz) {
try {
await this.waitForCompletion();
try {
const constructor = clazz;
return constructor.fromString(this.output.join(''));
}
catch (error) {
throw new KindParserException(`Failed to deserialize the output into the specified class: ${clazz.name}`, error);
}
}
catch (error) {
if (error instanceof KindExecutionException || error instanceof KindParserException) {
throw error;
}
throw new KindExecutionException(1, this.output.join(''), this.errOutput.join(''));
}
}
async responseAsList(clazz) {
try {
await this.waitForCompletion();
const lines = this.output
.join('')
.split('\n')
.filter(line => line.trim().length > 0);
const result = [];
for (const line of lines) {
try {
const constructor = clazz;
result.push(constructor.fromString(line));
}
catch (error) {
throw new KindParserException(`Failed to deserialize the output into a list of the specified class: ${clazz.name}`, error);
}
}
return result;
}
catch (error) {
if (error instanceof KindExecutionException || error instanceof KindParserException) {
throw error;
}
throw new KindExecutionException(1, this.output.join(''), this.errOutput.join(''));
}
}
}
describe('KindExecution', () => {
let execution;
beforeEach(() => {
// Create a test execution using our mock class
execution = new MockKindExecution(['kind', 'create', 'cluster'], '/test/working/dir', { TEST_ENV: 'value' });
});
afterEach(() => {
Sinon.restore();
});
describe('constructor', () => {
it('should initialize the mock process correctly', () => {
expect(execution).to.be.instanceOf(MockKindExecution);
});
});
describe('waitForCompletion', () => {
it('should resolve when process exits with code 0', async () => {
const promise = execution.waitForCompletion();
// Emit exit event with success code
execution.emitExit(0);
await expect(promise).to.be.fulfilled;
});
it('should reject when process exits with non-zero code', async () => {
const promise = execution.waitForCompletion();
// Emit stdout and stderr data
execution.emitStdout('Some output');
execution.emitStderr('Some error');
// Emit exit event with error code
execution.emitExit(1);
await expect(promise).to.be.rejectedWith(KindExecutionException);
});
it('should reject when process has an error', async () => {
const promise = execution.waitForCompletion();
// Emit error event
execution.emitError(new Error('Process error'));
await expect(promise).to.be.rejectedWith(Error, 'Process error');
});
it('should timeout if the process takes too long', async () => {
const clock = Sinon.useFakeTimers();
const promise = execution.waitForCompletion(100); // 100ms timeout
// Advance the timer beyond the timeout
clock.tick(200);
await expect(promise).to.be.rejectedWith(/Timed out/);
clock.restore();
});
});
describe('responseAs', () => {
// Create a test class for responseAs testing
class TestResponse {
value = '';
static fromString(string_) {
const instance = new TestResponse();
instance.value = string_.trim();
return instance;
}
}
it('should parse successful response into the specified class', async () => {
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof TestResponse is not assignable to parameter of type
const result = await execution.responseAs(TestResponse);
expect(result).to.be.instanceOf(TestResponse);
expect(result.value).to.equal('test response');
resolve(true);
}
catch {
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStdout('test response');
execution.emitExit(0);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([true, true]);
});
it('should reject if the process exits with error', async () => {
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof TestResponse is not assignable to parameter of type
await execution.responseAs(TestResponse);
resolve(true);
}
catch (error) {
expect(error.name).to.be.equal(KindExecutionException.name);
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStderr('error output');
execution.emitExit(1);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([false, true]);
});
it('should reject if parsing fails', async () => {
// Setup a class that will throw during parsing
class FailingClass {
static fromString() {
throw new Error('Parsing error');
}
}
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof FailingClass is not assignable to parameter of type
await execution.responseAs(FailingClass);
resolve(true);
}
catch (error) {
expect(error.name).to.be.equal(KindParserException.name);
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStdout('some output');
execution.emitExit(0);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([false, true]);
});
});
describe('responseAsList', () => {
class TestItem {
value = '';
static fromString(string_) {
const instance = new TestItem();
instance.value = string_.trim();
return instance;
}
}
it('should parse successful response into a list of the specified class', async () => {
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof TestItem is not assignable to parameter of type
const result = await execution.responseAsList(TestItem);
expect(result).to.be.an('array').with.lengthOf(3);
expect(result[0]).to.be.instanceOf(TestItem);
expect(result[0].value).to.equal('item1');
expect(result[1].value).to.equal('item2');
expect(result[2].value).to.equal('item3');
resolve(true);
}
catch {
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStdout('item1\nitem2\nitem3');
execution.emitExit(0);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([true, true]);
});
it('should handle empty output', async () => {
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof TestItem is not assignable to parameter of type
const result = await execution.responseAsList(TestItem);
expect(result).to.be.an('array').with.lengthOf(0);
resolve(true);
}
catch {
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStdout('');
execution.emitExit(0);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([true, true]);
});
it('should reject if parsing fails', async () => {
class FailingItem {
static fromString() {
throw new Error('Parsing error');
}
}
const allPassing = await Promise.all([
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve, _reject) => {
try {
// @ts-expect-error TS2345: Argument of type typeof FailingItem is not assignable to parameter of type
const result = await execution.responseAsList(FailingItem);
expect(result).to.be.an('array').with.lengthOf(0);
resolve(true);
}
catch (error) {
expect(error.name).to.be.equal(KindParserException.name);
resolve(false);
}
}),
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
execution.emitStdout('item1\nitem2');
execution.emitExit(0);
resolve(true);
}),
]);
expect(allPassing).to.deep.equal([false, true]);
});
});
});
//# sourceMappingURL=kind-execution.test.js.map