diff --git a/src/ConnectorRuntime.ts b/src/ConnectorRuntime.ts index 8b24a345..3a8491b1 100644 --- a/src/ConnectorRuntime.ts +++ b/src/ConnectorRuntime.ts @@ -341,7 +341,7 @@ export class ConnectorRuntime extends Runtime { } } - protected async stop(): Promise { + public async stop(): Promise { if (this.isStarted) { try { await super.stop(); diff --git a/src/cli/BaseCommand.ts b/src/cli/BaseCommand.ts index d455557e..29a68ced 100644 --- a/src/cli/BaseCommand.ts +++ b/src/cli/BaseCommand.ts @@ -1,7 +1,10 @@ import yargs from "yargs"; +import { ConnectorRuntime } from "../ConnectorRuntime"; +import { ConnectorRuntimeConfig } from "../ConnectorRuntimeConfig"; +import { createConnectorConfig } from "../CreateConnectorConfig"; export interface ConfigFileOptions { - config: string | undefined; + config?: string; } export const configOptionBuilder = (yargs: yargs.Argv<{}>): yargs.Argv => { @@ -13,3 +16,47 @@ Can also be set via the CUSTOM_CONFIG_LOCATION env variable`, demandOption: false }); }; + +export abstract class BaseCommand { + private connectorConfig?: ConnectorRuntimeConfig; + protected cliRuntime?: ConnectorRuntime; + protected log = console; + + public async run(configPath: string | undefined): Promise { + if (configPath) { + process.env.CUSTOM_CONFIG_LOCATION = configPath; + } + + try { + this.connectorConfig = createConnectorConfig(); + this.connectorConfig.infrastructure.httpServer.enabled = false; + this.connectorConfig.modules.coreHttpApi.enabled = false; + this.connectorConfig.logging = { + appenders: { + console: { type: "console" } + }, + categories: { + default: { appenders: ["console"], level: "OFF" } + } + }; + return await this.runInternal(this.connectorConfig); + } catch (error: any) { + this.log.log("Error creating identity: ", error); + } finally { + if (this.cliRuntime) { + await this.cliRuntime.stop(); + } + } + } + + protected async createRuntime(): Promise { + if (this.cliRuntime) { + return; + } + if (!this.connectorConfig) throw new Error("Connector config not initialized"); + this.cliRuntime = await ConnectorRuntime.create(this.connectorConfig); + await this.cliRuntime.start(); + } + + protected abstract runInternal(connectorConfig: ConnectorRuntimeConfig): Promise; +} diff --git a/src/cli/commands/identity/status.ts b/src/cli/commands/identity/status.ts new file mode 100644 index 00000000..3b27c1a8 --- /dev/null +++ b/src/cli/commands/identity/status.ts @@ -0,0 +1,43 @@ +import { IdentityDeletionProcessStatus } from "@nmshd/runtime"; +import { DateTime } from "luxon"; +import { CommandModule } from "yargs"; +import { BaseCommand, ConfigFileOptions, configOptionBuilder } from "../../BaseCommand"; + +export const identityStatusHandler = async ({ config }: ConfigFileOptions): Promise => { + await new IdentityStatus().run(config); +}; + +export const yargsIdentityStatusCommand: CommandModule<{}, ConfigFileOptions> = { + command: "status", + describe: "Show the status of the identity", + handler: identityStatusHandler, + builder: configOptionBuilder +}; + +export class IdentityStatus extends BaseCommand { + protected async runInternal(): Promise { + await this.createRuntime(); + if (!this.cliRuntime) { + throw new Error("Failed to initialize runtime"); + } + + try { + const identityInfoResult = await this.cliRuntime.getServices().transportServices.account.getIdentityInfo(); + const identityDeletionProcessResult = await this.cliRuntime.getServices().transportServices.identityDeletionProcesses.getActiveIdentityDeletionProcess(); + + const identityInfo = identityInfoResult.value; + let message = `Id: ${identityInfo.address}`; + + if (identityDeletionProcessResult.isSuccess) { + const identityDeletionProcess = identityDeletionProcessResult.value; + message += `\nIdentity deletion status: ${identityDeletionProcess.status}`; + if (identityDeletionProcess.gracePeriodEndsAt && identityDeletionProcess.status === IdentityDeletionProcessStatus.Approved) { + message += `\nEnd of grace period: ${DateTime.fromISO(identityDeletionProcess.gracePeriodEndsAt).toLocaleString()}`; + } + } + this.log.log(message); + } catch (e: any) { + this.log.error(e); + } + } +} diff --git a/src/cli/commands/identityDeletion/cancel.ts b/src/cli/commands/identityDeletion/cancel.ts new file mode 100644 index 00000000..efc01c8a --- /dev/null +++ b/src/cli/commands/identityDeletion/cancel.ts @@ -0,0 +1,30 @@ +import { CommandModule } from "yargs"; +import { BaseCommand, ConfigFileOptions, configOptionBuilder } from "../../BaseCommand"; + +export const identityDeletionCancelHandler = async ({ config }: ConfigFileOptions): Promise => { + await new CancelIdentityDeletion().run(config); +}; + +export const yargsIdentityDeletionCancelCommand: CommandModule<{}, ConfigFileOptions> = { + command: "cancel", + describe: "Cancel the identity deletion", + handler: identityDeletionCancelHandler, + builder: configOptionBuilder +}; + +export default class CancelIdentityDeletion extends BaseCommand { + protected async runInternal(): Promise { + await this.createRuntime(); + if (!this.cliRuntime) { + throw new Error("Failed to initialize runtime"); + } + + const identityDeletionCancellationResult = await this.cliRuntime.getServices().transportServices.identityDeletionProcesses.cancelIdentityDeletionProcess(); + + if (identityDeletionCancellationResult.isSuccess) { + this.log.log("Identity deletion cancelled"); + return; + } + this.log.log(identityDeletionCancellationResult.error.toString()); + } +} diff --git a/src/cli/commands/identityDeletion/init.ts b/src/cli/commands/identityDeletion/init.ts new file mode 100644 index 00000000..7f79d029 --- /dev/null +++ b/src/cli/commands/identityDeletion/init.ts @@ -0,0 +1,31 @@ +import { CommandModule } from "yargs"; +import { BaseCommand, ConfigFileOptions, configOptionBuilder } from "../../BaseCommand"; + +export const identityDeletionInitHandler = async ({ config }: ConfigFileOptions): Promise => { + const command = new InitIdentityDeletion(); + await command.run(config); +}; + +export const yargsIdentityDeletionInitCommand: CommandModule<{}, ConfigFileOptions> = { + command: "init", + describe: "Initialize the identity deletion", + handler: identityDeletionInitHandler, + builder: configOptionBuilder +}; + +export default class InitIdentityDeletion extends BaseCommand { + protected async runInternal(): Promise { + await this.createRuntime(); + if (!this.cliRuntime) { + throw new Error("Failed to initialize runtime"); + } + + const identityDeletionInitResult = await this.cliRuntime.getServices().transportServices.identityDeletionProcesses.initiateIdentityDeletionProcess(); + + if (identityDeletionInitResult.isSuccess) { + this.log.log("Identity deletion initiated"); + return; + } + this.log.error(identityDeletionInitResult.error.toString()); + } +} diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index e22c4a1d..9ac0289c 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -1 +1,4 @@ +export * from "./identity/status"; +export * from "./identityDeletion/cancel"; +export * from "./identityDeletion/init"; export * from "./startConnector"; diff --git a/src/index.ts b/src/index.ts index 0e186d07..214c7de6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,29 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { startConnectorCommand } from "./cli/commands"; +import { startConnectorCommand, yargsIdentityDeletionCancelCommand, yargsIdentityDeletionInitCommand, yargsIdentityStatusCommand } from "./cli/commands"; yargs(hideBin(process.argv)) + .command({ + command: "identity [command]", + describe: "Identity related commands", + builder: (yargs) => { + return yargs.command(yargsIdentityStatusCommand); + }, + handler: () => { + yargs.showHelp("log"); + } + }) + .command({ + command: "identityDeletion [command]", + describe: "Identity deletion related commands", + builder: (yargs) => { + return yargs.command(yargsIdentityDeletionInitCommand).command(yargsIdentityDeletionCancelCommand); + }, + handler: () => { + yargs.showHelp("log"); + } + }) .command(startConnectorCommand) .demandCommand(1, 1, "Please specify a command") .scriptName("") diff --git a/test/modules/cli/identity/identityStatus.test.ts b/test/modules/cli/identity/identityStatus.test.ts new file mode 100644 index 00000000..5a53eaec --- /dev/null +++ b/test/modules/cli/identity/identityStatus.test.ts @@ -0,0 +1,37 @@ +import { sleep } from "@js-soft/ts-utils"; +import { identityDeletionInitHandler, identityStatusHandler } from "../../../../dist/cli/commands"; +import { resetDB, setupEnvironment } from "../setup"; + +describe("Identity status", () => { + const identityStatusPattern = /Id: did:e:((([A-Za-z0-9]+(-[A-Za-z0-9]+)*)\.)+[a-z]{2,}|localhost):dids:[0-9a-f]{22}/; + + beforeAll(() => { + setupEnvironment(); + }); + + afterAll(async () => { + await resetDB(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test("show identity status", async () => { + const consoleSpy = jest.spyOn(console, "log"); + await identityStatusHandler({}); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy.mock.lastCall![0]).toMatch(identityStatusPattern); + + await identityDeletionInitHandler({}); + await sleep(1000); + expect(consoleSpy).toHaveBeenCalledWith("Identity deletion initiated"); + expect(consoleSpy).toHaveBeenCalledTimes(2); + + await identityStatusHandler({}); + expect(consoleSpy).toHaveBeenCalledTimes(3); + expect(consoleSpy.mock.lastCall![0]).toMatch(identityStatusPattern); + expect(consoleSpy.mock.lastCall![0]).toContain("Identity deletion status: Approved"); + expect(consoleSpy.mock.lastCall![0]).toMatch(/End of grace period:/); + }); +}); diff --git a/test/modules/cli/identityDeletion/identityDeletion.test.ts b/test/modules/cli/identityDeletion/identityDeletion.test.ts new file mode 100644 index 00000000..5cc2fc99 --- /dev/null +++ b/test/modules/cli/identityDeletion/identityDeletion.test.ts @@ -0,0 +1,30 @@ +import { identityDeletionCancelHandler, identityDeletionInitHandler } from "../../../../dist/cli/commands"; +import { resetDB, setupEnvironment } from "../setup"; + +describe("Identity deletion", () => { + beforeAll(() => { + setupEnvironment(); + }); + + afterAll(async () => { + await resetDB(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("initiate identity deletion", async () => { + const consoleSpy = jest.spyOn(console, "log"); + await identityDeletionInitHandler({}); + expect(consoleSpy).toHaveBeenCalledWith("Identity deletion initiated"); + expect(consoleSpy).toHaveBeenCalledTimes(1); + }); + + test("cancel identity deletion", async () => { + const consoleSpy = jest.spyOn(console, "log"); + await identityDeletionCancelHandler({}); + expect(consoleSpy).toHaveBeenCalledWith("Identity deletion cancelled"); + expect(consoleSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/modules/cli/setup.ts b/test/modules/cli/setup.ts new file mode 100644 index 00000000..4347200e --- /dev/null +++ b/test/modules/cli/setup.ts @@ -0,0 +1,27 @@ +import { rm } from "fs/promises"; +import { join } from "path"; +import getPort from "../../lib/getPort"; + +export function setupEnvironment(): void { + process.env.database = JSON.stringify({ + driver: "lokijs", + folder: "./", + dbName: `default${process.pid}`, + dbNamePrefix: "test-" + }); + process.env.NODE_CONFIG_ENV = "test"; + process.env.API_KEY = "test"; + process.env["infrastructure:httpServer:port"] = getPort().toString(); + + process.env["transportLibrary:baseUrl"] = process.env["NMSHD_TEST_BASEURL"]; + process.env["transportLibrary:platformClientId"] = process.env["NMSHD_TEST_CLIENTID"]; + process.env["transportLibrary:platformClientSecret"] = process.env["NMSHD_TEST_CLIENTSECRET"]; +} + +export async function resetDB(): Promise { + try { + await rm(join(__dirname, `../../../test-default${process.pid}.db`)); + } catch (_e) { + // ignore + } +} diff --git a/tsconfig.json b/tsconfig.json index 8512fefe..8b0bd29b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "es2021", "module": "commonjs", "sourceMap": true, + "declarationMap": true, "declaration": true, "outDir": "dist", "strict": true,