Repository: hashgraph/hedera-local-node
Pull request #516 (2024-01-25 19:09:15)

feat: Create unit test for RecoveryState #513
**Description**:
Create unit test for RecoveryState

**Related issue(s)**:
[#513](https://github.com/hashgraph/hedera-local-node/issues/513)

sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate failed**  
Failed conditions

[4.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate failed**  
Failed conditions

[4.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)


stefan-stefanooov: > ## [![Quality Gate Failed](https://camo.githubusercontent.com/5333b8e2ff49bd5efdc6d2efd30ec08d7ac8d67c5e80190d03b6c2cb29a5de8b/68747470733a2f2f736f6e6172736f757263652e6769746875622e696f2f736f6e6172636c6f75642d6769746875622d7374617469632d7265736f75726365732f76322f636865636b732f5175616c6974794761746542616467652f71672d6661696c65642d323070782e706e67)](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate failed**
> Failed conditions
> 
> [4.1% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)
> 
> [See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)

That's an exaggeration ... the code if perfect !
sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate failed**  
Failed conditions

[8.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=516&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=516&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=516&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=516&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=516&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=516)



Patch:
diff --git a/src/constants.ts b/src/constants.ts
index b83d024..dc6ed00 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -85,3 +85,7 @@ export const STOP_STATE_STOPPED_MESSAGE = 'Hedera Local Node was stopped success
 // Docker commands
 export const DOCKER_STOPPING_CONTAINERS_MESSAGE = 'Stopping the docker containers...';
 export const DOCKER_CLEANING_VALUMES_MESSAGE = 'Cleaning the volumes and temp files...';
+
+// Recovery state
+export const RECOVERY_STATE_INIT_MESSAGE = 'Recovery State Initialized!';
+export const RECOVERY_STATE_STARTING_MESSAGE = "Starting Recovery State...";
\ No newline at end of file
diff --git a/src/services/DockerService.ts b/src/services/DockerService.ts
index 38dc143..90d284c 100644
--- a/src/services/DockerService.ts
+++ b/src/services/DockerService.ts
@@ -91,12 +91,12 @@ export class DockerService implements IService{
      * @public
      * @returns {string} - The null output path.
      */
-    public getNullOutput () {
+    public getNullOutput(): "null" | "/dev/null" {
         if (IS_WINDOWS) return 'null';
         return '/dev/null';
     }
 
-      /**
+    /**
      * Checks if Docker is running.
      * 
      * @public
@@ -317,7 +317,6 @@ export class DockerService implements IService{
         });
     }
 
-
     /**
      * Executes the docker compose up command.
      * 
@@ -375,4 +374,20 @@ export class DockerService implements IService{
         } 
           return [];
     }
+
+    /**
+     * Tries to recover the state by performing Docker recovery steps.
+     * Stops the docker containers, cleans volumes and temp files, and tries to startup again.
+     * @returns {Promise<void>} A promise that resolves when the recovery steps have completed.
+     */
+    public async tryDockerRecovery(stateName: string): Promise<void> {
+        const nullOutput = this.getNullOutput();
+        this.logger.trace('Stopping the docker containers...', stateName);
+        shell.exec(`docker compose kill --remove-orphans 2>${nullOutput}`);
+        shell.exec(`docker compose down -v --remove-orphans 2>${nullOutput}`);
+        this.logger.trace('Cleaning the volumes and temp files...', stateName);
+        shell.exec(`rm -rf network-logs/* >${nullOutput} 2>&1`);
+        shell.exec(`docker network prune -f 2>${nullOutput}`);
+        this.logger.info('Trying to startup again...', stateName);
+    }
 }
diff --git a/src/state/RecoveryState.ts b/src/state/RecoveryState.ts
index 7eafa04..f647685 100644
--- a/src/state/RecoveryState.ts
+++ b/src/state/RecoveryState.ts
@@ -18,13 +18,13 @@
  *
  */
 
-import shell from 'shelljs';
 import { IOBserver } from '../controller/IObserver';
 import { LoggerService } from '../services/LoggerService';
 import { ServiceLocator } from '../services/ServiceLocator';
 import { EventType } from '../types/EventType';
 import { IState } from './IState';
-import { IS_WINDOWS } from '../constants';
+import { DockerService } from '../services/DockerService';
+import { RECOVERY_STATE_INIT_MESSAGE, RECOVERY_STATE_STARTING_MESSAGE } from '../constants';
 
 /**
  * Represents the recovery state of the Hedera Local Node.
@@ -50,6 +50,11 @@ export class RecoveryState implements IState{
      */
     private stateName: string;
 
+    /**
+     * Represents the Docker service used by the StartState class.
+     */
+    private dockerService: DockerService;
+
     /**
      * The type of event associated with the recovery state.
      */
@@ -62,7 +67,8 @@ export class RecoveryState implements IState{
     constructor(eventType: EventType) {
         this.stateName = RecoveryState.name;
         this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name);
-        this.logger.trace('Recovery State Initialized!', this.stateName);
+        this.dockerService = ServiceLocator.Current.get<DockerService>(DockerService.name);
+        this.logger.trace(RECOVERY_STATE_INIT_MESSAGE, this.stateName);
         this.eventType = eventType;
     }
 
@@ -79,41 +85,14 @@ export class RecoveryState implements IState{
      * @returns {Promise<void>} A promise that resolves when the recovery state has started.
      */
     public async onStart(): Promise<void> {
-        this.logger.info("Starting Recovery State...", this.stateName);
+        this.logger.info(RECOVERY_STATE_STARTING_MESSAGE, this.stateName);
 
         switch (this.eventType) {
             case EventType.DockerError:
-                await this.tryDockerRecovery();
+                await this.dockerService.tryDockerRecovery(this.stateName);
             default:
                 this.observer?.update(EventType.UnknownError);
                 break;
         }
     }
-
-    /**
-     * Tries to recover the state by performing Docker recovery steps.
-     * Stops the docker containers, cleans volumes and temp files, and tries to startup again.
-     * @returns {Promise<void>} A promise that resolves when the recovery steps have completed.
-     */
-    private async tryDockerRecovery(): Promise<void> {
-        const nullOutput = this.getNullOutput();
-        this.logger.trace('Stopping the docker containers...', this.stateName);
-        shell.exec(`docker compose kill --remove-orphans 2>${nullOutput}`);
-        shell.exec(`docker compose down -v --remove-orphans 2>${nullOutput}`);
-        this.logger.trace('Cleaning the volumes and temp files...', this.stateName);
-        shell.exec(`rm -rf network-logs/* >${nullOutput} 2>&1`);
-        shell.exec(`docker network prune -f 2>${nullOutput}`);
-        this.logger.info('Trying to startup again...', this.stateName);
-    }
-
-    /**
-     * Returns the appropriate output path for null based on the operating system.
-     * On Windows, it returns 'null'. On other operating systems, it returns '/dev/null'.
-     * 
-     * @returns {string} The output path for null.
-     */
-    private getNullOutput (): string {
-        if (IS_WINDOWS) return 'null';
-        return '/dev/null';
-    }
 }


Test patch:
diff --git a/test/unit/states/StopState.spec.ts b/test/unit/states/StopState.spec.ts
index 1d68771..bed9bab 100644
--- a/test/unit/states/StopState.spec.ts
+++ b/test/unit/states/StopState.spec.ts
@@ -1,4 +1,3 @@
-
 /*-
  *
  * Hedera Local Node
@@ -22,7 +21,6 @@
 import { expect } from 'chai';
 import sinon from 'sinon';
 import { StopState } from '../../../src/state/StopState';
-import shell from 'shelljs';
 import {
   DOCKER_CLEANING_VALUMES_MESSAGE,
   DOCKER_STOPPING_CONTAINERS_MESSAGE,
diff --git a/test/unit/states/recoveryState.spec.ts b/test/unit/states/recoveryState.spec.ts
new file mode 100644
index 0000000..fea6cd2
--- /dev/null
+++ b/test/unit/states/recoveryState.spec.ts
@@ -0,0 +1,93 @@
+/*-
+ *
+ * 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 { RecoveryState } from '../../../src/state/RecoveryState';
+import { getTestBed } from '../testBed';
+import {
+    RECOVERY_STATE_INIT_MESSAGE,
+    RECOVERY_STATE_STARTING_MESSAGE,
+} from '../../../src/constants';
+import { DockerService } from '../../../src/services/DockerService';
+import { EventType } from '../../../src/types/EventType';
+
+describe('RecoveryState tests', () => {
+    let recoveryState: RecoveryState,
+        testSandbox: SinonSandbox, 
+        loggerService: SinonStubbedInstance<LoggerService>,
+        serviceLocator: SinonStub,
+        dockerService: SinonStubbedInstance<DockerService>,
+        observerSpy: SinonSpy;
+
+    before(() => {
+        const { 
+            sandbox,
+            loggerServiceStub,
+            serviceLocatorStub,
+            dockerServiceStub,
+        } = getTestBed({
+            workDir: 'testDir',
+        });
+    
+        testSandbox = sandbox
+        dockerService = dockerServiceStub
+        loggerService = loggerServiceStub
+        serviceLocator = serviceLocatorStub
+
+        recoveryState = new RecoveryState(EventType.DockerError);
+        observerSpy = testSandbox.spy();
+        const observer = {
+            update: observerSpy
+        }
+        recoveryState.subscribe(observer);
+    });
+
+    afterEach(() => {
+        testSandbox.resetHistory();
+        observerSpy.resetHistory();
+        dockerService.tryDockerRecovery.reset()
+    });
+
+    it('should initialize the Recovery State', async () => {
+        expect(recoveryState).to.be.instanceOf(RecoveryState);
+        testSandbox.assert.calledWith(serviceLocator, LoggerService.name);
+        testSandbox.assert.calledWith(serviceLocator, DockerService.name);
+        testSandbox.assert.calledOnce(loggerService.trace);
+        testSandbox.assert.calledWith(loggerService.trace, RECOVERY_STATE_INIT_MESSAGE, RecoveryState.name);
+    })
+
+    it('should have a subscribe method', async () => {
+        expect(recoveryState.subscribe).to.be.a('function');
+    })
+
+    it('should execute onStart with EventType: DockerError', async () => {
+        await recoveryState.onStart();
+
+        // loggin messages
+        testSandbox.assert.calledOnce(loggerService.info)
+        testSandbox.assert.calledWith(loggerService.info, RECOVERY_STATE_STARTING_MESSAGE, RecoveryState.name);
+
+        testSandbox.assert.calledWith(observerSpy, EventType.UnknownError);
+
+        testSandbox.assert.calledOnceWithExactly(dockerService.tryDockerRecovery, RecoveryState.name);
+    })
+});
diff --git a/test/unit/states/startState.spec.ts b/test/unit/states/startState.spec.ts
index 5500a36..b927600 100644
--- a/test/unit/states/startState.spec.ts
+++ b/test/unit/states/startState.spec.ts
@@ -20,7 +20,7 @@
 
 import { expect } from 'chai';
 import { SinonSandbox, SinonSpy, SinonStub, SinonStubbedInstance } from 'sinon';
-import shell, { ShellString } from 'shelljs';
+import { ShellString } from 'shelljs';
 import { LoggerService } from '../../../src/services/LoggerService';
 import { StartState } from '../../../src/state/StartState';
 import { getTestBed } from '../testBed';
@@ -33,21 +33,20 @@ import {
 import { resolve } from 'path';
 import { DockerService } from '../../../src/services/DockerService';
 import { EventType } from '../../../src/types/EventType';
-import { assert } from 'console';
 import { ConnectionService } from '../../../src/services/ConnectionService';
 import { LocalNodeErrors } from '../../../src/Errors/LocalNodeErrors';
 
 describe('StartState tests', () => {
     let startState: StartState,
-    testSandbox: SinonSandbox, 
-    loggerService: SinonStubbedInstance<LoggerService>,
-    serviceLocator: SinonStub,
-    dockerService: SinonStubbedInstance<DockerService>,
-    stateDir: string,
-    processTestBed: {[key: string]: SinonStub},
-    shellTestBed: {[key: string]: SinonStub},
-    observerSpy: SinonSpy,
-    connectionService: SinonStubbedInstance<ConnectionService>;
+        testSandbox: SinonSandbox, 
+        loggerService: SinonStubbedInstance<LoggerService>,
+        serviceLocator: SinonStub,
+        dockerService: SinonStubbedInstance<DockerService>,
+        stateDir: string,
+        processTestBed: {[key: string]: SinonStub},
+        shellTestBed: {[key: string]: SinonStub},
+        observerSpy: SinonSpy,
+        connectionService: SinonStubbedInstance<ConnectionService>;
 
     before(() => {
         const { 
@@ -154,5 +153,4 @@ describe('StartState tests', () => {
         testSandbox.assert.match(observerSpy.args[0], EventType.UnknownError);
         testSandbox.assert.notCalled(loggerService.error);
     })
-
-});
\ No newline at end of file
+});
diff --git a/test/unit/testBed.ts b/test/unit/testBed.ts
index 9e09bf7..1b2e535 100644
--- a/test/unit/testBed.ts
+++ b/test/unit/testBed.ts
@@ -1,3 +1,23 @@
+
+/*-
+ *
+ * Hedera Local Node
+ *
+ * Copyright (C) 2023 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 sinon from "sinon";
 import shell from 'shelljs';
 import { LoggerService } from "../../src/services/LoggerService";
@@ -15,7 +35,7 @@ export interface LocalNodeTestBed {
     serviceLocatorStub: sinon.SinonStub;
     proccesStubs: {
         processCWDStub: sinon.SinonStub;
-        processPlatformStub: sinon.SinonStub
+        processPlatformStub: sinon.SinonStub;
     },
     shellStubs: {
         shellCDStub: sinon.SinonStub;
@@ -23,33 +43,29 @@ export interface LocalNodeTestBed {
     }
 }
 
-let testBead: LocalNodeTestBed
+let testBed: LocalNodeTestBed;
 
 export function getTestBed(cliServiceArgs?: any) {
-    if (testBead) {
-        resetTestBedHistory(testBead)
+    if (testBed) {
+        resetTestBedHistory(testBed)
         return {
-            ...testBead
+            ...testBed
         };
     }
 
     const sandbox = sinon.createSandbox();
-    testBead = {
+    testBed = {
         sandbox,
         ...generateProccessStub(sandbox),
         ...generateShellStubs(sandbox),
         ...generateLocalNodeStubs(sandbox, cliServiceArgs),
     }
 
-    return testBead;
+    return testBed;
 }
 
 function resetTestBedHistory(testBead: LocalNodeTestBed) {
     testBead.sandbox.resetHistory();
-    // testBead.loggerServiceStub.trace.resetHistory();
-    // testBead.loggerServiceStub.info.resetHistory();
-    // testBead.proccesStubs.processCWDStub.resetHistory();
-    // testBead.proccesStubs.processPlatformStub.resetHistory();
 }
 
 function generateLocalNodeStubs(sandbox:sinon.SinonSandbox, cliServiceArgs?: any) {
@@ -101,4 +117,4 @@ function generateShellStubs(sandbox: sinon.SinonSandbox) {
             shellCDStub,
             shellExecStub
     }}
-}
\ No newline at end of file
+}