Skip to content

Commit

Permalink
feat: Create unit test for NetworkPrepState #515 (#518)
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Stefanov <[email protected]>
  • Loading branch information
stefan-stefanooov authored Jan 30, 2024
1 parent bbdc9de commit 1f8edeb
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 15 deletions.
10 changes: 9 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,12 @@ export const DOCKER_CLEANING_VALUMES_MESSAGE = 'Cleaning the volumes and temp fi

// Recovery state
export const RECOVERY_STATE_INIT_MESSAGE = 'Recovery State Initialized!';
export const RECOVERY_STATE_STARTING_MESSAGE = "Starting Recovery State...";
export const RECOVERY_STATE_STARTING_MESSAGE = "Starting Recovery State...";

// Network Prep State
export const NETWORK_PREP_STATE_INIT_MESSAGE = 'Network Preparation State Initialized!'
export const NETWORK_PREP_STATE_STARTING_MESSAGE = 'Starting Network Preparation State...';
export const NETWORK_PREP_STATE_IMPORT_FEES_START = 'Starting Fees import...';
export const NETWORK_PREP_STATE_IMPORT_FEES_END = 'Imported fees successfully';
export const NETWORK_PREP_STATE_WAITING_TOPIC_CREATION = 'Waiting for topic creation...';
export const NETWORK_PREP_STATE_TOPIC_CREATED = 'Topic was created!';
39 changes: 27 additions & 12 deletions src/state/NetworkPrepState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ import { ServiceLocator } from '../services/ServiceLocator';
import { EventType } from '../types/EventType';
import { IState } from './IState';
import { DockerService } from '../services/DockerService';
import {
NETWORK_PREP_STATE_IMPORT_FEES_END,
NETWORK_PREP_STATE_IMPORT_FEES_START,
NETWORK_PREP_STATE_INIT_MESSAGE,
NETWORK_PREP_STATE_STARTING_MESSAGE,
NETWORK_PREP_STATE_TOPIC_CREATED,
NETWORK_PREP_STATE_WAITING_TOPIC_CREATION
} from '../constants';

/**
* Represents the network preparation state of the Hedera Local Node.
Expand Down Expand Up @@ -67,7 +75,7 @@ export class NetworkPrepState implements IState {
this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name);
this.clientService = ServiceLocator.Current.get<ClientService>(ClientService.name);
this.dockerService = ServiceLocator.Current.get<DockerService>(DockerService.name);
this.logger.trace('Network Preparation State Initialized!', this.stateName);
this.logger.trace(NETWORK_PREP_STATE_INIT_MESSAGE, this.stateName);
}

/**
Expand All @@ -83,7 +91,7 @@ export class NetworkPrepState implements IState {
* @returns {Promise<void>} A promise that resolves when the network preparation is complete.
*/
public async onStart(): Promise<void> {
this.logger.info('Starting Network Preparation State...', this.stateName);
this.logger.info(NETWORK_PREP_STATE_STARTING_MESSAGE, this.stateName);
const client = this.clientService.getClient();

await this.importFees(client);
Expand All @@ -98,35 +106,42 @@ export class NetworkPrepState implements IState {
* @returns {Promise<void>} A promise that resolves when the import is complete.
*/
private async importFees(client: Client): Promise<void> {
this.logger.trace('Starting Fees import...', this.stateName);
this.logger.trace(NETWORK_PREP_STATE_IMPORT_FEES_START, this.stateName);

const feesFileId = 111;
const exchangeRatesFileId = 112;

const timestamp = Date.now();
const nullOutput = this.dockerService.getNullOutput();

const queryFees = new FileContentsQuery().setFileId(
`0.0.${feesFileId}`
);
const queryFees = this.buildQueryFees(feesFileId);
const fees = Buffer.from(await queryFees.execute(client)).toString('hex');
await shell.exec(
`docker exec mirror-node-db psql mirror_node -U mirror_node -c "INSERT INTO public.file_data(file_data, consensus_timestamp, entity_id, transaction_type) VALUES (decode('${fees}', 'hex'), ${
timestamp + '000000'
}, ${feesFileId}, 17);" >> ${nullOutput}`
);

const queryExchangeRates = new FileContentsQuery().setFileId(
`0.0.${exchangeRatesFileId}`
);
const queryExchangeRates = this.buildQueryFees(exchangeRatesFileId);
const exchangeRates = Buffer.from(await queryExchangeRates.execute(client)).toString('hex');
await shell.exec(
`docker exec mirror-node-db psql mirror_node -U mirror_node -c "INSERT INTO public.file_data(file_data, consensus_timestamp, entity_id, transaction_type) VALUES (decode('${exchangeRates}', 'hex'), ${
timestamp + '000001'
}, ${exchangeRatesFileId}, 17);" >> ${nullOutput}`
);

this.logger.info('Imported fees successfully', this.stateName);
this.logger.info(NETWORK_PREP_STATE_IMPORT_FEES_END, this.stateName);
}

/**
* Builds a query for the fees file.
* @param {number} feesFileId - The fees file ID.
* @returns {FileContentsQuery} The query for the fees file.
*/
private buildQueryFees(feesFileId: number): FileContentsQuery {
return new FileContentsQuery().setFileId(
`0.0.${feesFileId}`
)
}

/**
Expand All @@ -136,7 +151,7 @@ export class NetworkPrepState implements IState {
* @returns {Promise<void>}
*/
private async waitForTopicCreation(): Promise<void> {
this.logger.trace('Waiting for topic creation...', this.stateName);
this.logger.trace(NETWORK_PREP_STATE_WAITING_TOPIC_CREATION, this.stateName);
const LOG_SEARCH_TEXT = 'Created TOPIC entity';

return new Promise((resolve, reject) => {
Expand All @@ -148,7 +163,7 @@ export class NetworkPrepState implements IState {
if (data.indexOf(LOG_SEARCH_TEXT) !== -1) {
command.kill('SIGINT');
command.stdout!.destroy();
this.logger.info('Topic was created!', this.stateName);
this.logger.info(NETWORK_PREP_STATE_TOPIC_CREATED, this.stateName);
resolve();
}
});
Expand Down
222 changes: 222 additions & 0 deletions test/unit/states/networkPrepState.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*-
*
* Hedera Local Node
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import { expect } from 'chai';
import { SinonSandbox, SinonSpy, SinonStub, SinonStubbedInstance } from 'sinon';
import { LoggerService } from '../../../src/services/LoggerService';
import { NetworkPrepState } from '../../../src/state/NetworkPrepState';
import { getTestBed } from '../testBed';
import {
NETWORK_PREP_STATE_IMPORT_FEES_END,
NETWORK_PREP_STATE_INIT_MESSAGE,
NETWORK_PREP_STATE_STARTING_MESSAGE,
NETWORK_PREP_STATE_TOPIC_CREATED,
NETWORK_PREP_STATE_WAITING_TOPIC_CREATION,
} from '../../../src/constants';
import { DockerService } from '../../../src/services/DockerService';
import { EventType } from '../../../src/types/EventType';
import { ClientService } from '../../../src/services/ClientService';

describe('NetworkPrepState tests', () => {
let networkPrepState: NetworkPrepState,
testSandbox: SinonSandbox,
loggerService: SinonStubbedInstance<LoggerService>,
serviceLocator: SinonStub,
dockerService: SinonStubbedInstance<DockerService>,
observerSpy: SinonSpy,
shellTestBed: {[key: string]: SinonStub};

before(() => {
const {
sandbox,
loggerServiceStub,
serviceLocatorStub,
dockerServiceStub,
shellStubs
} = getTestBed({
workDir: 'testDir',
});

testSandbox = sandbox
dockerService = dockerServiceStub
loggerService = loggerServiceStub
serviceLocator = serviceLocatorStub
shellTestBed = shellStubs

networkPrepState = new NetworkPrepState();
observerSpy = testSandbox.spy();
const observer = {
update: observerSpy
}
networkPrepState.subscribe(observer);
});

afterEach(() => {
testSandbox.resetHistory();
observerSpy.resetHistory();
dockerService.tryDockerRecovery.reset()
});

it('should initialize the Network Prep State', async () => {
expect(networkPrepState).to.be.instanceOf(NetworkPrepState);
testSandbox.assert.calledWith(serviceLocator, LoggerService.name);
testSandbox.assert.calledWith(serviceLocator, ClientService.name);
testSandbox.assert.calledOnce(loggerService.trace);
testSandbox.assert.calledWith(loggerService.trace, NETWORK_PREP_STATE_INIT_MESSAGE, NetworkPrepState.name);
})

it('should have a subscribe method', async () => {
expect(networkPrepState.subscribe).to.be.a('function');
})

it('should have a onStart method', async () => {
expect(networkPrepState.onStart).to.be.a('function');
})

describe('onStart', () => {
let importStub: SinonStub, topicStub: SinonStub;

before(async () => {
importStub = testSandbox.stub(networkPrepState as any, 'importFees').resolves();
topicStub = testSandbox.stub(networkPrepState as any, 'waitForTopicCreation').resolves();
})

after(() => {
importStub.restore();
topicStub.restore();
})

beforeEach(async () => {
await networkPrepState.onStart();
})

it('should execute and log into LoggerService', async () => {
testSandbox.assert.calledOnce(loggerService.info);
testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_STARTING_MESSAGE, NetworkPrepState.name);
})

it('should execute and fire an event into observer', async () => {
testSandbox.assert.calledWith(observerSpy, EventType.Finish);
})

it('should execute and call "importFees" function', async () => {
testSandbox.assert.calledOnce(importStub);
})

it('should execute and call "importFees" function', async () => {
testSandbox.assert.calledOnce(importStub);
})
})

describe('importFees', () => {
let topicStub: SinonStub,
buildQueryFeesStub: SinonStub;

before(async () => {
class MockFileContentsQuery {
setFileId() {
return this
}
execute() {
return Promise.resolve('test')
}
}
topicStub = testSandbox.stub(networkPrepState as any, 'waitForTopicCreation').resolves();
buildQueryFeesStub = testSandbox.stub(networkPrepState as any, 'buildQueryFees').returns(new MockFileContentsQuery);
})

after(() => {
topicStub.restore();
})

beforeEach(async () => {
await networkPrepState.onStart();
})

it('should execute and log into LoggerService', async () => {
testSandbox.assert.calledTwice(loggerService.info);
testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_STARTING_MESSAGE, NetworkPrepState.name);
testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_IMPORT_FEES_END, NetworkPrepState.name);
})

it('should execute and call shell.exec', async () => {
const { shellExecStub } = shellTestBed;
testSandbox.assert.calledTwice(shellExecStub);
})

it('should execute and call "buildQueryFees" function', async () => {
testSandbox.assert.calledTwice(buildQueryFeesStub);
})

it('should execute and call "getNullOutput" function', async () => {
testSandbox.assert.calledOnce(dockerService.getNullOutput);
})

})

describe('waitForTopicCreation', () => {
let importStub: SinonStub,
shellTestBedExec: SinonStub,
destroyFake: SinonSpy<any[], any>,
killFake: SinonSpy<any[], any>;

before(async () => {
importStub = testSandbox.stub(networkPrepState as any, 'importFees').resolves();
const { shellExecStub } = shellTestBed;
shellTestBedExec = shellExecStub;
destroyFake = testSandbox.fake();
killFake = testSandbox.fake();
shellTestBedExec.returns({
stdout: {
on: testSandbox.fake.yields(
"Created TOPIC entity: 0.0.111",
),
destroy: destroyFake
},
kill: killFake
})
})

after(() => {
importStub.restore();
})

beforeEach(async () => {
await networkPrepState.onStart();
})

it('should execute and log into LoggerService', async () => {
testSandbox.assert.calledTwice(loggerService.info);
testSandbox.assert.calledOnce(loggerService.trace);
testSandbox.assert.calledWith(loggerService.trace, NETWORK_PREP_STATE_WAITING_TOPIC_CREATION, NetworkPrepState.name);
testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_TOPIC_CREATED, NetworkPrepState.name);
})

it('should execute and call shell.exec', async () => {
testSandbox.assert.calledOnceWithExactly(
shellTestBedExec,
'docker logs mirror-node-monitor -f',
{ silent: true, async: true }
);
testSandbox.assert.calledOnce(destroyFake);
testSandbox.assert.calledOnce(killFake);
})
})
});
2 changes: 1 addition & 1 deletion test/unit/states/startState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('StartState tests', () => {
afterEach(() => {
testSandbox.resetHistory();
observerSpy.resetHistory();
dockerService.dockerComposeUp.reset()
dockerService.dockerComposeUp.reset();
});

it('should initialize the Start State', async () => {
Expand Down
6 changes: 5 additions & 1 deletion test/unit/testBed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/*-
*
* Hedera Local Node
Expand All @@ -25,13 +24,15 @@ import { CLIService } from "../../src/services/CLIService";
import { ServiceLocator } from "../../src/services/ServiceLocator";
import { DockerService } from "../../src/services/DockerService";
import { ConnectionService } from "../../src/services/ConnectionService";
import { ClientService } from "../../src/services/ClientService";

export interface LocalNodeTestBed {
sandbox: sinon.SinonSandbox;
loggerServiceStub: sinon.SinonStubbedInstance<LoggerService>;
cliServiceStub: sinon.SinonStubbedInstance<CLIService>;
dockerServiceStub: sinon.SinonStubbedInstance<DockerService>;
connectionServiceStub: sinon.SinonStubbedInstance<ConnectionService>;
clientServiceStub: sinon.SinonStubbedInstance<ClientService>;
serviceLocatorStub: sinon.SinonStub;
proccesStubs: {
processCWDStub: sinon.SinonStub;
Expand Down Expand Up @@ -72,6 +73,7 @@ function generateLocalNodeStubs(sandbox:sinon.SinonSandbox, cliServiceArgs?: any
const dockerServiceStub = sandbox.createStubInstance(DockerService);
const connectionServiceStub = sandbox.createStubInstance(ConnectionService);
const loggerServiceStub = sandbox.createStubInstance(LoggerService);
const clientServiceStub = sandbox.createStubInstance(ClientService);
const cliServiceStub = sandbox.createStubInstance(CLIService, {
getCurrentArgv: {
...cliServiceArgs,
Expand All @@ -83,12 +85,14 @@ function generateLocalNodeStubs(sandbox:sinon.SinonSandbox, cliServiceArgs?: any
getStub.withArgs(CLIService.name).returns(cliServiceStub);
getStub.withArgs(DockerService.name).returns(dockerServiceStub);
getStub.withArgs(ConnectionService.name).returns(connectionServiceStub);
getStub.withArgs(ClientService.name).returns(clientServiceStub);

sandbox.replaceGetter(ServiceLocator, 'Current', () => sandbox.createStubInstance(ServiceLocator, {
get: getStub
}));

return {
clientServiceStub,
connectionServiceStub,
dockerServiceStub,
loggerServiceStub,
Expand Down

0 comments on commit 1f8edeb

Please sign in to comment.