diff --git a/__tests__/unit/core-cli/action-factory.test.ts b/__tests__/unit/core-cli/action-factory.test.ts new file mode 100644 index 0000000000..707f528914 --- /dev/null +++ b/__tests__/unit/core-cli/action-factory.test.ts @@ -0,0 +1,32 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { ActionFactory, Container } from "@packages/core-cli/src"; + +let cli; +beforeEach(() => (cli = new Console())); + +describe("ActionFactory", () => { + it("should create an instance", () => { + expect(cli.app.resolve(ActionFactory)).toBeInstanceOf(ActionFactory); + }); + + describe.each([ + ["abortErroredProcess", Container.Identifiers.AbortErroredProcess], + ["abortMissingProcess", Container.Identifiers.AbortMissingProcess], + ["abortRunningProcess", Container.Identifiers.AbortRunningProcess], + ["abortStoppedProcess", Container.Identifiers.AbortStoppedProcess], + ["abortUnknownProcess", Container.Identifiers.AbortUnknownProcess], + ["daemonizeProcess", Container.Identifiers.DaemonizeProcess], + ["restartProcess", Container.Identifiers.RestartProcess], + ["restartRunningProcess", Container.Identifiers.RestartRunningProcess], + ["restartRunningProcessWithPrompt", Container.Identifiers.RestartRunningProcessWithPrompt], + ])("%s", (method, binding) => { + it("should call be called", async () => { + const spy = jest.spyOn(cli.app.get(binding), "execute").mockImplementation(); + + await cli.app.resolve(ActionFactory)[method](); + + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/unit/core-cli/actions/abort-errored-process.test.ts b/__tests__/unit/core-cli/actions/abort-errored-process.test.ts new file mode 100644 index 0000000000..1ae23f386f --- /dev/null +++ b/__tests__/unit/core-cli/actions/abort-errored-process.test.ts @@ -0,0 +1,42 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { AbortErroredProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AbortErroredProcess) + .to(AbortErroredProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.AbortErroredProcess); +}); + +describe("AbortErroredProcess", () => { + it("should not throw if the process does exist", () => { + const spy = jest.spyOn(processManager, "isErrored").mockReturnValue(false); + + expect(action.execute(processName)).toBeUndefined(); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", () => { + const spy = jest.spyOn(processManager, "isErrored").mockReturnValue(true); + + expect(() => action.execute(processName)).toThrow(`The "${processName}" process has errored.`); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/abort-missing-process.test.ts b/__tests__/unit/core-cli/actions/abort-missing-process.test.ts new file mode 100644 index 0000000000..290a88ca69 --- /dev/null +++ b/__tests__/unit/core-cli/actions/abort-missing-process.test.ts @@ -0,0 +1,42 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { AbortMissingProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AbortMissingProcess) + .to(AbortMissingProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.AbortMissingProcess); +}); + +describe("AbortMissingProcess", () => { + it("should not throw if the process does exist", () => { + const spy = jest.spyOn(processManager, "missing").mockReturnValue(false); + + expect(action.execute(processName)).toBeUndefined(); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", () => { + const spy = jest.spyOn(processManager, "missing").mockReturnValue(true); + + expect(() => action.execute(processName)).toThrow(`The "${processName}" process does not exist.`); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/abort-running-process.test.ts b/__tests__/unit/core-cli/actions/abort-running-process.test.ts new file mode 100644 index 0000000000..0ec6a1006a --- /dev/null +++ b/__tests__/unit/core-cli/actions/abort-running-process.test.ts @@ -0,0 +1,42 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { AbortRunningProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AbortRunningProcess) + .to(AbortRunningProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.AbortRunningProcess); +}); + +describe("AbortRunningProcess", () => { + it("should not throw if the process does exist", () => { + const spy = jest.spyOn(processManager, "isOnline").mockReturnValue(false); + + expect(action.execute(processName)).toBeUndefined(); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", () => { + const spy = jest.spyOn(processManager, "isOnline").mockReturnValue(true); + + expect(() => action.execute(processName)).toThrow(`The "${processName}" process is already running.`); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/abort-stopped-process.test.ts b/__tests__/unit/core-cli/actions/abort-stopped-process.test.ts new file mode 100644 index 0000000000..557d0a4322 --- /dev/null +++ b/__tests__/unit/core-cli/actions/abort-stopped-process.test.ts @@ -0,0 +1,42 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { AbortStoppedProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AbortStoppedProcess) + .to(AbortStoppedProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.AbortStoppedProcess); +}); + +describe("AbortStoppedProcess", () => { + it("should not throw if the process does exist", () => { + const spy = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + + expect(action.execute(processName)).toBeUndefined(); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", () => { + const spy = jest.spyOn(processManager, "isStopped").mockReturnValue(true); + + expect(() => action.execute(processName)).toThrow(`The "${processName}" process is not running.`); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/abort-unknown-process.test.ts b/__tests__/unit/core-cli/actions/abort-unknown-process.test.ts new file mode 100644 index 0000000000..5f27cca174 --- /dev/null +++ b/__tests__/unit/core-cli/actions/abort-unknown-process.test.ts @@ -0,0 +1,42 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { AbortUnknownProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AbortUnknownProcess) + .to(AbortUnknownProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.AbortUnknownProcess); +}); + +describe("AbortUnknownProcess", () => { + it("should not throw if the process does exist", () => { + const spy = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); + + expect(action.execute(processName)).toBeUndefined(); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", () => { + const spy = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); + + expect(() => action.execute(processName)).toThrow(`The "${processName}" process has entered an unknown state.`); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core/common/process.test.ts b/__tests__/unit/core-cli/actions/daemonize-process.test.ts similarity index 52% rename from __tests__/unit/core/common/process.test.ts rename to __tests__/unit/core-cli/actions/daemonize-process.test.ts index 86cb4e811e..6c1b239ba7 100644 --- a/__tests__/unit/core/common/process.test.ts +++ b/__tests__/unit/core-cli/actions/daemonize-process.test.ts @@ -1,241 +1,33 @@ -import prompts from "prompts"; import os from "os"; -import { - daemonizeProcess, - restartProcess, - restartRunningProcess, - restartRunningProcessWithPrompt, - abortRunningProcess, - abortStoppedProcess, - abortErroredProcess, - abortUnknownProcess, - abortMissingProcess, -} from "@packages/core/src/common/process"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; -const processName: string = "ark-core"; +import { DaemonizeProcess } from "@packages/core-cli/src/actions"; -describe("restartProcess", () => { - it("should restart the process", async () => { - const spy = jest.spyOn(processManager, "restart").mockImplementation(undefined); +let cli; +let processManager; +let action; - restartProcess("ark-core"); +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); - expect(spy).toHaveBeenCalledWith(processName); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", async () => { - const spy = jest.spyOn(processManager, "restart").mockImplementation(() => { - throw new Error("hello world"); - }); - - expect(() => restartProcess("ark-core")).toThrow("hello world"); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist (with stderr)", async () => { - const spy = jest.spyOn(processManager, "restart").mockImplementation(() => { - const error: Error = new Error("hello world"); - // @ts-ignore - error.stderr = "error output"; - - throw error; - }); - - expect(() => restartProcess("ark-core")).toThrow("hello world: error output"); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); -}); - -describe("restartRunningProcess", () => { - it("should not restart the process if it is not online", async () => { - const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(false); - const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - - await restartRunningProcess(processName); - - expect(spyOnline).toHaveBeenCalled(); - expect(spyRestart).not.toHaveBeenCalled(); - - spyOnline.mockClear(); - spyRestart.mockClear(); - }); - - it("should restart the process", async () => { - const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); - const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - - await restartRunningProcess(processName); - - expect(spyOnline).toHaveBeenCalled(); - expect(spyRestart).toHaveBeenCalled(); - - spyOnline.mockClear(); - spyRestart.mockClear(); - }); -}); - -describe("restartRunningProcessWithPrompt", () => { - it("should not restart the process if it is not online", async () => { - const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(false); - const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - - await restartRunningProcessWithPrompt(processName); - - expect(spyOnline).toHaveBeenCalled(); - expect(spyRestart).not.toHaveBeenCalled(); - - spyOnline.mockClear(); - spyRestart.mockClear(); - }); - - it("should restart the process", async () => { - const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); - const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - - prompts.inject([true]); - - await restartRunningProcessWithPrompt(processName); - - expect(spyOnline).toHaveBeenCalled(); - expect(spyRestart).toHaveBeenCalled(); - - spyOnline.mockClear(); - spyRestart.mockClear(); - }); - - it("should not restart the process if it is not confirmed", async () => { - const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); - const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - - prompts.inject([false]); - - await restartRunningProcessWithPrompt(processName); - - expect(spyOnline).toHaveBeenCalled(); - expect(spyRestart).not.toHaveBeenCalled(); - - spyOnline.mockClear(); - spyRestart.mockClear(); - }); -}); - -describe("abortRunningProcess", () => { - it("should not throw if the process does exist", () => { - const spy = jest.spyOn(processManager, "isOnline").mockReturnValue(false); - - expect(abortRunningProcess(processName)).toBeUndefined(); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", () => { - const spy = jest.spyOn(processManager, "isOnline").mockReturnValue(true); - - expect(() => abortRunningProcess(processName)).toThrow(`The "${processName}" process is already running.`); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); -}); - -describe("abortStoppedProcess", () => { - it("should not throw if the process does exist", () => { - const spy = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - - expect(abortStoppedProcess(processName)).toBeUndefined(); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", () => { - const spy = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - - expect(() => abortStoppedProcess(processName)).toThrow(`The "${processName}" process is not running.`); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); -}); - -describe("abortErroredProcess", () => { - it("should not throw if the process does exist", () => { - const spy = jest.spyOn(processManager, "isErrored").mockReturnValue(false); - - expect(abortErroredProcess(processName)).toBeUndefined(); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", () => { - const spy = jest.spyOn(processManager, "isErrored").mockReturnValue(true); - - expect(() => abortErroredProcess(processName)).toThrow(`The "${processName}" process has errored.`); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); -}); - -describe("abortUnknownProcess", () => { - it("should not throw if the process does exist", () => { - const spy = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); - - expect(abortUnknownProcess(processName)).toBeUndefined(); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", () => { - const spy = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); - - expect(() => abortUnknownProcess(processName)).toThrow( - `The "${processName}" process has entered an unknown state.`, - ); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); -}); - -describe("abortMissingProcess", () => { - it("should not throw if the process does exist", () => { - const spy = jest.spyOn(processManager, "missing").mockReturnValue(false); - - expect(abortMissingProcess(processName)).toBeUndefined(); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); - - it("should throw if the process does not exist", () => { - const spy = jest.spyOn(processManager, "missing").mockReturnValue(true); - - expect(() => abortMissingProcess(processName)).toThrow(`The "${processName}" process does not exist.`); - expect(spy).toHaveBeenCalled(); - - spy.mockClear(); - }); + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.DaemonizeProcess) + .to(DaemonizeProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.DaemonizeProcess); }); -describe("daemonizeProcess", () => { +describe("DaemonizeProcess", () => { it("should throw if the process has entered an unknown state", () => { const has = jest.spyOn(processManager, "has").mockReturnValue(true); const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); expect(() => - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", @@ -258,7 +50,7 @@ describe("daemonizeProcess", () => { const isOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); expect(() => - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", @@ -285,7 +77,7 @@ describe("daemonizeProcess", () => { const totalmem = jest.spyOn(os, "totalmem").mockReturnValue(99999999999); const start = jest.spyOn(processManager, "start").mockImplementation(undefined); - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", @@ -326,7 +118,7 @@ describe("daemonizeProcess", () => { const totalmem = jest.spyOn(os, "totalmem").mockReturnValue(1); const start = jest.spyOn(processManager, "start").mockImplementation(undefined); - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", @@ -375,7 +167,7 @@ describe("daemonizeProcess", () => { }); expect(() => - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", @@ -424,7 +216,7 @@ describe("daemonizeProcess", () => { }); expect(() => - daemonizeProcess( + action.execute( { name: "ark-core", script: "script", diff --git a/__tests__/unit/core-cli/actions/restart-process.test.ts b/__tests__/unit/core-cli/actions/restart-process.test.ts new file mode 100644 index 0000000000..604a8a1257 --- /dev/null +++ b/__tests__/unit/core-cli/actions/restart-process.test.ts @@ -0,0 +1,60 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { RestartProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.RestartProcess) + .to(RestartProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.RestartProcess); +}); + +describe("RestartProcess", () => { + it("should restart the process", async () => { + const spy = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + action.execute(processName); + + expect(spy).toHaveBeenCalledWith(processName); + + spy.mockClear(); + }); + + it("should throw if the process does not exist", async () => { + const spy = jest.spyOn(processManager, "restart").mockImplementation(() => { + throw new Error("hello world"); + }); + + expect(() => action.execute(processName)).toThrow("hello world"); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); + + it("should throw if the process does not exist (with stderr)", async () => { + const spy = jest.spyOn(processManager, "restart").mockImplementation(() => { + const error: Error = new Error("hello world"); + // @ts-ignore + error.stderr = "error output"; + + throw error; + }); + + expect(() => action.execute(processName)).toThrow("hello world: error output"); + expect(spy).toHaveBeenCalled(); + + spy.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/restart-running-process-with-prompt.test.ts b/__tests__/unit/core-cli/actions/restart-running-process-with-prompt.test.ts new file mode 100644 index 0000000000..ae85f6eaae --- /dev/null +++ b/__tests__/unit/core-cli/actions/restart-running-process-with-prompt.test.ts @@ -0,0 +1,68 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { RestartRunningProcessWithPrompt } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.RestartRunningProcessWithPrompt) + .to(RestartRunningProcessWithPrompt) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.RestartRunningProcessWithPrompt); +}); + +describe("RestartRunningProcessWithPrompt", () => { + it("should not restart the process if it is not online", async () => { + const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(false); + const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + await action.execute(processName); + + expect(spyOnline).toHaveBeenCalled(); + expect(spyRestart).not.toHaveBeenCalled(); + + spyOnline.mockClear(); + spyRestart.mockClear(); + }); + + it("should restart the process", async () => { + const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); + const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + prompts.inject([true]); + + await action.execute(processName); + + expect(spyOnline).toHaveBeenCalled(); + expect(spyRestart).toHaveBeenCalled(); + + spyOnline.mockClear(); + spyRestart.mockClear(); + }); + + it("should not restart the process if it is not confirmed", async () => { + const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); + const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + prompts.inject([false]); + + await action.execute(processName); + + expect(spyOnline).toHaveBeenCalled(); + expect(spyRestart).not.toHaveBeenCalled(); + + spyOnline.mockClear(); + spyRestart.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/actions/restart-running-process.test.ts b/__tests__/unit/core-cli/actions/restart-running-process.test.ts new file mode 100644 index 0000000000..2f5949e8bc --- /dev/null +++ b/__tests__/unit/core-cli/actions/restart-running-process.test.ts @@ -0,0 +1,50 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { RestartRunningProcess } from "@packages/core-cli/src/actions"; + +const processName: string = "ark-core"; + +let cli; +let processManager; +let action; + +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.RestartRunningProcess) + .to(RestartRunningProcess) + .inSingletonScope(); + action = cli.app.get(Container.Identifiers.RestartRunningProcess); +}); + +describe("RestartRunningProcess", () => { + it("should not restart the process if it is not online", async () => { + const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(false); + const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + await action.execute(processName); + + expect(spyOnline).toHaveBeenCalled(); + expect(spyRestart).not.toHaveBeenCalled(); + + spyOnline.mockClear(); + spyRestart.mockClear(); + }); + + it("should restart the process", async () => { + const spyOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); + const spyRestart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + + await action.execute(processName); + + expect(spyOnline).toHaveBeenCalled(); + expect(spyRestart).toHaveBeenCalled(); + + spyOnline.mockClear(); + spyRestart.mockClear(); + }); +}); diff --git a/__tests__/unit/core-cli/application-factory.test.ts b/__tests__/unit/core-cli/application-factory.test.ts new file mode 100644 index 0000000000..d88087ea31 --- /dev/null +++ b/__tests__/unit/core-cli/application-factory.test.ts @@ -0,0 +1,23 @@ +import { Application, ApplicationFactory, Container, Utils } from "@packages/core-cli/src"; + +describe("ApplicationFactory", () => { + it("should create an application instance with the given container", () => { + expect( + ApplicationFactory.make(new Container.Container(), { + name: "@arkecosystem/core", + description: "Core of the ARK Blockchain", + version: "3.0.0-next.0", + }), + ).toBeInstanceOf(Application); + }); + + it("should expose the ProcessFactory", () => { + const app = ApplicationFactory.make(new Container.Container(), { + name: "@arkecosystem/core", + description: "Core of the ARK Blockchain", + version: "3.0.0-next.0", + }); + + expect(app.get(Container.Identifiers.ProcessFactory)("ark", "core")).toBeInstanceOf(Utils.Process); + }); +}); diff --git a/__tests__/unit/core-cli/application.test.ts b/__tests__/unit/core-cli/application.test.ts new file mode 100644 index 0000000000..f8b90888e8 --- /dev/null +++ b/__tests__/unit/core-cli/application.test.ts @@ -0,0 +1,100 @@ +import envPaths from "env-paths"; + +import { Application, Container } from "@packages/core-cli/src"; + +@Container.injectable() +class StubClass {} + +let app; +beforeEach(() => (app = new Application(new Container.Container()))); + +describe("ActionFactory", () => { + it("should bind a value to the IoC container", () => { + expect(app.isBound("key")).toBeFalse(); + + app.bind("key").toConstantValue("value"); + + expect(app.isBound("key")).toBeTrue(); + }); + + it("should rebind a value to the IoC container", () => { + expect(app.isBound("key")).toBeFalse(); + + app.bind("key").toConstantValue("value"); + + expect(app.get("key")).toBe("value"); + expect(app.isBound("key")).toBeTrue(); + + app.rebind("key").toConstantValue("value-new"); + + expect(app.get("key")).toBe("value-new"); + }); + + it("should unbind a value from the IoC container", () => { + app.bind("key").toConstantValue("value"); + + expect(app.isBound("key")).toBeTrue(); + + app.unbind("key"); + + expect(app.isBound("key")).toBeFalse(); + }); + + it("should get a value from the IoC container", () => { + app.bind("key").toConstantValue("value"); + + expect(app.get("key")).toBe("value"); + }); + + it("should resolve a value from the IoC container", () => { + expect(app.resolve(StubClass)).toBeInstanceOf(StubClass); + }); + + it("should get core paths", () => { + const paths = envPaths("ark", { suffix: "core" }); + + app.bind(Container.Identifiers.ApplicationPaths).toConstantValue(paths); + + expect(app.getCorePath("data")).toEqual(paths.data); + expect(app.getCorePath("config")).toEqual(paths.config); + expect(app.getCorePath("cache")).toEqual(paths.cache); + expect(app.getCorePath("log")).toEqual(paths.log); + expect(app.getCorePath("temp")).toEqual(paths.temp); + }); + + it("should get console paths with a file", () => { + const paths = envPaths("ark", { suffix: "core" }); + + app.bind(Container.Identifiers.ApplicationPaths).toConstantValue(paths); + + expect(app.getCorePath("data", "file")).toEqual(`${paths.data}/file`); + expect(app.getCorePath("config", "file")).toEqual(`${paths.config}/file`); + expect(app.getCorePath("cache", "file")).toEqual(`${paths.cache}/file`); + expect(app.getCorePath("log", "file")).toEqual(`${paths.log}/file`); + expect(app.getCorePath("temp", "file")).toEqual(`${paths.temp}/file`); + }); + + it("should get console paths", () => { + const paths = envPaths("ark", { suffix: "core" }); + + app.bind(Container.Identifiers.ConsolePaths).toConstantValue(paths); + + expect(app.getConsolePath("data")).toEqual(paths.data); + expect(app.getConsolePath("config")).toEqual(paths.config); + expect(app.getConsolePath("cache")).toEqual(paths.cache); + expect(app.getConsolePath("log")).toEqual(paths.log); + expect(app.getConsolePath("temp")).toEqual(paths.temp); + }); + + it("should get console paths with a file", () => { + const paths = envPaths("ark", { suffix: "core" }); + + app.bind(Container.Identifiers.ConsolePaths).toConstantValue(paths); + + expect(app.getConsolePath("data", "file")).toEqual(`${paths.data}/file`); + expect(app.getConsolePath("config", "file")).toEqual(`${paths.config}/file`); + expect(app.getConsolePath("cache", "file")).toEqual(`${paths.cache}/file`); + expect(app.getConsolePath("log", "file")).toEqual(`${paths.log}/file`); + expect(app.getConsolePath("temp", "file")).toEqual(`${paths.temp}/file`); + }); +}); diff --git a/__tests__/unit/core-cli/commands/__stubs__/command-without-definition.ts b/__tests__/unit/core-cli/commands/__stubs__/command-without-definition.ts new file mode 100644 index 0000000000..79127726c5 --- /dev/null +++ b/__tests__/unit/core-cli/commands/__stubs__/command-without-definition.ts @@ -0,0 +1,11 @@ +import { Commands, Container } from "@arkecosystem/core-cli"; + +@Container.injectable() +export class CommandWithoutDefinition extends Commands.Command { + public signature: string = "config:cli"; + public description: string = "Update the CLI configuration."; + + public async execute(): Promise { + // Do nothing... + } +} diff --git a/__tests__/unit/core-cli/commands/__stubs__/command.ts b/__tests__/unit/core-cli/commands/__stubs__/command.ts new file mode 100644 index 0000000000..7b586f835c --- /dev/null +++ b/__tests__/unit/core-cli/commands/__stubs__/command.ts @@ -0,0 +1,22 @@ +import { Commands, Container } from "@arkecosystem/core-cli"; +import Joi from "@hapi/joi"; + +@Container.injectable() +export class Command extends Commands.Command { + public signature: string = "config:cli"; + public description: string = "Update the CLI configuration."; + + public configure(): void { + this.definition.setArgument("someArgument1", "...", Joi.string()); + this.definition.setArgument("someArgument11", "...", Joi.string()); + this.definition.setArgument("someArgument111", "...", Joi.string()); + + this.definition.setFlag("someFlag1", "...", Joi.string()); + this.definition.setFlag("someFlag11", "...", Joi.string()); + this.definition.setFlag("someFlag111", "...", Joi.string()); + } + + public async execute(): Promise { + // Do nothing... + } +} diff --git a/__tests__/unit/core-cli/commands/command-help.test.ts b/__tests__/unit/core-cli/commands/command-help.test.ts new file mode 100644 index 0000000000..cec0a61556 --- /dev/null +++ b/__tests__/unit/core-cli/commands/command-help.test.ts @@ -0,0 +1,27 @@ +import { Console } from "@arkecosystem/core-test-framework"; +import { setGracefulCleanup } from "tmp"; + +import { Command } from "./__stubs__/command"; +import { CommandWithoutDefinition } from "./__stubs__/command-without-definition"; +import { CommandHelp } from "@packages/core-cli/src/commands"; + +let cli; +let cmd; + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + cli = new Console(); + + cmd = cli.app.resolve(CommandHelp); +}); + +describe("CommandHelp", () => { + it("should render the help if a command has arguments and flags", () => { + expect(cmd.render(cli.app.resolve(Command))).toBeString(); + }); + + it("should render the help if a command does not have arguments or flags", () => { + expect(cmd.render(cli.app.resolve(CommandWithoutDefinition))).toBeString(); + }); +}); diff --git a/__tests__/unit/core-cli/commands/discover-commands.test.ts b/__tests__/unit/core-cli/commands/discover-commands.test.ts new file mode 100644 index 0000000000..7d0fbe5bc0 --- /dev/null +++ b/__tests__/unit/core-cli/commands/discover-commands.test.ts @@ -0,0 +1,27 @@ +import { Console } from "@arkecosystem/core-test-framework"; +import { setGracefulCleanup } from "tmp"; +import { resolve } from "path"; + +import { DiscoverCommands } from "@packages/core-cli/src/commands"; + +let cli; +let cmd; + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + cli = new Console(); + + cmd = cli.app.resolve(DiscoverCommands); +}); + +describe("DiscoverCommands", () => { + it("should discover commands within the given directory", () => { + const commandPath: string = resolve(__dirname, "../../../../packages/core/dist/commands"); + + const commands = cmd.within(commandPath); + + expect(commands).toBeObject(); + expect(commands).not.toBeEmpty(); + }); +}); diff --git a/__tests__/unit/core-cli/commands/discover-network.test.ts b/__tests__/unit/core-cli/commands/discover-network.test.ts new file mode 100644 index 0000000000..cf45599614 --- /dev/null +++ b/__tests__/unit/core-cli/commands/discover-network.test.ts @@ -0,0 +1,83 @@ +import { Console } from "@arkecosystem/core-test-framework"; +import envPaths from "env-paths"; +import { dirSync, setGracefulCleanup } from "tmp"; + +import { ensureDirSync } from "fs-extra"; +import prompts from "prompts"; + +import { DiscoverNetwork } from "@packages/core-cli/src/commands"; + +let cli; +let cmd; +let configPath; + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + cli = new Console(); + + cmd = cli.app.resolve(DiscoverNetwork); + + configPath = envPaths("ark", { suffix: "core" }).config; +}); + +describe("DiscoverNetwork", () => { + it("should throw if no configurations can be detected", async () => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + await expect(cmd.discover(configPath)).rejects.toThrow( + 'We were unable to detect a network configuration. Please run "ark config:publish" and try again.', + ); + + delete process.env.CORE_PATH_CONFIG; + }); + + it("should choose the first network if only a single network is found", async () => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); + + await expect(cmd.discover(configPath)).resolves.toBe("mainnet"); + + delete process.env.CORE_PATH_CONFIG; + }); + + it("should choose the selected network if multiple networks are found", async () => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); + + prompts.inject(["devnet", true]); + + await expect(cmd.discover(configPath)).resolves.toBe("devnet"); + + delete process.env.CORE_PATH_CONFIG; + }); + + it("should throw if the network selection is not confirmed", async () => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); + + prompts.inject(["devnet", false]); + + await expect(cmd.discover(configPath)).rejects.toThrow("You'll need to confirm the network to continue."); + + delete process.env.CORE_PATH_CONFIG; + }); + + it("should throw if the network selection is not valid", async () => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); + ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); + + prompts.inject(["randomnet", true]); + + await expect(cmd.discover(configPath)).rejects.toThrow(`The given network "randomnet" is not valid.`); + + delete process.env.CORE_PATH_CONFIG; + }); +}); diff --git a/__tests__/unit/core-cli/component-factory.test.ts b/__tests__/unit/core-cli/component-factory.test.ts new file mode 100644 index 0000000000..eda30ca0e7 --- /dev/null +++ b/__tests__/unit/core-cli/component-factory.test.ts @@ -0,0 +1,49 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { ComponentFactory, Container } from "@packages/core-cli/src"; + +let cli; +beforeEach(() => (cli = new Console())); + +describe("ComponentFactory", () => { + it("should create an instance", () => { + expect(cli.app.resolve(ComponentFactory)).toBeInstanceOf(ComponentFactory); + }); + + describe.each([ + ["appHeader", Container.Identifiers.AppHeader], + ["ask", Container.Identifiers.Ask], + ["askDate", Container.Identifiers.AskDate], + ["askHidden", Container.Identifiers.AskHidden], + ["askNumber", Container.Identifiers.AskNumber], + ["askPassword", Container.Identifiers.AskPassword], + ["autoComplete", Container.Identifiers.AutoComplete], + ["box", Container.Identifiers.Box], + ["clear", Container.Identifiers.Clear], + ["confirm", Container.Identifiers.Confirm], + ["error", Container.Identifiers.Error], + ["fatal", Container.Identifiers.Fatal], + ["info", Container.Identifiers.Info], + ["listing", Container.Identifiers.Listing], + ["log", Container.Identifiers.Log], + ["multiSelect", Container.Identifiers.MultiSelect], + ["newLine", Container.Identifiers.NewLine], + ["prompt", Container.Identifiers.Prompt], + ["select", Container.Identifiers.Select], + ["spinner", Container.Identifiers.Spinner], + ["success", Container.Identifiers.Success], + ["table", Container.Identifiers.Table], + ["taskList", Container.Identifiers.TaskList], + ["title", Container.Identifiers.Title], + ["toggle", Container.Identifiers.Toggle], + ["warning", Container.Identifiers.Warning], + ])("%s", (method, binding) => { + it("should call be called", async () => { + const spy = jest.spyOn(cli.app.get(binding), "render").mockImplementation(); + + await cli.app.resolve(ComponentFactory)[method](); + + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/unit/core-cli/components/app-header.test.ts b/__tests__/unit/core-cli/components/app-header.test.ts new file mode 100644 index 0000000000..2630974db1 --- /dev/null +++ b/__tests__/unit/core-cli/components/app-header.test.ts @@ -0,0 +1,30 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { red, white } from "kleur"; +import os from "os"; + +import { AppHeader } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AppHeader) + .to(AppHeader) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AppHeader); +}); + +describe("AppHeader", () => { + it("should render the component", () => { + expect(component.render()).toBe( + `${red().bold(`${cli.pkg.description}`)} ${white().bold( + `[${cli.pkg.version} | ${process.version} | ${os.platform()}@${os.arch()}]`, + )}`, + ); + }); +}); diff --git a/__tests__/unit/core-cli/components/ask-date.test.ts b/__tests__/unit/core-cli/components/ask-date.test.ts new file mode 100644 index 0000000000..8c1d85c63d --- /dev/null +++ b/__tests__/unit/core-cli/components/ask-date.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { AskDate } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AskDate) + .to(AskDate) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AskDate); +}); + +describe("AskDate", () => { + it("should render the component", async () => { + prompts.inject(["2020-01-01"]); + + await expect(component.render("Hello World")).resolves.toBe("2020-01-01"); + }); +}); diff --git a/__tests__/unit/core-cli/components/ask-hidden.test.ts b/__tests__/unit/core-cli/components/ask-hidden.test.ts new file mode 100644 index 0000000000..bbe1a13541 --- /dev/null +++ b/__tests__/unit/core-cli/components/ask-hidden.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { AskHidden } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AskHidden) + .to(AskHidden) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AskHidden); +}); + +describe("AskHidden", () => { + it("should render the component", async () => { + prompts.inject(["hidden"]); + + await expect(component.render("Hello World")).resolves.toBe("hidden"); + }); +}); diff --git a/__tests__/unit/core-cli/components/ask-number.test.ts b/__tests__/unit/core-cli/components/ask-number.test.ts new file mode 100644 index 0000000000..f2d6ce1a02 --- /dev/null +++ b/__tests__/unit/core-cli/components/ask-number.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { AskNumber } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AskNumber) + .to(AskNumber) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AskNumber); +}); + +describe("AskNumber", () => { + it("should render the component", async () => { + prompts.inject([123]); + + await expect(component.render("Hello World")).resolves.toBe(123); + }); +}); diff --git a/__tests__/unit/core-cli/components/ask-password.test.ts b/__tests__/unit/core-cli/components/ask-password.test.ts new file mode 100644 index 0000000000..7921d4161c --- /dev/null +++ b/__tests__/unit/core-cli/components/ask-password.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { AskPassword } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AskPassword) + .to(AskPassword) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AskPassword); +}); + +describe("AskPassword", () => { + it("should render the component", async () => { + prompts.inject(["password"]); + + await expect(component.render("Hello World")).resolves.toBe("password"); + }); +}); diff --git a/__tests__/unit/core-cli/components/ask.test.ts b/__tests__/unit/core-cli/components/ask.test.ts new file mode 100644 index 0000000000..73fad8fb17 --- /dev/null +++ b/__tests__/unit/core-cli/components/ask.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { Ask } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Ask) + .to(Ask) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Ask); +}); + +describe("Ask", () => { + it("should render the component", async () => { + prompts.inject(["john doe"]); + + await expect(component.render("Hello World")).resolves.toBe("john doe"); + }); +}); diff --git a/__tests__/unit/core-cli/components/auto-complete.test.ts b/__tests__/unit/core-cli/components/auto-complete.test.ts new file mode 100644 index 0000000000..24a8754416 --- /dev/null +++ b/__tests__/unit/core-cli/components/auto-complete.test.ts @@ -0,0 +1,35 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { AutoComplete } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.AutoComplete) + .to(AutoComplete) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.AutoComplete); +}); + +describe("AutoComplete", () => { + it("should render the component", async () => { + prompts.inject(["Clooney"]); + + await expect( + component.render("Pick your favorite actor", [ + { title: "Cage" }, + { title: "Clooney" }, + { title: "Gyllenhaal" }, + { title: "Gibson" }, + { title: "Grant" }, + ]), + ).resolves.toBe("Clooney"); + }); +}); diff --git a/__tests__/unit/core-cli/components/box.test.ts b/__tests__/unit/core-cli/components/box.test.ts new file mode 100644 index 0000000000..52982ce7b8 --- /dev/null +++ b/__tests__/unit/core-cli/components/box.test.ts @@ -0,0 +1,28 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Box } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Box) + .to(Box) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Box); +}); + +describe("Box", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "log"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/unit/core-cli/components/clear.test.ts b/__tests__/unit/core-cli/components/clear.test.ts new file mode 100644 index 0000000000..3d9cbae6c1 --- /dev/null +++ b/__tests__/unit/core-cli/components/clear.test.ts @@ -0,0 +1,29 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Clear } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Clear) + .to(Clear) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Clear); +}); + +describe("Clear", () => { + it("should render the component", () => { + const spyWrite = jest.spyOn(process.stdout, "write"); + + component.render(); + + expect(spyWrite).toHaveBeenCalledWith("\x1b[2J"); + expect(spyWrite).toHaveBeenCalledWith("\x1b[0f"); + }); +}); diff --git a/__tests__/unit/core-cli/components/confirm.test.ts b/__tests__/unit/core-cli/components/confirm.test.ts new file mode 100644 index 0000000000..bfa0dd8efd --- /dev/null +++ b/__tests__/unit/core-cli/components/confirm.test.ts @@ -0,0 +1,29 @@ +import "jest-extended"; + +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { Confirm } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Confirm) + .to(Confirm) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Confirm); +}); + +describe("Confirm", () => { + it("should render the component", async () => { + prompts.inject([true]); + + await expect(component.render("Hello World")).resolves.toBeTrue(); + }); +}); diff --git a/__tests__/unit/core-cli/components/error.test.ts b/__tests__/unit/core-cli/components/error.test.ts new file mode 100644 index 0000000000..c58c8b3e16 --- /dev/null +++ b/__tests__/unit/core-cli/components/error.test.ts @@ -0,0 +1,29 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { white } from "kleur"; + +import { Error } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Error) + .to(Error) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Error); +}); + +describe("Error", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "error"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith(white().bgRed(`[ERROR] Hello World`)); + }); +}); diff --git a/__tests__/unit/core-cli/components/fatal.test.ts b/__tests__/unit/core-cli/components/fatal.test.ts new file mode 100644 index 0000000000..36a26bdeb0 --- /dev/null +++ b/__tests__/unit/core-cli/components/fatal.test.ts @@ -0,0 +1,24 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Fatal } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Fatal) + .to(Fatal) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Fatal); +}); + +describe("Fatal", () => { + it("should render the component", () => { + expect(() => component.render("Hello World")).toThrow("Hello World"); + }); +}); diff --git a/__tests__/unit/core-cli/components/info.test.ts b/__tests__/unit/core-cli/components/info.test.ts new file mode 100644 index 0000000000..4cc5bc498c --- /dev/null +++ b/__tests__/unit/core-cli/components/info.test.ts @@ -0,0 +1,29 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { white } from "kleur"; + +import { Info } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Info) + .to(Info) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Info); +}); + +describe("Info", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "info"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith(white().bgBlue(`[INFO] Hello World`)); + }); +}); diff --git a/__tests__/unit/core-cli/components/listing.test.ts b/__tests__/unit/core-cli/components/listing.test.ts new file mode 100644 index 0000000000..4abe4c737a --- /dev/null +++ b/__tests__/unit/core-cli/components/listing.test.ts @@ -0,0 +1,31 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Listing } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Listing) + .to(Listing) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Listing); +}); + +describe("Listing", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "log"); + + component.render([1, 2, 3]); + + expect(spyLogger).toHaveBeenCalledTimes(3); + expect(spyLogger).toHaveBeenCalledWith(" * 1"); + expect(spyLogger).toHaveBeenCalledWith(" * 2"); + expect(spyLogger).toHaveBeenCalledWith(" * 3"); + }); +}); diff --git a/__tests__/unit/core-cli/components/log.test.ts b/__tests__/unit/core-cli/components/log.test.ts new file mode 100644 index 0000000000..044898ad99 --- /dev/null +++ b/__tests__/unit/core-cli/components/log.test.ts @@ -0,0 +1,28 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Log } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Log) + .to(Log) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Log); +}); + +describe("Log", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "log"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith("Hello World"); + }); +}); diff --git a/__tests__/unit/core-cli/components/multi-select.test.ts b/__tests__/unit/core-cli/components/multi-select.test.ts new file mode 100644 index 0000000000..bf47339bcd --- /dev/null +++ b/__tests__/unit/core-cli/components/multi-select.test.ts @@ -0,0 +1,33 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { MultiSelect } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.MultiSelect) + .to(MultiSelect) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.MultiSelect); +}); + +describe("MultiSelect", () => { + it("should render the component", async () => { + prompts.inject([["#ff0000", "#0000ff"]]); + + await expect( + component.render("Pick Colors", [ + { title: "Red", value: "#ff0000" }, + { title: "Green", value: "#00ff00", disabled: true }, + { title: "Blue", value: "#0000ff", selected: true }, + ]), + ).resolves.toEqual(["#ff0000", "#0000ff"]); + }); +}); diff --git a/__tests__/unit/core-cli/components/new-line.test.ts b/__tests__/unit/core-cli/components/new-line.test.ts new file mode 100644 index 0000000000..c5be47d63b --- /dev/null +++ b/__tests__/unit/core-cli/components/new-line.test.ts @@ -0,0 +1,28 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { NewLine } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.NewLine) + .to(NewLine) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.NewLine); +}); + +describe("NewLine", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "log"); + + component.render(); + + expect(spyLogger).toHaveBeenCalledWith("\n"); + }); +}); diff --git a/__tests__/unit/core-cli/components/prompt.test.ts b/__tests__/unit/core-cli/components/prompt.test.ts new file mode 100644 index 0000000000..cb63475fa5 --- /dev/null +++ b/__tests__/unit/core-cli/components/prompt.test.ts @@ -0,0 +1,33 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { Prompt } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Prompt) + .to(Prompt) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Prompt); +}); + +describe("Prompt", () => { + it("should render the component", async () => { + prompts.inject(["johndoe"]); + + await expect( + component.render({ + type: "text", + name: "value", + message: "What's your twitter handle?", + }), + ).resolves.toEqual({ value: "johndoe" }); + }); +}); diff --git a/__tests__/unit/core-cli/components/select.test.ts b/__tests__/unit/core-cli/components/select.test.ts new file mode 100644 index 0000000000..354f5c494a --- /dev/null +++ b/__tests__/unit/core-cli/components/select.test.ts @@ -0,0 +1,33 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { Select } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Select) + .to(Select) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Select); +}); + +describe("Select", () => { + it("should render the component", async () => { + prompts.inject(["#0000ff"]); + + await expect( + component.render("Pick a color", [ + { title: "Red", description: "This option has a description", value: "#ff0000" }, + { title: "Green", value: "#00ff00", disabled: true }, + { title: "Blue", value: "#0000ff" }, + ]), + ).resolves.toBe("#0000ff"); + }); +}); diff --git a/__tests__/unit/core-cli/components/spinner.test.ts b/__tests__/unit/core-cli/components/spinner.test.ts new file mode 100644 index 0000000000..2b90155601 --- /dev/null +++ b/__tests__/unit/core-cli/components/spinner.test.ts @@ -0,0 +1,24 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Spinner } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Spinner) + .to(Spinner) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Spinner); +}); + +describe("Spinner", () => { + it("should render the component", () => { + expect(component.render("Hello World")).toBeObject(); + }); +}); diff --git a/__tests__/unit/core-cli/components/success.test.ts b/__tests__/unit/core-cli/components/success.test.ts new file mode 100644 index 0000000000..5a539eb25b --- /dev/null +++ b/__tests__/unit/core-cli/components/success.test.ts @@ -0,0 +1,29 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { white } from "kleur"; + +import { Success } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Success) + .to(Success) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Success); +}); + +describe("Success", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "info"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith(white().bgGreen(`[OK] Hello World`)); + }); +}); diff --git a/__tests__/unit/core-cli/components/table.test.ts b/__tests__/unit/core-cli/components/table.test.ts new file mode 100644 index 0000000000..8ecfcdc0a0 --- /dev/null +++ b/__tests__/unit/core-cli/components/table.test.ts @@ -0,0 +1,37 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Table } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Table) + .to(Table) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Table); +}); + +describe("Table", () => { + it("should render the component", () => { + let message: string; + jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); + + component.render(["ID", "Name"], table => { + table.push([1, "John Doe"]); + table.push([2, "Jane Doe"]); + }); + + expect(message).toContain("ID"); + expect(message).toContain("Name"); + expect(message).toContain(1); + expect(message).toContain("John Doe"); + expect(message).toContain(2); + expect(message).toContain("Jane Doe"); + }); +}); diff --git a/__tests__/unit/core-cli/components/task-list.test.ts b/__tests__/unit/core-cli/components/task-list.test.ts new file mode 100644 index 0000000000..8b701166b3 --- /dev/null +++ b/__tests__/unit/core-cli/components/task-list.test.ts @@ -0,0 +1,33 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { TaskList } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.TaskList) + .to(TaskList) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.TaskList); +}); + +describe("TaskList", () => { + it("should render the component", async () => { + const fakeTask = jest.fn(); + + await component.render([ + { + title: "...", + task: fakeTask, + }, + ]); + + expect(fakeTask).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/unit/core-cli/components/title.test.ts b/__tests__/unit/core-cli/components/title.test.ts new file mode 100644 index 0000000000..d0f34a3805 --- /dev/null +++ b/__tests__/unit/core-cli/components/title.test.ts @@ -0,0 +1,30 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { yellow } from "kleur"; + +import { Title } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Title) + .to(Title) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Title); +}); + +describe("Title", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "log"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith(yellow("Hello World")); + expect(spyLogger).toHaveBeenCalledWith(yellow("===========")); + }); +}); diff --git a/__tests__/unit/core-cli/components/toggle.test.ts b/__tests__/unit/core-cli/components/toggle.test.ts new file mode 100644 index 0000000000..d12e38106c --- /dev/null +++ b/__tests__/unit/core-cli/components/toggle.test.ts @@ -0,0 +1,27 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; + +import { Toggle } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Toggle) + .to(Toggle) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Toggle); +}); + +describe("Toggle", () => { + it("should render the component", async () => { + prompts.inject(["yes"]); + + await expect(component.render("Hello World")).resolves.toBe("yes"); + }); +}); diff --git a/__tests__/unit/core-cli/components/warning.test.ts b/__tests__/unit/core-cli/components/warning.test.ts new file mode 100644 index 0000000000..4e9eed51fd --- /dev/null +++ b/__tests__/unit/core-cli/components/warning.test.ts @@ -0,0 +1,29 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { white } from "kleur"; + +import { Warning } from "@packages/core-cli/src/components"; + +let cli; +let component; + +beforeEach(() => { + cli = new Console(); + + // Bind from src instead of dist to collect coverage. + cli.app + .rebind(Container.Identifiers.Warning) + .to(Warning) + .inSingletonScope(); + component = cli.app.get(Container.Identifiers.Warning); +}); + +describe("Warning", () => { + it("should render the component", () => { + const spyLogger = jest.spyOn(cli.app.get(Container.Identifiers.Logger), "warning"); + + component.render("Hello World"); + + expect(spyLogger).toHaveBeenCalledWith(white().bgYellow(`[WARNING] Hello World`)); + }); +}); diff --git a/__tests__/unit/core-cli/input/definition.test.ts b/__tests__/unit/core-cli/input/definition.test.ts new file mode 100644 index 0000000000..19e06c420e --- /dev/null +++ b/__tests__/unit/core-cli/input/definition.test.ts @@ -0,0 +1,85 @@ +import "jest-extended"; + +import Joi from "@hapi/joi"; + +import { InputDefinition } from "@packages/core-cli/src/input"; + +describe("InputDefinition", () => { + it("should get all arguments", () => { + const definition = new InputDefinition(); + definition.setArgument("firstName", "...", Joi.string()); + definition.setArgument("lastName", "...", Joi.string()); + + expect(definition.getArguments()).toEqual({ + firstName: { description: "...", schema: Joi.string() }, + lastName: { description: "...", schema: Joi.string() }, + }); + }); + + it("should get the value of an argument by name", () => { + const definition = new InputDefinition(); + definition.setArgument("firstName", "...", Joi.string()); + + expect(definition.getArgument("firstName")).toEqual({ description: "...", schema: Joi.string() }); + }); + + it("should set the value of an argument by name", () => { + const definition = new InputDefinition(); + definition.setArgument("firstName", "...", Joi.string()); + + expect(definition.getArgument("firstName")).toEqual({ description: "...", schema: Joi.string() }); + + definition.setArgument("firstName", "...", Joi.number()); + + expect(definition.getArgument("firstName")).toEqual({ description: "...", schema: Joi.number() }); + }); + + it("should check if an argument exists", () => { + const definition = new InputDefinition(); + + expect(definition.hasArgument("middleName")).toBeFalse(); + + definition.setArgument("middleName", "...", Joi.number()); + + expect(definition.hasArgument("middleName")).toBeTrue(); + }); + + it("should get all flags", () => { + const definition = new InputDefinition(); + definition.setFlag("firstName", "...", Joi.string()); + definition.setFlag("lastName", "...", Joi.string()); + + expect(definition.getFlags()).toEqual({ + firstName: { description: "...", schema: Joi.string() }, + lastName: { description: "...", schema: Joi.string() }, + }); + }); + + it("should get the value of a flag by name", () => { + const definition = new InputDefinition(); + definition.setFlag("firstName", "...", Joi.string()); + + expect(definition.getFlag("firstName")).toEqual({ description: "...", schema: Joi.string() }); + }); + + it("should set the value of a flag by name", () => { + const definition = new InputDefinition(); + definition.setFlag("firstName", "...", Joi.string()); + + expect(definition.getFlag("firstName")).toEqual({ description: "...", schema: Joi.string() }); + + definition.setFlag("firstName", "...", Joi.number()); + + expect(definition.getFlag("firstName")).toEqual({ description: "...", schema: Joi.number() }); + }); + + it("should check if a flag exists", () => { + const definition = new InputDefinition(); + + expect(definition.hasFlag("middleName")).toBeFalse(); + + definition.setFlag("middleName", "...", Joi.number()); + + expect(definition.hasFlag("middleName")).toBeTrue(); + }); +}); diff --git a/__tests__/unit/core-cli/input/input.test.ts b/__tests__/unit/core-cli/input/input.test.ts new file mode 100644 index 0000000000..02908d990a --- /dev/null +++ b/__tests__/unit/core-cli/input/input.test.ts @@ -0,0 +1,153 @@ +import "jest-extended"; + +import { Console } from "@arkecosystem/core-test-framework"; +import Joi from "@hapi/joi"; + +import { Input, InputDefinition } from "@packages/core-cli/src/input"; + +let cli; +beforeEach(() => (cli = new Console())); + +const createInput = (args?: string[]): Input => { + const definition = new InputDefinition(); + definition.setArgument("firstName", "...", Joi.string()); + definition.setArgument("lastName", "...", Joi.string()); + definition.setFlag("hello", "...", Joi.string()); + definition.setFlag("firstName", "...", Joi.string()); + definition.setFlag("lastName", "...", Joi.string()); + + const input = cli.app.resolve(Input); + input.parse(args || ["env:paths", "john", "doe", "--hello=world"], definition); + input.bind(); + input.validate(); + + return input; +}; + +describe("Input", () => { + it("should parse, bind and validate the arguments and flags", () => { + const input = createInput(); + + expect(input.getArgument("firstName")).toBe("john"); + expect(input.getArgument("lastName")).toBe("doe"); + expect(input.getFlag("hello")).toBe("world"); + }); + + it("should parse, bind and validate the arguments", () => { + const definition = new InputDefinition(); + definition.setArgument("firstName", "...", Joi.string()); + definition.setArgument("lastName", "...", Joi.string()); + + const input = cli.app.resolve(Input); + input.parse(["env:paths", "john", "doe"], definition); + input.bind(); + input.validate(); + + expect(input.getArgument("firstName")).toBe("john"); + expect(input.getArgument("lastName")).toBe("doe"); + }); + + it("should parse, bind and validate the flags", () => { + const definition = new InputDefinition(); + definition.setFlag("hello", "...", Joi.string()); + + const input = cli.app.resolve(Input); + input.parse(["env:paths", "--hello=world"], definition); + input.bind(); + input.validate(); + + expect(input.getFlag("hello")).toBe("world"); + }); + + it("should get all arguments", () => { + const input = createInput(); + + expect(input.getArguments()).toEqual({ + firstName: "john", + lastName: "doe", + }); + }); + + it("should get all arguments merged with the given values", () => { + const input = createInput(); + + expect(input.getArguments({ middleName: "jane" })).toEqual({ + firstName: "john", + lastName: "doe", + middleName: "jane", + }); + }); + + it("should get an argument by name", () => { + const input = createInput(); + + expect(input.getArgument("firstName")).toBe("john"); + }); + + it("should set the value of an argument by name", () => { + const input = createInput(); + + expect(input.getArgument("firstName")).toBe("john"); + + input.setArgument("firstName", "jane"); + + expect(input.getArgument("firstName")).toBe("jane"); + }); + + it("should check if an argument exists", () => { + const input = createInput(); + + expect(input.hasArgument("middleName")).toBeFalse(); + + input.setArgument("middleName", "jane"); + + expect(input.hasArgument("middleName")).toBeTrue(); + }); + + it("should get all flags", () => { + const input = createInput(["env:paths", "--firstName=john", "--lastName=doe"]); + + expect(input.getFlags()).toEqual({ + firstName: "john", + lastName: "doe", + v: 0, + }); + }); + + it("should get all flags merged with the given values", () => { + const input = createInput(["env:paths", "--firstName=john", "--lastName=doe"]); + + expect(input.getFlags({ middleName: "jane" })).toEqual({ + firstName: "john", + lastName: "doe", + middleName: "jane", + v: 0, + }); + }); + + it("should get a flag by name", () => { + const input = createInput(["env:paths", "--firstName=john", "--lastName=doe"]); + + expect(input.getFlag("firstName")).toBe("john"); + }); + + it("should set the value of a flag by name", () => { + const input = createInput(["env:paths", "--firstName=john", "--lastName=doe"]); + + expect(input.getFlag("firstName")).toBe("john"); + + input.setFlag("firstName", "jane"); + + expect(input.getFlag("firstName")).toBe("jane"); + }); + + it("should check if a flag exists", () => { + const input = createInput(["env:paths", "--firstName=john", "--lastName=doe"]); + + expect(input.hasFlag("middleName")).toBeFalse(); + + input.setFlag("middleName", "jane"); + + expect(input.hasFlag("middleName")).toBeTrue(); + }); +}); diff --git a/__tests__/unit/core-cli/input/parser.test.ts b/__tests__/unit/core-cli/input/parser.test.ts new file mode 100644 index 0000000000..1e755208a7 --- /dev/null +++ b/__tests__/unit/core-cli/input/parser.test.ts @@ -0,0 +1,12 @@ +import "jest-extended"; + +import { InputParser } from "@packages/core-cli/src/input"; + +describe("InputParser", () => { + it("should parse the arguments and flags", () => { + const { args, flags } = InputParser.parseArgv(["env:set", "john", "doe", "--force"]); + + expect(args).toEqual(["env:set", "john", "doe"]); + expect(flags.force).toBeTrue(); + }); +}); diff --git a/__tests__/unit/core-cli/input/validator.test.ts b/__tests__/unit/core-cli/input/validator.test.ts new file mode 100644 index 0000000000..b3ef96d910 --- /dev/null +++ b/__tests__/unit/core-cli/input/validator.test.ts @@ -0,0 +1,39 @@ +import "jest-extended"; + +import { Console } from "@arkecosystem/core-test-framework"; +import Joi from "@hapi/joi"; + +import { InputValidator } from "@packages/core-cli/src/input"; + +let cli; +let validator; +beforeEach(() => { + cli = new Console(); + validator = cli.app.resolve(InputValidator); +}); + +describe("InputValidator", () => { + it("should validate the data and return it", () => { + expect( + validator.validate( + { firstName: "john", lastName: "doe" }, + { + firstName: Joi.string(), + lastName: Joi.string(), + }, + ), + ).toEqual({ firstName: "john", lastName: "doe" }); + }); + + it("should throw if the data is valid", () => { + expect(() => + validator.validate( + { firstName: "john", lastName: "doe" }, + { + firstName: Joi.string(), + lastName: Joi.number(), + }, + ), + ).toThrow('"lastName" must be a number'); + }); +}); diff --git a/__tests__/unit/core-cli/output/output.test.ts b/__tests__/unit/core-cli/output/output.test.ts new file mode 100644 index 0000000000..8bcf4004ce --- /dev/null +++ b/__tests__/unit/core-cli/output/output.test.ts @@ -0,0 +1,79 @@ +// public mute() { +// public unmute() { + +import { Console } from "@arkecosystem/core-test-framework"; + +import { Output } from "@packages/core-cli/src/output"; + +let cli; +let output; + +beforeEach(() => { + cli = new Console(); + + output = cli.app.resolve(Output); +}); + +describe("Output", () => { + it("should mute and unmute the output", () => { + const spyWrite = jest.spyOn(process.stdout, "write"); + + console.log("this should be written to stdout"); + + output.mute(); + + console.log("this should not be written to stdout"); + + output.unmute(); + + expect(spyWrite).toHaveBeenCalled(); + }); + + it("should get and set the verbosity level", () => { + expect(output.getVerbosity()).toBe(1); + + output.setVerbosity(2); + + expect(output.getVerbosity()).toBe(2); + }); + + it("should determine if the verbosity level is quiet", () => { + output.setVerbosity(0); + + expect(output.isQuiet()).toBeTrue(); + + output.setVerbosity(1); + + expect(output.isQuiet()).toBeFalse(); + }); + + it("should determine if the verbosity level is normal", () => { + output.setVerbosity(1); + + expect(output.isNormal()).toBeTrue(); + + output.setVerbosity(0); + + expect(output.isNormal()).toBeFalse(); + }); + + it("should determine if the verbosity level is verbose", () => { + output.setVerbosity(2); + + expect(output.isVerbose()).toBeTrue(); + + output.setVerbosity(0); + + expect(output.isVerbose()).toBeFalse(); + }); + + it("should determine if the verbosity level is debug", () => { + output.setVerbosity(3); + + expect(output.isDebug()).toBeTrue(); + + output.setVerbosity(0); + + expect(output.isDebug()).toBeFalse(); + }); +}); diff --git a/__tests__/unit/core-cli/plugins/suggest.test.ts b/__tests__/unit/core-cli/plugins/suggest.test.ts new file mode 100644 index 0000000000..c0c44f3b13 --- /dev/null +++ b/__tests__/unit/core-cli/plugins/suggest.test.ts @@ -0,0 +1,47 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import prompts from "prompts"; +import { blue, red } from "kleur"; + +import { SuggestCommand } from "@packages/core-cli/src/plugins/suggest"; + +let cli; +let cmd; +beforeEach(() => { + cli = new Console(); + cmd = cli.app.resolve(SuggestCommand); +}); + +describe("SuggestCommand", () => { + it("should immediately return if there is no signature", async () => { + expect(await cmd.execute({ signature: "", signatures: [], bin: "ark" })).toBeUndefined(); + }); + + it("should immediately return if there are no signatures", async () => { + expect(await cmd.execute({ signature: "topic:command", signatures: [], bin: "ark" })).toBeUndefined(); + }); + + it("should update the bin help if a topic is found", async () => { + const spyWarning = jest.spyOn(cli.app.get(Container.Identifiers.Warning), "render"); + + prompts.inject([true]); + + await cmd.execute({ signature: "topic:command", signatures: ["topic:command1"], bin: "ark" }); + + expect(spyWarning).toHaveBeenCalledWith(`${red("topic:command")} is not a ark command.`); + }); + + it("should throw if suggestion is not confirmed", async () => { + const spyInfo = jest.spyOn(cli.app.get(Container.Identifiers.Info), "render"); + + prompts.inject([false]); + + await cmd.execute({ + signature: "topic:command", + signatures: ["topic:command1"], + bin: "ark", + }); + + expect(spyInfo).toHaveBeenCalledWith(`Run ${blue("ark --help")} for a list of available commands.`); + }); +}); diff --git a/__tests__/unit/core/common/__fixtures__/app.js b/__tests__/unit/core-cli/services/__fixtures__/app.js similarity index 100% rename from __tests__/unit/core/common/__fixtures__/app.js rename to __tests__/unit/core-cli/services/__fixtures__/app.js diff --git a/__tests__/unit/core/common/__fixtures__/latest-version.ts b/__tests__/unit/core-cli/services/__fixtures__/latest-version.ts similarity index 100% rename from __tests__/unit/core/common/__fixtures__/latest-version.ts rename to __tests__/unit/core-cli/services/__fixtures__/latest-version.ts diff --git a/__tests__/unit/core-cli/services/config.test.ts b/__tests__/unit/core-cli/services/config.test.ts new file mode 100644 index 0000000000..570dbc89b6 --- /dev/null +++ b/__tests__/unit/core-cli/services/config.test.ts @@ -0,0 +1,42 @@ +import "jest-extended"; + +import { Console } from "@arkecosystem/core-test-framework"; +import { setGracefulCleanup } from "tmp"; + +import { Config } from "@packages/core-cli/src/services"; + +let cli; +let config; + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + cli = new Console(); + + config = cli.app.resolve(Config); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("Config", () => { + it("should setup a new config with default values", () => { + expect(config.get("token")).toBe("ark"); + expect(config.get("channel")).toBeOneOf(["latest", "next"]); + }); + + it("should set and get a value", () => { + expect(config.get("token")).toBe("ark"); + + config.set("token", "btc"); + + expect(config.get("token")).toBe("btc"); + + config.forget("token", "btc"); + + expect(config.get("token")).toBeUndefined(); + + config.set("token", "btc"); + + expect(config.get("token")).toBe("btc"); + }); +}); diff --git a/__tests__/unit/core-cli/services/environment.test.ts b/__tests__/unit/core-cli/services/environment.test.ts new file mode 100644 index 0000000000..17cb856b82 --- /dev/null +++ b/__tests__/unit/core-cli/services/environment.test.ts @@ -0,0 +1,50 @@ +import "jest-extended"; + +import { Console } from "@arkecosystem/core-test-framework"; +import envfile from "envfile"; +import fs from "fs-extra"; + +import { Environment } from "@packages/core-cli/src/services"; + +let cli; +let environment; + +beforeEach(() => { + cli = new Console(); + + environment = cli.app.resolve(Environment); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("Environment", () => { + it("should get all paths for the given token and network", async () => { + expect(environment.getPaths("ark", "testnet")).toContainAllKeys(["data", "config", "cache", "log", "temp"]); + }); + + it("should fail to update the variables if the file doesn't exist", async () => { + expect(() => environment.updateVariables("some-file", {})).toThrowError( + "No environment file found at some-file.", + ); + }); + + it("should update the variables", async () => { + // Arrange + const existsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); + const parseFileSync = jest.spyOn(envfile, "parseFileSync").mockImplementation(() => ({})); + const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); + + // Act + environment.updateVariables("stub", { key: "value" }); + + // Assert + expect(existsSync).toHaveBeenCalledWith("stub"); + expect(parseFileSync).toHaveBeenCalledWith("stub"); + expect(writeFileSync).toHaveBeenCalledWith("stub", "key=value"); + + // Reset + existsSync.mockReset(); + parseFileSync.mockReset(); + writeFileSync.mockReset(); + }); +}); diff --git a/__tests__/unit/core-cli/services/installer.test.ts b/__tests__/unit/core-cli/services/installer.test.ts new file mode 100644 index 0000000000..7be57b7f7b --- /dev/null +++ b/__tests__/unit/core-cli/services/installer.test.ts @@ -0,0 +1,72 @@ +import "jest-extended"; + +import { Console } from "@arkecosystem/core-test-framework"; +import { setGracefulCleanup } from "tmp"; +// import path from "path"; + +import { Installer } from "@packages/core-cli/src/services"; + +import execa from "../../../../__mocks__/execa"; + +let cli; +let installer; + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + cli = new Console(); + + installer = cli.app.resolve(Installer); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("Installer", () => { + describe("#install", () => { + it("should be ok if [stdout] output is present", () => { + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + + installer.install("@arkecosystem/core"); + + expect(spySync).toHaveBeenCalledWith("yarn global add @arkecosystem/core", { shell: true }); + }); + + it("should not be ok if [stderr] output is present", () => { + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: "stderr", + }); + + expect(() => installer.install("@arkecosystem/core")).toThrow("stderr"); + + expect(spySync).toHaveBeenCalledWith("yarn global add @arkecosystem/core", { shell: true }); + }); + }); + + describe("#installFromChannel", () => { + it("should be ok if no [stderr] output is present", () => { + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + + installer.installFromChannel("@arkecosystem/core", "latest"); + + expect(spySync).toHaveBeenCalledWith("yarn global add @arkecosystem/core@latest", { shell: true }); + }); + + it("should be not ok if [stderr] output is present", () => { + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: "stderr", + }); + + expect(() => installer.installFromChannel("@arkecosystem/core", "latest")).toThrow("stderr"); + + expect(spySync).toHaveBeenCalledWith("yarn global add @arkecosystem/core@latest", { shell: true }); + }); + }); +}); diff --git a/__tests__/unit/core-cli/services/logger.test.ts b/__tests__/unit/core-cli/services/logger.test.ts new file mode 100644 index 0000000000..dbbda3c78b --- /dev/null +++ b/__tests__/unit/core-cli/services/logger.test.ts @@ -0,0 +1,99 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Logger } from "@packages/core-cli/src/services"; + +let cli; +let logger; + +beforeEach(() => { + cli = new Console(); + + logger = cli.app.resolve(Logger); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("Logger", () => { + it("should log an emergency message", () => { + const spyConsole = jest.spyOn(console, "error"); + + logger.emergency("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log an alert message", () => { + const spyConsole = jest.spyOn(console, "error"); + + logger.alert("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log a critical message", () => { + const spyConsole = jest.spyOn(console, "error"); + + logger.critical("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log an error message", () => { + const spyConsole = jest.spyOn(console, "error"); + + logger.error("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log a warning message", () => { + const spyConsole = jest.spyOn(console, "warn"); + + logger.warning("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log a notice message", () => { + const spyConsole = jest.spyOn(console, "info"); + + logger.notice("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log an info message", () => { + const spyConsole = jest.spyOn(console, "info"); + + logger.info("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log a debug message", () => { + const spyConsole = jest.spyOn(console, "debug"); + + logger.debug("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should log a message", () => { + const spyConsole = jest.spyOn(console, "log"); + + logger.log("this should be written to stdout"); + + expect(spyConsole).toHaveBeenCalled(); + }); + + it("should not log a message if the output is quiet", () => { + cli.app.get(Container.Identifiers.Output).setVerbosity(0); + + const spyConsole = jest.spyOn(console, "log"); + + logger.log("this should be written to stdout"); + + expect(spyConsole).not.toHaveBeenCalled(); + }); +}); diff --git a/__tests__/unit/core-cli/services/process-manager.test.ts b/__tests__/unit/core-cli/services/process-manager.test.ts new file mode 100644 index 0000000000..6627d24e5f --- /dev/null +++ b/__tests__/unit/core-cli/services/process-manager.test.ts @@ -0,0 +1,740 @@ +import "jest-extended"; + +import { Contracts } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { ProcessManager } from "@packages/core-cli/src/services"; + +import execa from "../../../../__mocks__/execa"; + +let cli; +let processManager; + +beforeAll(() => { + cli = new Console(); + + processManager = cli.app.resolve(ProcessManager); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("ProcessManager", () => { + describe(".list()", () => { + it("should return an empty array if stdout is empty", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const processes: Contracts.ProcessDescription[] | undefined = processManager.list(); + + // Assert... + expect(processes).toBeArray(); + expect(processes).toBeEmpty(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return an empty array if stdout is empty after trimming", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "\n", + stderr: undefined, + failed: false, + }); + + // Act... + const processes: Contracts.ProcessDescription[] | undefined = processManager.list(); + + // Assert... + expect(processes).toBeArray(); + expect(processes).toBeEmpty(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return an empty array if stdout is invalid JSON", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "{", + stderr: undefined, + failed: false, + }); + + // Act... + const processes: Contracts.ProcessDescription[] | undefined = processManager.list(); + + // Assert... + expect(processes).toBeArray(); + expect(processes).toBeEmpty(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return an empty array if an exception is thrown", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockImplementation(() => { + throw new Error("Whoops"); + }); + + // Act... + const processes: Contracts.ProcessDescription[] | undefined = processManager.list(); + + // Assert... + expect(processes).toBeArray(); + expect(processes).toBeEmpty(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return an array if stdout is valid JSON", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "key": "value" }]', + stderr: undefined, + failed: false, + }); + + // Act... + const processes: Contracts.ProcessDescription[] | undefined = processManager.list(); + + // Assert... + expect(processes).toEqual([{ key: "value" }]); + expect(processes).not.toBeEmpty(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#describe", () => { + it("should return an object if the process exists", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "unknown" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const process: Contracts.ProcessDescription | undefined = processManager.describe("stub"); + + // Assert... + expect(process).toBeObject(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return undefined if the process does not exist", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub-other", "pm2_env": { "status": "unknown" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const process: Contracts.ProcessDescription | undefined = processManager.describe("stub"); + + // Assert... + expect(process).toBeUndefined(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return undefined if stdout is an empty array", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "[]", + stderr: undefined, + failed: false, + }); + + // Act... + const process: Contracts.ProcessDescription | undefined = processManager.describe("stub"); + + // Assert... + expect(process).toBeUndefined(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("return undefined if an exception is thrown", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockImplementation(() => { + throw new Error("Whoops"); + }); + + // Act... + const process: Contracts.ProcessDescription | undefined = processManager.describe("stub"); + + // Assert... + expect(process).toBeUndefined(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#start", () => { + it("should be OK if failed is false", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.start( + { + script: "stub.js", + }, + { name: "stub" }, + ); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 start stub.js --name=stub", { shell: true }); + }); + + it("should respect the given node_args", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.start( + { + script: "stub.js", + node_args: { max_old_space_size: 500 }, + }, + { name: "stub" }, + ); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith( + 'pm2 start stub.js --node-args="--max_old_space_size=500" --name=stub', + { shell: true }, + ); + }); + + it("should respect the given args", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.start( + { + script: "stub.js", + args: "core:run --daemon", + }, + { name: "stub" }, + ); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 start stub.js --name=stub -- core:run --daemon", { shell: true }); + }); + }); + + describe("#stop", () => { + it("should be OK if failed is false", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.stop("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 stop stub", { shell: true }); + }); + + it("should respect the given flags", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.stop("stub", { key: "value" }); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 stop stub --key=value", { shell: true }); + }); + }); + + describe("#restart", () => { + it("should be OK if failed is false", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.restart("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 restart stub --update-env", { shell: true }); + }); + + it("should respect the given flags", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.restart("stub", { key: "value" }); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 restart stub --key=value", { shell: true }); + }); + }); + + describe("#reload", () => { + it(".reload()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.reload("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 reload stub", { shell: true }); + }); + }); + + describe("#reset", () => { + it(".reset()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.reset("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 reset stub", { shell: true }); + }); + }); + + describe("#delete", () => { + it(".delete()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.delete("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 delete stub", { shell: true }); + }); + }); + + describe("#flush", () => { + it(".flush()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.flush("stub"); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 flush", { shell: true }); + }); + }); + + describe("#reloadLogs", () => { + it(".reloadLogs()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.reloadLogs(); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 reloadLogs", { shell: true }); + }); + }); + + describe("#ping", () => { + it(".ping()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.ping(); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 ping", { shell: true }); + }); + }); + + describe("#update", () => { + it(".update()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: null, + stderr: undefined, + failed: false, + }); + + // Act... + const { failed } = processManager.update(); + + // Assert... + expect(failed).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 update", { shell: true }); + }); + }); + + describe("#status", () => { + it("should return the status if the process exists", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "online" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.status("stub"); + + // Assert... + expect(status).toBe("online"); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("return undefined if an exception is thrown", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockImplementation(() => { + throw new Error("Whoops"); + }); + + // Act... + const status = processManager.status("stub"); + + // Assert... + expect(status).toBeUndefined(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#status", () => { + it(".status()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub-other", "pm2_env": { "status": "online" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.status("stub"); + + // Assert... + expect(status).toBeUndefined(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isOnline", () => { + it(".isOnline()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "online" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isOnline("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isStopped", () => { + it(".isStopped()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "stopped" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isStopped("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isStopping", () => { + it(".isStopping()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "stopping" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isStopping("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isWaiting", () => { + it(".isWaiting()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "waiting restart" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isWaiting("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isLaunching", () => { + it(".isLaunching()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "launching" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isLaunching("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isErrored", () => { + it(".isErrored()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "errored" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isErrored("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isOneLaunch", () => { + it(".isOneLaunch()", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "one-launch-status" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isOneLaunch("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#isUnknown", () => { + it("should return true if the process has a status of [unknown]", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "unknown" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isUnknown("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("should return false if the process has a status other than [unknown]", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: '[{ "id": "stub", "pm2_env": { "status": "online" } }]', + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.isUnknown("stub"); + + // Assert... + expect(status).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + + it("return true if an exception is thrown", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockImplementation(() => { + throw new Error("Whoops"); + }); + + // Act... + const status = processManager.isUnknown("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 jlist", { shell: true }); + }); + }); + + describe("#has", () => { + it("should return true if the process ID is a number", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: 1, + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.has("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 id stub | awk '{ print $2 }'", { shell: true }); + }); + + it("return false if the process ID is not a number", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "", + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.has("stub"); + + // Assert... + expect(status).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 id stub | awk '{ print $2 }'", { shell: true }); + }); + + it("return false if an exception is thrown", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockImplementation(() => { + throw new Error("Whoops"); + }); + + // Act... + const status = processManager.has("stub"); + + // Assert... + expect(status).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 id stub | awk '{ print $2 }'", { shell: true }); + }); + }); + + describe("#missing", () => { + it("return true if the process ID is not a number", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "", + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.missing("stub"); + + // Assert... + expect(status).toBeTrue(); + expect(spySync).toHaveBeenCalledWith("pm2 id stub | awk '{ print $2 }'", { shell: true }); + }); + + it("return false if the process ID is a number", () => { + // Arrange... + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: 1, + stderr: undefined, + failed: false, + }); + + // Act... + const status = processManager.missing("stub"); + + // Assert... + expect(status).toBeFalse(); + expect(spySync).toHaveBeenCalledWith("pm2 id stub | awk '{ print $2 }'", { shell: true }); + }); + }); +}); diff --git a/__tests__/unit/core-cli/services/updater.test.ts b/__tests__/unit/core-cli/services/updater.test.ts new file mode 100644 index 0000000000..dce11b9b67 --- /dev/null +++ b/__tests__/unit/core-cli/services/updater.test.ts @@ -0,0 +1,207 @@ +import "jest-extended"; + +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import nock from "nock"; +import prompts from "prompts"; + +import { Updater } from "@packages/core-cli/src/services/updater"; + +import execa from "../../../../__mocks__/execa"; + +import { versionNext } from "./__fixtures__/latest-version"; + +let cli; +// let processManager; +let updater; +let config; + +beforeEach(() => { + nock.cleanAll(); + + cli = new Console(); + // processManager = cli.app.get(Container.Identifiers.ProcessManager); + updater = cli.app.resolve(Updater); + config = cli.app.get(Container.Identifiers.Config); +}); + +afterEach(() => jest.resetAllMocks()); + +beforeAll(() => nock.disableNetConnect()); + +afterAll(() => nock.enableNetConnect()); + +describe("Updater", () => { + describe("#check", () => { + it("should forget the latest version if it has one from a previous check", async () => { + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, versionNext); + + config.set("latestVersion", {}); + + const spyForget = jest.spyOn(config, "forget"); + + await expect(updater.check()).resolves.toBeFalse(); + expect(spyForget).toHaveBeenCalledWith("latestVersion"); + }); + + it("should return false if the latest version cannot be retrieved", async () => { + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, {}); + + const spyWarning = jest.spyOn(cli.app.get(Container.Identifiers.Warning), "render"); + + await expect(updater.check()).resolves.toBeFalse(); + expect(spyWarning).toHaveBeenCalledWith('We were unable to find any releases for the "next" channel.'); + }); + + it("should return false if the latest version is already installed", async () => { + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, versionNext); + + await expect(updater.check()).resolves.toBeFalse(); + }); + + it("should return false if the last check has been within the last 24 hours ago", async () => { + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, versionNext); + + config.set("lastUpdateCheck", Date.now()); + + await expect(updater.check()).resolves.toBeFalse(); + }); + + it("should return true if a new version is available", async () => { + const response = { ...versionNext }; + response["dist-tags"].next = "4.0.0-next.0"; + response.versions["4.0.0-next.0"] = { ...response.versions["2.5.0-next.10"] }; + response.versions["4.0.0-next.0"] = { + ...response.versions["2.5.0-next.10"], + ...{ version: "4.0.0-next.0" }, + }; + + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, response); + + config.set("latestVersion", {}); + + const spySet = jest.spyOn(config, "set"); + + await expect(updater.check()).resolves.toBeTrue(); + expect(spySet).toHaveBeenCalled(); + }); + }); + + describe("#update", () => { + it("should return early if the latest version is not present", async () => { + await expect(updater.update()).resolves.toBeFalse(); + }); + + it("should update without a prompt if a new version is available", async () => { + // Arrange... + const response = { ...versionNext }; + response["dist-tags"].next = "4.0.0-next.0"; + response.versions["4.0.0-next.0"] = { ...response.versions["2.5.0-next.10"] }; + response.versions["4.0.0-next.0"] = { + ...response.versions["2.5.0-next.10"], + ...{ version: "4.0.0-next.0" }, + }; + + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, response); + + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + const spySpinner = jest.spyOn(cli.app.get(Container.Identifiers.Spinner), "render"); + const spyInstaller = jest.spyOn(cli.app.get(Container.Identifiers.Installer), "installFromChannel"); + + // Act... + await updater.check(); + + const update = await updater.update(true); + + // Assert... + expect(update).toBeTrue(); + expect(spySync).toHaveBeenCalled(); + expect(spySpinner).toHaveBeenCalled(); + expect(spyInstaller).toHaveBeenCalled(); + }); + + it("should update with a prompt if a new version is available", async () => { + // Arrange... + const response = { ...versionNext }; + response["dist-tags"].next = "4.0.0-next.0"; + response.versions["4.0.0-next.0"] = { ...response.versions["2.5.0-next.10"] }; + response.versions["4.0.0-next.0"] = { + ...response.versions["2.5.0-next.10"], + ...{ version: "4.0.0-next.0" }, + }; + + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, response); + + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + const spySpinner = jest.spyOn(cli.app.get(Container.Identifiers.Spinner), "render"); + const spyInstaller = jest.spyOn(cli.app.get(Container.Identifiers.Installer), "installFromChannel"); + + prompts.inject([true]); + + // Act... + await updater.check(); + + const update = await updater.update(); + + // Assert... + expect(update).toBeTrue(); + expect(spySync).toHaveBeenCalled(); + expect(spySpinner).toHaveBeenCalled(); + expect(spyInstaller).toHaveBeenCalled(); + }); + + it("should fail to update without a confirmation", async () => { + // Arrange... + const response = { ...versionNext }; + response["dist-tags"].next = "4.0.0-next.0"; + response.versions["4.0.0-next.0"] = { ...response.versions["2.5.0-next.10"] }; + response.versions["4.0.0-next.0"] = { + ...response.versions["2.5.0-next.10"], + ...{ version: "4.0.0-next.0" }, + }; + + nock(/.*/) + .get("/@arkecosystem%2Fcore") + .reply(200, response); + + const spySync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + const spySpinner = jest.spyOn(cli.app.get(Container.Identifiers.Spinner), "render"); + const spyInstaller = jest.spyOn(cli.app.get(Container.Identifiers.Installer), "installFromChannel"); + + prompts.inject([false]); + + // Act... + await updater.check(); + await expect(updater.update()).rejects.toThrow("You'll need to confirm the update to continue."); + + // Assert... + expect(spySync).not.toHaveBeenCalled(); + expect(spySpinner).not.toHaveBeenCalled(); + expect(spyInstaller).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/unit/core-cli/utils/__fixtures__/app.js b/__tests__/unit/core-cli/utils/__fixtures__/app.js new file mode 100644 index 0000000000..7b420028db --- /dev/null +++ b/__tests__/unit/core-cli/utils/__fixtures__/app.js @@ -0,0 +1,8 @@ +module.exports = { + forger: { + plugins: [{ package: "@arkecosystem/core-forger" }], + }, + relay: { + plugins: [{ package: "@arkecosystem/core-forger" }], + }, +}; diff --git a/__tests__/unit/core-cli/utils/__fixtures__/latest-version.ts b/__tests__/unit/core-cli/utils/__fixtures__/latest-version.ts new file mode 100644 index 0000000000..dc16095dbb --- /dev/null +++ b/__tests__/unit/core-cli/utils/__fixtures__/latest-version.ts @@ -0,0 +1,420 @@ +export const versionLatest = { + _id: "@arkecosystem/core", + _rev: "124-6952dbe729e4e231817e92cb9871148f", + name: "@arkecosystem/core", + "dist-tags": { + latest: "2.5.24", + next: "2.5.0-next.10", + }, + versions: { + "2.5.24": { + name: "@arkecosystem/core", + version: "2.5.24", + description: "Core of the ARK Blockchain", + license: "MIT", + contributors: [ + { + name: "François-Xavier Thoorens", + email: "fx@ark.io", + }, + + { + name: "Kristjan Košič", + email: "kristjan@ark.io", + }, + + { + name: "Brian Faust", + email: "brian@ark.io", + }, + + { + name: "Alex Barnsley", + email: "alex@ark.io", + }, + ], + main: "dist/index", + types: "dist/index", + bin: { + ark: "./bin/run", + }, + scripts: { + ark: "./bin/run", + build: "yarn clean && yarn compile && yarn copy", + "build:watch": "yarn clean && yarn copy && yarn compile -w", + clean: "del dist", + compile: "../../node_modules/typescript/bin/tsc", + copy: "cd ./src && cpy './config' '../dist/' --parents && cd ..", + "debug:forger": "node --inspect-brk yarn ark forger:run", + "debug:relay": "node --inspect-brk yarn ark relay:run", + "debug:start": "node --inspect-brk yarn ark core:run", + "forger:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark forger:run", + "forger:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark forger:run", + "forger:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark forger:run --env=test", + "full:testnet": + "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --networkStart --env=test", + prepack: "../../node_modules/.bin/oclif-dev manifest && npm shrinkwrap", + postpack: "rm -f oclif.manifest.json", + prepublishOnly: "yarn build", + "relay:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark relay:run", + "relay:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark relay:run", + "relay:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark relay:run --env=test", + "start:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark core:run", + "start:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark core:run", + "start:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --env=test", + }, + dependencies: { + "@arkecosystem/core-api": "^2.5.24", + "@arkecosystem/core-blockchain": "^2.5.24", + "@arkecosystem/core-container": "^2.5.24", + "@arkecosystem/core-database-postgres": "^2.5.24", + "@arkecosystem/core-event-emitter": "^2.5.24", + "@arkecosystem/core-exchange-json-rpc": "^2.5.24", + "@arkecosystem/core-forger": "^2.5.24", + "@arkecosystem/core-logger-pino": "^2.5.24", + "@arkecosystem/core-p2p": "^2.5.24", + "@arkecosystem/core-snapshots": "^2.5.24", + "@arkecosystem/core-state": "^2.5.24", + "@arkecosystem/core-transaction-pool": "^2.5.24", + "@arkecosystem/core-utils": "^2.5.24", + "@arkecosystem/core-wallet-api": "^2.5.24", + "@arkecosystem/core-webhooks": "^2.5.24", + "@arkecosystem/crypto": "^2.5.24", + "@oclif/command": "^1.5.16", + "@oclif/config": "^1.13.2", + "@oclif/plugin-autocomplete": "^0.1.1", + "@oclif/plugin-commands": "^1.2.2", + "@oclif/plugin-help": "^2.2.0", + "@oclif/plugin-not-found": "^1.2.2", + "@oclif/plugin-plugins": "^1.7.8", + "@typeskrift/foreman": "^0.2.1", + bip39: "^3.0.2", + bytebuffer: "^5.0.1", + chalk: "^2.4.2", + clear: "^0.1.0", + "cli-progress": "^2.1.1", + "cli-table3": "^0.5.1", + "cli-ux": "^5.3.1", + dayjs: "^1.8.15", + "env-paths": "^2.2.0", + envfile: "^3.0.0", + execa: "^2.0.3", + "fast-levenshtein": "^2.0.6", + "fs-extra": "^8.1.0", + "latest-version": "^5.1.0", + listr: "^0.14.3", + "lodash.minby": "^4.6.0", + "nodejs-tail": "^1.1.0", + "pretty-bytes": "^5.2.0", + "pretty-ms": "^5.0.0", + prompts: "^2.1.0", + "read-last-lines": "^1.7.1", + semver: "^6.2.0", + wif: "^2.0.6", + }, + devDependencies: { + "@types/bip39": "^2.4.2", + "@types/bytebuffer": "^5.0.40", + "@types/cli-progress": "^1.8.1", + "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "^8.0.0", + "@types/got": "^9.6.1", + "@types/listr": "^0.14.0", + "@types/lodash.minby": "^4.6.6", + "@types/pretty-ms": "^4.0.0", + "@types/prompts": "^2.4.0", + "@types/semver": "^6.0.1", + "@types/wif": "^2.0.1", + }, + engines: { + node: ">=10.x", + }, + publishConfig: { + access: "public", + }, + oclif: { + commands: "./dist/commands", + hooks: { + init: ["./dist/hooks/init/config", "./dist/hooks/init/update"], + command_not_found: ["./dist/hooks/command_not_found/suggest"], + }, + bin: "ark", + topics: { + config: { + description: "manage core config variables", + }, + env: { + description: "manage core environment variables", + }, + core: { + description: "manage a core instance (relay & forger)", + }, + forger: { + description: "manage a forger instance", + }, + relay: { + description: "manage a relay instance", + }, + snapshot: { + description: "manage a relay snapshots", + }, + chain: { + description: "commands to interact with the blockchain", + }, + network: { + description: "manage network configurations and tasks", + }, + }, + plugins: ["@oclif/plugin-autocomplete", "@oclif/plugin-commands", "@oclif/plugin-help"], + }, + _id: "@arkecosystem/core@2.5.24", + _nodeVersion: "10.16.0", + _npmVersion: "6.9.0", + dist: { + integrity: + "sha512-l6SfF1F7dGHda0yvSFx9prao7FLSR9HhWOyQvp9NVGAxERvw5ne21xkWwb5HEZyh5UGpt68JbmLvIuNPlvCAMg==", + shasum: "531cdc7e9d5be02a14b8572d427e6c7c0bf80de2", + tarball: "https://registry.npmjs.org/@arkecosystem/core/-/core-2.5.24.tgz", + fileCount: 192, + unpackedSize: 997808, + "npm-signature": + "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJdbymACRA9TVsSAnZWagAAvbwP/3+wSJgA1ya33pVBisdw\n3+tDZbz8xdBigUd8TGtHkt4rkv+nQNsUyfZMhNBhCq/hiSMskJH6FAn9ULOH\nw9730vS8T1OILqzJR9f3dau+UPc2JFgmyt/4fV7R0Xt/HyiTZfeNJyQN9C+g\nJu2Mm9P5lZYl2+/QvS2QW7ZiRy0S3gIG8p4HbJWyM/s/XHSxmQLCvHTthoQT\n62oyBctIzYRFXqblc1+L1B/JaQ1wkjDDoztfUqnupOcj/vrZcq8e2TB8L7Si\nUlPuRjjXDlqeJrKMkwL54rOzN/kOSiI27ULYbnBVWm+7RHrQ07bP9EyfvaHb\nEQzmfPJaI0d1Q71NVRhFircT2zdgzGs26PMj3lEh+9J8P0oCUEKqk2MZtQ3b\nsVr6xkG1EO5ShtJPeOtUuqvp72ZbcpA50EmhCYsdW9yAe2NyI6Ggz9GTMi1c\n+QCOeidbNoxfBWe6CYw0bgqlbGGm+0e3BnMe2/4gcgnxohYc9SqjOABo+j50\nFt4Xi9UJsNenI1fYZTMU0mCVhtg1mHVgDSatHARQc9v2zZhCZ5oEsc/oWcZA\nTmrQYKNukITZjTAHW5CyT4LlixtrlUjY8DHiWcIbOJOCX/Chx4IDaYmLwCRk\nEdSnFEb0BJnd9dpZ9sXA0OgShN45IKFg178BQWyouWmS8OKugDdfpwi+Hprt\njymO\r\n=wd14\r\n-----END PGP SIGNATURE-----\r\n", + }, + }, + }, +}; + +export const versionNext = { + _id: "@arkecosystem/core", + _rev: "124-6952dbe729e4e231817e92cb9871148f", + name: "@arkecosystem/core", + "dist-tags": { + latest: "2.5.24", + next: "2.5.0-next.10", + }, + versions: { + "2.5.0-next.10": { + name: "@arkecosystem/core", + version: "2.5.0-next.10", + description: "Core of the ARK Blockchain", + license: "MIT", + contributors: [ + { + name: "François-Xavier Thoorens", + email: "fx@ark.io", + }, + + { + name: "Kristjan Košič", + email: "kristjan@ark.io", + }, + + { + name: "Brian Faust", + email: "brian@ark.io", + }, + + { + name: "Alex Barnsley", + email: "alex@ark.io", + }, + ], + main: "dist/index", + types: "dist/index", + bin: { + ark: "./bin/run", + }, + scripts: { + ark: "./bin/run", + build: "yarn clean && yarn compile && yarn copy", + "build:watch": "yarn clean && yarn copy && yarn compile -w", + clean: "del dist", + compile: "../../node_modules/typescript/bin/tsc", + copy: "cd ./src && cpy './config' '../dist/' --parents && cd ..", + "debug:forger": "node --inspect-brk yarn ark forger:run", + "debug:relay": "node --inspect-brk yarn ark relay:run", + "debug:start": "node --inspect-brk yarn ark core:run", + "forger:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark forger:run", + "forger:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark forger:run", + "forger:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark forger:run --env=test", + "full:testnet": + "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --networkStart --env=test", + prepack: "../../node_modules/.bin/oclif-dev manifest && npm shrinkwrap", + postpack: "rm -f oclif.manifest.json", + prepublishOnly: "yarn build", + "relay:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark relay:run", + "relay:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark relay:run", + "relay:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark relay:run --env=test", + "start:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark core:run", + "start:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark core:run", + "start:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --env=test", + }, + dependencies: { + "@arkecosystem/core-api": "^2.5.0-next.10", + "@arkecosystem/core-blockchain": "^2.5.0-next.10", + "@arkecosystem/core-container": "^2.5.0-next.10", + "@arkecosystem/core-database-postgres": "^2.5.0-next.10", + "@arkecosystem/core-event-emitter": "^2.5.0-next.10", + "@arkecosystem/core-exchange-json-rpc": "^2.5.0-next.10", + "@arkecosystem/core-forger": "^2.5.0-next.10", + "@arkecosystem/core-logger-pino": "^2.5.0-next.10", + "@arkecosystem/core-p2p": "^2.5.0-next.10", + "@arkecosystem/core-snapshots": "^2.5.0-next.10", + "@arkecosystem/core-state": "^2.5.0-next.10", + "@arkecosystem/core-transaction-pool": "^2.5.0-next.10", + "@arkecosystem/core-utils": "^2.5.0-next.10", + "@arkecosystem/core-wallet-api": "^2.5.0-next.10", + "@arkecosystem/core-webhooks": "^2.5.0-next.10", + "@arkecosystem/crypto": "^2.5.0-next.10", + "@oclif/command": "^1.5.16", + "@oclif/config": "^1.13.2", + "@oclif/plugin-autocomplete": "^0.1.1", + "@oclif/plugin-commands": "^1.2.2", + "@oclif/plugin-help": "^2.2.0", + "@oclif/plugin-not-found": "^1.2.2", + "@oclif/plugin-plugins": "^1.7.8", + "@typeskrift/foreman": "^0.2.1", + bip39: "^3.0.2", + bytebuffer: "^5.0.1", + chalk: "^2.4.2", + clear: "^0.1.0", + "cli-progress": "^2.1.1", + "cli-table3": "^0.5.1", + "cli-ux": "^5.3.1", + dayjs: "^1.8.15", + "env-paths": "^2.2.0", + envfile: "^3.0.0", + execa: "^2.0.3", + "fast-levenshtein": "^2.0.6", + "fs-extra": "^8.1.0", + "latest-version": "^5.1.0", + listr: "^0.14.3", + "lodash.minby": "^4.6.0", + "nodejs-tail": "^1.1.0", + "pretty-bytes": "^5.2.0", + "pretty-ms": "^5.0.0", + prompts: "^2.1.0", + "read-last-lines": "^1.7.1", + semver: "^6.2.0", + wif: "^2.0.6", + }, + devDependencies: { + "@types/bip39": "^2.4.2", + "@types/bytebuffer": "^5.0.40", + "@types/cli-progress": "^1.8.1", + "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "^8.0.0", + "@types/got": "^9.6.1", + "@types/listr": "^0.14.0", + "@types/lodash.minby": "^4.6.6", + "@types/pretty-ms": "^4.0.0", + "@types/prompts": "^2.4.0", + "@types/semver": "^6.0.1", + "@types/wif": "^2.0.1", + }, + engines: { + node: ">=10.x", + }, + publishConfig: { + access: "public", + }, + oclif: { + commands: "./dist/commands", + hooks: { + init: ["./dist/hooks/init/config", "./dist/hooks/init/update"], + command_not_found: ["./dist/hooks/command_not_found/suggest"], + }, + bin: "ark", + topics: { + config: { + description: "manage core config variables", + }, + env: { + description: "manage core environment variables", + }, + core: { + description: "manage a core instance (relay & forger)", + }, + forger: { + description: "manage a forger instance", + }, + relay: { + description: "manage a relay instance", + }, + snapshot: { + description: "manage a relay snapshots", + }, + chain: { + description: "commands to interact with the blockchain", + }, + network: { + description: "manage network configurations and tasks", + }, + }, + plugins: ["@oclif/plugin-autocomplete", "@oclif/plugin-commands", "@oclif/plugin-help"], + }, + readme: + '# ARK Core - Core\n\n

\n \n

\n\n## Documentation\n\nYou can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://docs.ark.io/guidebook/core/plugins/required/core.html).\n\n## Security\n\nIf you discover a security vulnerability within this package, please send an e-mail to security@ark.io. All security vulnerabilities will be promptly addressed.\n\n## Credits\n\nThis project exists thanks to all the people who [contribute](../../../../contributors).\n\n## License\n\n[MIT](LICENSE) © [ARK Ecosystem](https://ark.io)\n', + readmeFilename: "README.md", + _id: "@arkecosystem/core@2.5.0-next.10", + _nodeVersion: "10.16.0", + _npmVersion: "6.9.0", + dist: { + integrity: + "sha512-J7hNdeVVDj6QKlFwAtyVathZCGGvwGREDBfVr7zS9h7NG1RJUMRv+VDOQpRKAil6X7DHBFN3KgNqovAuUzoudQ==", + shasum: "fb977ee50721ee854e672f7de1410e479e5fcbc9", + tarball: "https://registry.npmjs.org/@arkecosystem/core/-/core-2.5.0-next.10.tgz", + fileCount: 192, + unpackedSize: 945581, + "npm-signature": + "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJdQBH0CRA9TVsSAnZWagAAgawP/1fcDjs9OYf6eBYIOHxi\nq8yh5fRdqxBDOC1h30ZSI+b6ovJ3FNLev3IWZWoK74IZZtZv0uIYpjSphWGM\nNRHXokau3jiATmQ1myKAm2yzkwmPdL31vqvVGHm9d0nK2HBUsDOzlL3ZomKs\ndmboDImAJ6WYMkp79R/J2hPryIjFhMDZMrxse8q1Hcfs4usacdZFA3oxhNFV\nz+kr5TIRdW1JsiiKTXSwq5P8f+NXoyLgnwEkLfC4yPIY6uWtDwko+NEHLDb/\n88r0tRr1wzSHJbyZtDm/i+0++ZAwrAfLIPlZu6VjPLH7xqEDfGoQC/dEdYG1\n2uOWcFgD3U/WyAXEbbarCgBdUJ3+xXbA01Nx76+QonT7S5rL1bt1y5Fwubmy\nf58aJnjrtQxk+vhUSHEltoT154m1olEidHjjDbanMwsVOWFOqa/u2fEHIUVC\nDl0yYnEJXuUUF0pPkqPwz5wrdNEqp6/kHOe8xaoAzQJF3PXBnmUZO4ZgvccB\nGVPlpu/lsPpeHK2WiE6rsO83ntpynmppnmKC1gw/0VRYkVweWClAKKEALcYa\nBuFjA/+QKpHB7Wem/YdZv0TwC0lVMV+TRU+HUZzcIX+juy7GdbcDULAowYti\nYMjXGnxPmvZePSrFD2/QVNZ6uYBJ+kmoMTAYNNvCCcFuDA5HlsVBc3OMTysG\nK/KF\r\n=pjde\r\n-----END PGP SIGNATURE-----\r\n", + }, + maintainers: [ + { + email: "alex@barnsleyservices.com", + name: "alexbarnsley", + }, + + { + email: "fx.thoorens@ark.io", + name: "arkio", + }, + + { + email: "hello@basecode.sh", + name: "faustbrian", + }, + + { + email: "dev@jaml.pro", + name: "j-a-m-l", + }, + + { + email: "kristjan@ark.io", + name: "kristjankosic", + }, + + { + email: "luciorubeens@gmail.com", + name: "luciorubeens", + }, + + { + email: "joshua@ark.io", + name: "supaiku", + }, + ], + _npmUser: { + name: "faustbrian", + email: "hello@basecode.sh", + }, + directories: {}, + _npmOperationalInternal: { + host: "s3://npm-registry-packages", + tmp: "tmp/core_2.5.0-next.10_1564479986828_0.13168983569100656", + }, + _hasShrinkwrap: false, + }, + }, +}; diff --git a/__tests__/unit/core/common/builder.test.ts b/__tests__/unit/core-cli/utils/builder.test.ts similarity index 75% rename from __tests__/unit/core/common/builder.test.ts rename to __tests__/unit/core-cli/utils/builder.test.ts index af24469682..62847adb1c 100644 --- a/__tests__/unit/core/common/builder.test.ts +++ b/__tests__/unit/core-cli/utils/builder.test.ts @@ -1,6 +1,6 @@ -import { buildPeerOptions } from "@packages/core/src/common/builder"; +import { buildPeerFlags } from "@packages/core-cli/src/utils/builder"; -describe("buildPeerOptions", () => { +describe("buildPeerFlags", () => { it("should build the configuration object", () => { const flags = { networkStart: "networkStart", @@ -9,7 +9,7 @@ describe("buildPeerOptions", () => { ignoreMinimumNetworkReach: "ignoreMinimumNetworkReach", }; - expect(buildPeerOptions(flags)).toEqual(flags); + expect(buildPeerFlags(flags)).toEqual(flags); }); it("should handle seed mode", () => { @@ -20,7 +20,7 @@ describe("buildPeerOptions", () => { ignoreMinimumNetworkReach: "ignoreMinimumNetworkReach", }; - expect(buildPeerOptions({ ...flags, ...{ launchMode: "seed" } })).toEqual({ + expect(buildPeerFlags({ ...flags, ...{ launchMode: "seed" } })).toEqual({ ...flags, ...{ skipDiscovery: true, diff --git a/__tests__/unit/core/common/flags.test.ts b/__tests__/unit/core-cli/utils/flags.test.ts similarity index 75% rename from __tests__/unit/core/common/flags.test.ts rename to __tests__/unit/core-cli/utils/flags.test.ts index c65f33eaa9..af80222cd5 100644 --- a/__tests__/unit/core/common/flags.test.ts +++ b/__tests__/unit/core-cli/utils/flags.test.ts @@ -1,9 +1,9 @@ -import { flagsToStrings } from "@packages/core/src/common/flags"; +import { castFlagsToString } from "@packages/core-cli/src/utils/flags"; -describe("flagsToStrings", () => { +describe("castFlagsToString", () => { it("should handle strings", () => { expect( - flagsToStrings({ + castFlagsToString({ key: "value", }), ).toEqual("--key=value"); @@ -11,7 +11,7 @@ describe("flagsToStrings", () => { it("should handle strings with spaces", () => { expect( - flagsToStrings({ + castFlagsToString({ key: "hello world", }), ).toEqual('--key="hello world"'); @@ -19,7 +19,7 @@ describe("flagsToStrings", () => { it("should handle integers", () => { expect( - flagsToStrings({ + castFlagsToString({ key: 1, }), ).toEqual("--key=1"); @@ -27,7 +27,7 @@ describe("flagsToStrings", () => { it("should handle booleans", () => { expect( - flagsToStrings({ + castFlagsToString({ key: true, }), ).toEqual("--key"); @@ -35,7 +35,7 @@ describe("flagsToStrings", () => { it("should ignore keys", () => { expect( - flagsToStrings( + castFlagsToString( { key: "value", ignore: "value", diff --git a/__tests__/unit/core-cli/utils/process.test.ts b/__tests__/unit/core-cli/utils/process.test.ts new file mode 100644 index 0000000000..e8aeb2b536 --- /dev/null +++ b/__tests__/unit/core-cli/utils/process.test.ts @@ -0,0 +1,228 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { fileSync, setGracefulCleanup } from "tmp"; + +import { Process } from "@packages/core-cli/src/utils"; + +jest.mock("nodejs-tail"); + +let cli; +let proc; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); + + cli.app + .rebind(Container.Identifiers.ProcessFactory) + .toFactory((context: Container.interfaces.Context) => (token: string, type: string): Process => { + const process: Process = context.container.resolve(Process); + process.initialize(token, type); + + return process; + }); + + proc = cli.app.get(Container.Identifiers.ProcessFactory)("ark", "core"); +}); + +afterEach(() => jest.restoreAllMocks()); + +afterAll(() => setGracefulCleanup()); + +describe("Process", () => { + describe("#stop", () => { + it("should throw if the process does not exist", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); + const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + + expect(() => proc.stop()).toThrowError('The "ark-core" process does not exist.'); + + missing.mockReset(); + isUnknown.mockReset(); + isStopped.mockReset(); + }); + + it("should throw if the process entered an unknown state", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + + expect(() => proc.stop()).toThrowError('The "ark-core" process has entered an unknown state.'); + + missing.mockReset(); + isUnknown.mockReset(); + isStopped.mockReset(); + }); + + it("should throw if the process is stopped", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); + + expect(() => proc.stop()).toThrowError('The "ark-core" process is not running.'); + + missing.mockReset(); + isUnknown.mockReset(); + isStopped.mockReset(); + }); + + it("should stop the process if the [--daemon] flag is not present", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + const deleteSpy = jest.spyOn(processManager, "delete").mockImplementation(); + + proc.stop(true); + + expect(deleteSpy).toHaveBeenCalled(); + + missing.mockReset(); + isUnknown.mockReset(); + isStopped.mockReset(); + deleteSpy.mockReset(); + }); + + it("should delete the process if the [--daemon] flag is present", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + const stop = jest.spyOn(processManager, "stop").mockImplementation(); + + proc.stop(); + + expect(stop).toHaveBeenCalled(); + + missing.mockReset(); + isUnknown.mockReset(); + isStopped.mockReset(); + stop.mockReset(); + }); + }); + + describe("#restart", () => { + it("should throw if the process does not exist", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + + expect(() => proc.restart()).toThrowError('The "ark-core" process does not exist.'); + + missing.mockReset(); + isStopped.mockReset(); + }); + + it("should throw if the process is stopped", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); + + expect(() => proc.restart()).toThrowError('The "ark-core" process is not running.'); + + missing.mockReset(); + isStopped.mockReset(); + }); + + it("should restart the process", () => { + const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); + const restart = jest.spyOn(processManager, "restart").mockImplementation(); + + proc.restart(); + + expect(restart).toHaveBeenCalled(); + + missing.mockReset(); + isStopped.mockReset(); + restart.mockReset(); + }); + }); + + describe("#status", () => { + it("should throw if the process does not exist", async () => { + expect(() => proc.status()).toThrow('The "ark-core" process does not exist.'); + }); + + it("should render a table with the process information", async () => { + jest.spyOn(processManager, "missing").mockReturnValue(false); + jest.spyOn(processManager, "describe").mockReturnValue({ + pid: 1, + name: "ark-core", + pm2_env: { + version: "1.0.0", + status: "online", + pm_uptime: 1387045673686, + }, + monit: { cpu: 2, memory: 2048 }, + }); + + let message: string; + jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); + + proc.status(); + + expect(message).toIncludeMultiple(["ID", "Name", "Version", "Status", "Uptime", "CPU", "RAM"]); + expect(message).toIncludeMultiple([ + "1", + "ark-core", + "1.0.0", + "online", + // "5y 267d 19h 31m 28.1s", + "2%", + "2.05 kB", + ]); + }); + }); + + describe("#log", () => { + it("should throw if the process does not exist", async () => { + await expect(proc.log()).rejects.toThrow('The "ark-core" process does not exist.'); + }); + + it("should log to pm_out_log_path", async () => { + jest.spyOn(cli.app.get(Container.Identifiers.AbortMissingProcess), "execute").mockImplementation(); + jest.spyOn(processManager, "describe").mockReturnValue({ + pid: 1, + name: "ark-core", + pm2_env: { + version: "1.0.0", + status: "online", + pm_uptime: 1387045673686, + pm_err_log_path: fileSync().name, + pm_out_log_path: fileSync().name, + }, + monit: { cpu: 2, memory: 2048 }, + }); + + const spyLog = jest.spyOn(console, "log"); + + await proc.log(false, 15); + + expect(spyLog).toHaveBeenCalledWith( + "Tailing last 15 lines for [ark-core] process (change the value with --lines option)", + ); + }); + + it("should log to pm_err_log_path", async () => { + jest.spyOn(cli.app.get(Container.Identifiers.AbortMissingProcess), "execute").mockImplementation(); + jest.spyOn(processManager, "describe").mockReturnValue({ + pid: 1, + name: "ark-core", + pm2_env: { + version: "1.0.0", + status: "online", + pm_uptime: 1387045673686, + pm_err_log_path: fileSync().name, + pm_out_log_path: fileSync().name, + }, + monit: { cpu: 2, memory: 2048 }, + }); + + const spyLog = jest.spyOn(console, "log"); + + await proc.log(true, 15); + + expect(spyLog).toHaveBeenCalledWith( + "Tailing last 15 lines for [ark-core] process (change the value with --lines option)", + ); + }); + }); +}); diff --git a/__tests__/unit/core/common/update.test.ts b/__tests__/unit/core-cli/utils/update.ts similarity index 100% rename from __tests__/unit/core/common/update.test.ts rename to __tests__/unit/core-cli/utils/update.ts diff --git a/__tests__/unit/core/common/update/check-for-updates.test.ts b/__tests__/unit/core-cli/utils/update/check-for-updates.ts similarity index 100% rename from __tests__/unit/core/common/update/check-for-updates.test.ts rename to __tests__/unit/core-cli/utils/update/check-for-updates.ts diff --git a/__tests__/unit/core/__support__/app.ts b/__tests__/unit/core/__support__/app.ts new file mode 100644 index 0000000000..38f02e7c5f --- /dev/null +++ b/__tests__/unit/core/__support__/app.ts @@ -0,0 +1,18 @@ +import { ApplicationFactory, Commands, Container, Services } from "@arkecosystem/core-cli"; + +export const executeCommand = async (command): Promise => { + const app = ApplicationFactory.make(new Container.Container(), { + name: "@arkecosystem/core", + version: "3.0.0-next.0", + }); + + app.rebind(Container.Identifiers.ApplicationPaths).toConstantValue( + app.get(Container.Identifiers.Environment).getPaths("ark", "testnet"), + ); + + const cmd = app.resolve(command); + + cmd.register(["--token=ark", "--network=testnet"]); + + await cmd.run(); +}; diff --git a/__tests__/unit/core/commands/config-cli.test.ts b/__tests__/unit/core/commands/config-cli.test.ts new file mode 100644 index 0000000000..cb76fabe4c --- /dev/null +++ b/__tests__/unit/core/commands/config-cli.test.ts @@ -0,0 +1,67 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/config-cli"; + +import execa from "../../../../__mocks__/execa"; + +let cli; +let config; + +beforeEach(() => { + cli = new Console(); + config = cli.app.get(Container.Identifiers.Config); +}); + +afterEach(() => jest.resetAllMocks()); + +describe("Command", () => { + it("should change the token", async () => { + await cli.execute(Command); + + expect(config.get("token")).toBe("ark"); + + await cli.withFlags({ token: "btc" }).execute(Command); + + expect(config.get("token")).toBe("btc"); + }); + + it("should change the channel and install the new version", async () => { + jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + + const installFromChannel: jest.SpyInstance = jest + .spyOn(cli.app.get(Container.Identifiers.Installer), "installFromChannel") + .mockImplementation(); + + await cli.withFlags({ channel: "latest" }).execute(Command); + + expect(config.get("channel")).toBe("latest"); + expect(installFromChannel).toHaveBeenCalledWith("@arkecosystem/core", "latest"); + + await cli.withFlags({ channel: "next" }).execute(Command); + + expect(config.get("channel")).toBe("next"); + expect(installFromChannel).toHaveBeenCalledWith("@arkecosystem/core", "next"); + + await cli.withFlags({ channel: "latest" }).execute(Command); + + expect(config.get("channel")).toBe("latest"); + expect(installFromChannel).toHaveBeenCalledWith("@arkecosystem/core", "latest"); + }); + + it("should fail to change the channel if the new and old are the same", async () => { + jest.spyOn(execa, "sync").mockReturnValue({ + stdout: "stdout", + stderr: undefined, + }); + + await cli.withFlags({ channel: "latest" }).execute(Command); + + await expect(cli.withFlags({ channel: "latest" }).execute(Command)).rejects.toThrow( + 'You are already on the "latest" channel.', + ); + }); +}); diff --git a/__tests__/unit/core/commands/config/database.test.ts b/__tests__/unit/core/commands/config-database.test.ts similarity index 86% rename from __tests__/unit/core/commands/config/database.test.ts rename to __tests__/unit/core/commands/config-database.test.ts index 6565a9117a..eb0135033f 100644 --- a/__tests__/unit/core/commands/config/database.test.ts +++ b/__tests__/unit/core/commands/config-database.test.ts @@ -1,23 +1,28 @@ +import { Console } from "@arkecosystem/core-test-framework"; import envfile from "envfile"; import prompts from "prompts"; import { dirSync, setGracefulCleanup } from "tmp"; -import { DatabaseCommand } from "@packages/core/src/commands/config/database"; +import { Command } from "@packages/core/src/commands/config-database"; import fs from "fs-extra"; +let cli; let envFile: string; -describe("DatabaseCommand", () => { - beforeEach(() => { - process.env.CORE_PATH_CONFIG = dirSync().name; - envFile = `${process.env.CORE_PATH_CONFIG}/.env`; - }); +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; - afterAll(() => setGracefulCleanup()); + envFile = `${process.env.CORE_PATH_CONFIG}/.env`; + + cli = new Console(); +}); +afterAll(() => setGracefulCleanup()); + +describe("DatabaseCommand", () => { describe("Flags", () => { it("should throw if the .env file does not exist", async () => { - await expect(DatabaseCommand.run(["--token=ark", "--network=mainnet", "--host=localhost"])).rejects.toThrow( + await expect(cli.withFlags({ host: "localhost" }).execute(Command)).rejects.toThrow( `No environment file found at ${process.env.CORE_PATH_CONFIG}/.env.`, ); }); @@ -29,7 +34,7 @@ describe("DatabaseCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); // Act - await DatabaseCommand.run(["--token=ark", "--network=mainnet", "--host=localhost"]); + await cli.withFlags({ host: "localhost" }).execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -49,7 +54,7 @@ describe("DatabaseCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); // Act - await DatabaseCommand.run(["--token=ark", "--network=mainnet", "--port=5432"]); + await cli.withFlags({ port: "5432" }).execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -69,7 +74,7 @@ describe("DatabaseCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); // Act - await DatabaseCommand.run(["--token=ark", "--network=mainnet", "--database=ark_mainnet"]); + await cli.withFlags({ database: "ark_mainnet" }).execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -89,7 +94,7 @@ describe("DatabaseCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); // Act - await DatabaseCommand.run(["--token=ark", "--network=mainnet", "--username=ark"]); + await cli.withFlags({ username: "ark" }).execute(Command, { flags: { username: "ark" } }); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -109,7 +114,7 @@ describe("DatabaseCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); // Act - await DatabaseCommand.run(["--token=ark", "--network=mainnet", "--password=password"]); + await cli.withFlags({ password: "password" }).execute(Command, { flags: { password: "password" } }); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -125,7 +130,7 @@ describe("DatabaseCommand", () => { describe("Prompts", () => { it("should throw if the .env file does not exist", async () => { - await expect(DatabaseCommand.run(["--token=ark", "--network=mainnet", "--host=localhost"])).rejects.toThrow( + await expect(cli.withFlags({ host: "localhost" }).execute(Command)).rejects.toThrow( `No environment file found at ${process.env.CORE_PATH_CONFIG}/.env.`, ); }); @@ -139,7 +144,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject(["localhost", undefined, undefined, undefined, undefined, true]); - await DatabaseCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -161,7 +166,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject([undefined, 5432, undefined, undefined, undefined, true]); - await DatabaseCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -183,7 +188,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject([undefined, undefined, "ark_mainnet", undefined, undefined, true]); - await DatabaseCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -205,7 +210,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject([undefined, undefined, undefined, "ark", undefined, true]); - await DatabaseCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -227,7 +232,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject([undefined, undefined, undefined, undefined, "password", true]); - await DatabaseCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); // Assert expect(existsSync).toHaveBeenCalledWith(envFile); @@ -249,9 +254,7 @@ describe("DatabaseCommand", () => { // Act prompts.inject([undefined, undefined, undefined, undefined, "password", false]); - await expect(DatabaseCommand.run(["--token=ark", "--network=mainnet"])).rejects.toThrow( - "You'll need to confirm the input to continue.", - ); + await expect(cli.execute(Command)).rejects.toThrow("You'll need to confirm the input to continue."); // Assert expect(existsSync).not.toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/config/forger/bip38.test.ts b/__tests__/unit/core/commands/config-forger-bip38.test.ts similarity index 54% rename from __tests__/unit/core/commands/config/forger/bip38.test.ts rename to __tests__/unit/core/commands/config-forger-bip38.test.ts index 2af39adbec..e9a33386f5 100644 --- a/__tests__/unit/core/commands/config/forger/bip38.test.ts +++ b/__tests__/unit/core/commands/config-forger-bip38.test.ts @@ -1,31 +1,32 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; +import { writeJSONSync } from "fs-extra"; import prompts from "prompts"; import { dirSync, setGracefulCleanup } from "tmp"; -import { BIP38Command } from "@packages/core/src/commands/config/forger/bip38"; -import { configManager } from "@packages/core/src/common/config-manager"; -import { writeJSONSync } from "fs-extra"; +import { Command } from "@packages/core/src/commands/config-forger-bip38"; const bip39: string = "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire"; const bip39Flags: string = "venue below waste gather spin cruise title still boost mother flash tuna"; const bip39Prompt: string = "craft imitate step mixture patch forest volcano business charge around girl confirm"; const password: string = "password"; -describe("BIP38Command", () => { - beforeEach(() => { - process.env.CORE_PATH_CONFIG = dirSync().name; +let cli; +beforeAll(() => setGracefulCleanup()); - writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, {}); +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; - configManager.initialize({ - configDir: process.env.CORE_PATH_CONFIG, - version: "3.0.0-next.0", - }); - }); + writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, {}); + + cli = new Console(); +}); - afterAll(() => setGracefulCleanup()); +afterEach(() => jest.resetAllMocks()); +describe("BIP38Command", () => { it("should configure from flags", async () => { - await BIP38Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39Flags}`, `--password=${password}`]); + await cli.withFlags({ bip39: bip39Flags, password }).execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ bip38: "6PYSTpJLxqjj8CFJSY5LSPVeyB52U9dqqZCL7DBJe7n5LUWZZfUJktGy31", @@ -36,7 +37,7 @@ describe("BIP38Command", () => { it("should configure from a prompt if it receives a valid bip39, password and confirmation", async () => { prompts.inject([bip39Prompt, password, true]); - await BIP38Command.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ bip38: "6PYVDkKQRjbiWGHwwkXL4dpfCx2AuvCnjqyoMQs83NVNJ27MGUKqYMoMGG", @@ -45,16 +46,21 @@ describe("BIP38Command", () => { }); it("should fail to configure from a prompt if it receives a valid bip39 and password but no confirmation", async () => { - await BIP38Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39}`, `--password=${password}`]); + await cli.withFlags({ bip39, password }).execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ bip38: "6PYTQC4c3Te5FCbnU5Z59uZCav121nypLmxanYn21ZoNTdc81eB9wTqeTe", secrets: [], }); - prompts.inject([bip39Prompt, password, false]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + bip39: bip39Prompt, + password, + confirm: false, + }); - await BIP38Command.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ bip38: "6PYTQC4c3Te5FCbnU5Z59uZCav121nypLmxanYn21ZoNTdc81eB9wTqeTe", @@ -63,33 +69,42 @@ describe("BIP38Command", () => { }); it("should fail to configure from a prompt if it receives an invalid bip39", async () => { - await BIP38Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39}`, `--password=${password}`]); + await cli.withFlags({ bip39, password }).execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ bip38: "6PYTQC4c3Te5FCbnU5Z59uZCav121nypLmxanYn21ZoNTdc81eB9wTqeTe", secrets: [], }); - prompts.inject(["random-string", password, true]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + bip39: "random-string", + password, + confirm: true, + }); - await expect(BIP38Command.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "Failed to verify the given passphrase as BIP39 compliant.", - ); + await expect(cli.execute(Command)).rejects.toThrow("Failed to verify the given passphrase as BIP39 compliant."); }); it("should fail to configure from a prompt if it doesn't receive a bip39", async () => { - prompts.inject([null, password, true]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + bip39: null, + password, + confirm: true, + }); - await expect(BIP38Command.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "Failed to verify the given passphrase as BIP39 compliant.", - ); + await expect(cli.execute(Command)).rejects.toThrow("Failed to verify the given passphrase as BIP39 compliant."); }); it("should fail to configure from a prompt if it doesn't receive a password", async () => { - prompts.inject([bip39, null, true]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + bip39, + password: null, + confirm: true, + }); - await expect(BIP38Command.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The BIP38 password has to be a string.", - ); + await expect(cli.execute(Command)).rejects.toThrow("The BIP38 password has to be a string."); }); }); diff --git a/__tests__/unit/core/commands/config/forger/bip39.test.ts b/__tests__/unit/core/commands/config-forger-bip39.test.ts similarity index 55% rename from __tests__/unit/core/commands/config/forger/bip39.test.ts rename to __tests__/unit/core/commands/config-forger-bip39.test.ts index a55e53430e..29b76129ae 100644 --- a/__tests__/unit/core/commands/config/forger/bip39.test.ts +++ b/__tests__/unit/core/commands/config-forger-bip39.test.ts @@ -1,30 +1,28 @@ +import { Console } from "@arkecosystem/core-test-framework"; import prompts from "prompts"; import { dirSync, setGracefulCleanup } from "tmp"; - -import { BIP39Command } from "@packages/core/src/commands/config/forger/bip39"; -import { configManager } from "@packages/core/src/common/config-manager"; import { writeJSONSync } from "fs-extra"; +import { Command } from "@packages/core/src/commands/config-forger-bip39"; + const bip39: string = "clay harbor enemy utility margin pretty hub comic piece aerobic umbrella acquire"; const bip39Flags: string = "venue below waste gather spin cruise title still boost mother flash tuna"; const bip39Prompt: string = "craft imitate step mixture patch forest volcano business charge around girl confirm"; -describe("BIP39Command", () => { - beforeEach(() => { - process.env.CORE_PATH_CONFIG = dirSync().name; +let cli; +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; - writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, {}); + writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, {}); - configManager.initialize({ - configDir: process.env.CORE_PATH_CONFIG, - version: "3.0.0-next.0", - }); - }); + cli = new Console(); +}); - afterAll(() => setGracefulCleanup()); +afterAll(() => setGracefulCleanup()); +describe("BIP39Command", () => { it("should configure from flags", async () => { - await BIP39Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39Flags}`]); + await cli.withFlags({ bip39: bip39Flags }).execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ secrets: [bip39Flags] }); }); @@ -32,29 +30,27 @@ describe("BIP39Command", () => { it("should configure from a prompt if it receives a valid bip39 and confirmation", async () => { prompts.inject([bip39Prompt, true]); - await BIP39Command.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ secrets: [bip39Prompt] }); }); it("should fail to configure from a prompt if it receives a valid bip39 and but no confirmation", async () => { - await BIP39Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39}`]); + await cli.withFlags({ bip39 }).execute(Command); prompts.inject([bip39Prompt, false]); - await BIP39Command.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ secrets: [bip39] }); }); it("should fail to configure from a prompt if it receives an invalid bip39", async () => { - await BIP39Command.run(["--token=ark", "--network=testnet", `--bip39=${bip39}`]); + await cli.withFlags({ bip39 }).execute(Command); prompts.inject(["random-string", true]); - await expect(BIP39Command.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "Failed to verify the given passphrase as BIP39 compliant.", - ); + await expect(cli.execute(Command)).rejects.toThrow("Failed to verify the given passphrase as BIP39 compliant."); expect(require(`${process.env.CORE_PATH_CONFIG}/delegates.json`)).toEqual({ secrets: [bip39] }); }); @@ -62,8 +58,6 @@ describe("BIP39Command", () => { it("should fail to configure from a prompt if it doesn't receive a bip39", async () => { prompts.inject([null]); - await expect(BIP39Command.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "Failed to verify the given passphrase as BIP39 compliant.", - ); + await expect(cli.execute(Command)).rejects.toThrow("Failed to verify the given passphrase as BIP39 compliant."); }); }); diff --git a/__tests__/unit/core/commands/config/publish.test.ts b/__tests__/unit/core/commands/config-publish.test.ts similarity index 52% rename from __tests__/unit/core/commands/config/publish.test.ts rename to __tests__/unit/core/commands/config-publish.test.ts index 5dc6e4b52a..1f79998b3e 100644 --- a/__tests__/unit/core/commands/config/publish.test.ts +++ b/__tests__/unit/core/commands/config-publish.test.ts @@ -1,67 +1,61 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import { dirSync, setGracefulCleanup } from "tmp"; - -import { PublishCommand } from "@packages/core/src/commands/config/publish"; import fs from "fs-extra"; -import prompts from "prompts"; -jest.mock("fs-extra"); +import { Command } from "@packages/core/src/commands/config-publish"; -describe("PublishCommand", () => { - beforeEach(() => (process.env.CORE_PATH_CONFIG = dirSync().name)); +// jest.mock("fs-extra"); + +let cli; +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + cli = new Console(); +}); - afterAll(() => setGracefulCleanup()); +afterEach(() => jest.resetAllMocks()); +afterAll(() => setGracefulCleanup()); + +describe("PublishCommand", () => { it("should throw if the network is invalid", async () => { - await expect(PublishCommand.run(["--token=ark", "--network=invalid"])).rejects.toThrow( - /Expected --network=invalid to be one of: devnet, mainnet, testnet/, + await expect(cli.withFlags({ network: "invalid" }).execute(Command)).rejects.toThrow( + '[ERROR] "network" must be one of [devnet, mainnet, testnet]', ); }); it("should throw if the destination already exists", async () => { - const spyExists = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); + jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - await expect(PublishCommand.run(["--token=ark", "--network=mainnet"])).rejects.toThrow( - /Please use the --reset flag if you wish to reset your configuration./, + await expect(cli.execute(Command)).rejects.toThrow( + "[ERROR] Please use the --reset flag if you wish to reset your configuration.", ); - - spyExists.mockClear(); }); it("should throw if the configuration files cannot be found", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(false); - await expect(PublishCommand.run(["--token=ark", "--network=mainnet"])).rejects.toThrow( - /Couldn't find the core configuration files/, - ); - - spyExists.mockClear(); + await expect(cli.execute(Command)).rejects.toThrow("Couldn't find the core configuration files"); }); it("should throw if the environment file cannot be found", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(false); const spyEnsure = jest.spyOn(fs, "ensureDirSync"); - await expect(PublishCommand.run(["--token=ark", "--network=mainnet"])).rejects.toThrow( - /Couldn't find the environment file/, - ); + await expect(cli.execute(Command)).rejects.toThrow("Couldn't find the environment file"); expect(spyEnsure).toHaveBeenCalled(); - - spyExists.mockClear(); - spyEnsure.mockClear(); }); it("should publish the configuration", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); @@ -69,19 +63,14 @@ describe("PublishCommand", () => { const spyEnsure = jest.spyOn(fs, "ensureDirSync"); const spyCopy = jest.spyOn(fs, "copySync"); - await PublishCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); expect(spyEnsure).toHaveBeenCalled(); expect(spyCopy).toHaveBeenCalledTimes(2); - - spyExists.mockClear(); - spyEnsure.mockClear(); - spyCopy.mockClear(); }); it("should reset the configuration", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); @@ -90,21 +79,15 @@ describe("PublishCommand", () => { const spyEnsure = jest.spyOn(fs, "ensureDirSync"); const spyCopy = jest.spyOn(fs, "copySync"); - await PublishCommand.run(["--token=ark", "--network=mainnet", "--reset"]); + await cli.withFlags({ reset: true }).execute(Command); expect(spyRemove).toHaveBeenCalled(); expect(spyEnsure).toHaveBeenCalled(); expect(spyCopy).toHaveBeenCalledTimes(2); - - spyExists.mockClear(); - spyRemove.mockClear(); - spyEnsure.mockClear(); - spyCopy.mockClear(); }); it("should publish the configuration via prompt", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); @@ -112,21 +95,20 @@ describe("PublishCommand", () => { const spyEnsure = jest.spyOn(fs, "ensureDirSync"); const spyCopy = jest.spyOn(fs, "copySync"); - prompts.inject(["mainnet", true]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + network: "mainnet", + confirm: true, + }); - await PublishCommand.run(["--token=ark"]); + await cli.execute(Command); expect(spyEnsure).toHaveBeenCalled(); expect(spyCopy).toHaveBeenCalledTimes(2); - - spyExists.mockClear(); - spyEnsure.mockClear(); - spyCopy.mockClear(); }); it("should throw if no network is selected via prompt", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); @@ -134,23 +116,22 @@ describe("PublishCommand", () => { const spyEnsure = jest.spyOn(fs, "ensureDirSync"); const spyCopy = jest.spyOn(fs, "copySync"); - prompts.inject([undefined, true]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + network: undefined, + confirm: true, + }); - await expect(PublishCommand.run(["--token=ark"])).rejects.toThrow( - "You'll need to select the network to continue.", + await expect(cli.withFlags({ network: undefined }).execute(Command)).rejects.toThrow( + "[ERROR] You'll need to select the network to continue.", ); expect(spyEnsure).not.toHaveBeenCalled(); expect(spyCopy).not.toHaveBeenCalled(); - - spyExists.mockClear(); - spyEnsure.mockClear(); - spyCopy.mockClear(); }); it("should throw if the selected network is invalid via prompt", async () => { - const spyExists = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true); @@ -158,17 +139,17 @@ describe("PublishCommand", () => { const spyEnsure = jest.spyOn(fs, "ensureDirSync"); const spyCopy = jest.spyOn(fs, "copySync"); - prompts.inject(["mainnet", false]); + jest.spyOn(cli.app.get(Container.Identifiers.Prompt), "render").mockReturnValue({ + // @ts-ignore + network: "mainnet", + confirm: false, + }); - await expect(PublishCommand.run(["--token=ark"])).rejects.toThrow( - "You'll need to confirm the network to continue.", + await expect(cli.withFlags({ network: undefined }).execute(Command)).rejects.toThrow( + "[ERROR] You'll need to confirm the network to continue.", ); expect(spyEnsure).not.toHaveBeenCalled(); expect(spyCopy).not.toHaveBeenCalled(); - - spyExists.mockClear(); - spyEnsure.mockClear(); - spyCopy.mockClear(); }); }); diff --git a/__tests__/unit/core/commands/config/cli.test.ts b/__tests__/unit/core/commands/config/cli.test.ts deleted file mode 100644 index da794e1880..0000000000 --- a/__tests__/unit/core/commands/config/cli.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { dirSync, setGracefulCleanup } from "tmp"; - -import { CommandLineInterfaceCommand } from "@packages/core/src/commands/config/cli"; -import { configManager } from "@packages/core/src/common/config-manager"; -import execa from "../../../../../__mocks__/execa"; - -describe("CommandLineInterfaceCommand", () => { - beforeEach(() => configManager.initialize({ configDir: dirSync().name, version: "3.0.0-next.0" })); - - afterAll(() => setGracefulCleanup()); - - it("should change the token", async () => { - await CommandLineInterfaceCommand.run(["--token=ark"]); - - expect(configManager.get("token")).toBe("ark"); - - await CommandLineInterfaceCommand.run(["--token=btc"]); - - expect(configManager.get("token")).toBe("btc"); - }); - - it("should change the channel and install the new version", async () => { - const sync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ - stdout: "stdout", - stderr: undefined, - }); - - await CommandLineInterfaceCommand.run(["--token=ark", "--channel=latest"]); - - expect(configManager.get("channel")).toBe("latest"); - - await CommandLineInterfaceCommand.run(["--token=ark", "--channel=next"]); - - expect(configManager.get("channel")).toBe("next"); - - await CommandLineInterfaceCommand.run(["--token=ark", "--channel=latest"]); - - expect(configManager.get("channel")).toBe("latest"); - - expect(sync).toHaveBeenCalledWith("yarn global add @arkecosystem/core@latest"); - expect(sync).toHaveBeenCalledWith("yarn global add @arkecosystem/core@next"); - expect(sync).toHaveBeenCalledWith("yarn global add @arkecosystem/core@latest"); - - sync.mockReset(); - }); - - it("should fail to change the channel if the new and old are the same", async () => { - const sync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ - stdout: "stdout", - stderr: undefined, - }); - - await CommandLineInterfaceCommand.run(["--token=ark", "--channel=latest"]); - - await expect(CommandLineInterfaceCommand.run(["--token=ark", "--channel=latest"])).rejects.toThrowError( - 'You are already on the "latest" channel.', - ); - - sync.mockReset(); - }); -}); diff --git a/__tests__/unit/core/commands/core-log.test.ts b/__tests__/unit/core/commands/core-log.test.ts new file mode 100644 index 0000000000..840f613700 --- /dev/null +++ b/__tests__/unit/core/commands/core-log.test.ts @@ -0,0 +1,12 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/core-log"; + +let cli; +beforeEach(() => (cli = new Console())); + +describe("LogCommand", () => { + it("should throw if the process does not exist", async () => { + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-core" process does not exist.'); + }); +}); diff --git a/__tests__/unit/core/commands/core/restart.test.ts b/__tests__/unit/core/commands/core-restart.test.ts similarity index 64% rename from __tests__/unit/core/commands/core/restart.test.ts rename to __tests__/unit/core/commands/core-restart.test.ts index 84543b3b9e..06be6e714d 100644 --- a/__tests__/unit/core/commands/core/restart.test.ts +++ b/__tests__/unit/core/commands/core-restart.test.ts @@ -1,15 +1,21 @@ -import { CLIError } from "@oclif/errors"; -import { RestartCommand } from "@packages/core/src/commands/core/restart"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/core-restart"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("RestartCommand", () => { it("should throw if the process does not exist", async () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-core" process does not exist.'); missing.mockReset(); isStopped.mockReset(); @@ -19,9 +25,7 @@ describe("RestartCommand", () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-core" process is not running.'); missing.mockReset(); isStopped.mockReset(); @@ -32,7 +36,7 @@ describe("RestartCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const restart = jest.spyOn(processManager, "restart").mockImplementation(); - await RestartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(restart).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/core-run.test.ts b/__tests__/unit/core/commands/core-run.test.ts new file mode 100644 index 0000000000..c1a8cbd122 --- /dev/null +++ b/__tests__/unit/core/commands/core-run.test.ts @@ -0,0 +1,41 @@ +import { writeJSONSync } from "fs-extra"; +import { dirSync, setGracefulCleanup } from "tmp"; + +import { Command } from "@packages/core/src/commands/core-run"; + +import { executeCommand } from "../__support__/app"; + +const app = { + bootstrap: jest.fn(), + boot: jest.fn(), +}; + +jest.mock("@arkecosystem/core-kernel", () => ({ + __esModule: true, + Application: jest.fn(() => app), + Container: { + Container: jest.fn(), + }, +})); + +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); +}); + +// something funky is going on here which results in the core-test-framework +// CLI helper being unable to be used because the core-kernel mock causes problems +describe("RunCommand", () => { + it("should throw if the process does not exist", async () => { + const spyBootstrap = jest.spyOn(app, "bootstrap").mockImplementation(undefined); + const spyBoot = jest.spyOn(app, "boot").mockImplementation(undefined); + + await executeCommand(Command); + + expect(spyBootstrap).toHaveBeenCalled(); + expect(spyBoot).toHaveBeenCalled(); + }); +}); diff --git a/__tests__/unit/core/commands/core/start.test.ts b/__tests__/unit/core/commands/core-start.test.ts similarity index 71% rename from __tests__/unit/core/commands/core/start.test.ts rename to __tests__/unit/core/commands/core-start.test.ts index cb7a8778a2..bbfa1adf26 100644 --- a/__tests__/unit/core/commands/core/start.test.ts +++ b/__tests__/unit/core/commands/core-start.test.ts @@ -1,14 +1,21 @@ -import { StartCommand } from "@packages/core/src/commands/core/start"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import { resolve } from "path"; import { dirSync, setGracefulCleanup } from "tmp"; import { writeJSONSync } from "fs-extra"; import os from "os"; +import { Command } from "@packages/core/src/commands/core-start"; + +let cli; +let processManager; beforeEach(() => { process.env.CORE_PATH_CONFIG = dirSync().name; writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); + + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); }); afterAll(() => setGracefulCleanup()); @@ -20,18 +27,18 @@ describe("StartCommand", () => { const spyStart = jest.spyOn(processManager, "start").mockImplementation(undefined); - await StartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(spyStart).toHaveBeenCalledWith( { - args: "core:run --token=ark --network=testnet --suffix=core --env=production", + args: "core:run --token=ark --network=testnet --v=0 --env=production", env: { CORE_ENV: "production", NODE_ENV: "production", }, name: "ark-core", node_args: undefined, - script: resolve(__dirname, "../../../../../packages/core/src/commands/core/start.ts"), + script: resolve(__dirname, "../../../../packages/core/bin/run"), }, { "kill-timeout": 30000, "max-restarts": 5, name: "ark-core" }, ); diff --git a/__tests__/unit/core/commands/core/status.test.ts b/__tests__/unit/core/commands/core-status.test.ts similarity index 68% rename from __tests__/unit/core/commands/core/status.test.ts rename to __tests__/unit/core/commands/core-status.test.ts index 4e6dd9dc88..ef0345f6aa 100644 --- a/__tests__/unit/core/commands/core/status.test.ts +++ b/__tests__/unit/core/commands/core-status.test.ts @@ -1,14 +1,20 @@ import "jest-extended"; -import { CLIError } from "@oclif/errors"; -import { StatusCommand } from "@packages/core/src/commands/core/status"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/core-status"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StatusCommand", () => { it("should throw if the process does not exist", async () => { - await expect(StatusCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-core" process does not exist.'); }); it("should render a table with the process information", async () => { @@ -27,7 +33,7 @@ describe("StatusCommand", () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await StatusCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(message).toIncludeMultiple(["ID", "Name", "Version", "Status", "Uptime", "CPU", "RAM"]); expect(message).toIncludeMultiple([ diff --git a/__tests__/unit/core/commands/core/stop.test.ts b/__tests__/unit/core/commands/core-stop.test.ts similarity index 74% rename from __tests__/unit/core/commands/core/stop.test.ts rename to __tests__/unit/core/commands/core-stop.test.ts index c6093bb2c7..233908cabe 100644 --- a/__tests__/unit/core/commands/core/stop.test.ts +++ b/__tests__/unit/core/commands/core-stop.test.ts @@ -1,6 +1,14 @@ -import { CLIError } from "@oclif/errors"; -import { StopCommand } from "@packages/core/src/commands/core/stop"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/core-stop"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StopCommand", () => { it("should throw if the process does not exist", async () => { @@ -8,9 +16,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-core" process does not exist.'); missing.mockReset(); isUnknown.mockReset(); @@ -22,8 +28,8 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process has entered an unknown state.'), + await expect(cli.execute(Command)).rejects.toThrowError( + '[ERROR] The "ark-core" process has entered an unknown state.', ); missing.mockReset(); @@ -36,9 +42,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-core" process is not running.'); missing.mockReset(); isUnknown.mockReset(); @@ -51,7 +55,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const deleteSpy = jest.spyOn(processManager, "delete").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet", "--daemon"]); + await cli.withFlags({ daemon: true }).execute(Command); expect(deleteSpy).toHaveBeenCalled(); @@ -67,7 +71,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const stop = jest.spyOn(processManager, "stop").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(stop).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/core/log.test.ts b/__tests__/unit/core/commands/core/log.test.ts deleted file mode 100644 index dcadd9e15a..0000000000 --- a/__tests__/unit/core/commands/core/log.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CLIError } from "@oclif/errors"; -import { LogCommand } from "@packages/core/src/commands/core/log"; - -describe("LogCommand", () => { - it("should throw if the process does not exist", async () => { - await expect(LogCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-core" process does not exist.'), - ); - }); -}); diff --git a/__tests__/unit/core/commands/core/run.test.ts b/__tests__/unit/core/commands/core/run.test.ts deleted file mode 100644 index 244b048208..0000000000 --- a/__tests__/unit/core/commands/core/run.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RunCommand } from "@packages/core/src/commands/core/run"; - -export const app = { - bootstrap: jest.fn(), - boot: jest.fn(), -}; - -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - }, -})); - -describe("RunCommand", () => { - it.skip("should throw if the process does not exist", async () => { - const spyBootstrap = jest.spyOn(app, "bootstrap").mockImplementation(undefined); - const spyBoot = jest.spyOn(app, "boot").mockImplementation(undefined); - - await RunCommand.run(["--token=ark", "--network=testnet"]); - - expect(spyBootstrap).toHaveBeenCalled(); - expect(spyBoot).toHaveBeenCalled(); - }); -}); diff --git a/__tests__/unit/core/commands/env/get.test.ts b/__tests__/unit/core/commands/env-get.test.ts similarity index 63% rename from __tests__/unit/core/commands/env/get.test.ts rename to __tests__/unit/core/commands/env-get.test.ts index e4e1aa269c..f9e4f752cd 100644 --- a/__tests__/unit/core/commands/env/get.test.ts +++ b/__tests__/unit/core/commands/env-get.test.ts @@ -1,38 +1,42 @@ +import { Console } from "@arkecosystem/core-test-framework"; import { dirSync, setGracefulCleanup } from "tmp"; -import { GetCommand } from "@packages/core/src/commands/env/get"; +import { Command } from "@packages/core/src/commands/env-get"; import { ensureFileSync, writeFileSync, ensureDirSync } from "fs-extra"; +let cli; +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + cli = new Console(); +}); + afterAll(() => setGracefulCleanup()); describe("GetCommand", () => { it("should get the value of an environment variable", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - writeFileSync(`${process.env.CORE_PATH_CONFIG}/.env`, "CORE_LOG_LEVEL=emergency"); let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await GetCommand.run(["--token=ark", "CORE_LOG_LEVEL"]); + await cli.withFlags({ key: "CORE_LOG_LEVEL" }).execute(Command); expect(message).toBe("emergency"); }); it("should fail to get the value of a non-existent environment variable", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - ensureFileSync(`${process.env.CORE_PATH_CONFIG}/.env`); - await expect(GetCommand.run(["--token=ark", "FAKE_KEY"])).rejects.toThrow('The "FAKE_KEY" doesn\'t exist.'); + await expect(cli.withFlags({ key: "FAKE_KEY" }).execute(Command)).rejects.toThrow( + 'The "FAKE_KEY" doesn\'t exist.', + ); }); it("should fail if the environment configuration doesn't exist", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/jestnet`); - await expect(GetCommand.run(["--token=btc", "FAKE_KEY"])).rejects.toThrow( + await expect(cli.withFlags({ key: "FAKE_KEY" }).execute(Command)).rejects.toThrow( `No environment file found at ${process.env.CORE_PATH_CONFIG}/.env`, ); }); diff --git a/__tests__/unit/core/commands/env/list.test.ts b/__tests__/unit/core/commands/env-list.test.ts similarity index 62% rename from __tests__/unit/core/commands/env/list.test.ts rename to __tests__/unit/core/commands/env-list.test.ts index b4dc35b3ed..6e5d3fbe9e 100644 --- a/__tests__/unit/core/commands/env/list.test.ts +++ b/__tests__/unit/core/commands/env-list.test.ts @@ -1,33 +1,34 @@ +import { Console } from "@arkecosystem/core-test-framework"; import { dirSync, setGracefulCleanup } from "tmp"; -import { ListCommand } from "@packages/core/src/commands/env/list"; +import { Command } from "@packages/core/src/commands/env-list"; import { removeSync, writeFileSync } from "fs-extra"; +let cli; +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + cli = new Console(); +}); + afterAll(() => setGracefulCleanup()); describe("ListCommand", () => { it("should fail if the environment configuration doesn't exist", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - let message: string; - jest.spyOn(console, "error").mockImplementationOnce(m => (message = m)); - - await ListCommand.run(["--token=ark", "--network=mainnet"]); - - expect(message).toContain(`No environment file found`); + await expect(cli.execute(Command)).rejects.toThrow( + `[ERROR] No environment file found at ${process.env.CORE_PATH_CONFIG}/.env`, + ); }); it("should list all environment variables", async () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - process.env.CORE_PATH_CONFIG = dirSync().name; - const envFile: string = `${process.env.CORE_PATH_CONFIG}/.env`; removeSync(envFile); writeFileSync(envFile, "someKey=someValue", { flag: "w" }); - await ListCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); expect(message).toContain("Key"); expect(message).toContain("Value"); diff --git a/__tests__/unit/core/commands/env/paths.test.ts b/__tests__/unit/core/commands/env-paths.test.ts similarity index 72% rename from __tests__/unit/core/commands/env/paths.test.ts rename to __tests__/unit/core/commands/env-paths.test.ts index a919019742..05c1130a8d 100644 --- a/__tests__/unit/core/commands/env/paths.test.ts +++ b/__tests__/unit/core/commands/env-paths.test.ts @@ -1,13 +1,17 @@ +import { Console } from "@arkecosystem/core-test-framework"; import envPaths, { Paths } from "env-paths"; -import { PathsCommand } from "@packages/core/src/commands/env/paths"; +import { Command } from "@packages/core/src/commands/env-paths"; + +let cli; +beforeEach(() => (cli = new Console())); describe("PathsCommand", () => { it("should list all system paths", async () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await PathsCommand.run(["--token=ark", "--network=mainnet"]); + await cli.execute(Command); const paths: Paths = envPaths("ark", { suffix: "core" }); diff --git a/__tests__/unit/core/commands/env/set.test.ts b/__tests__/unit/core/commands/env-set.test.ts similarity index 56% rename from __tests__/unit/core/commands/env/set.test.ts rename to __tests__/unit/core/commands/env-set.test.ts index 6b74dda1cf..2cbc2131c0 100644 --- a/__tests__/unit/core/commands/env/set.test.ts +++ b/__tests__/unit/core/commands/env-set.test.ts @@ -1,25 +1,31 @@ +import { Console } from "@arkecosystem/core-test-framework"; import { dirSync, setGracefulCleanup } from "tmp"; import envfile from "envfile"; - -import { SetCommand } from "@packages/core/src/commands/env/set"; import { ensureFileSync, removeSync } from "fs-extra"; -afterAll(() => setGracefulCleanup()); +import { Command } from "@packages/core/src/commands/env-set"; + +let cli; +beforeAll(() => setGracefulCleanup()); + +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + cli = new Console(); +}); describe("SetCommand", () => { it("should set the value of an environment variable", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - const envFile: string = `${process.env.CORE_PATH_CONFIG}/.env`; removeSync(envFile); ensureFileSync(envFile); - await SetCommand.run(["--token=ark", "--network=mainnet", "key1", "value"]); + await cli.withFlags({ key: "key1", value: "value" }).execute(Command); expect(envfile.parseFileSync(envFile)).toEqual({ key1: "value" }); - await SetCommand.run(["--token=ark", "--network=mainnet", "key2", "value"]); + await cli.withFlags({ key: "key2", value: "value" }).execute(Command); expect(envfile.parseFileSync(envFile)).toEqual({ key1: "value", key2: "value" }); }); diff --git a/__tests__/unit/core/commands/forger-log.test.ts b/__tests__/unit/core/commands/forger-log.test.ts new file mode 100644 index 0000000000..7d9fd95bbb --- /dev/null +++ b/__tests__/unit/core/commands/forger-log.test.ts @@ -0,0 +1,12 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/forger-log"; + +let cli; +beforeEach(() => (cli = new Console())); + +describe("LogCommand", () => { + it("should throw if the process does not exist", async () => { + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-forger" process does not exist.'); + }); +}); diff --git a/__tests__/unit/core/commands/relay/restart.test.ts b/__tests__/unit/core/commands/forger-restart.test.ts similarity index 64% rename from __tests__/unit/core/commands/relay/restart.test.ts rename to __tests__/unit/core/commands/forger-restart.test.ts index e46cee0121..a66a6193d2 100644 --- a/__tests__/unit/core/commands/relay/restart.test.ts +++ b/__tests__/unit/core/commands/forger-restart.test.ts @@ -1,15 +1,21 @@ -import { CLIError } from "@oclif/errors"; -import { RestartCommand } from "@packages/core/src/commands/relay/restart"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/forger-restart"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("RestartCommand", () => { it("should throw if the process does not exist", async () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-forger" process does not exist.'); missing.mockReset(); isStopped.mockReset(); @@ -19,9 +25,7 @@ describe("RestartCommand", () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-forger" process is not running.'); missing.mockReset(); isStopped.mockReset(); @@ -32,7 +36,7 @@ describe("RestartCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const restart = jest.spyOn(processManager, "restart").mockImplementation(); - await RestartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(restart).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/forger/run.test.ts b/__tests__/unit/core/commands/forger-run.test.ts similarity index 56% rename from __tests__/unit/core/commands/forger/run.test.ts rename to __tests__/unit/core/commands/forger-run.test.ts index 866af4eb0a..c2f8df7d9d 100644 --- a/__tests__/unit/core/commands/forger/run.test.ts +++ b/__tests__/unit/core/commands/forger-run.test.ts @@ -1,8 +1,11 @@ -import { RunCommand } from "@packages/core/src/commands/forger/run"; import { dirSync, setGracefulCleanup } from "tmp"; import { writeJSONSync } from "fs-extra"; -export const app = { +import { Command } from "@packages/core/src/commands/forger-run"; + +import { executeCommand } from "../__support__/app"; + +const app = { bootstrap: jest.fn(), boot: jest.fn(), }; @@ -15,18 +18,22 @@ jest.mock("@arkecosystem/core-kernel", () => ({ }, })); -afterAll(() => setGracefulCleanup()); +beforeEach(() => { + setGracefulCleanup(); -describe("RunCommand", () => { - it("should throw if the process does not exist", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; + process.env.CORE_PATH_CONFIG = dirSync().name; - writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); + writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); +}); +// something funky is going on here which results in the core-test-framework +// CLI helper being unable to be used because the core-kernel mock causes problems +describe("RunCommand", () => { + it("should throw if the process does not exist", async () => { const spyBootstrap = jest.spyOn(app, "bootstrap").mockImplementation(undefined); const spyBoot = jest.spyOn(app, "boot").mockImplementation(undefined); - await RunCommand.run(["--token=ark", "--network=testnet"]); + await executeCommand(Command); expect(spyBootstrap).toHaveBeenCalled(); expect(spyBoot).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/forger/start.test.ts b/__tests__/unit/core/commands/forger-start.test.ts similarity index 60% rename from __tests__/unit/core/commands/forger/start.test.ts rename to __tests__/unit/core/commands/forger-start.test.ts index cb527e8f4c..ce9fc11050 100644 --- a/__tests__/unit/core/commands/forger/start.test.ts +++ b/__tests__/unit/core/commands/forger-start.test.ts @@ -1,35 +1,44 @@ -import { StartCommand } from "@packages/core/src/commands/forger/start"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import { resolve } from "path"; import { dirSync, setGracefulCleanup } from "tmp"; import { writeJSONSync } from "fs-extra"; import os from "os"; +import { Command } from "@packages/core/src/commands/forger-start"; + +let cli; +let processManager; +beforeEach(() => { + process.env.CORE_PATH_CONFIG = dirSync().name; + + writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); + + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); + afterAll(() => setGracefulCleanup()); describe("StartCommand", () => { it("should throw if the process does not exist", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - writeJSONSync(`${process.env.CORE_PATH_CONFIG}/delegates.json`, { secrets: ["bip39"] }); - jest.spyOn(os, "freemem").mockReturnValue(99999999999); jest.spyOn(os, "totalmem").mockReturnValue(99999999999); const spyStart = jest.spyOn(processManager, "start").mockImplementation(undefined); - await StartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(spyStart).toHaveBeenCalledWith( { - args: "forger:run --token=ark --network=testnet --suffix=forger --env=production", + args: "forger:run --token=ark --network=testnet --v=0 --env=production", env: { CORE_ENV: "production", NODE_ENV: "production", }, name: "ark-forger", node_args: undefined, - script: resolve(__dirname, "../../../../../packages/core/src/commands/forger/start.ts"), + script: resolve(__dirname, "../../../../packages/core/bin/run"), }, { "kill-timeout": 30000, "max-restarts": 5, name: "ark-forger" }, ); diff --git a/__tests__/unit/core/commands/forger/status.test.ts b/__tests__/unit/core/commands/forger-status.test.ts similarity index 68% rename from __tests__/unit/core/commands/forger/status.test.ts rename to __tests__/unit/core/commands/forger-status.test.ts index 0ed12533a3..b3e04f2cde 100644 --- a/__tests__/unit/core/commands/forger/status.test.ts +++ b/__tests__/unit/core/commands/forger-status.test.ts @@ -1,14 +1,20 @@ import "jest-extended"; -import { CLIError } from "@oclif/errors"; -import { StatusCommand } from "@packages/core/src/commands/forger/status"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/forger-status"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StatusCommand", () => { it("should throw if the process does not exist", async () => { - await expect(StatusCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-forger" process does not exist.'); }); it("should render a table with the process information", async () => { @@ -27,7 +33,7 @@ describe("StatusCommand", () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await StatusCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(message).toIncludeMultiple(["ID", "Name", "Version", "Status", "Uptime", "CPU", "RAM"]); expect(message).toIncludeMultiple([ diff --git a/__tests__/unit/core/commands/relay/stop.test.ts b/__tests__/unit/core/commands/forger-stop.test.ts similarity index 74% rename from __tests__/unit/core/commands/relay/stop.test.ts rename to __tests__/unit/core/commands/forger-stop.test.ts index b184859d71..aae0ce681b 100644 --- a/__tests__/unit/core/commands/relay/stop.test.ts +++ b/__tests__/unit/core/commands/forger-stop.test.ts @@ -1,6 +1,14 @@ -import { CLIError } from "@oclif/errors"; -import { StopCommand } from "@packages/core/src/commands/relay/stop"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/forger-stop"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StopCommand", () => { it("should throw if the process does not exist", async () => { @@ -8,9 +16,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-forger" process does not exist.'); missing.mockReset(); isUnknown.mockReset(); @@ -22,8 +28,8 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process has entered an unknown state.'), + await expect(cli.execute(Command)).rejects.toThrowError( + '[ERROR] The "ark-forger" process has entered an unknown state.', ); missing.mockReset(); @@ -36,9 +42,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-forger" process is not running.'); missing.mockReset(); isUnknown.mockReset(); @@ -51,7 +55,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const deleteSpy = jest.spyOn(processManager, "delete").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet", "--daemon"]); + await cli.withFlags({ daemon: true }).execute(Command); expect(deleteSpy).toHaveBeenCalled(); @@ -67,7 +71,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const stop = jest.spyOn(processManager, "stop").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(stop).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/forger/log.test.ts b/__tests__/unit/core/commands/forger/log.test.ts deleted file mode 100644 index 4d579f93e2..0000000000 --- a/__tests__/unit/core/commands/forger/log.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CLIError } from "@oclif/errors"; -import { LogCommand } from "@packages/core/src/commands/forger/log"; - -describe("LogCommand", () => { - it("should throw if the process does not exist", async () => { - await expect(LogCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process does not exist.'), - ); - }); -}); diff --git a/__tests__/unit/core/commands/network/generate.test.ts b/__tests__/unit/core/commands/network-generate.test.ts similarity index 57% rename from __tests__/unit/core/commands/network/generate.test.ts rename to __tests__/unit/core/commands/network-generate.test.ts index 21c27881d0..707ca04abb 100644 --- a/__tests__/unit/core/commands/network/generate.test.ts +++ b/__tests__/unit/core/commands/network-generate.test.ts @@ -1,14 +1,21 @@ import "jest-extended"; -import { GenerateCommand } from "@packages/core/src/commands/network/generate"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/network-generate"; import { resolve } from "path"; import prompts from "prompts"; import fs from "fs-extra"; -jest.mock("fs-extra"); +// jest.mock("fs-extra"); + +const configCore: string = resolve(__dirname, "../../../../packages/core/bin/config/mynet7"); +const configCrypto: string = resolve(__dirname, "../../../../packages/crypto/src/networks/mynet7"); -const configCore: string = resolve(__dirname, "../../../../../packages/core/bin/config/mynet7"); -const configCrypto: string = resolve(__dirname, "../../../../../packages/crypto/src/networks/mynet7"); +let cli; +beforeEach(() => (cli = new Console())); + +afterEach(() => jest.resetAllMocks()); describe("GenerateCommand", () => { it("should generate a new configuration", async () => { @@ -18,22 +25,24 @@ describe("GenerateCommand", () => { const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); const copyFileSync = jest.spyOn(fs, "copyFileSync").mockImplementation(); - await GenerateCommand.run([ - "--network=mynet7", - "--premine=120000000000", - "--delegates=47", - "--blocktime=9", - "--maxTxPerBlock=122", - "--maxBlockPayload=123444", - "--rewardHeight=23000", - "--rewardAmount=66000", - "--pubKeyHash=168", - "--wif=27", - "--token=myn", - "--symbol=my", - "--explorer=myex.io", - "--distribute", - ]); + await cli + .withFlags({ + network: "mynet7", + premine: "120000000000", + delegates: "47", + blocktime: "9", + maxTxPerBlock: "122", + maxBlockPayload: "123444", + rewardHeight: "23000", + rewardAmount: "66000", + pubKeyHash: "168", + wif: "27", + token: "myn", + symbol: "my", + explorer: "myex.io", + distribute: true, + }) + .execute(Command); expect(existsSync).toHaveBeenCalledWith(configCore); expect(existsSync).toHaveBeenCalledWith(configCrypto); @@ -45,65 +54,58 @@ describe("GenerateCommand", () => { expect(writeFileSync).toHaveBeenCalled(); expect(copyFileSync).toHaveBeenCalledTimes(2); - - existsSync.mockReset(); - ensureDirSync.mockReset(); - writeJSONSync.mockReset(); - writeFileSync.mockReset(); - copyFileSync.mockReset(); }); it("should throw if the core configuration destination already exists", async () => { - const existsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); + jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); await expect( - GenerateCommand.run([ - "--network=mynet7", - "--premine=120000000000", - "--delegates=47", - "--blocktime=9", - "--maxTxPerBlock=122", - "--maxBlockPayload=123444", - "--rewardHeight=23000", - "--rewardAmount=66000", - "--pubKeyHash=168", - "--wif=27", - "--token=myn", - "--symbol=my", - "--explorer=myex.io", - "--distribute", - ]), + cli + .withFlags({ + network: "mynet7", + premine: "120000000000", + delegates: "47", + blocktime: "9", + maxTxPerBlock: "122", + maxBlockPayload: "123444", + rewardHeight: "23000", + rewardAmount: "66000", + pubKeyHash: "168", + wif: "27", + token: "myn", + symbol: "my", + explorer: "myex.io", + distribute: "true", + }) + .execute(Command), ).rejects.toThrow(`${configCore} already exists.`); - - existsSync.mockReset(); }); it("should throw if the crypto configuration destination already exists", async () => { - const existsSync = jest - .spyOn(fs, "existsSync") + jest.spyOn(fs, "existsSync") .mockReturnValueOnce(false) .mockReturnValueOnce(true); await expect( - GenerateCommand.run([ - "--network=mynet7", - "--premine=120000000000", - "--delegates=47", - "--blocktime=9", - "--maxTxPerBlock=122", - "--maxBlockPayload=123444", - "--rewardHeight=23000", - "--rewardAmount=66000", - "--pubKeyHash=168", - "--wif=27", - "--token=myn", - "--symbol=my", - "--explorer=myex.io", - "--distribute", - ]), + cli + .withFlags({ + network: "mynet7", + premine: "120000000000", + delegates: "47", + blocktime: "9", + maxTxPerBlock: "122", + maxBlockPayload: "123444", + rewardHeight: "23000", + rewardAmount: "66000", + pubKeyHash: "168", + wif: "27", + token: "myn", + symbol: "my", + explorer: "myex.io", + distribute: "true", + }) + .execute(Command), ).rejects.toThrow(`${configCrypto} already exists.`); - - existsSync.mockReset(); }); it("should throw if some properties are not provided", async () => { @@ -123,7 +125,7 @@ describe("GenerateCommand", () => { undefined, ]); - await expect(GenerateCommand.run([])).rejects.toThrow("Please provide all flags and try again!"); + await expect(cli.execute(Command)).rejects.toThrow("Please provide all flags and try again!"); }); it("should throw if the properties are not confirmed", async () => { @@ -145,7 +147,7 @@ describe("GenerateCommand", () => { false, ]); - await expect(GenerateCommand.run([])).rejects.toThrow("You'll need to confirm the input to continue."); + await expect(cli.execute(Command)).rejects.toThrow("You'll need to confirm the input to continue."); }); it("should generate a new configuration if the properties are confirmed", async () => { @@ -173,7 +175,7 @@ describe("GenerateCommand", () => { true, ]); - await GenerateCommand.run([]); + await cli.execute(Command); expect(existsSync).toHaveBeenCalledWith(configCore); expect(existsSync).toHaveBeenCalledWith(configCrypto); @@ -185,11 +187,5 @@ describe("GenerateCommand", () => { expect(writeFileSync).toHaveBeenCalled(); expect(copyFileSync).toHaveBeenCalledTimes(2); - - existsSync.mockReset(); - ensureDirSync.mockReset(); - writeJSONSync.mockReset(); - writeFileSync.mockReset(); - copyFileSync.mockReset(); }); }); diff --git a/__tests__/unit/core/commands/reinstall.test.ts b/__tests__/unit/core/commands/reinstall.test.ts index 0415362e07..8bbf7d97e6 100644 --- a/__tests__/unit/core/commands/reinstall.test.ts +++ b/__tests__/unit/core/commands/reinstall.test.ts @@ -1,10 +1,20 @@ import "jest-extended"; -import { ReinstallCommand } from "@packages/core/src/commands/reinstall"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import prompts from "prompts"; + +import { Command } from "@packages/core/src/commands/reinstall"; + import execa from "../../../../__mocks__/execa"; +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); + describe("ReinstallCommand", () => { it("should reinstall without a prompt if the [--force] flag is used", async () => { const sync: jest.SpyInstance = jest.spyOn(execa, "sync").mockReturnValue({ @@ -12,7 +22,7 @@ describe("ReinstallCommand", () => { stderr: undefined, }); - await ReinstallCommand.run(["--force"]); + await cli.withFlags({ force: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(4); // install > check core > check relay > check forger @@ -27,7 +37,7 @@ describe("ReinstallCommand", () => { prompts.inject([true]); - await ReinstallCommand.run([]); + await cli.execute(Command); expect(sync).toHaveBeenCalledTimes(4); // install > check core > check relay > check forger @@ -42,7 +52,7 @@ describe("ReinstallCommand", () => { prompts.inject([false]); - await ReinstallCommand.run([]); + await expect(cli.execute(Command)).rejects.toThrow("[ERROR] You'll need to confirm the reinstall to continue."); expect(sync).not.toHaveBeenCalled(); @@ -62,7 +72,7 @@ describe("ReinstallCommand", () => { prompts.inject([true]); // restart relay prompts.inject([true]); // restart forger - await ReinstallCommand.run(["--force"]); + await cli.withFlags({ force: true }).execute(Command); expect(sync).toHaveBeenCalled(); expect(isOnline).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/relay/log.test.ts b/__tests__/unit/core/commands/relay-log.test.ts similarity index 55% rename from __tests__/unit/core/commands/relay/log.test.ts rename to __tests__/unit/core/commands/relay-log.test.ts index 8d499cd1c4..30f3f00efd 100644 --- a/__tests__/unit/core/commands/relay/log.test.ts +++ b/__tests__/unit/core/commands/relay-log.test.ts @@ -1,16 +1,16 @@ +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import { fileSync, setGracefulCleanup } from "tmp"; -import { CLIError } from "@oclif/errors"; -import { LogCommand } from "@packages/core/src/commands/relay/log"; -import { processManager } from "@packages/core/src/common/process-manager"; -jest.mock("nodejs-tail"); +import { Command } from "@packages/core/src/commands/relay-log"; -let messages; +jest.mock("nodejs-tail"); +let cli; +let processManager; beforeEach(() => { - messages = []; - - jest.spyOn(process.stdout, "write").mockImplementation((value: string) => messages.push(value.trim())); + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); }); afterEach(() => jest.restoreAllMocks()); @@ -19,17 +19,11 @@ afterAll(() => setGracefulCleanup()); describe("LogCommand", () => { it("should throw if the process does not exist", async () => { - const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); - - await expect(LogCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process does not exist.'), - ); - - missing.mockReset(); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-relay" process does not exist.'); }); it("should log to pm_out_log_path", async () => { - jest.spyOn(processManager, "missing").mockReturnValue(false); + jest.spyOn(cli.app.get(Container.Identifiers.AbortMissingProcess), "execute").mockImplementation(); jest.spyOn(processManager, "describe").mockReturnValue({ pid: 1, name: "ark-relay", @@ -43,19 +37,17 @@ describe("LogCommand", () => { monit: { cpu: 2, memory: 2048 }, }); - const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const spyLog = jest.spyOn(console, "log"); - await LogCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); - expect(messages).toContain( + expect(spyLog).toHaveBeenCalledWith( "Tailing last 15 lines for [ark-relay] process (change the value with --lines option)", ); - - missing.mockReset(); }); it("should log to pm_err_log_path", async () => { - jest.spyOn(processManager, "missing").mockReturnValue(false); + jest.spyOn(cli.app.get(Container.Identifiers.AbortMissingProcess), "execute").mockImplementation(); jest.spyOn(processManager, "describe").mockReturnValue({ pid: 1, name: "ark-relay", @@ -69,14 +61,12 @@ describe("LogCommand", () => { monit: { cpu: 2, memory: 2048 }, }); - const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); + const spyLog = jest.spyOn(console, "log"); - await LogCommand.run(["--token=ark", "--network=testnet", "--error"]); + await cli.withFlags({ error: true }).execute(Command); - expect(messages).toContain( + expect(spyLog).toHaveBeenCalledWith( "Tailing last 15 lines for [ark-relay] process (change the value with --lines option)", ); - - missing.mockReset(); }); }); diff --git a/__tests__/unit/core/commands/forger/restart.test.ts b/__tests__/unit/core/commands/relay-restart.test.ts similarity index 63% rename from __tests__/unit/core/commands/forger/restart.test.ts rename to __tests__/unit/core/commands/relay-restart.test.ts index 96c2a64caa..5c6b6e83dc 100644 --- a/__tests__/unit/core/commands/forger/restart.test.ts +++ b/__tests__/unit/core/commands/relay-restart.test.ts @@ -1,15 +1,21 @@ -import { CLIError } from "@oclif/errors"; -import { RestartCommand } from "@packages/core/src/commands/forger/restart"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/relay-restart"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("RestartCommand", () => { it("should throw if the process does not exist", async () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-relay" process does not exist.'); missing.mockReset(); isStopped.mockReset(); @@ -19,9 +25,7 @@ describe("RestartCommand", () => { const missing = jest.spyOn(processManager, "missing").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(RestartCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrowError('[ERROR] The "ark-relay" process is not running.'); missing.mockReset(); isStopped.mockReset(); @@ -32,7 +36,7 @@ describe("RestartCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const restart = jest.spyOn(processManager, "restart").mockImplementation(); - await RestartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(restart).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/relay/run.test.ts b/__tests__/unit/core/commands/relay-run.test.ts similarity index 64% rename from __tests__/unit/core/commands/relay/run.test.ts rename to __tests__/unit/core/commands/relay-run.test.ts index e3c03b0894..3f9d7a2afb 100644 --- a/__tests__/unit/core/commands/relay/run.test.ts +++ b/__tests__/unit/core/commands/relay-run.test.ts @@ -1,6 +1,8 @@ -import { RunCommand } from "@packages/core/src/commands/relay/run"; +import { Command } from "@packages/core/src/commands/relay-run"; -export const app = { +import { executeCommand } from "../__support__/app"; + +const app = { bootstrap: jest.fn(), boot: jest.fn(), }; @@ -13,12 +15,14 @@ jest.mock("@arkecosystem/core-kernel", () => ({ }, })); +// something funky is going on here which results in the core-test-framework +// CLI helper being unable to be used because the core-kernel mock causes problems describe("RunCommand", () => { it("should throw if the process does not exist", async () => { const spyBootstrap = jest.spyOn(app, "bootstrap").mockImplementation(undefined); const spyBoot = jest.spyOn(app, "boot").mockImplementation(undefined); - await RunCommand.run(["--token=ark", "--network=testnet"]); + await executeCommand(Command); expect(spyBootstrap).toHaveBeenCalled(); expect(spyBoot).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/relay-share.test.ts b/__tests__/unit/core/commands/relay-share.test.ts new file mode 100644 index 0000000000..c8a41286b2 --- /dev/null +++ b/__tests__/unit/core/commands/relay-share.test.ts @@ -0,0 +1,21 @@ +import { Console } from "@arkecosystem/core-test-framework"; +import ngrok from "ngrok"; + +import { Command } from "@packages/core/src/commands/relay-share"; + +let cli; +beforeEach(() => (cli = new Console())); + +describe("ShareCommand", () => { + it("should throw if the process does not exist", async () => { + const spyConnect = jest.spyOn(ngrok, "connect").mockImplementation(undefined); + + await cli.execute(Command); + + expect(spyConnect).toHaveBeenCalledWith({ + addr: 4003, + proto: "http", + region: "eu", + }); + }); +}); diff --git a/__tests__/unit/core/commands/relay/start.test.ts b/__tests__/unit/core/commands/relay-start.test.ts similarity index 63% rename from __tests__/unit/core/commands/relay/start.test.ts rename to __tests__/unit/core/commands/relay-start.test.ts index f7fb402282..06a7443988 100644 --- a/__tests__/unit/core/commands/relay/start.test.ts +++ b/__tests__/unit/core/commands/relay-start.test.ts @@ -1,8 +1,17 @@ -import { StartCommand } from "@packages/core/src/commands/relay/start"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; import { resolve } from "path"; import os from "os"; +import { Command } from "@packages/core/src/commands/relay-start"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); + describe("StartCommand", () => { it("should throw if the process does not exist", async () => { jest.spyOn(os, "freemem").mockReturnValue(99999999999); @@ -10,18 +19,18 @@ describe("StartCommand", () => { const spyStart = jest.spyOn(processManager, "start").mockImplementation(undefined); - await StartCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(spyStart).toHaveBeenCalledWith( { - args: "relay:run --token=ark --network=testnet --suffix=relay --env=production", + args: "relay:run --token=ark --network=testnet --v=0 --env=production", env: { CORE_ENV: "production", NODE_ENV: "production", }, name: "ark-relay", node_args: undefined, - script: resolve(__dirname, "../../../../../packages/core/src/commands/relay/start.ts"), + script: resolve(__dirname, "../../../../packages/core/bin/run"), }, { "kill-timeout": 30000, "max-restarts": 5, name: "ark-relay" }, ); diff --git a/__tests__/unit/core/commands/relay/status.test.ts b/__tests__/unit/core/commands/relay-status.test.ts similarity index 68% rename from __tests__/unit/core/commands/relay/status.test.ts rename to __tests__/unit/core/commands/relay-status.test.ts index d9d50e2743..ee2f5fb6c2 100644 --- a/__tests__/unit/core/commands/relay/status.test.ts +++ b/__tests__/unit/core/commands/relay-status.test.ts @@ -1,14 +1,20 @@ import "jest-extended"; -import { CLIError } from "@oclif/errors"; -import { StatusCommand } from "@packages/core/src/commands/relay/status"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/relay-status"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StatusCommand", () => { it("should throw if the process does not exist", async () => { - await expect(StatusCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-relay" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-relay" process does not exist.'); }); it("should render a table with the process information", async () => { @@ -27,7 +33,7 @@ describe("StatusCommand", () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await StatusCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(message).toIncludeMultiple(["ID", "Name", "Version", "Status", "Uptime", "CPU", "RAM"]); expect(message).toIncludeMultiple([ diff --git a/__tests__/unit/core/commands/forger/stop.test.ts b/__tests__/unit/core/commands/relay-stop.test.ts similarity index 74% rename from __tests__/unit/core/commands/forger/stop.test.ts rename to __tests__/unit/core/commands/relay-stop.test.ts index 99f4d5b694..497a928a16 100644 --- a/__tests__/unit/core/commands/forger/stop.test.ts +++ b/__tests__/unit/core/commands/relay-stop.test.ts @@ -1,6 +1,14 @@ -import { CLIError } from "@oclif/errors"; -import { StopCommand } from "@packages/core/src/commands/forger/stop"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/relay-stop"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("StopCommand", () => { it("should throw if the process does not exist", async () => { @@ -8,9 +16,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process does not exist.'), - ); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-relay" process does not exist.'); missing.mockReset(); isUnknown.mockReset(); @@ -22,8 +28,8 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(true); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process has entered an unknown state.'), + await expect(cli.execute(Command)).rejects.toThrow( + '[ERROR] The "ark-relay" process has entered an unknown state.', ); missing.mockReset(); @@ -36,9 +42,7 @@ describe("StopCommand", () => { const isUnknown = jest.spyOn(processManager, "isUnknown").mockReturnValue(false); const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(true); - await expect(StopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrowError( - new CLIError('The "ark-forger" process is not running.'), - ); + await expect(cli.execute(Command)).rejects.toThrow('[ERROR] The "ark-relay" process is not running.'); missing.mockReset(); isUnknown.mockReset(); @@ -51,7 +55,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const deleteSpy = jest.spyOn(processManager, "delete").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet", "--daemon"]); + await cli.withFlags({ daemon: true }).execute(Command); expect(deleteSpy).toHaveBeenCalled(); @@ -67,7 +71,7 @@ describe("StopCommand", () => { const isStopped = jest.spyOn(processManager, "isStopped").mockReturnValue(false); const stop = jest.spyOn(processManager, "stop").mockImplementation(); - await StopCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(stop).toHaveBeenCalled(); diff --git a/__tests__/unit/core/commands/relay/share.test.ts b/__tests__/unit/core/commands/relay/share.test.ts deleted file mode 100644 index 61001cee63..0000000000 --- a/__tests__/unit/core/commands/relay/share.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ShareCommand } from "@packages/core/src/commands/relay/share"; -import ngrok from "ngrok"; - -describe("ShareCommand", () => { - // todo: Cannot call write after a stream was destroyedError [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed - it.skip("should throw if the process does not exist", async () => { - const spyConnect = jest.spyOn(ngrok, "connect").mockImplementation(undefined); - - await ShareCommand.run([]); - - expect(spyConnect).toHaveBeenCalledWith({ - addr: 4003, - proto: "http", - region: "eu", - }); - }); -}); diff --git a/__tests__/unit/core/commands/snapshot-dump.test.ts b/__tests__/unit/core/commands/snapshot-dump.test.ts new file mode 100644 index 0000000000..578f5789ee --- /dev/null +++ b/__tests__/unit/core/commands/snapshot-dump.test.ts @@ -0,0 +1,43 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/snapshot-dump"; + +export const app = { + bootstrap: jest.fn(), + boot: jest.fn(), + isBound: jest.fn(), + get: jest.fn(), +}; + +// jest.mock("@arkecosystem/core-kernel", () => ({ +// __esModule: true, +// Application: jest.fn(() => app), +// Container: { +// Container: jest.fn(), +// Identifiers: { +// BlockchainService: Symbol("BlockchainService"), +// }, +// }, +// })); + +let cli; +beforeEach(() => (cli = new Console())); + +describe.skip("DumpCommand", () => { + it("should be called if the snapshot service is available", async () => { + app.isBound = jest.fn().mockReturnValue(true); + + const dump = jest.fn(); + app.get = jest.fn().mockReturnValue({ dump }); + + await cli.execute(Command); + + await expect(dump).toHaveBeenCalled(); + }); + + it("should throw if the snapshot service is not available", async () => { + app.isBound = jest.fn().mockReturnValue(false); + + await expect(cli.execute(Command)).rejects.toThrow("The @arkecosystem/core-snapshots plugin is not installed."); + }); +}); diff --git a/__tests__/unit/core/commands/snapshots/restore.test.ts b/__tests__/unit/core/commands/snapshot-restore.test.ts similarity index 65% rename from __tests__/unit/core/commands/snapshots/restore.test.ts rename to __tests__/unit/core/commands/snapshot-restore.test.ts index 04c83a64f8..fc94789dcf 100644 --- a/__tests__/unit/core/commands/snapshots/restore.test.ts +++ b/__tests__/unit/core/commands/snapshot-restore.test.ts @@ -1,5 +1,6 @@ +import { Console } from "@arkecosystem/core-test-framework"; import { dirSync, setGracefulCleanup } from "tmp"; -import { RestoreCommand } from "@packages/core/src/commands/snapshot/restore"; +import { Command } from "@packages/core/src/commands/snapshot-restore"; import { ensureDirSync } from "fs-extra"; import prompts from "prompts"; @@ -10,16 +11,19 @@ export const app = { get: jest.fn(), }; -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - Identifiers: { - BlockchainService: Symbol("BlockchainService"), - }, - }, -})); +// jest.mock("@arkecosystem/core-kernel", () => ({ +// __esModule: true, +// Application: jest.fn(() => app), +// Container: { +// Container: jest.fn(), +// Identifiers: { +// BlockchainService: Symbol("BlockchainService"), +// }, +// }, +// })); + +let cli; +beforeEach(() => (cli = new Console())); afterAll(() => setGracefulCleanup()); @@ -30,7 +34,7 @@ describe.skip("RestoreCommand", () => { const restore = jest.fn(); app.get = jest.fn().mockReturnValue({ listen: jest.fn(), import: restore }); - await RestoreCommand.run(["--token=ark", "--network=testnet", "--blocks=1"]); + await cli.execute(Command, { flags: { blocks: 1 } }); await expect(restore).toHaveBeenCalled(); }); @@ -48,7 +52,7 @@ describe.skip("RestoreCommand", () => { prompts.inject(["1"]); - await RestoreCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); await expect(restore).toHaveBeenCalled(); }); @@ -56,8 +60,6 @@ describe.skip("RestoreCommand", () => { it("should throw if the snapshot service is not available", async () => { app.isBound = jest.fn().mockReturnValue(false); - await expect(RestoreCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The @arkecosystem/core-snapshots plugin is not installed.", - ); + await expect(cli.execute(Command)).rejects.toThrow("The @arkecosystem/core-snapshots plugin is not installed."); }); }); diff --git a/__tests__/unit/core/commands/snapshots/rollback.test.ts b/__tests__/unit/core/commands/snapshot-rollback.test.ts similarity index 59% rename from __tests__/unit/core/commands/snapshots/rollback.test.ts rename to __tests__/unit/core/commands/snapshot-rollback.test.ts index b0df4e992a..be2523ff15 100644 --- a/__tests__/unit/core/commands/snapshots/rollback.test.ts +++ b/__tests__/unit/core/commands/snapshot-rollback.test.ts @@ -1,4 +1,6 @@ -import { RollbackCommand } from "@packages/core/src/commands/snapshot/rollback"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/snapshot-rollback"; export const app = { bootstrap: jest.fn(), @@ -7,16 +9,19 @@ export const app = { get: jest.fn(), }; -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - Identifiers: { - BlockchainService: Symbol("BlockchainService"), - }, - }, -})); +// jest.mock("@arkecosystem/core-kernel", () => ({ +// __esModule: true, +// Application: jest.fn(() => app), +// Container: { +// Container: jest.fn(), +// Identifiers: { +// BlockchainService: Symbol("BlockchainService"), +// }, +// }, +// })); + +let cli; +beforeEach(() => (cli = new Console())); describe.skip("RollbackCommand", () => { it("should call [rollbackByHeight] if a height is given", async () => { @@ -25,7 +30,7 @@ describe.skip("RollbackCommand", () => { const rollbackByHeight = jest.fn(); app.get = jest.fn().mockReturnValue({ rollbackByHeight }); - await RollbackCommand.run(["--token=ark", "--network=testnet", "--height=1"]); + await cli.execute(Command, { flags: { height: 1 } }); await expect(rollbackByHeight).toHaveBeenCalled(); }); @@ -36,7 +41,7 @@ describe.skip("RollbackCommand", () => { const rollbackByNumber = jest.fn(); app.get = jest.fn().mockReturnValue({ rollbackByNumber }); - await RollbackCommand.run(["--token=ark", "--network=testnet", "--number=1"]); + await cli.execute(Command, { flags: { number: 1 } }); await expect(rollbackByNumber).toHaveBeenCalled(); }); @@ -44,7 +49,7 @@ describe.skip("RollbackCommand", () => { it("should throw if no height or number is given", async () => { app.isBound = jest.fn().mockReturnValue(true); - await expect(RollbackCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( + await expect(cli.execute(Command)).rejects.toThrow( "Please specify either a height or number of blocks to roll back.", ); }); @@ -52,8 +57,6 @@ describe.skip("RollbackCommand", () => { it("should throw if the snapshot service is not available", async () => { app.isBound = jest.fn().mockReturnValue(false); - await expect(RollbackCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The @arkecosystem/core-snapshots plugin is not installed.", - ); + await expect(cli.execute(Command)).rejects.toThrow("The @arkecosystem/core-snapshots plugin is not installed."); }); }); diff --git a/__tests__/unit/core/commands/snapshot-truncate.test.ts b/__tests__/unit/core/commands/snapshot-truncate.test.ts new file mode 100644 index 0000000000..a312e9f78c --- /dev/null +++ b/__tests__/unit/core/commands/snapshot-truncate.test.ts @@ -0,0 +1,43 @@ +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/snapshot-truncate"; + +export const app = { + bootstrap: jest.fn(), + boot: jest.fn(), + isBound: jest.fn(), + get: jest.fn(), +}; + +// jest.mock("@arkecosystem/core-kernel", () => ({ +// __esModule: true, +// Application: jest.fn(() => app), +// Container: { +// Container: jest.fn(), +// Identifiers: { +// BlockchainService: Symbol("BlockchainService"), +// }, +// }, +// })); + +let cli; +beforeEach(() => (cli = new Console())); + +describe.skip("TruncateCommand", () => { + it("should be called if the snapshot service is available", async () => { + app.isBound = jest.fn().mockReturnValue(true); + + const truncate = jest.fn(); + app.get = jest.fn().mockReturnValue({ truncate }); + + await cli.execute(Command); + + await expect(truncate).toHaveBeenCalled(); + }); + + it("should throw if the snapshot service is not available", async () => { + app.isBound = jest.fn().mockReturnValue(false); + + await expect(cli.execute(Command)).rejects.toThrow("The @arkecosystem/core-snapshots plugin is not installed."); + }); +}); diff --git a/__tests__/unit/core/commands/snapshots/dump.test.ts b/__tests__/unit/core/commands/snapshots/dump.test.ts deleted file mode 100644 index ae67d72e80..0000000000 --- a/__tests__/unit/core/commands/snapshots/dump.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DumpCommand } from "@packages/core/src/commands/snapshot/dump"; - -export const app = { - bootstrap: jest.fn(), - boot: jest.fn(), - isBound: jest.fn(), - get: jest.fn(), -}; - -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - Identifiers: { - BlockchainService: Symbol("BlockchainService"), - }, - }, -})); - -describe.skip("DumpCommand", () => { - it("should be called if the snapshot service is available", async () => { - app.isBound = jest.fn().mockReturnValue(true); - - const dump = jest.fn(); - app.get = jest.fn().mockReturnValue({ dump }); - - await DumpCommand.run(["--token=ark", "--network=testnet"]); - - await expect(dump).toHaveBeenCalled(); - }); - - it("should throw if the snapshot service is not available", async () => { - app.isBound = jest.fn().mockReturnValue(false); - - await expect(DumpCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The @arkecosystem/core-snapshots plugin is not installed.", - ); - }); -}); diff --git a/__tests__/unit/core/commands/snapshots/truncate.test.ts b/__tests__/unit/core/commands/snapshots/truncate.test.ts deleted file mode 100644 index 609ac1e3d7..0000000000 --- a/__tests__/unit/core/commands/snapshots/truncate.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TruncateCommand } from "@packages/core/src/commands/snapshot/truncate"; - -export const app = { - bootstrap: jest.fn(), - boot: jest.fn(), - isBound: jest.fn(), - get: jest.fn(), -}; - -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - Identifiers: { - BlockchainService: Symbol("BlockchainService"), - }, - }, -})); - -describe.skip("TruncateCommand", () => { - it("should be called if the snapshot service is available", async () => { - app.isBound = jest.fn().mockReturnValue(true); - - const truncate = jest.fn(); - app.get = jest.fn().mockReturnValue({ truncate }); - - await TruncateCommand.run(["--token=ark", "--network=testnet"]); - - await expect(truncate).toHaveBeenCalled(); - }); - - it("should throw if the snapshot service is not available", async () => { - app.isBound = jest.fn().mockReturnValue(false); - - await expect(TruncateCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The @arkecosystem/core-snapshots plugin is not installed.", - ); - }); -}); diff --git a/__tests__/unit/core/commands/snapshots/verify.test.ts b/__tests__/unit/core/commands/snapshots/verify.test.ts deleted file mode 100644 index 4fc25c48eb..0000000000 --- a/__tests__/unit/core/commands/snapshots/verify.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { dirSync, setGracefulCleanup } from "tmp"; -import { VerifyCommand } from "@packages/core/src/commands/snapshot/verify"; -import { ensureDirSync } from "fs-extra"; -import prompts from "prompts"; - -export const app = { - bootstrap: jest.fn(), - boot: jest.fn(), - isBound: jest.fn(), - get: jest.fn(), -}; - -jest.mock("@arkecosystem/core-kernel", () => ({ - __esModule: true, - Application: jest.fn(() => app), - Container: { - Container: jest.fn(), - Identifiers: { - BlockchainService: Symbol("BlockchainService"), - }, - }, -})); - -afterAll(() => setGracefulCleanup()); - -describe.skip("VerifyCommand", () => { - it("should be called if a snapshot is specified via flag", async () => { - app.isBound = jest.fn().mockReturnValue(true); - - const verify = jest.fn(); - app.get = jest.fn().mockReturnValue({ verify }); - - await VerifyCommand.run(["--token=ark", "--network=testnet", "--blocks=1"]); - - await expect(verify).toHaveBeenCalled(); - }); - - it("should be called if a snapshot is specified via prompt", async () => { - process.env.CORE_PATH_DATA = dirSync().name; - - ensureDirSync(`${process.env.CORE_PATH_DATA}/snapshots`); - ensureDirSync(`${process.env.CORE_PATH_DATA}/snapshots/1`); - - app.isBound = jest.fn().mockReturnValue(true); - - const verify = jest.fn(); - app.get = jest.fn().mockReturnValue({ verify }); - - prompts.inject(["1"]); - - await VerifyCommand.run(["--token=ark", "--network=testnet"]); - - await expect(verify).toHaveBeenCalled(); - }); - - it("should throw if the snapshot service is not available", async () => { - app.isBound = jest.fn().mockReturnValue(false); - - await expect(VerifyCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow( - "The @arkecosystem/core-snapshots plugin is not installed.", - ); - }); -}); diff --git a/__tests__/unit/core/commands/top.test.ts b/__tests__/unit/core/commands/top.test.ts index 6141aa465e..195bd9d146 100644 --- a/__tests__/unit/core/commands/top.test.ts +++ b/__tests__/unit/core/commands/top.test.ts @@ -1,7 +1,16 @@ import "jest-extended"; -import { TopCommand } from "@packages/core/src/commands/top"; -import { processManager } from "@packages/core/src/common/process-manager"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + +import { Command } from "@packages/core/src/commands/top"; + +let cli; +let processManager; +beforeEach(() => { + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); describe("TopCommand", () => { it("should render a table with process information", async () => { @@ -31,7 +40,7 @@ describe("TopCommand", () => { let message: string; jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - await TopCommand.run(["--token=ark", "--network=testnet"]); + await cli.execute(Command); expect(message).toIncludeMultiple(["ID", "Name", "Version", "Status", "Uptime", "CPU", "RAM"]); expect(message).toIncludeMultiple([ @@ -48,6 +57,6 @@ describe("TopCommand", () => { it("should throw if no processes are running", async () => { jest.spyOn(processManager, "list").mockReturnValue([]); - await expect(TopCommand.run(["--token=ark", "--network=testnet"])).rejects.toThrow("No processes are running."); + await expect(cli.execute(Command)).rejects.toThrow("No processes are running."); }); }); diff --git a/__tests__/unit/core/commands/update.test.ts b/__tests__/unit/core/commands/update.test.ts index 30a9312d54..a8464aee97 100644 --- a/__tests__/unit/core/commands/update.test.ts +++ b/__tests__/unit/core/commands/update.test.ts @@ -1,14 +1,28 @@ import "jest-extended"; +import { Container } from "@arkecosystem/core-cli"; +import { Console } from "@arkecosystem/core-test-framework"; + import nock from "nock"; -import { UpdateCommand } from "@packages/core/src/commands/update"; -import { processManager } from "@packages/core/src/common/process-manager"; import prompts from "prompts"; + +import { Command } from "@packages/core/src/commands/update"; + import execa from "../../../../__mocks__/execa"; -import { versionNext } from "../common/__fixtures__/latest-version"; +import { versionNext } from "../internal/__fixtures__/latest-version"; + +let cli; +let processManager; + +beforeEach(() => { + nock.cleanAll(); + + cli = new Console(); + processManager = cli.app.get(Container.Identifiers.ProcessManager); +}); -beforeEach(() => nock.cleanAll()); +afterEach(() => jest.resetAllMocks()); beforeAll(() => nock.disableNetConnect()); @@ -20,9 +34,11 @@ describe("UpdateCommand", () => { .get("/@arkecosystem%2Fcore") .reply(200, versionNext); - await expect(UpdateCommand.run(["--force"])).rejects.toThrow( - "You already have the latest version (3.0.0-next.0)", - ); + const spySuccess = jest.spyOn(cli.app.get(Container.Identifiers.Success), "render"); + + await cli.withFlags({ force: true }).execute(Command); + + expect(spySuccess).toHaveBeenCalledWith("You already have the latest version (3.0.0-next.0)"); }); it("should throw if the update is not confirmed", async () => { @@ -42,13 +58,9 @@ describe("UpdateCommand", () => { prompts.inject([false]); - await expect(UpdateCommand.run(["--token=ark"])).rejects.toThrow( - "You'll need to confirm the update to continue.", - ); + await expect(cli.execute(Command)).rejects.toThrow("You'll need to confirm the update to continue."); expect(sync).not.toHaveBeenCalled(); - - sync.mockReset(); }); it("should update without a prompt if the [--force] flag is present", async () => { @@ -66,11 +78,9 @@ describe("UpdateCommand", () => { stderr: undefined, }); - await UpdateCommand.run(["--token=ark", "--force"]); + await cli.withFlags({ force: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(4); // install > restart core > restart relay > restart forger - - sync.mockReset(); }); it("should update with a prompt if the [--force] flag is not present", async () => { @@ -90,7 +100,7 @@ describe("UpdateCommand", () => { prompts.inject([true]); - await UpdateCommand.run(["--token=ark"]); + await cli.execute(Command); expect(sync).toHaveBeenCalled(); @@ -110,14 +120,11 @@ describe("UpdateCommand", () => { stdout: "stdout", stderr: undefined, }); - const restart = jest.spyOn(processManager, "restart").mockImplementation(undefined); + jest.spyOn(processManager, "restart").mockImplementation(undefined); - await UpdateCommand.run(["--token=ark", "--force", "--restart"]); + await cli.withFlags({ force: true, restart: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(4); // install > restart core > restart relay > restart forger - - sync.mockReset(); - restart.mockClear(); }); it("should update and restart the core process if the [--restartCore] flag is present", async () => { @@ -136,16 +143,12 @@ describe("UpdateCommand", () => { const isOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); const restart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - await UpdateCommand.run(["--token=ark", "--force", "--restartCore"]); + await cli.withFlags({ force: true, restartCore: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(1); expect(isOnline).toHaveBeenCalled(); expect(restart).toHaveBeenCalledTimes(1); expect(restart).toHaveBeenCalledWith("ark-core"); - - sync.mockReset(); - isOnline.mockClear(); - restart.mockClear(); }); it("should update and restart the core process if the [--restartRelay] flag is present", async () => { @@ -164,16 +167,12 @@ describe("UpdateCommand", () => { const isOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); const restart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - await UpdateCommand.run(["--token=ark", "--force", "--restartRelay"]); + await cli.withFlags({ force: true, restartRelay: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(1); expect(isOnline).toHaveBeenCalled(); expect(restart).toHaveBeenCalledTimes(1); expect(restart).toHaveBeenCalledWith("ark-relay"); - - sync.mockReset(); - isOnline.mockClear(); - restart.mockClear(); }); it("should update and restart the core process if the [--restartForger] flag is present", async () => { @@ -192,16 +191,12 @@ describe("UpdateCommand", () => { const isOnline = jest.spyOn(processManager, "isOnline").mockReturnValue(true); const restart = jest.spyOn(processManager, "restart").mockImplementation(undefined); - await UpdateCommand.run(["--token=ark", "--force", "--restartForger"]); + await cli.withFlags({ force: true, restartForger: true }).execute(Command); expect(sync).toHaveBeenCalledTimes(1); expect(isOnline).toHaveBeenCalled(); expect(restart).toHaveBeenCalledTimes(1); expect(restart).toHaveBeenCalledWith("ark-forger"); - - sync.mockReset(); - isOnline.mockClear(); - restart.mockClear(); }); it("should update with a prompt and restart processes after confirmation", async () => { @@ -222,18 +217,14 @@ describe("UpdateCommand", () => { const restart = jest.spyOn(processManager, "restart").mockImplementation(undefined); prompts.inject([true]); // update - prompts.inject([true]); // restart relay - prompts.inject([true]); // restart forger + prompts.inject([true]); // restart core prompts.inject([true]); // restart forger + prompts.inject([true]); // restart relay - await UpdateCommand.run(["--token=ark"]); + await cli.execute(Command); expect(sync).toHaveBeenCalled(); expect(isOnline).toHaveBeenCalled(); expect(restart).toHaveBeenCalledTimes(3); - - sync.mockReset(); - isOnline.mockClear(); - restart.mockClear(); }); }); diff --git a/__tests__/unit/core/common/config-manager.test.ts b/__tests__/unit/core/common/config-manager.test.ts deleted file mode 100644 index d2b9d9bfa1..0000000000 --- a/__tests__/unit/core/common/config-manager.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { dirSync, setGracefulCleanup } from "tmp"; - -import { ConfigManager } from "@packages/core/src/common/config-manager"; - -let configManager: ConfigManager; - -beforeEach(() => (configManager = new ConfigManager())); - -afterAll(() => setGracefulCleanup()); - -describe("config", () => { - it("should setup a new config with default values (latest channel)", () => { - configManager.initialize({ configDir: dirSync().name, version: "3.0.0", bin: "ark" }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("latest"); - }); - - it("should setup a new config with default values (next channel)", () => { - configManager.initialize({ configDir: dirSync().name, version: "3.0.0-next.0", bin: "ark" }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("next"); - }); - - it("should set and get a value", () => { - configManager.initialize({ configDir: dirSync().name, version: "3.0.0", bin: "ark" }); - - expect(configManager.get("token")).toBe("ark"); - - configManager.set("token", "btc"); - - expect(configManager.get("token")).toBe("btc"); - }); - - it("should merge the given data", () => { - configManager.initialize({ configDir: dirSync().name, version: "3.0.0", bin: "ark" }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("latest"); - - configManager.update({ token: "btc", channel: "next" }); - - expect(configManager.get("token")).toBe("btc"); - expect(configManager.get("channel")).toBe("next"); - }); -}); diff --git a/__tests__/unit/core/common/parser.test.ts b/__tests__/unit/core/common/parser.test.ts deleted file mode 100644 index f8a60bd208..0000000000 --- a/__tests__/unit/core/common/parser.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { dirSync, setGracefulCleanup } from "tmp"; - -import { parseWithNetwork } from "@packages/core/src/common/parser"; -import { configManager } from "@packages/core/src/common/config-manager"; -import { getPaths } from "@packages/core/src/common/env"; -import { ensureDirSync } from "fs-extra"; -import prompts from "prompts"; - -beforeEach(() => { - configManager.initialize({ configDir: dirSync().name, version: "3.0.0", bin: "ark" }); -}); - -afterAll(() => setGracefulCleanup()); - -describe("parseWithNetwork", () => { - it("should use the configured token if none is specified via flag", async () => { - configManager.set("token", "random"); - - const { args, flags, paths } = await parseWithNetwork({ - args: [], - flags: { network: "mainnet" }, - }); - - expect(args).toEqual([]); - expect(flags).toEqual({ network: "mainnet", token: "random" }); - expect(paths.cache).toEqual(getPaths("random", "mainnet").cache); - expect(paths.config).toEqual(getPaths("random", "mainnet").config); - expect(paths.data).toEqual(getPaths("random", "mainnet").data); - expect(paths.log).toEqual(getPaths("random", "mainnet").log); - expect(paths.temp).toEqual(getPaths("random", "mainnet").temp); - }); - - it("should use the given token via flags", async () => { - const { args, flags, paths } = await parseWithNetwork({ - args: [], - flags: { token: "ark", network: "mainnet" }, - }); - - expect(args).toEqual([]); - expect(flags).toEqual({ network: "mainnet", token: "ark" }); - expect(paths.cache).toEqual(getPaths("ark", "mainnet").cache); - expect(paths.config).toEqual(getPaths("ark", "mainnet").config); - expect(paths.data).toEqual(getPaths("ark", "mainnet").data); - expect(paths.log).toEqual(getPaths("ark", "mainnet").log); - expect(paths.temp).toEqual(getPaths("ark", "mainnet").temp); - }); - - it("should throw if the given configuration does not exist", async () => { - process.env.CORE_PATH_CONFIG = "does-not-exist"; - - await expect( - parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }), - ).rejects.toThrow(`The given config "${process.env.CORE_PATH_CONFIG}" does not exist.`); - - delete process.env.CORE_PATH_CONFIG; - }); - - it("should throw if no configurations can be detected", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - await expect( - parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }), - ).rejects.toThrow('We were unable to detect any configuration. Please run "ark config:publish" and try again.'); - - delete process.env.CORE_PATH_CONFIG; - }); - - it("should choose the first network if only a single network is found", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); - - const { flags } = await parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }); - - expect(flags).toEqual({ network: "mainnet", token: "ark" }); - - delete process.env.CORE_PATH_CONFIG; - }); - - it("should choose the selected network if multiple networks are found", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); - - prompts.inject(["devnet", true]); - - const { flags } = await parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }); - - expect(flags).toEqual({ network: "devnet", token: "ark" }); - - delete process.env.CORE_PATH_CONFIG; - }); - - it("should throw if the network selection is not confirmed", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); - - prompts.inject(["devnet", false]); - - await expect( - parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }), - ).rejects.toThrow("You'll need to confirm the network to continue."); - - delete process.env.CORE_PATH_CONFIG; - }); - - it("should throw if the network selection is not valid", async () => { - process.env.CORE_PATH_CONFIG = dirSync().name; - - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/mainnet`); - ensureDirSync(`${process.env.CORE_PATH_CONFIG}/devnet`); - - prompts.inject(["randomnet", true]); - - await expect( - parseWithNetwork({ - args: [], - flags: { token: "ark" }, - }), - ).rejects.toThrow(`The given network "randomnet" is not valid.`); - - delete process.env.CORE_PATH_CONFIG; - }); -}); diff --git a/__tests__/unit/core/common/snapshot.test.ts b/__tests__/unit/core/common/snapshot.test.ts deleted file mode 100644 index 6d57c2262a..0000000000 --- a/__tests__/unit/core/common/snapshot.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import prompts from "prompts"; - -import { chooseSnapshot } from "@packages/core/src/common/snapshot"; -import fs from "fs-extra"; - -/** - * Please note that this test scenario makes heavy use of mocks ONLY for the filesystem. - * - * Hitting the filesystem is expensive and slow while our only concern is if methods are - * called. The functionality of the filesystem actions is covered by the fs-extra tests. - * - * Another hidden cost is that tests that rely on real filesystems can be slow and fragile - * across platforms because of minor difference. - */ -describe("chooseSnapshot", () => { - it("should throw if the snapshots directory cannot be found", async () => { - await expect(chooseSnapshot("not-found", "message")).rejects.toThrow( - "The snapshots directory could not be found", - ); - }); - - it("should throw if no snapshots can be found", async () => { - const spyExistsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const spyReaddirSync = jest.spyOn(fs, "readdirSync").mockReturnValueOnce([]); - - await expect(chooseSnapshot("dataPath", "message")).rejects.toThrow("Failed to find any snapshots."); - - expect(spyExistsSync).toHaveBeenCalled(); - expect(spyReaddirSync).toHaveBeenCalled(); - }); - - it("should choose the first snapshot if only a single snapshot is found", async () => { - const spyExistsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const spyReaddirSync = jest - .spyOn(fs, "readdirSync") - // @ts-ignore - .mockReturnValueOnce(["1"]); - const spyLstatSync = jest - .spyOn(fs, "lstatSync") - // @ts-ignore - .mockReturnValue({ isDirectory: jest.fn().mockReturnValue(true) }); - - expect(await chooseSnapshot("stub", "message")).toBe("1"); - - expect(spyExistsSync).toHaveBeenCalled(); - expect(spyReaddirSync).toHaveBeenCalled(); - expect(spyLstatSync).toHaveBeenCalled(); - }); - - it("should choose the selected snapshot if multiple snapshots are found", async () => { - const spyExistsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const spyReaddirSync = jest - .spyOn(fs, "readdirSync") - // @ts-ignore - .mockReturnValueOnce(["1", "2"]); - const spyLstatSync = jest - .spyOn(fs, "lstatSync") - // @ts-ignore - .mockReturnValue({ isDirectory: jest.fn().mockReturnValue(true) }); - - prompts.inject(["2", true]); - - expect(await chooseSnapshot("stub", "message")).toBe("2"); - - expect(spyExistsSync).toHaveBeenCalled(); - expect(spyReaddirSync).toHaveBeenCalled(); - expect(spyLstatSync).toHaveBeenCalled(); - }); - - it("should throw if the snapshot selection is not confirmed", async () => { - const spyExistsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const spyReaddirSync = jest - .spyOn(fs, "readdirSync") - // @ts-ignore - .mockReturnValueOnce(["1", "2"]); - const spyLstatSync = jest - .spyOn(fs, "lstatSync") - // @ts-ignore - .mockReturnValue({ isDirectory: jest.fn().mockReturnValue(true) }); - - prompts.inject(["2", false]); - - await expect(chooseSnapshot("stub", "message")).rejects.toThrow( - "You'll need to confirm the snapshot to continue.", - ); - - expect(spyExistsSync).toHaveBeenCalled(); - expect(spyReaddirSync).toHaveBeenCalled(); - expect(spyLstatSync).toHaveBeenCalled(); - }); - - it("should throw if no snapshot is selected", async () => { - const spyExistsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const spyReaddirSync = jest - .spyOn(fs, "readdirSync") - // @ts-ignore - .mockReturnValueOnce(["1", "2"]); - const spyLstatSync = jest - .spyOn(fs, "lstatSync") - // @ts-ignore - .mockReturnValue({ isDirectory: jest.fn().mockReturnValue(true) }); - - prompts.inject([undefined, true]); - - await expect(chooseSnapshot("stub", "message")).rejects.toThrow("Please select a snapshot and try again."); - - expect(spyExistsSync).toHaveBeenCalled(); - expect(spyReaddirSync).toHaveBeenCalled(); - expect(spyLstatSync).toHaveBeenCalled(); - }); -}); diff --git a/__tests__/unit/core/common/utils.test.ts b/__tests__/unit/core/common/utils.test.ts deleted file mode 100644 index a15df8ec3f..0000000000 --- a/__tests__/unit/core/common/utils.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import "jest-extended"; - -import Table from "cli-table3"; -import envfile from "envfile"; -import fs from "fs-extra"; - -import { renderTable, updateEnvironmentVariables } from "@packages/core/src/common/utils"; - -describe("renderTable", () => { - it("should render a table with the given data", async () => { - let message: string; - jest.spyOn(console, "log").mockImplementationOnce(m => (message = m)); - - renderTable(["ID", "Name"], (table: Table.Table) => { - // @ts-ignore - table.push([1, "John Doe"]); - // @ts-ignore - table.push([2, "Jane Doe"]); - }); - - expect(message).toContain("ID"); - expect(message).toContain("Name"); - expect(message).toContain(1); - expect(message).toContain("John Doe"); - expect(message).toContain(2); - expect(message).toContain("Jane Doe"); - }); -}); - -describe("updateEnvironmentVariables", () => { - it("should throw if the given file does not exist", async () => { - expect(() => updateEnvironmentVariables("some-file", {})).toThrowError( - "No environment file found at some-file.", - ); - }); - - it("should write key-value pairs", async () => { - // Arrange - const existsSync = jest.spyOn(fs, "existsSync").mockReturnValueOnce(true); - const parseFileSync = jest.spyOn(envfile, "parseFileSync").mockImplementation(() => ({})); - const writeFileSync = jest.spyOn(fs, "writeFileSync").mockImplementation(); - - // Act - updateEnvironmentVariables("stub", { key: "value" }); - - // Assert - expect(existsSync).toHaveBeenCalledWith("stub"); - expect(parseFileSync).toHaveBeenCalledWith("stub"); - expect(writeFileSync).toHaveBeenCalledWith("stub", "key=value"); - - // Reset - existsSync.mockReset(); - parseFileSync.mockReset(); - writeFileSync.mockReset(); - }); -}); diff --git a/__tests__/unit/core/hooks/command_not_found/suggest.test.ts b/__tests__/unit/core/hooks/command_not_found/suggest.test.ts deleted file mode 100644 index 08e61fd33a..0000000000 --- a/__tests__/unit/core/hooks/command_not_found/suggest.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { init } from "@packages/core/src/hooks/command_not_found/suggest"; -import prompts from "prompts"; -import Chalk from "chalk"; - -describe("init", () => { - it("should immediately return if there are no command IDs", async () => { - // @ts-ignore - expect(await init({ id: "topic:command", config: { bin: "ark" } })).toBeUndefined(); - }); - - it("should update the bin help if a topic is found", async () => { - const findTopic = jest.fn().mockReturnValue(true); - const runCommand = jest.fn(); - const warn = jest.fn(); - - prompts.inject([true]); - - // @ts-ignore - await init.call( - // @ts-ignore - { warn }, - { id: "topic:command", config: { bin: "ark", commandIDs: ["topic:command1"], findTopic, runCommand } }, - ); - - expect(findTopic).toHaveBeenCalledWith("topic"); - expect(runCommand).toHaveBeenCalled(); - expect(warn).toHaveBeenCalledWith(`${Chalk.redBright("topic:command")} is not a ark command.`); - }); - - it("should throw if suggestion is not confirmed", async () => { - prompts.inject([false]); - - await expect( - // @ts-ignore - init.call( - // @ts-ignore - { warn: jest.fn() }, - { - id: "topic:command", - config: { bin: "ark", commandIDs: ["topic:command1"], findTopic: jest.fn(), runCommand: jest.fn() }, - }, - ), - ).rejects.toThrow(`Run ${Chalk.blueBright("ark help")} for a list of available commands.`); - }); -}); diff --git a/__tests__/unit/core/hooks/init/config.test.ts b/__tests__/unit/core/hooks/init/config.test.ts deleted file mode 100644 index b566576591..0000000000 --- a/__tests__/unit/core/hooks/init/config.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { dirSync, setGracefulCleanup } from "tmp"; - -import { configManager } from "@packages/core/src/common/config-manager"; -import { init } from "@packages/core/src/hooks/init/config"; - -afterAll(() => setGracefulCleanup()); - -describe("init", () => { - it("should load the configuration", async () => { - // @ts-ignore - await init({ config: { configDir: dirSync().name, version: "3.0.0", bin: "ark" } }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("latest"); - }); - - it("should on first start set the channel to [next] if the version contains [-next.X]", async () => { - // @ts-ignore - await init({ config: { configDir: dirSync().name, version: "3.0.0-next.1", bin: "ark" } }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("next"); - }); - - it("should on post-first start set the channel to [next] if the version contains [-next.X]", async () => { - const configDir: string = dirSync().name; - - const spySet = jest.spyOn(configManager, "set"); - - // @ts-ignore - await init({ config: { configDir, version: "3.0.0", bin: "ark" } }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("latest"); - - // @ts-ignore - await init({ config: { configDir, version: "3.0.0-next.1", bin: "ark" } }); - - expect(configManager.get("token")).toBe("ark"); - expect(configManager.get("channel")).toBe("next"); - - expect(spySet).toHaveBeenLastCalledWith("channel", "next"); - }); -}); diff --git a/__tests__/unit/core/hooks/init/update.test.ts b/__tests__/unit/core/hooks/init/update.test.ts deleted file mode 100644 index 3eab3ea2c6..0000000000 --- a/__tests__/unit/core/hooks/init/update.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import "jest-extended"; - -import nock from "nock"; -import { dirSync, setGracefulCleanup } from "tmp"; -import cli from "cli-ux"; -import Chalk from "chalk"; - -import { init } from "@packages/core/src/hooks/init/update"; -import { versionLatest } from "../../common/__fixtures__/latest-version"; - -beforeEach(() => nock.cleanAll()); - -beforeAll(() => nock.disableNetConnect()); - -afterAll(() => { - nock.enableNetConnect(); - - setGracefulCleanup(); -}); - -describe.skip("Hooks > Init > Update", () => { - it("should not check for updates if the update command is running", async () => { - // Arrange - const start = jest.spyOn(cli.action, "start"); - - // Act - // @ts-ignore - await init({ - id: "update", - config: { configDir: dirSync().name, version: "3.0.0", bin: "ark" }, - }); - - // Assert - expect(start).not.toHaveBeenCalled(); - - // Reset - start.mockReset(); - }); - - it("should report the availability of an update", async () => { - // Arrange - nock(/.*/) - .get("/@arkecosystem%2Fcore") - .reply(200, versionLatest); - - // const url = jest.spyOn(cli, "url"); - const warn = jest.fn(); - - const config = { - configDir: dirSync().name, - name: "@arkecosystem/core", - version: "2.0.0", - bin: "ark", - }; - - // Act - // @ts-ignore - await init.call( - // @ts-ignore - { warn, config }, - { - id: "non-update", - config, - }, - ); - - // Assert - expect(warn).toHaveBeenCalledWith( - `${config.name} update available from ${Chalk.greenBright("2.0.0")} to ${Chalk.greenBright( - "2.5.24", - )}. Review the latest release and run "ark update" once you wish to update.`, - ); - // expect(url).toHaveBeenCalledWith( - // "Click here to read the changelog for 2.5.24.", - // "https://github.com/ARKEcosystem/core/blob/master/CHANGELOG.md", - // ); - }); - - it("should report the unavailability of an update", async () => { - // Arrange - nock(/.*/) - .get("/@arkecosystem%2Fcore") - .reply(200, versionLatest); - - const warn = jest.fn(); - - const config = { - configDir: dirSync().name, - name: "@arkecosystem/core", - version: "3.0.0", - bin: "ark", - }; - - // Act - // @ts-ignore - await init.call( - // @ts-ignore - { warn, config }, - { - id: "non-update", - config, - }, - ); - - // Assert - expect(warn).not.toHaveBeenCalled(); - }); -}); diff --git a/__tests__/unit/core/internal/__fixtures__/app.js b/__tests__/unit/core/internal/__fixtures__/app.js new file mode 100644 index 0000000000..7b420028db --- /dev/null +++ b/__tests__/unit/core/internal/__fixtures__/app.js @@ -0,0 +1,8 @@ +module.exports = { + forger: { + plugins: [{ package: "@arkecosystem/core-forger" }], + }, + relay: { + plugins: [{ package: "@arkecosystem/core-forger" }], + }, +}; diff --git a/__tests__/unit/core/internal/__fixtures__/latest-version.ts b/__tests__/unit/core/internal/__fixtures__/latest-version.ts new file mode 100644 index 0000000000..dc16095dbb --- /dev/null +++ b/__tests__/unit/core/internal/__fixtures__/latest-version.ts @@ -0,0 +1,420 @@ +export const versionLatest = { + _id: "@arkecosystem/core", + _rev: "124-6952dbe729e4e231817e92cb9871148f", + name: "@arkecosystem/core", + "dist-tags": { + latest: "2.5.24", + next: "2.5.0-next.10", + }, + versions: { + "2.5.24": { + name: "@arkecosystem/core", + version: "2.5.24", + description: "Core of the ARK Blockchain", + license: "MIT", + contributors: [ + { + name: "François-Xavier Thoorens", + email: "fx@ark.io", + }, + + { + name: "Kristjan Košič", + email: "kristjan@ark.io", + }, + + { + name: "Brian Faust", + email: "brian@ark.io", + }, + + { + name: "Alex Barnsley", + email: "alex@ark.io", + }, + ], + main: "dist/index", + types: "dist/index", + bin: { + ark: "./bin/run", + }, + scripts: { + ark: "./bin/run", + build: "yarn clean && yarn compile && yarn copy", + "build:watch": "yarn clean && yarn copy && yarn compile -w", + clean: "del dist", + compile: "../../node_modules/typescript/bin/tsc", + copy: "cd ./src && cpy './config' '../dist/' --parents && cd ..", + "debug:forger": "node --inspect-brk yarn ark forger:run", + "debug:relay": "node --inspect-brk yarn ark relay:run", + "debug:start": "node --inspect-brk yarn ark core:run", + "forger:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark forger:run", + "forger:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark forger:run", + "forger:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark forger:run --env=test", + "full:testnet": + "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --networkStart --env=test", + prepack: "../../node_modules/.bin/oclif-dev manifest && npm shrinkwrap", + postpack: "rm -f oclif.manifest.json", + prepublishOnly: "yarn build", + "relay:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark relay:run", + "relay:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark relay:run", + "relay:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark relay:run --env=test", + "start:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark core:run", + "start:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark core:run", + "start:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --env=test", + }, + dependencies: { + "@arkecosystem/core-api": "^2.5.24", + "@arkecosystem/core-blockchain": "^2.5.24", + "@arkecosystem/core-container": "^2.5.24", + "@arkecosystem/core-database-postgres": "^2.5.24", + "@arkecosystem/core-event-emitter": "^2.5.24", + "@arkecosystem/core-exchange-json-rpc": "^2.5.24", + "@arkecosystem/core-forger": "^2.5.24", + "@arkecosystem/core-logger-pino": "^2.5.24", + "@arkecosystem/core-p2p": "^2.5.24", + "@arkecosystem/core-snapshots": "^2.5.24", + "@arkecosystem/core-state": "^2.5.24", + "@arkecosystem/core-transaction-pool": "^2.5.24", + "@arkecosystem/core-utils": "^2.5.24", + "@arkecosystem/core-wallet-api": "^2.5.24", + "@arkecosystem/core-webhooks": "^2.5.24", + "@arkecosystem/crypto": "^2.5.24", + "@oclif/command": "^1.5.16", + "@oclif/config": "^1.13.2", + "@oclif/plugin-autocomplete": "^0.1.1", + "@oclif/plugin-commands": "^1.2.2", + "@oclif/plugin-help": "^2.2.0", + "@oclif/plugin-not-found": "^1.2.2", + "@oclif/plugin-plugins": "^1.7.8", + "@typeskrift/foreman": "^0.2.1", + bip39: "^3.0.2", + bytebuffer: "^5.0.1", + chalk: "^2.4.2", + clear: "^0.1.0", + "cli-progress": "^2.1.1", + "cli-table3": "^0.5.1", + "cli-ux": "^5.3.1", + dayjs: "^1.8.15", + "env-paths": "^2.2.0", + envfile: "^3.0.0", + execa: "^2.0.3", + "fast-levenshtein": "^2.0.6", + "fs-extra": "^8.1.0", + "latest-version": "^5.1.0", + listr: "^0.14.3", + "lodash.minby": "^4.6.0", + "nodejs-tail": "^1.1.0", + "pretty-bytes": "^5.2.0", + "pretty-ms": "^5.0.0", + prompts: "^2.1.0", + "read-last-lines": "^1.7.1", + semver: "^6.2.0", + wif: "^2.0.6", + }, + devDependencies: { + "@types/bip39": "^2.4.2", + "@types/bytebuffer": "^5.0.40", + "@types/cli-progress": "^1.8.1", + "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "^8.0.0", + "@types/got": "^9.6.1", + "@types/listr": "^0.14.0", + "@types/lodash.minby": "^4.6.6", + "@types/pretty-ms": "^4.0.0", + "@types/prompts": "^2.4.0", + "@types/semver": "^6.0.1", + "@types/wif": "^2.0.1", + }, + engines: { + node: ">=10.x", + }, + publishConfig: { + access: "public", + }, + oclif: { + commands: "./dist/commands", + hooks: { + init: ["./dist/hooks/init/config", "./dist/hooks/init/update"], + command_not_found: ["./dist/hooks/command_not_found/suggest"], + }, + bin: "ark", + topics: { + config: { + description: "manage core config variables", + }, + env: { + description: "manage core environment variables", + }, + core: { + description: "manage a core instance (relay & forger)", + }, + forger: { + description: "manage a forger instance", + }, + relay: { + description: "manage a relay instance", + }, + snapshot: { + description: "manage a relay snapshots", + }, + chain: { + description: "commands to interact with the blockchain", + }, + network: { + description: "manage network configurations and tasks", + }, + }, + plugins: ["@oclif/plugin-autocomplete", "@oclif/plugin-commands", "@oclif/plugin-help"], + }, + _id: "@arkecosystem/core@2.5.24", + _nodeVersion: "10.16.0", + _npmVersion: "6.9.0", + dist: { + integrity: + "sha512-l6SfF1F7dGHda0yvSFx9prao7FLSR9HhWOyQvp9NVGAxERvw5ne21xkWwb5HEZyh5UGpt68JbmLvIuNPlvCAMg==", + shasum: "531cdc7e9d5be02a14b8572d427e6c7c0bf80de2", + tarball: "https://registry.npmjs.org/@arkecosystem/core/-/core-2.5.24.tgz", + fileCount: 192, + unpackedSize: 997808, + "npm-signature": + "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJdbymACRA9TVsSAnZWagAAvbwP/3+wSJgA1ya33pVBisdw\n3+tDZbz8xdBigUd8TGtHkt4rkv+nQNsUyfZMhNBhCq/hiSMskJH6FAn9ULOH\nw9730vS8T1OILqzJR9f3dau+UPc2JFgmyt/4fV7R0Xt/HyiTZfeNJyQN9C+g\nJu2Mm9P5lZYl2+/QvS2QW7ZiRy0S3gIG8p4HbJWyM/s/XHSxmQLCvHTthoQT\n62oyBctIzYRFXqblc1+L1B/JaQ1wkjDDoztfUqnupOcj/vrZcq8e2TB8L7Si\nUlPuRjjXDlqeJrKMkwL54rOzN/kOSiI27ULYbnBVWm+7RHrQ07bP9EyfvaHb\nEQzmfPJaI0d1Q71NVRhFircT2zdgzGs26PMj3lEh+9J8P0oCUEKqk2MZtQ3b\nsVr6xkG1EO5ShtJPeOtUuqvp72ZbcpA50EmhCYsdW9yAe2NyI6Ggz9GTMi1c\n+QCOeidbNoxfBWe6CYw0bgqlbGGm+0e3BnMe2/4gcgnxohYc9SqjOABo+j50\nFt4Xi9UJsNenI1fYZTMU0mCVhtg1mHVgDSatHARQc9v2zZhCZ5oEsc/oWcZA\nTmrQYKNukITZjTAHW5CyT4LlixtrlUjY8DHiWcIbOJOCX/Chx4IDaYmLwCRk\nEdSnFEb0BJnd9dpZ9sXA0OgShN45IKFg178BQWyouWmS8OKugDdfpwi+Hprt\njymO\r\n=wd14\r\n-----END PGP SIGNATURE-----\r\n", + }, + }, + }, +}; + +export const versionNext = { + _id: "@arkecosystem/core", + _rev: "124-6952dbe729e4e231817e92cb9871148f", + name: "@arkecosystem/core", + "dist-tags": { + latest: "2.5.24", + next: "2.5.0-next.10", + }, + versions: { + "2.5.0-next.10": { + name: "@arkecosystem/core", + version: "2.5.0-next.10", + description: "Core of the ARK Blockchain", + license: "MIT", + contributors: [ + { + name: "François-Xavier Thoorens", + email: "fx@ark.io", + }, + + { + name: "Kristjan Košič", + email: "kristjan@ark.io", + }, + + { + name: "Brian Faust", + email: "brian@ark.io", + }, + + { + name: "Alex Barnsley", + email: "alex@ark.io", + }, + ], + main: "dist/index", + types: "dist/index", + bin: { + ark: "./bin/run", + }, + scripts: { + ark: "./bin/run", + build: "yarn clean && yarn compile && yarn copy", + "build:watch": "yarn clean && yarn copy && yarn compile -w", + clean: "del dist", + compile: "../../node_modules/typescript/bin/tsc", + copy: "cd ./src && cpy './config' '../dist/' --parents && cd ..", + "debug:forger": "node --inspect-brk yarn ark forger:run", + "debug:relay": "node --inspect-brk yarn ark relay:run", + "debug:start": "node --inspect-brk yarn ark core:run", + "forger:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark forger:run", + "forger:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark forger:run", + "forger:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark forger:run --env=test", + "full:testnet": + "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --networkStart --env=test", + prepack: "../../node_modules/.bin/oclif-dev manifest && npm shrinkwrap", + postpack: "rm -f oclif.manifest.json", + prepublishOnly: "yarn build", + "relay:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark relay:run", + "relay:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark relay:run", + "relay:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark relay:run --env=test", + "start:devnet": "cross-env CORE_PATH_CONFIG=./bin/config/devnet yarn ark core:run", + "start:mainnet": "cross-env CORE_PATH_CONFIG=./bin/config/mainnet yarn ark core:run", + "start:testnet": "cross-env CORE_PATH_CONFIG=./bin/config/testnet yarn ark core:run --env=test", + }, + dependencies: { + "@arkecosystem/core-api": "^2.5.0-next.10", + "@arkecosystem/core-blockchain": "^2.5.0-next.10", + "@arkecosystem/core-container": "^2.5.0-next.10", + "@arkecosystem/core-database-postgres": "^2.5.0-next.10", + "@arkecosystem/core-event-emitter": "^2.5.0-next.10", + "@arkecosystem/core-exchange-json-rpc": "^2.5.0-next.10", + "@arkecosystem/core-forger": "^2.5.0-next.10", + "@arkecosystem/core-logger-pino": "^2.5.0-next.10", + "@arkecosystem/core-p2p": "^2.5.0-next.10", + "@arkecosystem/core-snapshots": "^2.5.0-next.10", + "@arkecosystem/core-state": "^2.5.0-next.10", + "@arkecosystem/core-transaction-pool": "^2.5.0-next.10", + "@arkecosystem/core-utils": "^2.5.0-next.10", + "@arkecosystem/core-wallet-api": "^2.5.0-next.10", + "@arkecosystem/core-webhooks": "^2.5.0-next.10", + "@arkecosystem/crypto": "^2.5.0-next.10", + "@oclif/command": "^1.5.16", + "@oclif/config": "^1.13.2", + "@oclif/plugin-autocomplete": "^0.1.1", + "@oclif/plugin-commands": "^1.2.2", + "@oclif/plugin-help": "^2.2.0", + "@oclif/plugin-not-found": "^1.2.2", + "@oclif/plugin-plugins": "^1.7.8", + "@typeskrift/foreman": "^0.2.1", + bip39: "^3.0.2", + bytebuffer: "^5.0.1", + chalk: "^2.4.2", + clear: "^0.1.0", + "cli-progress": "^2.1.1", + "cli-table3": "^0.5.1", + "cli-ux": "^5.3.1", + dayjs: "^1.8.15", + "env-paths": "^2.2.0", + envfile: "^3.0.0", + execa: "^2.0.3", + "fast-levenshtein": "^2.0.6", + "fs-extra": "^8.1.0", + "latest-version": "^5.1.0", + listr: "^0.14.3", + "lodash.minby": "^4.6.0", + "nodejs-tail": "^1.1.0", + "pretty-bytes": "^5.2.0", + "pretty-ms": "^5.0.0", + prompts: "^2.1.0", + "read-last-lines": "^1.7.1", + semver: "^6.2.0", + wif: "^2.0.6", + }, + devDependencies: { + "@types/bip39": "^2.4.2", + "@types/bytebuffer": "^5.0.40", + "@types/cli-progress": "^1.8.1", + "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "^8.0.0", + "@types/got": "^9.6.1", + "@types/listr": "^0.14.0", + "@types/lodash.minby": "^4.6.6", + "@types/pretty-ms": "^4.0.0", + "@types/prompts": "^2.4.0", + "@types/semver": "^6.0.1", + "@types/wif": "^2.0.1", + }, + engines: { + node: ">=10.x", + }, + publishConfig: { + access: "public", + }, + oclif: { + commands: "./dist/commands", + hooks: { + init: ["./dist/hooks/init/config", "./dist/hooks/init/update"], + command_not_found: ["./dist/hooks/command_not_found/suggest"], + }, + bin: "ark", + topics: { + config: { + description: "manage core config variables", + }, + env: { + description: "manage core environment variables", + }, + core: { + description: "manage a core instance (relay & forger)", + }, + forger: { + description: "manage a forger instance", + }, + relay: { + description: "manage a relay instance", + }, + snapshot: { + description: "manage a relay snapshots", + }, + chain: { + description: "commands to interact with the blockchain", + }, + network: { + description: "manage network configurations and tasks", + }, + }, + plugins: ["@oclif/plugin-autocomplete", "@oclif/plugin-commands", "@oclif/plugin-help"], + }, + readme: + '# ARK Core - Core\n\n

\n \n

\n\n## Documentation\n\nYou can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://docs.ark.io/guidebook/core/plugins/required/core.html).\n\n## Security\n\nIf you discover a security vulnerability within this package, please send an e-mail to security@ark.io. All security vulnerabilities will be promptly addressed.\n\n## Credits\n\nThis project exists thanks to all the people who [contribute](../../../../contributors).\n\n## License\n\n[MIT](LICENSE) © [ARK Ecosystem](https://ark.io)\n', + readmeFilename: "README.md", + _id: "@arkecosystem/core@2.5.0-next.10", + _nodeVersion: "10.16.0", + _npmVersion: "6.9.0", + dist: { + integrity: + "sha512-J7hNdeVVDj6QKlFwAtyVathZCGGvwGREDBfVr7zS9h7NG1RJUMRv+VDOQpRKAil6X7DHBFN3KgNqovAuUzoudQ==", + shasum: "fb977ee50721ee854e672f7de1410e479e5fcbc9", + tarball: "https://registry.npmjs.org/@arkecosystem/core/-/core-2.5.0-next.10.tgz", + fileCount: 192, + unpackedSize: 945581, + "npm-signature": + "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJdQBH0CRA9TVsSAnZWagAAgawP/1fcDjs9OYf6eBYIOHxi\nq8yh5fRdqxBDOC1h30ZSI+b6ovJ3FNLev3IWZWoK74IZZtZv0uIYpjSphWGM\nNRHXokau3jiATmQ1myKAm2yzkwmPdL31vqvVGHm9d0nK2HBUsDOzlL3ZomKs\ndmboDImAJ6WYMkp79R/J2hPryIjFhMDZMrxse8q1Hcfs4usacdZFA3oxhNFV\nz+kr5TIRdW1JsiiKTXSwq5P8f+NXoyLgnwEkLfC4yPIY6uWtDwko+NEHLDb/\n88r0tRr1wzSHJbyZtDm/i+0++ZAwrAfLIPlZu6VjPLH7xqEDfGoQC/dEdYG1\n2uOWcFgD3U/WyAXEbbarCgBdUJ3+xXbA01Nx76+QonT7S5rL1bt1y5Fwubmy\nf58aJnjrtQxk+vhUSHEltoT154m1olEidHjjDbanMwsVOWFOqa/u2fEHIUVC\nDl0yYnEJXuUUF0pPkqPwz5wrdNEqp6/kHOe8xaoAzQJF3PXBnmUZO4ZgvccB\nGVPlpu/lsPpeHK2WiE6rsO83ntpynmppnmKC1gw/0VRYkVweWClAKKEALcYa\nBuFjA/+QKpHB7Wem/YdZv0TwC0lVMV+TRU+HUZzcIX+juy7GdbcDULAowYti\nYMjXGnxPmvZePSrFD2/QVNZ6uYBJ+kmoMTAYNNvCCcFuDA5HlsVBc3OMTysG\nK/KF\r\n=pjde\r\n-----END PGP SIGNATURE-----\r\n", + }, + maintainers: [ + { + email: "alex@barnsleyservices.com", + name: "alexbarnsley", + }, + + { + email: "fx.thoorens@ark.io", + name: "arkio", + }, + + { + email: "hello@basecode.sh", + name: "faustbrian", + }, + + { + email: "dev@jaml.pro", + name: "j-a-m-l", + }, + + { + email: "kristjan@ark.io", + name: "kristjankosic", + }, + + { + email: "luciorubeens@gmail.com", + name: "luciorubeens", + }, + + { + email: "joshua@ark.io", + name: "supaiku", + }, + ], + _npmUser: { + name: "faustbrian", + email: "hello@basecode.sh", + }, + directories: {}, + _npmOperationalInternal: { + host: "s3://npm-registry-packages", + tmp: "tmp/core_2.5.0-next.10_1564479986828_0.13168983569100656", + }, + _hasShrinkwrap: false, + }, + }, +}; diff --git a/__tests__/unit/core/common/crypto.test.ts b/__tests__/unit/core/internal/crypto.test.ts similarity index 97% rename from __tests__/unit/core/common/crypto.test.ts rename to __tests__/unit/core/internal/crypto.test.ts index 249cea36e6..9c8f4f9e21 100644 --- a/__tests__/unit/core/common/crypto.test.ts +++ b/__tests__/unit/core/internal/crypto.test.ts @@ -1,7 +1,7 @@ import { dirSync, setGracefulCleanup } from "tmp"; import prompts from "prompts"; -import { buildBIP38 } from "@packages/core/src/common/crypto"; +import { buildBIP38 } from "@packages/core/src/internal/crypto"; import { writeJSONSync } from "fs-extra"; beforeEach(() => (process.env.CORE_PATH_CONFIG = dirSync().name)); diff --git a/__tests__/unit/core/services/plugins/sources/file.test.ts b/__tests__/unit/core/source-providers/file.test.ts similarity index 95% rename from __tests__/unit/core/services/plugins/sources/file.test.ts rename to __tests__/unit/core/source-providers/file.test.ts index f430cf6793..8d638403fd 100644 --- a/__tests__/unit/core/services/plugins/sources/file.test.ts +++ b/__tests__/unit/core/source-providers/file.test.ts @@ -4,7 +4,7 @@ import { dirSync, fileSync, setGracefulCleanup } from "tmp"; import fs from "fs-extra"; import tar from "tar"; -import { File } from "@packages/core/src/services/plugins/sources"; +import { File } from "@packages/core/src/source-providers"; let dataPath: string; let source: File; diff --git a/__tests__/unit/core/services/plugins/sources/git.test.ts b/__tests__/unit/core/source-providers/git.test.ts similarity index 94% rename from __tests__/unit/core/services/plugins/sources/git.test.ts rename to __tests__/unit/core/source-providers/git.test.ts index a168445b8f..bab3a041d0 100644 --- a/__tests__/unit/core/services/plugins/sources/git.test.ts +++ b/__tests__/unit/core/source-providers/git.test.ts @@ -3,9 +3,9 @@ import "jest-extended"; import { parseGitUrl } from "@arkecosystem/utils"; import { dirSync, setGracefulCleanup } from "tmp"; import fs from "fs-extra"; -import execa from "../../../../../../__mocks__/execa"; +import execa from "../../../../__mocks__/execa"; -import { Git } from "@packages/core/src/services/plugins/sources"; +import { Git } from "@packages/core/src/source-providers"; let dataPath: string; let source: Git; diff --git a/__tests__/unit/core/services/plugins/sources/npm.test.ts b/__tests__/unit/core/source-providers/npm.test.ts similarity index 98% rename from __tests__/unit/core/services/plugins/sources/npm.test.ts rename to __tests__/unit/core/source-providers/npm.test.ts index 5e77c985a5..f232b6d697 100644 --- a/__tests__/unit/core/services/plugins/sources/npm.test.ts +++ b/__tests__/unit/core/source-providers/npm.test.ts @@ -4,7 +4,7 @@ import { dirSync, setGracefulCleanup } from "tmp"; import fs from "fs-extra"; import nock from "nock"; -import { NPM } from "@packages/core/src/services/plugins/sources"; +import { NPM } from "@packages/core/src/source-providers"; import { resolve } from "path"; let dataPath: string; diff --git a/__tests__/unit/core/services/plugins/sources/utils-0.9.1.tgz b/__tests__/unit/core/source-providers/utils-0.9.1.tgz similarity index 100% rename from __tests__/unit/core/services/plugins/sources/utils-0.9.1.tgz rename to __tests__/unit/core/source-providers/utils-0.9.1.tgz diff --git a/jest.config.js b/jest.config.js index 039cd4f046..fe1b2057f4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,7 +14,8 @@ module.exports = { collectCoverage: false, coverageDirectory: "/.coverage", collectCoverageFrom: [ - "packages/**/src/**/{!(index|manager),}.ts", + // "packages/**/src/**/{!(index|manager),}.ts", + "packages/core-cli/src/**/{!(index|manager),}.ts", "!packages/**/src/**/contracts/**", "!packages/**/src/**/enums/**", "!packages/**/src/**/exceptions/**", diff --git a/package.json b/package.json index 3a984d8b7f..ed226a5941 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@types/js-yaml": "^3.12.1", "@types/node": "^12.12.14", "@types/prettier": "^1.19.0", - "@types/prompts": "^2.4.0", "@types/rimraf": "^2.0.3", "@types/uuid": "^3.4.6", "@typescript-eslint/eslint-plugin": "^2.10.0", @@ -93,7 +92,6 @@ "nock": "^11.7.0", "npm-check-updates": "^3.2.2", "prettier": "^1.19.1", - "prompts": "^2.2.1", "rimraf": "^3.0.0", "sinon": "^7.5.0", "tmp": "^0.1.0", diff --git a/packages/core-cli/.gitattributes b/packages/core-cli/.gitattributes new file mode 100644 index 0000000000..63f6a5b970 --- /dev/null +++ b/packages/core-cli/.gitattributes @@ -0,0 +1,7 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/README.md export-ignore diff --git a/packages/core-cli/package.json b/packages/core-cli/package.json new file mode 100644 index 0000000000..4ce11d3319 --- /dev/null +++ b/packages/core-cli/package.json @@ -0,0 +1,60 @@ +{ + "name": "@arkecosystem/core-cli", + "version": "3.0.0-next.0", + "description": "Core of the ARK Blockchain", + "license": "MIT", + "contributors": [ + "Brian Faust " + ], + "files": [ + "/dist" + ], + "main": "dist/index", + "types": "dist/index", + "scripts": { + "ark": "./bin/run", + "build": "yarn clean && yarn compile && yarn copy", + "build:watch": "yarn clean && yarn copy && yarn compile -w", + "build:docs": "../../node_modules/typedoc/bin/typedoc --out docs src", + "clean": "del dist", + "compile": "../../node_modules/typescript/bin/tsc", + "copy": "cd ./src && cpy './config' '../dist/' --parents && cd ..", + "prepublishOnly": "yarn build" + }, + "dependencies": { + "@arkecosystem/core-kernel": "^3.0.0-next.0", + "@arkecosystem/crypto": "^3.0.0-next.0", + "@arkecosystem/utils": "^1.1.7", + "@hapi/joi": "^16.1.8", + "boxen": "^4.2.0", + "cli-table3": "^0.5.1", + "dayjs": "^1.8.17", + "env-paths": "^2.2.0", + "envfile": "^4.4.0", + "execa": "^3.4.0", + "fast-levenshtein": "^2.0.6", + "fs-extra": "^8.1.0", + "is-ci": "^2.0.0", + "kleur": "^3.0.3", + "latest-version": "^5.1.0", + "listr": "^0.14.3", + "ora": "^4.0.3", + "semver": "^6.3.0", + "type-fest": "^0.8.1", + "yargs-parser": "^16.1.0" + }, + "devDependencies": { + "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "^8.0.1", + "@types/is-ci": "^2.0.0", + "@types/listr": "^0.14.2", + "@types/semver": "^6.2.0", + "@types/yargs-parser": "^13.1.0" + }, + "engines": { + "node": ">=10.x" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/core-cli/src/action-factory.ts b/packages/core-cli/src/action-factory.ts new file mode 100644 index 0000000000..3f2558ac4c --- /dev/null +++ b/packages/core-cli/src/action-factory.ts @@ -0,0 +1,112 @@ +import { + AbortErroredProcess, + AbortMissingProcess, + AbortRunningProcess, + AbortStoppedProcess, + AbortUnknownProcess, + DaemonizeProcess, + RestartProcess, + RestartRunningProcess, + RestartRunningProcessWithPrompt, +} from "./actions"; +import { Application, ProcessOptions } from "./contracts"; +import { Identifiers, inject, injectable } from "./ioc"; + +/** + * @export + * @class ActionFactory + */ +@injectable() +export class ActionFactory { + /** + * @private + * @type {Application} + * @memberof ActionFactory + */ + @inject(Identifiers.Application) + protected readonly app!: Application; + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public abortErroredProcess(processName: string): void { + return this.app.get(Identifiers.AbortErroredProcess).execute(processName); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public abortMissingProcess(processName: string): void { + return this.app.get(Identifiers.AbortMissingProcess).execute(processName); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public abortRunningProcess(processName: string): void { + return this.app.get(Identifiers.AbortRunningProcess).execute(processName); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public abortStoppedProcess(processName: string): void { + return this.app.get(Identifiers.AbortStoppedProcess).execute(processName); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public abortUnknownProcess(processName: string): void { + return this.app.get(Identifiers.AbortUnknownProcess).execute(processName); + } + + /** + * @param {ProcessOptions} options + * @param {*} flags + * @returns {Promise} + * @memberof ActionFactory + */ + public async daemonizeProcess(options: ProcessOptions, flags): Promise { + return this.app.get(Identifiers.DaemonizeProcess).execute(options, flags); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public restartProcess(processName: string): void { + return this.app.get(Identifiers.RestartProcess).execute(processName); + } + + /** + * @param {string} processName + * @returns {Promise} + * @memberof ActionFactory + */ + public async restartRunningProcessWithPrompt(processName: string): Promise { + return this.app + .get(Identifiers.RestartRunningProcessWithPrompt) + .execute(processName); + } + + /** + * @param {string} processName + * @returns {void} + * @memberof ActionFactory + */ + public restartRunningProcess(processName: string): void { + return this.app.get(Identifiers.RestartRunningProcess).execute(processName); + } +} diff --git a/packages/core-cli/src/actions/abort-errored-process.ts b/packages/core-cli/src/actions/abort-errored-process.ts new file mode 100644 index 0000000000..2f8c30d018 --- /dev/null +++ b/packages/core-cli/src/actions/abort-errored-process.ts @@ -0,0 +1,28 @@ +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class AbortErroredProcess + */ +@injectable() +export class AbortErroredProcess { + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof AbortErroredProcess + */ + public execute(processName: string): void { + if (this.processManager.isErrored(processName)) { + throw new Error(`The "${processName}" process has errored.`); + } + } +} diff --git a/packages/core-cli/src/actions/abort-missing-process.ts b/packages/core-cli/src/actions/abort-missing-process.ts new file mode 100644 index 0000000000..1c41610300 --- /dev/null +++ b/packages/core-cli/src/actions/abort-missing-process.ts @@ -0,0 +1,28 @@ +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class AbortMissingProcess + */ +@injectable() +export class AbortMissingProcess { + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof AbortMissingProcess + */ + public execute(processName: string): void { + if (this.processManager.missing(processName)) { + throw new Error(`The "${processName}" process does not exist.`); + } + } +} diff --git a/packages/core-cli/src/actions/abort-running-process.ts b/packages/core-cli/src/actions/abort-running-process.ts new file mode 100644 index 0000000000..425a96e8b4 --- /dev/null +++ b/packages/core-cli/src/actions/abort-running-process.ts @@ -0,0 +1,28 @@ +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class AbortRunningProcess + */ +@injectable() +export class AbortRunningProcess { + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof AbortRunningProcess + */ + public execute(processName: string): void { + if (this.processManager.isOnline(processName)) { + throw new Error(`The "${processName}" process is already running.`); + } + } +} diff --git a/packages/core-cli/src/actions/abort-stopped-process.ts b/packages/core-cli/src/actions/abort-stopped-process.ts new file mode 100644 index 0000000000..849ac32036 --- /dev/null +++ b/packages/core-cli/src/actions/abort-stopped-process.ts @@ -0,0 +1,28 @@ +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class AbortStoppedProcess + */ +@injectable() +export class AbortStoppedProcess { + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof AbortStoppedProcess + */ + public execute(processName: string): void { + if (this.processManager.isStopped(processName)) { + throw new Error(`The "${processName}" process is not running.`); + } + } +} diff --git a/packages/core-cli/src/actions/abort-unknown-process.ts b/packages/core-cli/src/actions/abort-unknown-process.ts new file mode 100644 index 0000000000..bbf747ecdf --- /dev/null +++ b/packages/core-cli/src/actions/abort-unknown-process.ts @@ -0,0 +1,28 @@ +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class AbortUnknownProcess + */ +@injectable() +export class AbortUnknownProcess { + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof AbortUnknownProcess + */ + public execute(processName: string): void { + if (this.processManager.isUnknown(processName)) { + throw new Error(`The "${processName}" process has entered an unknown state.`); + } + } +} diff --git a/packages/core-cli/src/actions/daemonize-process.ts b/packages/core-cli/src/actions/daemonize-process.ts new file mode 100644 index 0000000000..24252fee0b --- /dev/null +++ b/packages/core-cli/src/actions/daemonize-process.ts @@ -0,0 +1,85 @@ +import { freemem, totalmem } from "os"; + +import { Application } from "../application"; +import { Spinner } from "../components"; +import { ProcessOptions } from "../contracts"; +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; +import { AbortRunningProcess } from "./abort-running-process"; +import { AbortUnknownProcess } from "./abort-unknown-process"; + +/** + * @export + * @class DaemonizeProcess + */ +@injectable() +export class DaemonizeProcess { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + private readonly app!: Application; + + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {ProcessOptions} options + * @param {*} flags + * @memberof DaemonizeProcess + */ + public execute(options: ProcessOptions, flags): void { + const processName: string = options.name; + + if (this.processManager.has(processName)) { + this.app.get(Identifiers.AbortUnknownProcess).execute(processName); + this.app.get(Identifiers.AbortRunningProcess).execute(processName); + } + + let spinner; + try { + spinner = this.app.get(Identifiers.Spinner).render(`Starting ${processName}`); + + const flagsProcess: Record = { + "max-restarts": 5, + "kill-timeout": 30000, + }; + + if (!flags.daemon) { + flagsProcess["no-daemon"] = true; + } + + flagsProcess.name = processName; + + const totalMemGb: number = totalmem() / Math.pow(1024, 3); + const freeMemGb: number = freemem() / Math.pow(1024, 3); + const potato: boolean = totalMemGb < 2 || freeMemGb < 1.5; + + this.processManager.start( + { + ...options, + ...{ + env: { + NODE_ENV: "production", + CORE_ENV: flags.env, + }, + node_args: potato ? { max_old_space_size: 500 } : undefined, + }, + }, + flagsProcess, + ); + } catch (error) { + throw new Error(error.stderr ? `${error.message}: ${error.stderr}` : error.message); + } finally { + spinner.stop(); + } + } +} diff --git a/packages/core-cli/src/actions/index.ts b/packages/core-cli/src/actions/index.ts new file mode 100644 index 0000000000..7a31e82f79 --- /dev/null +++ b/packages/core-cli/src/actions/index.ts @@ -0,0 +1,9 @@ +export * from "./abort-running-process"; +export * from "./abort-errored-process"; +export * from "./abort-missing-process"; +export * from "./abort-stopped-process"; +export * from "./abort-unknown-process"; +export * from "./daemonize-process"; +export * from "./restart-process"; +export * from "./restart-running-process-with-prompt"; +export * from "./restart-running-process"; diff --git a/packages/core-cli/src/actions/restart-process.ts b/packages/core-cli/src/actions/restart-process.ts new file mode 100644 index 0000000000..fcf4c19c0e --- /dev/null +++ b/packages/core-cli/src/actions/restart-process.ts @@ -0,0 +1,45 @@ +import { Application } from "../application"; +import { Spinner } from "../components"; +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; + +/** + * @export + * @class RestartProcess + */ +@injectable() +export class RestartProcess { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + private readonly app!: Application; + + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof RestartProcess + */ + public execute(processName: string): void { + let spinner; + try { + spinner = this.app.get(Identifiers.Spinner).render(`Restarting ${processName}`); + + this.processManager.restart(processName); + } catch (error) { + throw new Error(error.stderr ? `${error.message}: ${error.stderr}` : error.message); + } finally { + spinner.stop(); + } + } +} diff --git a/packages/core-cli/src/actions/restart-running-process-with-prompt.ts b/packages/core-cli/src/actions/restart-running-process-with-prompt.ts new file mode 100644 index 0000000000..7a20ae58c9 --- /dev/null +++ b/packages/core-cli/src/actions/restart-running-process-with-prompt.ts @@ -0,0 +1,50 @@ +import { Application } from "../application"; +import { Prompt } from "../components"; +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; +import { RestartProcess } from "./restart-process"; + +/** + * @export + * @class RestartRunningProcessWithPrompt + */ +@injectable() +export class RestartRunningProcessWithPrompt { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + private readonly app!: Application; + + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @returns {Promise} + * @memberof RestartRunningProcessWithPrompt + */ + public async execute(processName: string): Promise { + if (this.processManager.isOnline(processName)) { + const { confirm } = await this.app.resolve(Prompt).render([ + { + type: "confirm", + name: "confirm", + message: `Would you like to restart the ${processName} process?`, + }, + ]); + + if (confirm) { + this.app.get(Identifiers.RestartProcess).execute(processName); + } + } + } +} diff --git a/packages/core-cli/src/actions/restart-running-process.ts b/packages/core-cli/src/actions/restart-running-process.ts new file mode 100644 index 0000000000..e224d11a77 --- /dev/null +++ b/packages/core-cli/src/actions/restart-running-process.ts @@ -0,0 +1,38 @@ +import { Application } from "../application"; +import { Identifiers, inject, injectable } from "../ioc"; +import { ProcessManager } from "../services"; +import { RestartProcess } from "./restart-process"; + +/** + * @export + * @class RestartRunningProcess + */ +@injectable() +export class RestartRunningProcess { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + private readonly app!: Application; + + /** + * @private + * @type {ProcessManager} + * @memberof Command + */ + @inject(Identifiers.ProcessManager) + private readonly processManager!: ProcessManager; + + /** + * @static + * @param {string} processName + * @memberof RestartRunningProcess + */ + public execute(processName: string): void { + if (this.processManager.isOnline(processName)) { + this.app.get(Identifiers.RestartProcess).execute(processName); + } + } +} diff --git a/packages/core-cli/src/application-factory.ts b/packages/core-cli/src/application-factory.ts new file mode 100644 index 0000000000..51b34fe980 --- /dev/null +++ b/packages/core-cli/src/application-factory.ts @@ -0,0 +1,262 @@ +import envPaths from "env-paths"; +import { PackageJson } from "type-fest"; + +import { ActionFactory } from "./action-factory"; +import { + AbortErroredProcess, + AbortMissingProcess, + AbortRunningProcess, + AbortStoppedProcess, + AbortUnknownProcess, + DaemonizeProcess, + RestartProcess, + RestartRunningProcess, + RestartRunningProcessWithPrompt, +} from "./actions"; +import { Application } from "./application"; +import { ComponentFactory } from "./component-factory"; +import { + AppHeader, + Ask, + AskDate, + AskHidden, + AskNumber, + AskPassword, + AutoComplete, + Box, + Clear, + Confirm, + Error, + Fatal, + Info, + Listing, + Log, + MultiSelect, + NewLine, + Prompt, + Select, + Spinner, + Success, + Table, + TaskList, + Title, + Toggle, + Warning, +} from "./components"; +import { Input, InputValidator } from "./input"; +import { Container, Identifiers, interfaces } from "./ioc"; +import { Output } from "./output"; +import { Config, Environment, Installer, Logger, ProcessManager, Updater } from "./services"; +import { Process } from "./utils"; + +export class ApplicationFactory { + public static make(container: Container, pkg: PackageJson): Application { + const app: Application = new Application(container); + + // Package + app.bind(Identifiers.Package).toConstantValue(pkg); + + // Paths + app.bind(Identifiers.ConsolePaths).toConstantValue(envPaths(pkg.name!)); + + // Factories + app.bind(Identifiers.ActionFactory) + .to(ActionFactory) + .inSingletonScope(); + + app.bind(Identifiers.ComponentFactory) + .to(ComponentFactory) + .inSingletonScope(); + + app.bind(Identifiers.ProcessFactory).toFactory( + (context: interfaces.Context) => (token: string, type: string): Process => { + const process: Process = context.container.resolve(Process); + process.initialize(token, type); + + return process; + }, + ); + + // Services + app.bind(Identifiers.Output) + .to(Output) + .inSingletonScope(); + + app.bind(Identifiers.Logger) + .to(Logger) + .inSingletonScope(); + + app.bind(Identifiers.Config) + .to(Config) + .inSingletonScope(); + + app.bind(Identifiers.Updater) + .to(Updater) + .inSingletonScope(); + + app.bind(Identifiers.ProcessManager) + .to(ProcessManager) + .inSingletonScope(); + + app.bind(Identifiers.Installer) + .to(Installer) + .inSingletonScope(); + + app.bind(Identifiers.Environment) + .to(Environment) + .inSingletonScope(); + + // Input + app.bind(Identifiers.Input) + .to(Input) + .inSingletonScope(); + + app.bind(Identifiers.InputValidator) + .to(InputValidator) + .inSingletonScope(); + + // Actions + app.bind(Identifiers.AbortErroredProcess) + .to(AbortErroredProcess) + .inSingletonScope(); + + app.bind(Identifiers.AbortMissingProcess) + .to(AbortMissingProcess) + .inSingletonScope(); + + app.bind(Identifiers.AbortRunningProcess) + .to(AbortRunningProcess) + .inSingletonScope(); + + app.bind(Identifiers.AbortStoppedProcess) + .to(AbortStoppedProcess) + .inSingletonScope(); + + app.bind(Identifiers.AbortUnknownProcess) + .to(AbortUnknownProcess) + .inSingletonScope(); + + app.bind(Identifiers.DaemonizeProcess) + .to(DaemonizeProcess) + .inSingletonScope(); + + app.bind(Identifiers.RestartProcess) + .to(RestartProcess) + .inSingletonScope(); + + app.bind(Identifiers.RestartRunningProcess) + .to(RestartRunningProcess) + .inSingletonScope(); + + app.bind(Identifiers.RestartRunningProcessWithPrompt) + .to(RestartRunningProcessWithPrompt) + .inSingletonScope(); + + // Components + app.bind(Identifiers.AppHeader) + .to(AppHeader) + .inSingletonScope(); + + app.bind(Identifiers.Ask) + .to(Ask) + .inSingletonScope(); + + app.bind(Identifiers.AskDate) + .to(AskDate) + .inSingletonScope(); + + app.bind(Identifiers.AskHidden) + .to(AskHidden) + .inSingletonScope(); + + app.bind(Identifiers.AskNumber) + .to(AskNumber) + .inSingletonScope(); + + app.bind(Identifiers.AskPassword) + .to(AskPassword) + .inSingletonScope(); + + app.bind(Identifiers.AutoComplete) + .to(AutoComplete) + .inSingletonScope(); + + app.bind(Identifiers.Box) + .to(Box) + .inSingletonScope(); + + app.bind(Identifiers.Clear) + .to(Clear) + .inSingletonScope(); + + app.bind(Identifiers.Confirm) + .to(Confirm) + .inSingletonScope(); + + app.bind(Identifiers.Error) + .to(Error) + .inSingletonScope(); + + app.bind(Identifiers.Fatal) + .to(Fatal) + .inSingletonScope(); + + app.bind(Identifiers.Info) + .to(Info) + .inSingletonScope(); + + app.bind(Identifiers.Listing) + .to(Listing) + .inSingletonScope(); + + app.bind(Identifiers.Log) + .to(Log) + .inSingletonScope(); + + app.bind(Identifiers.MultiSelect) + .to(MultiSelect) + .inSingletonScope(); + + app.bind(Identifiers.NewLine) + .to(NewLine) + .inSingletonScope(); + + app.bind(Identifiers.Prompt) + .to(Prompt) + .inSingletonScope(); + + app.bind(Identifiers.Select) + .to(Select) + .inSingletonScope(); + + app.bind(Identifiers.Spinner) + .to(Spinner) + .inSingletonScope(); + + app.bind(Identifiers.Success) + .to(Success) + .inSingletonScope(); + + app.bind(Identifiers.Table) + .to(Table) + .inSingletonScope(); + + app.bind(Identifiers.TaskList) + .to(TaskList) + .inSingletonScope(); + + app.bind(Identifiers.Title) + .to(Title) + .inSingletonScope(); + + app.bind(Identifiers.Toggle) + .to(Toggle) + .inSingletonScope(); + + app.bind(Identifiers.Warning) + .to(Warning) + .inSingletonScope(); + + return app; + } +} diff --git a/packages/core-cli/src/application.ts b/packages/core-cli/src/application.ts new file mode 100644 index 0000000000..f51f5e71c3 --- /dev/null +++ b/packages/core-cli/src/application.ts @@ -0,0 +1,50 @@ +import { Paths } from "env-paths"; +import { resolve } from "path"; + +import { Identifiers, interfaces } from "./ioc"; + +export class Application { + public constructor(private readonly container: interfaces.Container) { + this.container.bind(Identifiers.Application).toConstantValue(this); + } + + public bind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax { + return this.container.bind(serviceIdentifier); + } + + public rebind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax { + if (this.container.isBound(serviceIdentifier)) { + this.container.unbind(serviceIdentifier); + } + + return this.container.bind(serviceIdentifier); + } + + public unbind(serviceIdentifier: interfaces.ServiceIdentifier): void { + return this.container.unbind(serviceIdentifier); + } + + public get(serviceIdentifier: interfaces.ServiceIdentifier): T { + return this.container.get(serviceIdentifier); + } + + public isBound(serviceIdentifier: interfaces.ServiceIdentifier): boolean { + return this.container.isBound(serviceIdentifier); + } + + public resolve(constructorFunction: interfaces.Newable): T { + return this.container.resolve(constructorFunction); + } + + public getCorePath(type: string, file?: string): string { + const path: string = this.get(Identifiers.ApplicationPaths)[type]; + + return resolve(file ? `${path}/${file}` : path); + } + + public getConsolePath(type: string, file?: string): string { + const path: string = this.get(Identifiers.ConsolePaths)[type]; + + return resolve(file ? `${path}/${file}` : path); + } +} diff --git a/packages/core-cli/src/commands/command-help.ts b/packages/core-cli/src/commands/command-help.ts new file mode 100644 index 0000000000..8b49b0d134 --- /dev/null +++ b/packages/core-cli/src/commands/command-help.ts @@ -0,0 +1,123 @@ +import { blue } from "kleur"; +import { PackageJson } from "type-fest"; + +import { AppHeader } from "../components"; +import { Application } from "../contracts"; +import { Identifiers, inject, injectable } from "../ioc"; + +/** + * @export + * @class CommandHelp + */ +@injectable() +export class CommandHelp { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + protected readonly app!: Application; + + /** + * @private + * @type {Application} + * @memberof DiscoverCommands + */ + @inject(Identifiers.Package) + protected readonly pkg!: PackageJson; + + /** + * @returns {string} + * @memberof CommandHelp + */ + public render(command): string { + let helpMessage: string = `${this.app.get(Identifiers.AppHeader).render()} + +${blue().bold("Description")} +${command.description}`; + + const args: string = this.buildArguments(command); + + if (args) { + helpMessage += `${blue().bold("\n\nArguments")} +${args}`; + } + + const flags: string = this.buildFlags(command); + + if (flags) { + helpMessage += `${blue().bold("\n\nFlags")} +${flags}`; + } + + return helpMessage; + } + + /** + * @private + * @returns {string} + * @memberof CommandHelp + */ + private buildArguments(command): string { + const args = command.definition.getArguments(); + + if (Object.keys(args).length <= 0) { + return ""; + } + + const { options, descriptions, longestProperty } = this.buildProperties(args); + + const output: string[] = []; + for (let i = 0; i < options.length; i++) { + output.push(`${options[i].padEnd(longestProperty, " ")} ${descriptions[i]}`); + } + + return output.join("\n"); + } + + /** + * @private + * @returns {string} + * @memberof CommandHelp + */ + private buildFlags(command): string { + const flags = command.definition.getFlags(); + + if (Object.keys(flags).length <= 0) { + return ""; + } + + const { options, descriptions, longestProperty } = this.buildProperties(flags); + + const output: string[] = []; + for (let i = 0; i < options.length; i++) { + output.push(`--${options[i].padEnd(longestProperty, " ")} ${descriptions[i]}`); + } + + return output.join("\n"); + } + + /** + * @private + * @template T + * @param {T} properties + * @returns + * @memberof CommandHelp + */ + private buildProperties(properties: T) { + const options: string[] = []; + const descriptions: string[] = []; + + for (const option of Object.keys(properties)) { + options.push(option); + descriptions.push(properties[option].description); + } + + return { + options, + descriptions, + longestProperty: options.reduce((a, b) => (a.length > b.length ? a : b)).length, + }; + } +} diff --git a/packages/core-cli/src/commands/command.ts b/packages/core-cli/src/commands/command.ts new file mode 100644 index 0000000000..b0e3157979 --- /dev/null +++ b/packages/core-cli/src/commands/command.ts @@ -0,0 +1,330 @@ +import envPaths from "env-paths"; +import { PackageJson } from "type-fest"; + +import { ActionFactory } from "../action-factory"; +import { ComponentFactory } from "../component-factory"; +import { Box } from "../components"; +import { Application } from "../contracts"; +import { Input } from "../input"; +import { InputDefinition } from "../input/definition"; +import { Identifiers, inject, injectable, postConstruct } from "../ioc"; +import { Output } from "../output"; +import { Config, Environment } from "../services"; +import { CommandHelp } from "./command-help"; +import { DiscoverNetwork } from "./discover-network"; + +/** + * @export + * @abstract + * @class Command + */ +@injectable() +export abstract class Command { + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Application) + protected readonly app!: Application; + + /** + * @private + * @type {Environment} + * @memberof Command + */ + @inject(Identifiers.Environment) + protected readonly env!: Environment; + + /** + * @private + * @type {Output} + * @memberof Command + */ + @inject(Identifiers.Output) + protected readonly output!: Output; + + /** + * @private + * @type {Contracts.Config} + * @memberof Command + */ + @inject(Identifiers.Config) + protected readonly config!: Config; + + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.Package) + protected readonly pkg!: PackageJson; + + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.ActionFactory) + protected readonly actions!: ActionFactory; + + /** + * @private + * @type {Application} + * @memberof Command + */ + @inject(Identifiers.ComponentFactory) + protected readonly components!: ComponentFactory; + + /** + * @type {InputDefinition} + * @memberof Command + */ + protected definition: InputDefinition = new InputDefinition(); + + /** + * @type {Input} + * @memberof Command + */ + protected input!: Input; + + /** + * The console command signature. + * + * @type {string} + * @memberof Command + */ + public signature!: string; + + /** + * The console command description. + * + * @type {(string | undefined)} + * @memberof Command + */ + public description: string | undefined; + + /** + * Indicates whether the command should be shown in the command list. + * + * @type {boolean} + * @memberof Command + */ + public isHidden: boolean = false; + + /** + * Indicates whether the command requires a network to be present. + * + * @type {boolean} + * @memberof Command + */ + public requiresNetwork: boolean = true; + + /** + * @memberof Command + */ + public register(argv: string[]) { + try { + this.input = this.app.resolve(Input); + this.input.parse(argv, this.definition); + this.input.bind(); + this.input.validate(); + + if (this.input.hasFlag("quiet")) { + this.output.setVerbosity(0); + } else if (this.input.hasFlag("v")) { + const verbosity: number = this.input.getFlag("v") || 1; + + this.output.setVerbosity(verbosity === undefined ? 1 : verbosity); + } + } catch (error) { + this.components.fatal(error.message); + } + } + + /** + * Configure the console command. + * + * @remarks + * This is executed before arguments are available in any way, shape or form. + * If your task requires arguments to be parsed and validated you should consider to use the initialize method. + * + * @returns {void} + * @memberof Command + */ + @postConstruct() + public configure(): void { + // Do nothing... + } + + /** + * Initialize the command after the input has been bound and before the input is validated. + * + * @returns {Promise} + * @memberof Command + */ + public async initialize(): Promise { + // Do nothing... + } + + /** + * Interact with the user. + * + * @returns {Promise} + * @memberof Command + */ + public async interact(): Promise { + // Do nothing... + } + + /** + * Runs the command. + * + * @returns {Promise} + * @memberof Command + */ + public async run(): Promise { + try { + if (this.requiresNetwork) { + await this.detectNetwork(); + } + + if (this.input.hasFlag("token") && this.input.hasFlag("network")) { + this.app + .rebind(Identifiers.ApplicationPaths) + .toConstantValue(this.env.getPaths(this.input.getFlag("token"), this.input.getFlag("network"))); + } + + await this.initialize(); + + if (this.input.interactive) { + await this.interact(); + } + + await this.execute(); + } catch (error) { + this.components.fatal(error.message); + } + } + + /** + * @abstract + * @returns {Promise} + * @memberof Command + */ + public abstract async execute(): Promise; + + /** + * @param {number} [exitCode=2] + * @memberof Command + */ + public showHelp(exitCode: number = 2): void { + this.app.get(Identifiers.Box).render(this.app.resolve(CommandHelp).render(this)); + + process.exit(exitCode); + } + + /** + * @param {string} type + * @param {string} [file] + * @returns {string} + * @memberof Command + */ + public getCorePath(type: string, file?: string): string { + return this.app.getCorePath(type, file); + } + + /** + * @returns {Record} + * @memberof Command + */ + public getArguments(): Record { + return this.input.getArguments(); + } + + /** + * @template T + * @param {string} name + * @returns {T} + * @memberof Command + */ + public getArgument(name: string) { + return this.input.getArgument(name); + } + + /** + * @param {string} name + * @returns {boolean} + * @memberof Command + */ + public hasArgument(name: string): boolean { + return this.input.hasArgument(name); + } + + /** + * @returns {Record} + * @memberof Command + */ + public getFlags(): Record { + return this.input.getFlags(); + } + + /** + * @template T + * @param {string} name + * @returns {T} + * @memberof Command + */ + public getFlag(name: string) { + return this.input.getFlag(name); + } + + /** + * @param {string} name + * @returns {boolean} + * @memberof Command + */ + public hasFlag(name: string): boolean { + return this.input.hasFlag(name); + } + + /** + * Sets the InputDefinition attached to this Command. + * + * @param {InputDefinition} definition + * @memberof Command + */ + public setDefinition(definition: InputDefinition): void { + this.definition = definition; + } + + /** + * Gets the InputDefinition attached to this Command. + * + * @returns {InputDefinition} + * @memberof Command + */ + public getDefinition(): InputDefinition { + return this.definition; + } + + /** + * @private + * @returns {Promise} + * @memberof Command + */ + private async detectNetwork(): Promise { + const requiresNetwork: boolean = Object.keys(this.definition.getFlags()).includes("network"); + + if (requiresNetwork && !this.input.hasFlag("network")) { + this.input.setFlag( + "network", + await this.app.resolve(DiscoverNetwork).discover( + envPaths(this.input.getFlag("token"), { + suffix: "core", + }).config, + ), + ); + } + } +} diff --git a/packages/core-cli/src/commands/discover-commands.ts b/packages/core-cli/src/commands/discover-commands.ts new file mode 100644 index 0000000000..34a97f06bf --- /dev/null +++ b/packages/core-cli/src/commands/discover-commands.ts @@ -0,0 +1,71 @@ +import { lstatSync, readdirSync } from "fs-extra"; + +import { Application, CommandList } from "../contracts"; +import { Identifiers, inject, injectable } from "../ioc"; +import { Command } from "./command"; + +/** + * @export + * @class DiscoverCommands + */ +@injectable() +export class DiscoverCommands { + /** + * @private + * @type {Application} + * @memberof DiscoverCommands + */ + @inject(Identifiers.Application) + private readonly app!: Application; + + /** + * @param {Context} context + * @param {string} path + * @returns {CommandList[]} + * @memberof DiscoverCommands + */ + public within(path: string): CommandList { + const commandFiles: string[] = readdirSync(path) + .map((item: string) => `${path}/${item}`) + .filter((item: string) => lstatSync(item).isFile()) + .filter((item: string) => item.endsWith(".js")); + + const commands: CommandList = {}; + + for (const file of commandFiles) { + const commandInstance: Command = this.app.resolve(require(file).Command); + + if (!commandInstance.isHidden) { + commands[commandInstance.signature] = commandInstance; + } + } + + return commands; + } + + /** + * @param {Context} context + * @param {string[]} plugins + * @returns {CommandList[]} + * @memberof DiscoverCommands + */ + public from(plugins: string[]): CommandList[] { + const commands: CommandList[] = []; + + if (!Array.isArray(plugins)) { + return commands; + } + + for (const plugin of plugins) { + for (const CMD of require(plugin).Commands) { + const commandInstance: Command = this.app.resolve(CMD); + + if (!commandInstance.isHidden) { + commands[commandInstance.signature] = commandInstance; + } + } + } + + return commands; + } +} diff --git a/packages/core-cli/src/commands/discover-network.ts b/packages/core-cli/src/commands/discover-network.ts new file mode 100644 index 0000000000..ca81808922 --- /dev/null +++ b/packages/core-cli/src/commands/discover-network.ts @@ -0,0 +1,78 @@ +import { Networks } from "@arkecosystem/crypto"; +import { ensureDirSync, existsSync, readdirSync } from "fs-extra"; +import prompts from "prompts"; + +import { injectable } from "../ioc"; + +/** + * @export + * @class DiscoverNetwork + */ +@injectable() +export class DiscoverNetwork { + /** + * @param {string} path + * @returns {Promise} + * @memberof DiscoverNetwork + */ + public async discover(path: string): Promise { + if (process.env.CORE_PATH_CONFIG) { + path = process.env.CORE_PATH_CONFIG; + } + + if (!existsSync(path)) { + ensureDirSync(path); + } + + const folders: string[] = readdirSync(path).filter(folder => this.isValidNetwork(folder)); + + if (!folders || folders.length === 0) { + throw new Error( + 'We were unable to detect a network configuration. Please run "ark config:publish" and try again.', + ); + } + + if (folders.length === 1) { + return folders[0]; + } + + return this.discoverWithPrompt(folders); + } + + /** + * @param {string[]} folders + * @returns {Promise} + * @memberof DiscoverNetwork + */ + public async discoverWithPrompt(folders: string[]): Promise { + const response = await prompts([ + { + type: "select", + name: "network", + message: "What network do you want to operate on?", + choices: folders + .filter(folder => this.isValidNetwork(folder)) + .map(folder => ({ title: folder, value: folder })), + }, + { + type: "confirm", + name: "confirm", + message: "Can you confirm?", + }, + ]); + + if (!response.confirm) { + throw new Error("You'll need to confirm the network to continue."); + } + + if (!this.isValidNetwork(response.network)) { + throw new Error(`The given network "${response.network}" is not valid.`); + } + + return response.network; + } + + private isValidNetwork(network: string): boolean { + return Object.keys(Networks).includes(network); + } +} diff --git a/packages/core-cli/src/commands/index.ts b/packages/core-cli/src/commands/index.ts new file mode 100644 index 0000000000..65d2e27aff --- /dev/null +++ b/packages/core-cli/src/commands/index.ts @@ -0,0 +1,4 @@ +export * from "./command-help"; +export * from "./command"; +export * from "./discover-commands"; +export * from "./discover-network"; diff --git a/packages/core-cli/src/component-factory.ts b/packages/core-cli/src/component-factory.ts new file mode 100644 index 0000000000..b5e171e24a --- /dev/null +++ b/packages/core-cli/src/component-factory.ts @@ -0,0 +1,295 @@ +import { Options, Ora } from "ora"; +import { JsonObject } from "type-fest"; + +import { + AppHeader, + Ask, + AskDate, + AskHidden, + AskNumber, + AskPassword, + AutoComplete, + Box, + Clear, + Confirm, + Error, + Fatal, + Info, + Listing, + Log, + MultiSelect, + NewLine, + Prompt, + Select, + Spinner, + Success, + Table, + TaskList, + Title, + Toggle, + Warning, +} from "./components"; +import { Application } from "./contracts"; +import { Identifiers, inject, injectable } from "./ioc"; + +/** + * @export + * @class ComponentFactory + */ +@injectable() +export class ComponentFactory { + /** + * @private + * @type {Application} + * @memberof ComponentFactory + */ + @inject(Identifiers.Application) + protected readonly app!: Application; + + /** + * @returns {string} + * @memberof ComponentFactory + */ + public appHeader(): string { + return this.app.get(Identifiers.AppHeader).render(); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async askDate(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.AskDate).render(message, opts); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async askHidden(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.AskHidden).render(message, opts); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async askNumber(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.AskNumber).render(message, opts); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async askPassword(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.AskPassword).render(message, opts); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async ask(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.Ask).render(message, opts); + } + + /** + * @param {string} message + * @param {any[]} choices + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async autoComplete(message: string, choices: any[], opts: object = {}): Promise { + return this.app.get(Identifiers.AutoComplete).render(message, choices, opts); + } + + /** + * @param {string} message + * @returns {void} + * @memberof ComponentFactory + */ + public box(message: string): void { + return this.app.get(Identifiers.Box).render(message); + } + + /** + * @returns {void} + * @memberof ComponentFactory + */ + public clear(): void { + return this.app.get(Identifiers.Clear).render(); + } + + /** + * @param {string} message + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async confirm(message: string, opts: object = {}): Promise { + return this.app.get(Identifiers.Confirm).render(message, opts); + } + + /** + * @param {string} message + * @returns {void} + * @memberof ComponentFactory + */ + public error(message: string): void { + return this.app.get(Identifiers.Error).render(message); + } + + /** + * @param {string} message + * @returns {void} + * @memberof ComponentFactory + */ + public fatal(message: string): void { + return this.app.get(Identifiers.Fatal).render(message); + } + + /** + * @param {string} message + * @returns {void} + * @memberof ComponentFactory + */ + public info(message: string): void { + return this.app.get(Identifiers.Info).render(message); + } + + /** + * @param {string[]} elements + * @returns {Promise} + * @memberof ComponentFactory + */ + public async listing(elements: string[]): Promise { + return this.app.get(Identifiers.Listing).render(elements); + } + + /** + * @param {string} message + * @returns {void} + * @memberof ComponentFactory + */ + public log(message: string): void { + return this.app.get(Identifiers.Log).render(message); + } + + /** + * @param {string} message + * @param {any[]} choices + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async multiSelect(message: string, choices: any[], opts: object = {}): Promise { + return this.app.get(Identifiers.MultiSelect).render(message, choices, opts); + } + + /** + * @param {number} [count=1] + * @returns {void} + * @memberof ComponentFactory + */ + public newLine(count: number = 1): void { + return this.app.get(Identifiers.NewLine).render(count); + } + + /** + * @param {object} options + * @returns {Promise} + * @memberof ComponentFactory + */ + public async prompt(options: object): Promise { + return this.app.get(Identifiers.Prompt).render(options); + } + + /** + * @param {string} message + * @param {any[]} choices + * @param {object} [opts={}] + * @returns {Promise} + * @memberof ComponentFactory + */ + public async select(message: string, choices: any[], opts: object = {}): Promise { + return this.app.get"), + ProcessFactory: Symbol.for("Factory"), + // Actions + AbortMissingProcess: Symbol.for("Action"), + AbortErroredProcess: Symbol.for("Action"), + AbortRunningProcess: Symbol.for("Action"), + AbortStoppedProcess: Symbol.for("Action"), + AbortUnknownProcess: Symbol.for("Action"), + DaemonizeProcess: Symbol.for("Action"), + RestartProcess: Symbol.for("Action"), + RestartRunningProcessWithPrompt: Symbol.for("Action"), + RestartRunningProcess: Symbol.for("Action"), + // Components + AppHeader: Symbol.for("Component"), + Ask: Symbol.for("Component"), + AskDate: Symbol.for("Component"), + AskHidden: Symbol.for("Component"), + AskNumber: Symbol.for("Component"), + AskPassword: Symbol.for("Component"), + AutoComplete: Symbol.for("Component"), + Box: Symbol.for("Component"), + Clear: Symbol.for("Component"), + Confirm: Symbol.for("Component"), + Error: Symbol.for("Component"), + Fatal: Symbol.for("Component"), + Info: Symbol.for("Component"), + Listing: Symbol.for("Component"), + Log: Symbol.for("Component"), + MultiSelect: Symbol.for("Component"), + NewLine: Symbol.for("Component"), + Prompt: Symbol.for("Component"), + Select: Symbol.for("Component