From e1bd4c755d43f42ee66c126be71c7dda17e0dcd9 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski <146972016+Milena-Czierlinski@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:10:54 +0100 Subject: [PATCH] Delete obsolete LocalAccounts on App startup (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add logic to start of runtime * feat: add backbone route * refactor: rename checkIdentityDeletionForUsername * feat: add checkIdentityDeletionForUsername use case * feat: get username in transport * refactor: rename to checkDeletionOfIdentity * test: identitycontroller * fix: rename consistently * test: add runtime test * feat: continue startApp * feat: delete accounts if identities are deleted * feat: bump backbone * fix: provide config for updated backbone * feat: add IdentityDeletionInfo object * test: add transport test for passed grace period * refactor: errors * Revert "feat: add IdentityDeletionInfo object" This reverts commit 01813a6c0c214103b32bf5bd42e14266de7df765. * test: add runtime test for expired grace period * refactor: naming * refactor: rename startAccounts * feat: configure gracePeriod initializing IdentityDeletionProcess * feat: allow to manually run deletion job on backbone * test: run deletion job in skipped test * test: deleted Identity * feat: log error * refactor: backbone response can be null * feat: check for deleted Identities; wip * fix: return early for error other than noAuthGrant * feat: rename offoardDevice * test: accounts of deleted identities are remove on app startup * refactor: rename offboardDevice on AccountService * feat: add deleteAccount to AccountServices * fix: app-runtime tests * refactor: rename offboardAccount * chore: simplify * chore: simplify * refactor: rename checkIfIdentityIsDeleted * test: make tests independent * test: offboard account of identity with ongoing grace period * test: adjust runDeletionJob * chore: package-lock * test: rename * fix: make more tests independent --------- Co-authored-by: Julian König <33655937+jkoenig134@users.noreply.github.com> Co-authored-by: Julian König --- packages/app-runtime/src/AppRuntime.ts | 43 ++++++++++++++++ .../src/multiAccount/AccountServices.ts | 4 ++ .../multiAccount/MultiAccountController.ts | 10 ++-- packages/app-runtime/test/lib/TestUtil.ts | 28 ++++++++++ .../test/runtime/Offboarding.test.ts | 40 +++++++++++++-- .../app-runtime/test/runtime/Startup.test.ts | 39 +++++++++++++- .../facades/transport/AccountFacade.ts | 9 +++- .../account/CheckIfIdentityIsDeleted.ts | 23 +++++++++ .../src/useCases/transport/account/index.ts | 1 + .../runtime/test/transport/account.test.ts | 19 +++++++ .../modules/accounts/IdentityController.ts | 24 ++++++++- .../BackboneCheckIfIdentityIsDeleted.ts | 4 ++ .../accounts/backbone/IdentityClient.ts | 5 ++ .../account/IdentityController.test.ts | 51 +++++++++++++++++++ 14 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 packages/runtime/src/useCases/transport/account/CheckIfIdentityIsDeleted.ts create mode 100644 packages/transport/src/modules/accounts/backbone/BackboneCheckIfIdentityIsDeleted.ts create mode 100644 packages/transport/test/modules/account/IdentityController.test.ts diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 42bc87722..59ca6c217 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -293,6 +293,12 @@ export class AppRuntime extends Runtime { return Promise.resolve(); } + public override async start(): Promise { + await super.start(); + + await this.startAccounts(); + } + public override async stop(): Promise { const logError = (e: any) => this.logger.error(e); @@ -300,6 +306,43 @@ export class AppRuntime extends Runtime { await this.lokiConnection.close().catch(logError); } + private async startAccounts(): Promise { + const accounts = await this._multiAccountController.getAccounts(); + + for (const account of accounts) { + const session = await this.selectAccount(account.id.toString()); + + session.accountController.authenticator.clear(); + try { + await session.accountController.authenticator.getToken(); + continue; + } catch (error) { + this.logger.error(error); + + if (!(typeof error === "object" && error !== null && "code" in error)) { + continue; + } + + if (!(error.code === "error.transport.request.noAuthGrant")) continue; + } + + const checkDeletionResult = await session.transportServices.account.checkIfIdentityIsDeleted(); + + if (checkDeletionResult.isError) { + this.logger.error(checkDeletionResult.error); + continue; + } + + if (checkDeletionResult.value.isDeleted) { + await this._multiAccountController.deleteAccount(account.id); + continue; + } + + const syncResult = await session.transportServices.account.syncDatawallet(); + if (syncResult.isError) this.logger.error(syncResult.error); + } + } + private translationProvider: INativeTranslationProvider = { translate: (key: string) => Promise.resolve(Result.ok(key)) }; diff --git a/packages/app-runtime/src/multiAccount/AccountServices.ts b/packages/app-runtime/src/multiAccount/AccountServices.ts index d71cf384c..3c36066de 100644 --- a/packages/app-runtime/src/multiAccount/AccountServices.ts +++ b/packages/app-runtime/src/multiAccount/AccountServices.ts @@ -38,6 +38,10 @@ export class AccountServices { return LocalAccountMapper.toLocalAccountDTO(localAccount); } + public async offboardAccount(id: string): Promise { + await this.multiAccountController.offboardAccount(CoreId.from(id)); + } + public async deleteAccount(id: string): Promise { await this.multiAccountController.deleteAccount(CoreId.from(id)); } diff --git a/packages/app-runtime/src/multiAccount/MultiAccountController.ts b/packages/app-runtime/src/multiAccount/MultiAccountController.ts index fe9e45f3f..54b6d17bc 100644 --- a/packages/app-runtime/src/multiAccount/MultiAccountController.ts +++ b/packages/app-runtime/src/multiAccount/MultiAccountController.ts @@ -134,13 +134,17 @@ export class MultiAccountController { return [localAccount, accountController]; } - public async deleteAccount(id: CoreId): Promise { - const [localAccount, accountController] = await this.selectAccount(id); + public async offboardAccount(id: CoreId): Promise { + const [_, accountController] = await this.selectAccount(id); await accountController.unregisterPushNotificationToken(); await accountController.activeDevice.markAsOffboarded(); await accountController.close(); - delete this._openAccounts[localAccount.id.toString()]; + await this.deleteAccount(id); + } + + public async deleteAccount(id: CoreId): Promise { + delete this._openAccounts[id.toString()]; await this.databaseConnection.deleteDatabase(`acc-${id.toString()}`); await this._localAccounts.delete({ id: id.toString() }); diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index 2190dbcf6..39693b803 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -15,6 +15,9 @@ import { SyncEverythingResponse } from "@nmshd/runtime"; import { IConfigOverwrite, TransportLoggerFactory } from "@nmshd/transport"; +import fs from "fs"; +import path from "path"; +import { GenericContainer, Wait } from "testcontainers"; import { LogLevel } from "typescript-logging"; import { AppConfig, AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession, createAppConfig as runtime_createAppConfig } from "../../src"; import { FakeUIBridge } from "./FakeUIBridge"; @@ -262,4 +265,29 @@ export class TestUtil { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition expect(result.isSuccess, `${result.error?.code} | ${result.error?.message}`).toBe(true); } + + public static async runDeletionJob(): Promise { + const backboneVersion = this.getBackboneVersion(); + const appsettingsOverrideLocation = process.env.APPSETTINGS_OVERRIDE_LOCATION ?? `${__dirname}/../../../../.dev/appsettings.override.json`; + + await new GenericContainer(`ghcr.io/nmshd/backbone-identity-deletion-jobs:${backboneVersion}`) + .withWaitStrategy(Wait.forOneShotStartup()) + .withCommand(["--Worker", "ActualDeletionWorker"]) + .withNetworkMode("backbone") + .withCopyFilesToContainer([{ source: appsettingsOverrideLocation, target: "/app/appsettings.override.json" }]) + .start(); + } + + private static getBackboneVersion() { + if (process.env.BACKBONE_VERSION) return process.env.BACKBONE_VERSION; + + const envFile = fs.readFileSync(path.resolve(`${__dirname}/../../../../.dev/compose.backbone.env`)); + const env = envFile + .toString() + .split("\n") + .map((line) => line.split("=")) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {} as Record); + + return env["BACKBONE_VERSION"]; + } } diff --git a/packages/app-runtime/test/runtime/Offboarding.test.ts b/packages/app-runtime/test/runtime/Offboarding.test.ts index 3c9cc0044..e6ccf3279 100644 --- a/packages/app-runtime/test/runtime/Offboarding.test.ts +++ b/packages/app-runtime/test/runtime/Offboarding.test.ts @@ -1,3 +1,4 @@ +import { IdentityDeletionProcessStatus } from "@nmshd/runtime"; import { AppRuntime, AppRuntimeServices } from "../../src"; import { TestUtil } from "../lib"; @@ -5,10 +6,11 @@ describe("Offboarding", function () { let runtime: AppRuntime; let services1: AppRuntimeServices; + let services2: AppRuntimeServices; let localAccount2Id: string; let device2Id: string; - beforeAll(async function () { + beforeEach(async function () { // as we can't pop up multiple runtimes we have to allow multiple accounts with // the same address to test offboarding const configOverride = { allowMultipleAccountsWithSameAddress: true }; @@ -25,18 +27,46 @@ describe("Offboarding", function () { const localAccount2 = await runtime.accountServices.onboardAccount(onboardingInfoResult.value); localAccount2Id = localAccount2.id; - const services2 = await runtime.getServices(localAccount2.id); + services2 = await runtime.getServices(localAccount2.id); await services2.transportServices.account.syncDatawallet(); await services1.transportServices.account.syncDatawallet(); }); - afterAll(async function () { + afterEach(async function () { await runtime.stop(); }); - test("delete account", async function () { - await runtime.accountServices.deleteAccount(localAccount2Id); + test("offboard Account for active Identity", async function () { + await runtime.accountServices.offboardAccount(localAccount2Id); + await services1.transportServices.account.syncDatawallet(); + + const accounts = await runtime.accountServices.getAccounts(); + expect(accounts).toHaveLength(1); + + const devicesResult = await services1.transportServices.devices.getDevices(); + const filteredDevice = devicesResult.value.find((d) => d.id === device2Id); + + expect(filteredDevice).toBeDefined(); + expect(filteredDevice!.isOffboarded).toBe(true); + + const deviceResult = await services1.transportServices.devices.getDevice({ id: device2Id }); + const device = deviceResult.value; + + expect(device.isOffboarded).toBe(true); + + await expect(runtime.getServices(localAccount2Id)).rejects.toThrow("error.transport.recordNotFound"); + await expect(runtime.selectAccount(localAccount2Id)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("offboard Account for Identity within grace period of IdentityDeletionProcess", async function () { + await services1.transportServices.identityDeletionProcesses.initiateIdentityDeletionProcess(); + await services2.transportServices.account.syncDatawallet(); + + const identityDeletionProcessOnSecondAccount = (await services2.transportServices.identityDeletionProcesses.getActiveIdentityDeletionProcess()).value; + expect(identityDeletionProcessOnSecondAccount.status).toStrictEqual(IdentityDeletionProcessStatus.Approved); + + await runtime.accountServices.offboardAccount(localAccount2Id); await services1.transportServices.account.syncDatawallet(); const accounts = await runtime.accountServices.getAccounts(); diff --git a/packages/app-runtime/test/runtime/Startup.test.ts b/packages/app-runtime/test/runtime/Startup.test.ts index 2649ea483..4574934e9 100644 --- a/packages/app-runtime/test/runtime/Startup.test.ts +++ b/packages/app-runtime/test/runtime/Startup.test.ts @@ -1,4 +1,4 @@ -import { AppRuntime, LocalAccountDTO } from "../../src"; +import { AppRuntime, LocalAccountDTO, LocalAccountSession } from "../../src"; import { EventListener, TestUtil } from "../lib"; describe("Runtime Startup", function () { @@ -64,3 +64,40 @@ describe("Runtime Startup", function () { expect(selectedAccount.account.id.toString()).toBe(localAccount.id.toString()); }); }); + +describe("Start Accounts", function () { + let runtime: AppRuntime; + let session: LocalAccountSession; + + beforeAll(async function () { + runtime = await TestUtil.createRuntime(); + await runtime.start(); + }); + + beforeEach(async function () { + const accounts = await TestUtil.provideAccounts(runtime, 1); + session = await runtime.selectAccount(accounts[0].id); + }); + + afterAll(async () => await runtime.stop()); + + test("should not delete Account running startAccounts for an active Identity", async function () { + await runtime["startAccounts"](); + await expect(runtime.selectAccount(session.account.id)).resolves.not.toThrow(); + }); + + test("should delete Account running startAccounts for an Identity with expired grace period", async function () { + await session.transportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(0); + + await runtime["startAccounts"](); + await expect(runtime.selectAccount(session.account.id)).rejects.toThrow("error.transport.recordNotFound"); + }); + + test("should delete Account running startAccounts for a deleted Identity", async function () { + await session.transportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(0); + await TestUtil.runDeletionJob(); + + await runtime["startAccounts"](); + await expect(runtime.selectAccount(session.account.id)).rejects.toThrow("error.transport.recordNotFound"); + }); +}); diff --git a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts index 80c63e456..0e0276a33 100644 --- a/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts +++ b/packages/runtime/src/extensibility/facades/transport/AccountFacade.ts @@ -2,6 +2,8 @@ import { ApplicationError, Result } from "@js-soft/ts-utils"; import { Inject } from "@nmshd/typescript-ioc"; import { DeviceDTO } from "../../../types"; import { + CheckIfIdentityIsDeletedResponse, + CheckIfIdentityIsDeletedUseCase, DisableAutoSyncUseCase, EnableAutoSyncUseCase, GetDeviceInfoUseCase, @@ -32,7 +34,8 @@ export class AccountFacade { @Inject private readonly getSyncInfoUseCase: GetSyncInfoUseCase, @Inject private readonly disableAutoSyncUseCase: DisableAutoSyncUseCase, @Inject private readonly enableAutoSyncUseCase: EnableAutoSyncUseCase, - @Inject private readonly loadItemFromTruncatedReferenceUseCase: LoadItemFromTruncatedReferenceUseCase + @Inject private readonly loadItemFromTruncatedReferenceUseCase: LoadItemFromTruncatedReferenceUseCase, + @Inject private readonly checkIfIdentityIsDeletedUseCase: CheckIfIdentityIsDeletedUseCase ) {} public async getIdentityInfo(): Promise> { @@ -74,4 +77,8 @@ export class AccountFacade { public async loadItemFromTruncatedReference(request: LoadItemFromTruncatedReferenceRequest): Promise> { return await this.loadItemFromTruncatedReferenceUseCase.execute(request); } + + public async checkIfIdentityIsDeleted(): Promise> { + return await this.checkIfIdentityIsDeletedUseCase.execute(); + } } diff --git a/packages/runtime/src/useCases/transport/account/CheckIfIdentityIsDeleted.ts b/packages/runtime/src/useCases/transport/account/CheckIfIdentityIsDeleted.ts new file mode 100644 index 000000000..56cffc890 --- /dev/null +++ b/packages/runtime/src/useCases/transport/account/CheckIfIdentityIsDeleted.ts @@ -0,0 +1,23 @@ +import { Result } from "@js-soft/ts-utils"; +import { IdentityController } from "@nmshd/transport"; +import { Inject } from "@nmshd/typescript-ioc"; +import { UseCase } from "../../common"; + +export interface CheckIfIdentityIsDeletedResponse { + isDeleted: boolean; + deletionDate?: string; +} + +export class CheckIfIdentityIsDeletedUseCase extends UseCase { + public constructor(@Inject private readonly identityController: IdentityController) { + super(); + } + + protected async executeInternal(): Promise> { + const result = await this.identityController.checkIfIdentityIsDeleted(); + + if (result.isError) return Result.fail(result.error); + + return Result.ok(result.value); + } +} diff --git a/packages/runtime/src/useCases/transport/account/index.ts b/packages/runtime/src/useCases/transport/account/index.ts index d6c3fbe3e..0982eb258 100644 --- a/packages/runtime/src/useCases/transport/account/index.ts +++ b/packages/runtime/src/useCases/transport/account/index.ts @@ -1,3 +1,4 @@ +export * from "./CheckIfIdentityIsDeleted"; export * from "./DisableAutoSync"; export * from "./EnableAutoSync"; export * from "./GetDeviceInfo"; diff --git a/packages/runtime/test/transport/account.test.ts b/packages/runtime/test/transport/account.test.ts index f6de20dd8..019495d5d 100644 --- a/packages/runtime/test/transport/account.test.ts +++ b/packages/runtime/test/transport/account.test.ts @@ -218,3 +218,22 @@ describe("Un-/RegisterPushNotificationToken", () => { expect(result).toBeSuccessful(); }); }); + +describe("CheckIfIdentityIsDeleted", () => { + test("check deletion of Identity that is not deleted", async () => { + const result = await sTransportServices.account.checkIfIdentityIsDeleted(); + expect(result.isSuccess).toBe(true); + expect(result.value.isDeleted).toBe(false); + expect(result.value.deletionDate).toBeUndefined(); + }); + + test("check deletion of Identity that has IdentityDeletionProcess with expired grace period", async () => { + const identityDeletionProcess = + await sTransportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(0); + + const result = await sTransportServices.account.checkIfIdentityIsDeleted(); + expect(result.isSuccess).toBe(true); + expect(result.value.isDeleted).toBe(true); + expect(result.value.deletionDate).toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString()); + }); +}); diff --git a/packages/transport/src/modules/accounts/IdentityController.ts b/packages/transport/src/modules/accounts/IdentityController.ts index fceba220a..61d1f1b28 100644 --- a/packages/transport/src/modules/accounts/IdentityController.ts +++ b/packages/transport/src/modules/accounts/IdentityController.ts @@ -1,12 +1,15 @@ -import { log } from "@js-soft/ts-utils"; +import { log, Result } from "@js-soft/ts-utils"; import { CoreAddress } from "@nmshd/core-types"; import { CoreBuffer, CryptoSignature, CryptoSignaturePrivateKey, CryptoSignaturePublicKey } from "@nmshd/crypto"; import { ControllerName, CoreCrypto, TransportController, TransportCoreErrors } from "../../core"; import { AccountController } from "../accounts/AccountController"; import { DeviceSecretType } from "../devices/DeviceSecretController"; +import { IdentityClient } from "./backbone/IdentityClient"; import { Identity } from "./data/Identity"; export class IdentityController extends TransportController { + public identityClient: IdentityClient; + public get address(): CoreAddress { return this._identity.address; } @@ -22,6 +25,8 @@ export class IdentityController extends TransportController { public constructor(parent: AccountController) { super(ControllerName.Identity, parent); + + this.identityClient = new IdentityClient(this.config, this.transport.correlator); } @log() @@ -57,4 +62,21 @@ export class IdentityController extends TransportController { const valid = await CoreCrypto.verify(content, signature, this.publicKey); return valid; } + + public async checkIfIdentityIsDeleted(): Promise< + Result<{ + isDeleted: boolean; + deletionDate?: string; + }> + > { + const currentDeviceCredentials = await this.parent.activeDevice.getCredentials(); + const identityDeletionResult = await this.identityClient.checkIfIdentityIsDeleted(currentDeviceCredentials.username); + + if (identityDeletionResult.isError) return Result.fail(identityDeletionResult.error); + + return Result.ok({ + isDeleted: identityDeletionResult.value.isDeleted, + deletionDate: identityDeletionResult.value.deletionDate ?? undefined + }); + } } diff --git a/packages/transport/src/modules/accounts/backbone/BackboneCheckIfIdentityIsDeleted.ts b/packages/transport/src/modules/accounts/backbone/BackboneCheckIfIdentityIsDeleted.ts new file mode 100644 index 000000000..580436813 --- /dev/null +++ b/packages/transport/src/modules/accounts/backbone/BackboneCheckIfIdentityIsDeleted.ts @@ -0,0 +1,4 @@ +export interface BackboneCheckIfIdentityIsDeletedResponse { + isDeleted: boolean; + deletionDate: string | null; +} diff --git a/packages/transport/src/modules/accounts/backbone/IdentityClient.ts b/packages/transport/src/modules/accounts/backbone/IdentityClient.ts index 85d241c50..113474618 100644 --- a/packages/transport/src/modules/accounts/backbone/IdentityClient.ts +++ b/packages/transport/src/modules/accounts/backbone/IdentityClient.ts @@ -1,5 +1,6 @@ import { RESTClient, RESTClientLogDirective } from "../../../core"; import { ClientResult } from "../../../core/backbone/ClientResult"; +import { BackboneCheckIfIdentityIsDeletedResponse } from "./BackboneCheckIfIdentityIsDeleted"; import { BackbonePostIdentityRequest, BackbonePostIdentityResponse } from "./BackbonePostIdentity"; export class IdentityClient extends RESTClient { @@ -8,4 +9,8 @@ export class IdentityClient extends RESTClient { public async createIdentity(value: BackbonePostIdentityRequest): Promise> { return await this.post("/api/v1/Identities", value); } + + public async checkIfIdentityIsDeleted(username: string): Promise> { + return await this.get(`/api/v1/Identities/IsDeleted?username=${username}`); + } } diff --git a/packages/transport/test/modules/account/IdentityController.test.ts b/packages/transport/test/modules/account/IdentityController.test.ts new file mode 100644 index 000000000..710b891cd --- /dev/null +++ b/packages/transport/test/modules/account/IdentityController.test.ts @@ -0,0 +1,51 @@ +import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { AccountController, Transport } from "../../../src"; +import { TestUtil } from "../../testHelpers/TestUtil"; + +describe("IdentityController", function () { + let connection: IDatabaseConnection; + let transport: Transport; + let account1: AccountController; + let account2: AccountController; + + beforeAll(async function () { + connection = await TestUtil.createDatabaseConnection(); + transport = TestUtil.createTransport(connection); + + await transport.init(); + + [account1, account2] = await TestUtil.provideAccounts(transport, 2); + await account1.init(); + await account2.init(); + }); + + afterAll(async function () { + await account1.close(); + await account2.close(); + await connection.close(); + }); + + test("should return Identity is not deleted for active Identity", async function () { + const result = await account1.identity.checkIfIdentityIsDeleted(); + expect(result.value.isDeleted).toBe(false); + expect(result.value.deletionDate).toBeUndefined(); + }); + + test("should return gracePeriodEndsAt for Identity having IdentityDeletionProcess with expired grace period", async function () { + const identityDeletionProcess = await account1.identityDeletionProcess.initiateIdentityDeletionProcess(0); + + const result = await account1.identity.checkIfIdentityIsDeleted(); + expect(result.value.isDeleted).toBe(true); + expect(result.value.deletionDate).toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString()); + }); + + test("should return actual deletionDate for Identity that is deleted", async function () { + const identityDeletionProcess = await account2.identityDeletionProcess.initiateIdentityDeletionProcess(0); + await TestUtil.runDeletionJob(); + + const result = await account2.identity.checkIfIdentityIsDeleted(); + expect(result.value.isDeleted).toBe(true); + expect(result.value.deletionDate).toBeDefined(); + expect(result.value.deletionDate).not.toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString()); + }); +});