From 65d4947c9f58d91b153f8b7b9e35bba0b0ec0980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 17 Oct 2024 12:45:55 +0200 Subject: [PATCH 01/37] feat: add enterPassword function to UIBridge --- packages/app-runtime/src/extensibility/ui/IUIBridge.ts | 1 + packages/app-runtime/test/lib/FakeUIBridge.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts index 6e3350699..d15e6f239 100644 --- a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts +++ b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts @@ -11,4 +11,5 @@ export interface IUIBridge { showRequest(account: LocalAccountDTO, request: LocalRequestDVO): Promise>; showError(error: UserfriendlyApplicationError, account?: LocalAccountDTO): Promise>; requestAccountSelection(possibleAccounts: LocalAccountDTO[], title?: string, description?: string): Promise>; + enterPassword(passwordType: number): Promise>; } diff --git a/packages/app-runtime/test/lib/FakeUIBridge.ts b/packages/app-runtime/test/lib/FakeUIBridge.ts index f9b9e7a6c..495e366f1 100644 --- a/packages/app-runtime/test/lib/FakeUIBridge.ts +++ b/packages/app-runtime/test/lib/FakeUIBridge.ts @@ -29,4 +29,8 @@ export class FakeUIBridge implements IUIBridge { public requestAccountSelection(): Promise> { throw new Error("Method not implemented."); } + + public enterPassword(_passwordType: number): Promise> { + throw new Error("Method not implemented."); + } } From c84004ba6687ee7d70a04e35a1471a623a78bf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 17 Oct 2024 16:57:10 +0200 Subject: [PATCH 02/37] feat: prepare tests --- packages/app-runtime/src/AppRuntime.ts | 15 +++++++- packages/app-runtime/src/AppRuntimeErrors.ts | 8 +++++ .../app-runtime/src/AppStringProcessor.ts | 12 +++---- packages/app-runtime/test/lib/TestUtil.ts | 6 ++-- .../test/runtime/AppStringProcessor.test.ts | 34 +++++++++++++++---- 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 8538ecca9..931da3ee8 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -195,10 +195,23 @@ export class AppRuntime extends Runtime { public async requestAccountSelection( title = "i18n://uibridge.accountSelection.title", - description = "i18n://uibridge.accountSelection.description" + description = "i18n://uibridge.accountSelection.description", + forIdentityTruncated?: string ): Promise> { const accounts = await this.accountServices.getAccounts(); + if (!forIdentityTruncated) return await this.selectAccountViaBridge(accounts, title, description); + + const accountsWithPostfix = accounts.filter((account) => account.address?.endsWith(forIdentityTruncated)); + if (accountsWithPostfix.length === 0) return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailableForIdentityTruncated()); + if (accountsWithPostfix.length === 1) return UserfriendlyResult.ok(accountsWithPostfix[0]); + + // This catches the extremely rare case where two accounts are available that have the same last 4 characters in their address. In that case + // the user will have to decide which account to use, which could not work because it is not the exactly same address specified when personalizing the object. + return await this.selectAccountViaBridge(accountsWithPostfix, title, description); + } + + private async selectAccountViaBridge(accounts: LocalAccountDTO[], title: string, description: string): Promise> { const bridge = await this.uiBridge(); const accountSelectionResult = await bridge.requestAccountSelection(accounts, title, description); if (accountSelectionResult.isError) { diff --git a/packages/app-runtime/src/AppRuntimeErrors.ts b/packages/app-runtime/src/AppRuntimeErrors.ts index 14685caf5..ed9339da9 100644 --- a/packages/app-runtime/src/AppRuntimeErrors.ts +++ b/packages/app-runtime/src/AppRuntimeErrors.ts @@ -30,6 +30,14 @@ class General { error ); } + + public noAccountAvailableForIdentityTruncated(): UserfriendlyApplicationError { + return new UserfriendlyApplicationError( + "error.appruntime.general.noAccountAvailableForIdentityTruncated", + "There is no account matching the given 'forIdentityTruncated'.", + "It seems no eligible account is available for this action, because the scanned code is intended for a specific Identity that is not available on this device." + ); + } } class Startup { diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index c8ed7d334..fc096f0d2 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -3,7 +3,7 @@ import { Serializable } from "@js-soft/ts-serval"; import { EventBus, Result } from "@js-soft/ts-utils"; import { ICoreAddress } from "@nmshd/core-types"; import { AnonymousServices, Base64ForIdPrefix, DeviceMapper } from "@nmshd/runtime"; -import { TokenContentDeviceSharedSecret } from "@nmshd/transport"; +import { Reference, TokenContentDeviceSharedSecret } from "@nmshd/transport"; import { AppRuntimeErrors } from "./AppRuntimeErrors"; import { AppRuntimeServices } from "./AppRuntimeServices"; import { IUIBridge } from "./extensibility"; @@ -17,7 +17,7 @@ export class AppStringProcessor { public constructor( protected readonly runtime: { get anonymousServices(): AnonymousServices; - requestAccountSelection(title?: string, description?: string): Promise>; + requestAccountSelection(title?: string, description?: string, forIdentityTruncated?: string): Promise>; uiBridge(): Promise; getServices(accountReference: string | ICoreAddress): Promise; translate(key: string, ...values: any[]): Promise>; @@ -42,9 +42,11 @@ export class AppStringProcessor { public async processTruncatedReference(truncatedReference: string, account?: LocalAccountDTO): Promise> { if (account) return await this._handleTruncatedReference(truncatedReference, account); + const reference = Reference.fromTruncated(truncatedReference); + // process Files and RelationshipTemplates and ask for an account if (truncatedReference.startsWith(Base64ForIdPrefix.File) || truncatedReference.startsWith(Base64ForIdPrefix.RelationshipTemplate)) { - const result = await this.runtime.requestAccountSelection(); + const result = await this.runtime.requestAccountSelection(undefined, undefined, reference.forIdentityTruncated); if (result.isError) { this.logger.error("Could not query account", result.error); return UserfriendlyResult.fail(result.error); @@ -99,9 +101,7 @@ export class AppStringProcessor { const services = await this.runtime.getServices(account.id); const uiBridge = await this.runtime.uiBridge(); - const result = await services.transportServices.account.loadItemFromTruncatedReference({ - reference: truncatedReference - }); + const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: truncatedReference }); if (result.isError) { if (result.error.code === "error.runtime.validation.invalidPropertyValue") { return UserfriendlyResult.fail( diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index 6b8b8f0e2..fe32bae79 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -16,18 +16,18 @@ import { } from "@nmshd/runtime"; import { IConfigOverwrite, TransportLoggerFactory } from "@nmshd/transport"; import { LogLevel } from "typescript-logging"; -import { AppConfig, AppRuntime, LocalAccountDTO, LocalAccountSession, createAppConfig as runtime_createAppConfig } from "../../src"; +import { AppConfig, AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession, createAppConfig as runtime_createAppConfig } from "../../src"; import { FakeUIBridge } from "./FakeUIBridge"; import { FakeNativeBootstrapper } from "./natives/FakeNativeBootstrapper"; export class TestUtil { - public static async createRuntime(configOverride?: any): Promise { + public static async createRuntime(configOverride?: any, uiBridge: IUIBridge = new FakeUIBridge()): Promise { const config = this.createAppConfig(configOverride); const nativeBootstrapper = new FakeNativeBootstrapper(); await nativeBootstrapper.init(); const runtime = await AppRuntime.create(nativeBootstrapper, config); - runtime.registerUIBridge(new FakeUIBridge()); + runtime.registerUIBridge(uiBridge); return runtime; } diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 2f1c1414f..f8003c20a 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -1,21 +1,41 @@ -import { AppRuntime } from "../../src"; +import { mock } from "ts-mockito"; +import { AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession } from "../../src"; import { TestUtil } from "../lib"; describe("AppStringProcessor", function () { - let runtime: AppRuntime; + let mockUiBridge: IUIBridge; + let runtime1: AppRuntime; + let account: LocalAccountDTO; + let runtime1Session: LocalAccountSession; + + let runtime2: AppRuntime; + + let runtime2SessionA: LocalAccountSession; + let runtime2SessionB: LocalAccountSession; beforeAll(async function () { - runtime = await TestUtil.createRuntime(); + runtime1 = await TestUtil.createRuntime(); + await runtime1.start(); + + account = await runtime1.accountServices.createAccount(Math.random().toString(36).substring(7)); + runtime1Session = await runtime1.selectAccount(account.id); + + mockUiBridge = mock(); + runtime2 = await TestUtil.createRuntime(undefined, mockUiBridge); + await runtime2.start(); + + const accounts = await TestUtil.provideAccounts(runtime2, 2); + runtime2SessionA = await runtime2.selectAccount(accounts[0].id); + runtime2SessionB = await runtime2.selectAccount(accounts[1].id); }); afterAll(async function () { - await runtime.stop(); + await runtime1.stop(); + await runtime2.stop(); }); test("should process a URL", async function () { - const account = await runtime.accountServices.createAccount(Math.random().toString(36).substring(7)); - - const result = await runtime.stringProcessor.processURL("nmshd://qr#", account); + const result = await runtime1.stringProcessor.processURL("nmshd://qr#", account); expect(result.isError).toBeDefined(); expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); From 352230e0ad85690a9bf55f2c871081da3aaf4d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 17:47:07 +0200 Subject: [PATCH 03/37] refactor: be more precise about what's going wrong --- packages/transport/src/core/Reference.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/transport/src/core/Reference.ts b/packages/transport/src/core/Reference.ts index 8b3a38bef..4c9c450a2 100644 --- a/packages/transport/src/core/Reference.ts +++ b/packages/transport/src/core/Reference.ts @@ -46,7 +46,9 @@ export class Reference extends Serializable implements IReference { const splitted = truncatedBuffer.toUtf8().split("|"); if (![3, 5].includes(splitted.length)) { - throw TransportCoreErrors.general.invalidTruncatedReference("A TruncatedReference must consist of either exactly 3 or exactly 5 components."); + throw TransportCoreErrors.general.invalidTruncatedReference( + `A TruncatedReference must consist of either exactly 3 or exactly 5 components, but it consists of '${splitted.length}'.` + ); } const idPart = splitted[0]; From 7c32255b424ff3d221204dee91d48b1e3915bbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 18:14:30 +0200 Subject: [PATCH 04/37] test: make app-runtimes EventBus mockable --- packages/app-runtime/test/lib/MockEventBus.ts | 56 +++++++++++++++++++ packages/app-runtime/test/lib/TestUtil.ts | 6 +- packages/app-runtime/test/lib/index.ts | 1 + 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 packages/app-runtime/test/lib/MockEventBus.ts diff --git a/packages/app-runtime/test/lib/MockEventBus.ts b/packages/app-runtime/test/lib/MockEventBus.ts new file mode 100644 index 000000000..04428c93a --- /dev/null +++ b/packages/app-runtime/test/lib/MockEventBus.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-standalone-expect */ +import { DataEvent, Event, EventBus, EventHandler, SubscriptionTarget } from "@js-soft/ts-utils"; +import { TransportDataEvent } from "@nmshd/transport"; + +export class MockEventBus extends EventBus { + private readonly _publishedEvents: { namespace: string; data?: any }[] = []; + public get publishedEvents(): { namespace: string; data?: any }[] { + return this._publishedEvents; + } + + public clearPublishedEvents(): void { + this._publishedEvents.splice(0); + } + + public subscribe(_subscriptionTarget: SubscriptionTarget, _handler: EventHandler): number { + // noop + return 0; + } + + public subscribeOnce(_subscriptionTarget: SubscriptionTarget, _handler: EventHandler): number { + // noop + return 0; + } + + public unsubscribe(_subscriptionId: number): boolean { + // noop + return true; + } + + public publish(event: Event): void { + this._publishedEvents.push({ + namespace: event.namespace, + data: event instanceof TransportDataEvent ? event.data : undefined + }); + } + + public close(): void { + // noop + } + + public expectLastPublishedEvent>( + eventConstructor: (new (...args: any[]) => TEvent) & { namespace: string }, + data?: Partial ? X : never> + ): void { + const lastEvent = this.publishedEvents[this.publishedEvents.length - 1]; + expect(lastEvent.namespace).toStrictEqual(eventConstructor.namespace); + + if (data) expect(lastEvent.data).toStrictEqual(expect.objectContaining(data)); + } + + public expectPublishedEvents(...eventContructors: ((new (...args: any[]) => DataEvent) & { namespace: string })[]): void { + const eventNamespaces = this.publishedEvents.map((e) => e.namespace); + const constructorNamespaces = eventContructors.map((c) => c.namespace); + expect(eventNamespaces).toStrictEqual(constructorNamespaces); + } +} diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index fe32bae79..aca739d1c 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -1,7 +1,7 @@ /* eslint-disable jest/no-standalone-expect */ import { ILoggerFactory } from "@js-soft/logging-abstractions"; import { SimpleLoggerFactory } from "@js-soft/simple-logger"; -import { Result, sleep, SubscriptionTarget } from "@js-soft/ts-utils"; +import { EventBus, Result, sleep, SubscriptionTarget } from "@js-soft/ts-utils"; import { ArbitraryMessageContent, ArbitraryRelationshipCreationContent, ArbitraryRelationshipTemplateContent } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { @@ -21,12 +21,12 @@ import { FakeUIBridge } from "./FakeUIBridge"; import { FakeNativeBootstrapper } from "./natives/FakeNativeBootstrapper"; export class TestUtil { - public static async createRuntime(configOverride?: any, uiBridge: IUIBridge = new FakeUIBridge()): Promise { + public static async createRuntime(configOverride?: any, uiBridge: IUIBridge = new FakeUIBridge(), eventBus?: EventBus): Promise { const config = this.createAppConfig(configOverride); const nativeBootstrapper = new FakeNativeBootstrapper(); await nativeBootstrapper.init(); - const runtime = await AppRuntime.create(nativeBootstrapper, config); + const runtime = await AppRuntime.create(nativeBootstrapper, config, eventBus); runtime.registerUIBridge(uiBridge); return runtime; diff --git a/packages/app-runtime/test/lib/index.ts b/packages/app-runtime/test/lib/index.ts index e2e0ce821..b4a94a2e8 100644 --- a/packages/app-runtime/test/lib/index.ts +++ b/packages/app-runtime/test/lib/index.ts @@ -1,3 +1,4 @@ export * from "./EventListener"; export * from "./FakeUIBridge"; +export * from "./MockEventBus"; export * from "./TestUtil"; From d7e026044b6e5f50e58692cca569338ec601ac0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 18:15:18 +0200 Subject: [PATCH 05/37] fix: make UIBridge mockable --- packages/app-runtime/src/AppRuntime.ts | 18 ++++--- .../app-runtime/src/AppStringProcessor.ts | 54 +++++++++++++++---- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 931da3ee8..4ebc6eeba 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -1,6 +1,6 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; import { LokiJsConnection } from "@js-soft/docdb-access-loki"; -import { Result } from "@js-soft/ts-utils"; +import { EventBus, Result } from "@js-soft/ts-utils"; import { ConsumptionController } from "@nmshd/consumption"; import { CoreId, ICoreAddress } from "@nmshd/core-types"; import { ModuleConfiguration, Runtime, RuntimeHealth } from "@nmshd/runtime"; @@ -31,9 +31,10 @@ import { UserfriendlyResult } from "./UserfriendlyResult"; export class AppRuntime extends Runtime { public constructor( private readonly _nativeEnvironment: INativeEnvironment, - appConfig: AppConfig + appConfig: AppConfig, + eventBus?: EventBus ) { - super(appConfig, _nativeEnvironment.loggerFactory); + super(appConfig, _nativeEnvironment.loggerFactory, eventBus); this._stringProcessor = new AppStringProcessor(this, this.loggerFactory); } @@ -45,16 +46,17 @@ export class AppRuntime extends Runtime { private _uiBridge: IUIBridge | undefined; private _uiBridgeResolver?: { promise: Promise; resolve(uiBridge: IUIBridge): void }; - public async uiBridge(): Promise { + public uiBridge(): Promise | IUIBridge { if (this._uiBridge) return this._uiBridge; - if (this._uiBridgeResolver) return await this._uiBridgeResolver.promise; + + if (this._uiBridgeResolver) return this._uiBridgeResolver.promise; let resolve: (uiBridge: IUIBridge) => void = () => ""; const promise = new Promise((r) => (resolve = r)); this._uiBridgeResolver = { promise, resolve }; try { - return await this._uiBridgeResolver.promise; + return this._uiBridgeResolver.promise; } finally { this._uiBridgeResolver = undefined; } @@ -250,7 +252,7 @@ export class AppRuntime extends Runtime { this._accountServices = new AccountServices(this._multiAccountController); } - public static async create(nativeBootstrapper: INativeBootstrapper, appConfig?: AppConfigOverwrite): Promise { + public static async create(nativeBootstrapper: INativeBootstrapper, appConfig?: AppConfigOverwrite, eventBus?: EventBus): Promise { // TODO: JSSNMSHDD-2524 (validate app config) if (!nativeBootstrapper.isInitialized) { @@ -283,7 +285,7 @@ export class AppRuntime extends Runtime { databaseFolder: databaseFolder }); - const runtime = new AppRuntime(nativeBootstrapper.nativeEnvironment, mergedConfig); + const runtime = new AppRuntime(nativeBootstrapper.nativeEnvironment, mergedConfig, eventBus); await runtime.init(); runtime.logger.trace("Runtime initialized"); diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index fc096f0d2..85814b846 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -18,7 +18,7 @@ export class AppStringProcessor { protected readonly runtime: { get anonymousServices(): AnonymousServices; requestAccountSelection(title?: string, description?: string, forIdentityTruncated?: string): Promise>; - uiBridge(): Promise; + uiBridge(): Promise | IUIBridge; getServices(accountReference: string | ICoreAddress): Promise; translate(key: string, ...values: any[]): Promise>; get eventBus(): EventBus; @@ -40,9 +40,16 @@ export class AppStringProcessor { } public async processTruncatedReference(truncatedReference: string, account?: LocalAccountDTO): Promise> { - if (account) return await this._handleTruncatedReference(truncatedReference, account); + let reference: Reference; + try { + reference = Reference.fromTruncated(truncatedReference); + } catch (_) { + return UserfriendlyResult.fail( + new UserfriendlyApplicationError("error.appStringProcessor.truncatedReferenceInvalid", "The given code does not contain a valid truncated reference.") + ); + } - const reference = Reference.fromTruncated(truncatedReference); + if (account) return await this._handleReference(reference, account); // process Files and RelationshipTemplates and ask for an account if (truncatedReference.startsWith(Base64ForIdPrefix.File) || truncatedReference.startsWith(Base64ForIdPrefix.RelationshipTemplate)) { @@ -57,7 +64,7 @@ export class AppStringProcessor { return UserfriendlyResult.ok(undefined); } - return await this._handleTruncatedReference(truncatedReference, result.value); + return await this._handleReference(reference, result.value); } if (!truncatedReference.startsWith(Base64ForIdPrefix.Token)) { @@ -65,6 +72,22 @@ export class AppStringProcessor { return UserfriendlyResult.fail(error); } + const promiseOrUiBridge = this.runtime.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + + let password: string | undefined; + if (reference.passwordType) { + const passwordResult = await uiBridge.enterPassword(reference.passwordType); + if (passwordResult.isError) { + return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); + } + + password = passwordResult.value; + } + + // TODO: use password to load the item from the truncated reference + this.logger.error(`user entered password: ${password}`); + const tokenResult = await this.runtime.anonymousServices.tokens.loadPeerToken({ reference: truncatedReference }); if (tokenResult.isError) { return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(tokenResult.error)); @@ -78,7 +101,6 @@ export class AppStringProcessor { } if (tokenContent instanceof TokenContentDeviceSharedSecret) { - const uiBridge = await this.runtime.uiBridge(); await uiBridge.showDeviceOnboarding(DeviceMapper.toDeviceOnboardingInfoDTO(tokenContent.sharedSecret)); return UserfriendlyResult.ok(undefined); } @@ -94,14 +116,28 @@ export class AppStringProcessor { return UserfriendlyResult.ok(undefined); } - return await this._handleTruncatedReference(truncatedReference, selectedAccount); + return await this._handleReference(reference, selectedAccount); } - private async _handleTruncatedReference(truncatedReference: string, account: LocalAccountDTO): Promise> { + private async _handleReference(reference: Reference, account: LocalAccountDTO): Promise> { const services = await this.runtime.getServices(account.id); - const uiBridge = await this.runtime.uiBridge(); + const promiseOrUiBridge = this.runtime.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + + let password: string | undefined; + if (reference.passwordType) { + const passwordResult = await uiBridge.enterPassword(reference.passwordType); + if (passwordResult.isError) { + return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); + } + + password = passwordResult.value; + } + + // TODO: use password to load the item from the truncated reference + this.logger.error(`user entered password: ${password}`); - const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: truncatedReference }); + const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: reference.truncate() }); if (result.isError) { if (result.error.code === "error.runtime.validation.invalidPropertyValue") { return UserfriendlyResult.fail( From 2bf8fd6bf2dd89ab48d899f046a89ace546f6a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 18:15:29 +0200 Subject: [PATCH 06/37] add eslint assert function --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index b6ba9e982..83e210136 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,6 +17,7 @@ "*.expectThrows*", "Then.*", "*.expectPublishedEvents", + "*.expectLastPublishedEvent", "*.executeTests", "expectThrows*" ] From eb360de0cd12188c690bd6a99a8089bc2d7e6641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 18:15:57 +0200 Subject: [PATCH 07/37] chore: add test for personalized RelationshipTemplate --- .../test/runtime/AppStringProcessor.test.ts | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index f8003c20a..11f7666aa 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -1,9 +1,25 @@ -import { mock } from "ts-mockito"; +import { ArbitraryRelationshipTemplateContentJSON } from "@nmshd/content"; +import { CoreDate } from "@nmshd/core-types"; +import { PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime"; +import assert from "assert"; +import { anyNumber, instance, mock, reset, verify, when } from "ts-mockito"; import { AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession } from "../../src"; -import { TestUtil } from "../lib"; +import { MockEventBus, TestUtil } from "../lib"; describe("AppStringProcessor", function () { - let mockUiBridge: IUIBridge; + const mockUiBridge: IUIBridge = mock(); + + when(mockUiBridge.enterPassword(anyNumber())).thenCall((passwordType: number) => { + switch (passwordType) { + case 1: + return "password"; + default: + return "0".repeat(passwordType); + } + }); + + const eventBus = new MockEventBus(); + let runtime1: AppRuntime; let account: LocalAccountDTO; let runtime1Session: LocalAccountSession; @@ -11,7 +27,8 @@ describe("AppStringProcessor", function () { let runtime2: AppRuntime; let runtime2SessionA: LocalAccountSession; - let runtime2SessionB: LocalAccountSession; + + const templateContent: ArbitraryRelationshipTemplateContentJSON = { "@type": "ArbitraryRelationshipTemplateContent", value: "value" }; beforeAll(async function () { runtime1 = await TestUtil.createRuntime(); @@ -20,13 +37,12 @@ describe("AppStringProcessor", function () { account = await runtime1.accountServices.createAccount(Math.random().toString(36).substring(7)); runtime1Session = await runtime1.selectAccount(account.id); - mockUiBridge = mock(); - runtime2 = await TestUtil.createRuntime(undefined, mockUiBridge); + runtime2 = await TestUtil.createRuntime(undefined, instance(mockUiBridge), eventBus); await runtime2.start(); const accounts = await TestUtil.provideAccounts(runtime2, 2); runtime2SessionA = await runtime2.selectAccount(accounts[0].id); - runtime2SessionB = await runtime2.selectAccount(accounts[1].id); + await runtime2.selectAccount(accounts[1].id); }); afterAll(async function () { @@ -34,10 +50,30 @@ describe("AppStringProcessor", function () { await runtime2.stop(); }); + afterEach(function () { + reset(mockUiBridge); + }); + test("should process a URL", async function () { const result = await runtime1.stringProcessor.processURL("nmshd://qr#", account); expect(result.isError).toBeDefined(); expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); }); + + test("should properly handle a personalized RelationshipTemplate", async function () { + const runtime2SessionAAddress = runtime2SessionA.account.address!; + assert(runtime2SessionAAddress); + + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + forIdentity: runtime2SessionAAddress + }); + + await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + + verify(mockUiBridge.enterPassword(anyNumber())).never(); + eventBus.expectLastPublishedEvent(PeerRelationshipTemplateLoadedEvent); + }); }); From 056e3b5d2a29722244bc286fa817afb0758fdac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 18 Oct 2024 18:18:19 +0200 Subject: [PATCH 08/37] test: add second test for no matching relationship --- .../test/runtime/AppStringProcessor.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 11f7666aa..8863ad9b4 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -61,7 +61,7 @@ describe("AppStringProcessor", function () { expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); }); - test("should properly handle a personalized RelationshipTemplate", async function () { + test("should properly handle a personalized RelationshipTemplate with the correct Identity available", async function () { const runtime2SessionAAddress = runtime2SessionA.account.address!; assert(runtime2SessionAAddress); @@ -71,9 +71,25 @@ describe("AppStringProcessor", function () { forIdentity: runtime2SessionAAddress }); - await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result.isSuccess).toBeTruthy(); verify(mockUiBridge.enterPassword(anyNumber())).never(); eventBus.expectLastPublishedEvent(PeerRelationshipTemplateLoadedEvent); }); + + test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { + const runtime2SessionAAddress = runtime1Session.account.address!; + assert(runtime2SessionAAddress); + + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + forIdentity: runtime2SessionAAddress + }); + + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result.isSuccess).toBeFalsy(); + expect(result.error.code).toBe("error.appruntime.general.noAccountAvailableForIdentityTruncated"); + }); }); From 7994c8cebf743abc84766fc3d80259e2445b23c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Tue, 26 Nov 2024 13:38:36 +0100 Subject: [PATCH 09/37] refactor: make password protection typesafe --- packages/transport/src/core/types/PasswordProtection.ts | 4 ++-- .../src/core/types/PasswordProtectionCreationParameters.ts | 4 ++-- .../transport/src/core/types/SharedPasswordProtection.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/transport/src/core/types/PasswordProtection.ts b/packages/transport/src/core/types/PasswordProtection.ts index 0f61087ff..09416a622 100644 --- a/packages/transport/src/core/types/PasswordProtection.ts +++ b/packages/transport/src/core/types/PasswordProtection.ts @@ -3,7 +3,7 @@ import { CoreBuffer, ICoreBuffer } from "@nmshd/crypto"; import { SharedPasswordProtection } from "./SharedPasswordProtection"; export interface IPasswordProtection extends ISerializable { - passwordType: string; + passwordType: "pw" | `pin${number}`; salt: ICoreBuffer; password: string; } @@ -11,7 +11,7 @@ export interface IPasswordProtection extends ISerializable { export class PasswordProtection extends Serializable implements IPasswordProtection { @validate({ regExp: /^(pw|pin(4|5|6|7|8|9|10|11|12|13|14|15|16))$/ }) @serialize() - public passwordType: string; + public passwordType: "pw" | `pin${number}`; @validate({ customValidator: (v: ICoreBuffer) => (v.buffer.byteLength === 16 ? undefined : "must be 16 bytes long") }) @serialize() diff --git a/packages/transport/src/core/types/PasswordProtectionCreationParameters.ts b/packages/transport/src/core/types/PasswordProtectionCreationParameters.ts index 5af7adc42..0780709f6 100644 --- a/packages/transport/src/core/types/PasswordProtectionCreationParameters.ts +++ b/packages/transport/src/core/types/PasswordProtectionCreationParameters.ts @@ -1,14 +1,14 @@ import { ISerializable, Serializable, serialize, validate } from "@js-soft/ts-serval"; export interface IPasswordProtectionCreationParameters extends ISerializable { - passwordType: string; + passwordType: "pw" | `pin${number}`; password: string; } export class PasswordProtectionCreationParameters extends Serializable implements IPasswordProtectionCreationParameters { @validate({ regExp: /^(pw|pin(4|5|6|7|8|9|10|11|12|13|14|15|16))$/ }) @serialize() - public passwordType: string; + public passwordType: "pw" | `pin${number}`; @validate({ min: 1 }) @serialize() diff --git a/packages/transport/src/core/types/SharedPasswordProtection.ts b/packages/transport/src/core/types/SharedPasswordProtection.ts index 629a3eecd..cd8267bff 100644 --- a/packages/transport/src/core/types/SharedPasswordProtection.ts +++ b/packages/transport/src/core/types/SharedPasswordProtection.ts @@ -3,14 +3,14 @@ import { CoreBuffer, ICoreBuffer } from "@nmshd/crypto"; import { TransportCoreErrors } from "../TransportCoreErrors"; export interface ISharedPasswordProtection extends ISerializable { - passwordType: string; + passwordType: "pw" | `pin${number}`; salt: ICoreBuffer; } export class SharedPasswordProtection extends Serializable implements ISharedPasswordProtection { @validate({ regExp: /^(pw|pin(4|5|6|7|8|9|10|11|12|13|14|15|16))$/ }) @serialize() - public passwordType: string; + public passwordType: "pw" | `pin${number}`; @validate({ customValidator: (v: ICoreBuffer) => (v.buffer.byteLength === 16 ? undefined : "must be 16 bytes long") }) @serialize() @@ -28,7 +28,7 @@ export class SharedPasswordProtection extends Serializable implements ISharedPas throw TransportCoreErrors.general.invalidTruncatedReference("The password part of a TruncatedReference must consist of exactly 2 components."); } - const passwordType = splittedPasswordParts[0]; + const passwordType = splittedPasswordParts[0] as "pw" | `pin${number}`; try { const salt = CoreBuffer.fromBase64(splittedPasswordParts[1]); return SharedPasswordProtection.from({ passwordType, salt }); From 094b599679aa4fe13ea4cad2d71d404b2708c5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Tue, 26 Nov 2024 13:39:34 +0100 Subject: [PATCH 10/37] refactor: adapt to more runtime changes --- packages/app-runtime/src/AppStringProcessor.ts | 14 ++++++++++---- .../app-runtime/src/extensibility/ui/IUIBridge.ts | 2 +- packages/app-runtime/test/lib/FakeUIBridge.ts | 2 +- .../test/runtime/AppStringProcessor.test.ts | 12 ++++++------ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 85814b846..d20a515d1 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -76,8 +76,11 @@ export class AppStringProcessor { const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; let password: string | undefined; - if (reference.passwordType) { - const passwordResult = await uiBridge.enterPassword(reference.passwordType); + if (reference.passwordProtection) { + const passwordResult = await uiBridge.enterPassword( + reference.passwordProtection!.passwordType === "pw" ? "pw" : "pin", + reference.passwordProtection!.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection!.passwordType) + ); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); } @@ -125,8 +128,11 @@ export class AppStringProcessor { const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; let password: string | undefined; - if (reference.passwordType) { - const passwordResult = await uiBridge.enterPassword(reference.passwordType); + if (reference.passwordProtection) { + const passwordResult = await uiBridge.enterPassword( + reference.passwordProtection!.passwordType === "pw" ? "pw" : "pin", + reference.passwordProtection!.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection!.passwordType) + ); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); } diff --git a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts index d15e6f239..f4ca4641b 100644 --- a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts +++ b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts @@ -11,5 +11,5 @@ export interface IUIBridge { showRequest(account: LocalAccountDTO, request: LocalRequestDVO): Promise>; showError(error: UserfriendlyApplicationError, account?: LocalAccountDTO): Promise>; requestAccountSelection(possibleAccounts: LocalAccountDTO[], title?: string, description?: string): Promise>; - enterPassword(passwordType: number): Promise>; + enterPassword(passwordType: "pw" | "pin", pinLength: number | undefined): Promise>; } diff --git a/packages/app-runtime/test/lib/FakeUIBridge.ts b/packages/app-runtime/test/lib/FakeUIBridge.ts index 495e366f1..cecc85dfe 100644 --- a/packages/app-runtime/test/lib/FakeUIBridge.ts +++ b/packages/app-runtime/test/lib/FakeUIBridge.ts @@ -30,7 +30,7 @@ export class FakeUIBridge implements IUIBridge { throw new Error("Method not implemented."); } - public enterPassword(_passwordType: number): Promise> { + public enterPassword(_passwordType: "pw" | "pin", _pinLength: number | undefined): Promise> { throw new Error("Method not implemented."); } } diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 8863ad9b4..fd77e26d2 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -2,19 +2,19 @@ import { ArbitraryRelationshipTemplateContentJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime"; import assert from "assert"; -import { anyNumber, instance, mock, reset, verify, when } from "ts-mockito"; +import { anyNumber, anyString, instance, mock, reset, verify, when } from "ts-mockito"; import { AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession } from "../../src"; import { MockEventBus, TestUtil } from "../lib"; describe("AppStringProcessor", function () { const mockUiBridge: IUIBridge = mock(); - when(mockUiBridge.enterPassword(anyNumber())).thenCall((passwordType: number) => { + when(mockUiBridge.enterPassword(anyString(), anyNumber())).thenCall((passwordType: "pw" | "pin", pinLength: number | undefined) => { switch (passwordType) { - case 1: + case "pw": return "password"; - default: - return "0".repeat(passwordType); + case "pin": + return "0".repeat(pinLength!); } }); @@ -74,7 +74,7 @@ describe("AppStringProcessor", function () { const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); expect(result.isSuccess).toBeTruthy(); - verify(mockUiBridge.enterPassword(anyNumber())).never(); + verify(mockUiBridge.enterPassword(anyString(), anyNumber())).never(); eventBus.expectLastPublishedEvent(PeerRelationshipTemplateLoadedEvent); }); From a4e3378298f2fe153223f26a0d8d063cfaa648a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 28 Nov 2024 15:16:25 +0100 Subject: [PATCH 11/37] chore: use any casts for testing --- packages/transport/test/modules/files/FileReference.test.ts | 2 +- .../relationshipTemplates/RelationshipTemplateReference.test.ts | 2 +- packages/transport/test/modules/tokens/TokenContent.test.ts | 2 +- packages/transport/test/modules/tokens/TokenReference.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/transport/test/modules/files/FileReference.test.ts b/packages/transport/test/modules/files/FileReference.test.ts index 04e23d1ea..e74b52be7 100644 --- a/packages/transport/test/modules/files/FileReference.test.ts +++ b/packages/transport/test/modules/files/FileReference.test.ts @@ -208,7 +208,7 @@ describe("FileReference", function () { key: await CryptoEncryption.generateKey(), id: await BackboneIds.file.generateUnsafe(), passwordProtection: { - passwordType: "pc", + passwordType: "pc" as any, salt: await CoreCrypto.random(16) } }); diff --git a/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateReference.test.ts b/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateReference.test.ts index 416e5d64f..ed6c2526c 100644 --- a/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateReference.test.ts +++ b/packages/transport/test/modules/relationshipTemplates/RelationshipTemplateReference.test.ts @@ -208,7 +208,7 @@ describe("RelationshipTemplateReference", function () { key: await CryptoEncryption.generateKey(), id: await BackboneIds.relationshipTemplate.generateUnsafe(), passwordProtection: { - passwordType: "pc", + passwordType: "pc" as any, salt: await CoreCrypto.random(16) } }); diff --git a/packages/transport/test/modules/tokens/TokenContent.test.ts b/packages/transport/test/modules/tokens/TokenContent.test.ts index c4c56f6c1..2b1039547 100644 --- a/packages/transport/test/modules/tokens/TokenContent.test.ts +++ b/packages/transport/test/modules/tokens/TokenContent.test.ts @@ -198,7 +198,7 @@ describe("TokenContent", function () { secretKey: await CryptoEncryption.generateKey(), templateId: await CoreIdHelper.notPrefixed.generate(), passwordProtection: { - passwordType: "pc", + passwordType: "pc" as any, salt: await CoreCrypto.random(16) } }); diff --git a/packages/transport/test/modules/tokens/TokenReference.test.ts b/packages/transport/test/modules/tokens/TokenReference.test.ts index 1396f98c3..3ce4de017 100644 --- a/packages/transport/test/modules/tokens/TokenReference.test.ts +++ b/packages/transport/test/modules/tokens/TokenReference.test.ts @@ -208,7 +208,7 @@ describe("TokenReference", function () { key: await CryptoEncryption.generateKey(), id: await BackboneIds.token.generateUnsafe(), passwordProtection: { - passwordType: "pc", + passwordType: "pc" as any, salt: await CoreCrypto.random(16) } }); From 52ee1bce212d22890bf103b7931aea0a27c315c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 28 Nov 2024 15:18:06 +0100 Subject: [PATCH 12/37] fix: eslint --- packages/app-runtime/src/AppStringProcessor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index d20a515d1..1515cb165 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -78,8 +78,8 @@ export class AppStringProcessor { let password: string | undefined; if (reference.passwordProtection) { const passwordResult = await uiBridge.enterPassword( - reference.passwordProtection!.passwordType === "pw" ? "pw" : "pin", - reference.passwordProtection!.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection!.passwordType) + reference.passwordProtection.passwordType === "pw" ? "pw" : "pin", + reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType.substring(3)) ); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); @@ -130,8 +130,8 @@ export class AppStringProcessor { let password: string | undefined; if (reference.passwordProtection) { const passwordResult = await uiBridge.enterPassword( - reference.passwordProtection!.passwordType === "pw" ? "pw" : "pin", - reference.passwordProtection!.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection!.passwordType) + reference.passwordProtection.passwordType === "pw" ? "pw" : "pin", + reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType) ); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); From d0c296010550da1e72c0adc3479016c1c36694ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 28 Nov 2024 15:18:39 +0100 Subject: [PATCH 13/37] fix: add substring --- packages/app-runtime/src/AppStringProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 1515cb165..6788b57ae 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -131,7 +131,7 @@ export class AppStringProcessor { if (reference.passwordProtection) { const passwordResult = await uiBridge.enterPassword( reference.passwordProtection.passwordType === "pw" ? "pw" : "pin", - reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType) + reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType.substring(3)) ); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); From b0380dfb9ebea0670ec43e8bb5e086dea5475670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 16:29:06 +0100 Subject: [PATCH 14/37] feat: use the provided password to load objects --- packages/app-runtime/src/AppStringProcessor.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 6788b57ae..8d97b5d57 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -88,11 +88,9 @@ export class AppStringProcessor { password = passwordResult.value; } - // TODO: use password to load the item from the truncated reference - this.logger.error(`user entered password: ${password}`); - - const tokenResult = await this.runtime.anonymousServices.tokens.loadPeerToken({ reference: truncatedReference }); + const tokenResult = await this.runtime.anonymousServices.tokens.loadPeerToken({ reference: truncatedReference, password: password }); if (tokenResult.isError) { + // TODO: should it be possible to ask for the password again when it was wrong? return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(tokenResult.error)); } @@ -140,10 +138,7 @@ export class AppStringProcessor { password = passwordResult.value; } - // TODO: use password to load the item from the truncated reference - this.logger.error(`user entered password: ${password}`); - - const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: reference.truncate() }); + const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: reference.truncate(), password: password }); if (result.isError) { if (result.error.code === "error.runtime.validation.invalidPropertyValue") { return UserfriendlyResult.fail( @@ -151,6 +146,7 @@ export class AppStringProcessor { ); } + // TODO: should it be possible to ask for the password again when it was wrong? return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(result.error)); } From b9ea22610deafcfcebb80ac16401e84f6a9114bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 16:29:39 +0100 Subject: [PATCH 15/37] feat: proper eventbus --- packages/app-runtime/package.json | 1 + packages/app-runtime/test/customMatchers.ts | 33 ++++++ packages/app-runtime/test/lib/MockEventBus.ts | 112 +++++++++++------- 3 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 packages/app-runtime/test/customMatchers.ts diff --git a/packages/app-runtime/package.json b/packages/app-runtime/package.json index 3140422b7..3dcbde9ca 100644 --- a/packages/app-runtime/package.json +++ b/packages/app-runtime/package.json @@ -37,6 +37,7 @@ "maxWorkers": 5, "preset": "ts-jest", "setupFilesAfterEnv": [ + "./test/customMatchers.ts", "jest-expect-message" ], "testEnvironment": "node", diff --git a/packages/app-runtime/test/customMatchers.ts b/packages/app-runtime/test/customMatchers.ts new file mode 100644 index 000000000..8401936c8 --- /dev/null +++ b/packages/app-runtime/test/customMatchers.ts @@ -0,0 +1,33 @@ +import { EventConstructor } from "@js-soft/ts-utils"; +import { MockEventBus } from "./lib"; + +expect.extend({ + async toHavePublished(eventBus: unknown, eventConstructor: EventConstructor, eventConditions?: (event: TEvent) => boolean) { + if (!(eventBus instanceof MockEventBus)) { + throw new Error("This method can only be used with expect(MockEventBus)."); + } + + await eventBus.waitForRunningEventHandlers(); + const matchingEvents = eventBus.publishedEvents.filter((x) => x instanceof eventConstructor && (eventConditions?.(x) ?? true)); + if (matchingEvents.length > 0) { + return { + pass: true, + message: () => + `There were one or more events that matched the specified criteria, even though there should be none. The matching events are: ${JSON.stringify( + matchingEvents, + undefined, + 2 + )}` + }; + } + return { pass: false, message: () => `The expected event wasn't published. The published events are: ${JSON.stringify(eventBus.publishedEvents, undefined, 2)}` }; + } +}); + +declare global { + namespace jest { + interface Matchers { + toHavePublished(eventConstructor: EventConstructor, eventConditions?: (event: TEvent) => boolean): Promise; + } + } +} diff --git a/packages/app-runtime/test/lib/MockEventBus.ts b/packages/app-runtime/test/lib/MockEventBus.ts index 04428c93a..7653e6f73 100644 --- a/packages/app-runtime/test/lib/MockEventBus.ts +++ b/packages/app-runtime/test/lib/MockEventBus.ts @@ -1,56 +1,86 @@ -/* eslint-disable jest/no-standalone-expect */ -import { DataEvent, Event, EventBus, EventHandler, SubscriptionTarget } from "@js-soft/ts-utils"; -import { TransportDataEvent } from "@nmshd/transport"; - -export class MockEventBus extends EventBus { - private readonly _publishedEvents: { namespace: string; data?: any }[] = []; - public get publishedEvents(): { namespace: string; data?: any }[] { - return this._publishedEvents; - } +import { Event, EventBus, EventEmitter2EventBus, getEventNamespaceFromObject, SubscriptionTarget } from "@js-soft/ts-utils"; - public clearPublishedEvents(): void { - this._publishedEvents.splice(0); - } +export class MockEventBus extends EventEmitter2EventBus { + public publishedEvents: Event[] = []; + private publishPromises: Promise[] = []; + private readonly publishPromisesWithName: { promise: Promise; name: string }[] = []; - public subscribe(_subscriptionTarget: SubscriptionTarget, _handler: EventHandler): number { - // noop - return 0; + public constructor() { + super((_) => { + // no-op + }); } - public subscribeOnce(_subscriptionTarget: SubscriptionTarget, _handler: EventHandler): number { - // noop - return 0; + public override publish(event: Event): void { + this.publishedEvents.push(event); + + const namespace = getEventNamespaceFromObject(event); + + if (!namespace) { + throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for a event."); + } + + const promise = this.emitter.emitAsync(namespace, event); + + this.publishPromises.push(promise); + this.publishPromisesWithName.push({ promise: promise, name: namespace }); } - public unsubscribe(_subscriptionId: number): boolean { - // noop - return true; + public async waitForEvent( + subscriptionTarget: SubscriptionTarget & { namespace: string }, + predicate?: (event: TEvent) => boolean + ): Promise { + const alreadyTriggeredEvents = this.publishedEvents.find( + (e) => + e.namespace === subscriptionTarget.namespace && + (typeof subscriptionTarget === "string" || e instanceof subscriptionTarget) && + (!predicate || predicate(e as TEvent)) + ) as TEvent | undefined; + if (alreadyTriggeredEvents) { + return alreadyTriggeredEvents; + } + + const event = await waitForEvent(this, subscriptionTarget, predicate); + return event; } - public publish(event: Event): void { - this._publishedEvents.push({ - namespace: event.namespace, - data: event instanceof TransportDataEvent ? event.data : undefined - }); + public async waitForRunningEventHandlers(): Promise { + await Promise.all(this.publishPromises); } - public close(): void { - // noop + public reset(): void { + this.publishedEvents = []; + this.publishPromises = []; } +} - public expectLastPublishedEvent>( - eventConstructor: (new (...args: any[]) => TEvent) & { namespace: string }, - data?: Partial ? X : never> - ): void { - const lastEvent = this.publishedEvents[this.publishedEvents.length - 1]; - expect(lastEvent.namespace).toStrictEqual(eventConstructor.namespace); +async function waitForEvent( + eventBus: EventBus, + subscriptionTarget: SubscriptionTarget, + assertionFunction?: (t: TEvent) => boolean, + timeout = 5000 +): Promise { + let subscriptionId: number; - if (data) expect(lastEvent.data).toStrictEqual(expect.objectContaining(data)); - } + const eventPromise = new Promise((resolve) => { + subscriptionId = eventBus.subscribe(subscriptionTarget, (event: TEvent) => { + if (assertionFunction && !assertionFunction(event)) return; - public expectPublishedEvents(...eventContructors: ((new (...args: any[]) => DataEvent) & { namespace: string })[]): void { - const eventNamespaces = this.publishedEvents.map((e) => e.namespace); - const constructorNamespaces = eventContructors.map((c) => c.namespace); - expect(eventNamespaces).toStrictEqual(constructorNamespaces); - } + resolve(event); + }); + }); + if (!timeout) return await eventPromise.finally(() => eventBus.unsubscribe(subscriptionId)); + + let timeoutId: NodeJS.Timeout; + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutId = setTimeout( + () => reject(new Error(`timeout exceeded for waiting for event ${typeof subscriptionTarget === "string" ? subscriptionTarget : subscriptionTarget.name}`)), + timeout + ); + }); + + return await Promise.race([eventPromise, timeoutPromise]).finally(() => { + eventBus.unsubscribe(subscriptionId); + clearTimeout(timeoutId); + }); } From 3872c1da95c9b8c003f3b8ade78dc9edfb89ba43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 16:30:48 +0100 Subject: [PATCH 16/37] fix: properly await the UIBridge --- packages/app-runtime/src/AppRuntime.ts | 6 ++++-- .../app-runtime/src/modules/appEvents/AppLaunchModule.ts | 4 +++- .../app-runtime/src/modules/appEvents/MailReceivedModule.ts | 5 ++++- .../src/modules/appEvents/OnboardingChangeReceivedModule.ts | 4 +++- .../appEvents/RelationshipTemplateProcessedModule.ts | 3 ++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 8b1cf5195..535dafea3 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -208,8 +208,10 @@ export class AppRuntime extends Runtime { } private async selectAccountViaBridge(accounts: LocalAccountDTO[], title: string, description: string): Promise> { - const bridge = await this.uiBridge(); - const accountSelectionResult = await bridge.requestAccountSelection(accounts, title, description); + const promiseOrUiBridge = this.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + + const accountSelectionResult = await uiBridge.requestAccountSelection(accounts, title, description); if (accountSelectionResult.isError) { return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailable(accountSelectionResult.error)); } diff --git a/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts b/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts index e0d8aa933..200951167 100644 --- a/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts +++ b/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts @@ -19,7 +19,9 @@ export class AppLaunchModule extends AppRuntimeModule { const result = await this.runtime.stringProcessor.processURL(event.url); if (result.isSuccess) return; - const uiBridge = await this.runtime.uiBridge(); + const promiseOrUiBridge = this.runtime.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + await uiBridge.showError(result.error); } diff --git a/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts b/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts index 601d8bb7e..467c68afb 100644 --- a/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts @@ -23,7 +23,10 @@ export class MailReceivedModule extends AppRuntimeModule { - await (await this.runtime.uiBridge()).showMessage(session.account, sender, mail); + const promiseOrUiBridge = this.runtime.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + + await uiBridge.showMessage(session.account, sender, mail); } }); } diff --git a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts index 7408b7c19..c65996a12 100644 --- a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts @@ -49,7 +49,9 @@ export class OnboardingChangeReceivedModule extends AppRuntimeModule { - const uiBridge = await this.runtime.uiBridge(); + const promiseOrUiBridge = this.runtime.uiBridge(); + const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + await uiBridge.showRelationship(session.account, identity); } }); diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index be018d576..e8149a8f1 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -15,7 +15,8 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule Date: Fri, 29 Nov 2024 16:31:32 +0100 Subject: [PATCH 17/37] fix: proper mock event bus usage --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index fd77e26d2..1b25d9535 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -75,7 +75,7 @@ describe("AppStringProcessor", function () { expect(result.isSuccess).toBeTruthy(); verify(mockUiBridge.enterPassword(anyString(), anyNumber())).never(); - eventBus.expectLastPublishedEvent(PeerRelationshipTemplateLoadedEvent); + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { From a61dcb80e1db372d7d888f13d934e8bf2ff0ebef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 16:31:32 +0100 Subject: [PATCH 18/37] fix: proper mock event bus usage --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index fd77e26d2..1b25d9535 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -75,7 +75,7 @@ describe("AppStringProcessor", function () { expect(result.isSuccess).toBeTruthy(); verify(mockUiBridge.enterPassword(anyString(), anyNumber())).never(); - eventBus.expectLastPublishedEvent(PeerRelationshipTemplateLoadedEvent); + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { From c5d9f7782e04068a2f7732c8f4dd61c7acf212f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:35:04 +0100 Subject: [PATCH 19/37] chore: add MockUIBridge --- .../src/extensibility/ui/IUIBridge.ts | 2 +- packages/app-runtime/test/lib/FakeUIBridge.ts | 2 +- packages/app-runtime/test/lib/MockUIBridge.ts | 59 +++++++++++++++++++ packages/app-runtime/test/lib/index.ts | 1 + 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/app-runtime/test/lib/MockUIBridge.ts diff --git a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts index f4ca4641b..afbbefacd 100644 --- a/packages/app-runtime/src/extensibility/ui/IUIBridge.ts +++ b/packages/app-runtime/src/extensibility/ui/IUIBridge.ts @@ -11,5 +11,5 @@ export interface IUIBridge { showRequest(account: LocalAccountDTO, request: LocalRequestDVO): Promise>; showError(error: UserfriendlyApplicationError, account?: LocalAccountDTO): Promise>; requestAccountSelection(possibleAccounts: LocalAccountDTO[], title?: string, description?: string): Promise>; - enterPassword(passwordType: "pw" | "pin", pinLength: number | undefined): Promise>; + enterPassword(passwordType: "pw" | "pin", pinLength?: number): Promise>; } diff --git a/packages/app-runtime/test/lib/FakeUIBridge.ts b/packages/app-runtime/test/lib/FakeUIBridge.ts index cecc85dfe..f411ab891 100644 --- a/packages/app-runtime/test/lib/FakeUIBridge.ts +++ b/packages/app-runtime/test/lib/FakeUIBridge.ts @@ -30,7 +30,7 @@ export class FakeUIBridge implements IUIBridge { throw new Error("Method not implemented."); } - public enterPassword(_passwordType: "pw" | "pin", _pinLength: number | undefined): Promise> { + public enterPassword(_passwordType: "pw" | "pin", _pinLength?: number): Promise> { throw new Error("Method not implemented."); } } diff --git a/packages/app-runtime/test/lib/MockUIBridge.ts b/packages/app-runtime/test/lib/MockUIBridge.ts new file mode 100644 index 000000000..2f8aa61dd --- /dev/null +++ b/packages/app-runtime/test/lib/MockUIBridge.ts @@ -0,0 +1,59 @@ +import { ApplicationError, Result } from "@js-soft/ts-utils"; +import { DeviceOnboardingInfoDTO, FileDVO, IdentityDVO, LocalRequestDVO, MailDVO, MessageDVO, RequestMessageDVO } from "@nmshd/runtime"; +import { IUIBridge, LocalAccountDTO, UserfriendlyApplicationError } from "../../src"; + +export class MockUIBridge implements IUIBridge { + private _accountIdToReturn: string | undefined; + public set accountIdToReturn(value: string | undefined) { + this._accountIdToReturn = value; + } + + private _passwordToReturn: string | undefined; + public set passwordToReturn(value: string | undefined) { + this._passwordToReturn = value; + } + + public reset(): void { + this._passwordToReturn = undefined; + this._accountIdToReturn = undefined; + } + + public showMessage(_account: LocalAccountDTO, _relationship: IdentityDVO, _message: MessageDVO | MailDVO | RequestMessageDVO): Promise> { + throw new Error("Method not implemented."); + } + + public showRelationship(_account: LocalAccountDTO, _relationship: IdentityDVO): Promise> { + throw new Error("Method not implemented."); + } + + public showFile(_account: LocalAccountDTO, _file: FileDVO): Promise> { + throw new Error("Method not implemented."); + } + + public showDeviceOnboarding(_deviceOnboardingInfo: DeviceOnboardingInfoDTO): Promise> { + throw new Error("Method not implemented."); + } + + public showRequest(_account: LocalAccountDTO, _request: LocalRequestDVO): Promise> { + throw new Error("Method not implemented."); + } + + public showError(_error: UserfriendlyApplicationError, _account?: LocalAccountDTO): Promise> { + throw new Error("Method not implemented."); + } + + public requestAccountSelection(possibleAccounts: LocalAccountDTO[], _title?: string, _description?: string): Promise> { + if (!this._accountIdToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); + + const foundAccount = possibleAccounts.find((x) => x.id === this._accountIdToReturn); + if (!foundAccount) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); + + return Promise.resolve(Result.ok(foundAccount)); + } + + public enterPassword(_passwordType: "pw" | "pin", _pinLength?: number): Promise> { + if (!this._passwordToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); + + return Promise.resolve(Result.ok(this._passwordToReturn)); + } +} diff --git a/packages/app-runtime/test/lib/index.ts b/packages/app-runtime/test/lib/index.ts index b4a94a2e8..b714eec25 100644 --- a/packages/app-runtime/test/lib/index.ts +++ b/packages/app-runtime/test/lib/index.ts @@ -1,4 +1,5 @@ export * from "./EventListener"; export * from "./FakeUIBridge"; export * from "./MockEventBus"; +export * from "./MockUIBridge"; export * from "./TestUtil"; From 7c711c3952465b9fe4702fb09d79a87f4458620e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:37:14 +0100 Subject: [PATCH 20/37] refactor: simplify tests --- packages/app-runtime/test/customMatchers.ts | 47 ++++++++++++++++++- .../test/runtime/AppStringProcessor.test.ts | 32 ++++--------- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/app-runtime/test/customMatchers.ts b/packages/app-runtime/test/customMatchers.ts index 8401936c8..7c003837a 100644 --- a/packages/app-runtime/test/customMatchers.ts +++ b/packages/app-runtime/test/customMatchers.ts @@ -1,7 +1,50 @@ -import { EventConstructor } from "@js-soft/ts-utils"; +import { ApplicationError, EventConstructor, Result } from "@js-soft/ts-utils"; import { MockEventBus } from "./lib"; expect.extend({ + toBeSuccessful(actual: Result) { + if (!(actual instanceof Result)) { + return { pass: false, message: () => "expected an instance of Result." }; + } + + return { + pass: actual.isSuccess, + message: () => `expected a successful result; got an error result with the error message '${actual.error.message}'.` + }; + }, + + toBeAnError(actual: Result, expectedMessage: string | RegExp, expectedCode: string | RegExp) { + if (!(actual instanceof Result)) { + return { + pass: false, + message: () => "expected an instance of Result." + }; + } + + if (!actual.isError) { + return { + pass: false, + message: () => "expected an error result, but it was successful." + }; + } + + if (actual.error.message.match(new RegExp(expectedMessage)) === null) { + return { + pass: false, + message: () => `expected the error message of the result to match '${expectedMessage}', but received '${actual.error.message}'.` + }; + } + + if (actual.error.code.match(new RegExp(expectedCode)) === null) { + return { + pass: false, + message: () => `expected the error code of the result to match '${expectedCode}', but received '${actual.error.code}'.` + }; + } + + return { pass: true, message: () => "" }; + }, + async toHavePublished(eventBus: unknown, eventConstructor: EventConstructor, eventConditions?: (event: TEvent) => boolean) { if (!(eventBus instanceof MockEventBus)) { throw new Error("This method can only be used with expect(MockEventBus)."); @@ -27,6 +70,8 @@ expect.extend({ declare global { namespace jest { interface Matchers { + toBeSuccessful(): R; + toBeAnError(expectedMessage: string | RegExp, expectedCode: string | RegExp): R; toHavePublished(eventConstructor: EventConstructor, eventConditions?: (event: TEvent) => boolean): Promise; } } diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 1b25d9535..798c2f4cc 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -2,26 +2,14 @@ import { ArbitraryRelationshipTemplateContentJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import { PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime"; import assert from "assert"; -import { anyNumber, anyString, instance, mock, reset, verify, when } from "ts-mockito"; -import { AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession } from "../../src"; -import { MockEventBus, TestUtil } from "../lib"; +import { AppRuntime, LocalAccountSession } from "../../src"; +import { MockEventBus, MockUIBridge, TestUtil } from "../lib"; describe("AppStringProcessor", function () { - const mockUiBridge: IUIBridge = mock(); - - when(mockUiBridge.enterPassword(anyString(), anyNumber())).thenCall((passwordType: "pw" | "pin", pinLength: number | undefined) => { - switch (passwordType) { - case "pw": - return "password"; - case "pin": - return "0".repeat(pinLength!); - } - }); - + const mockUiBridge = new MockUIBridge(); const eventBus = new MockEventBus(); let runtime1: AppRuntime; - let account: LocalAccountDTO; let runtime1Session: LocalAccountSession; let runtime2: AppRuntime; @@ -34,10 +22,10 @@ describe("AppStringProcessor", function () { runtime1 = await TestUtil.createRuntime(); await runtime1.start(); - account = await runtime1.accountServices.createAccount(Math.random().toString(36).substring(7)); + const account = await runtime1.accountServices.createAccount(Math.random().toString(36).substring(7)); runtime1Session = await runtime1.selectAccount(account.id); - runtime2 = await TestUtil.createRuntime(undefined, instance(mockUiBridge), eventBus); + runtime2 = await TestUtil.createRuntime(undefined, mockUiBridge, eventBus); await runtime2.start(); const accounts = await TestUtil.provideAccounts(runtime2, 2); @@ -51,11 +39,11 @@ describe("AppStringProcessor", function () { }); afterEach(function () { - reset(mockUiBridge); + mockUiBridge.reset(); }); test("should process a URL", async function () { - const result = await runtime1.stringProcessor.processURL("nmshd://qr#", account); + const result = await runtime1.stringProcessor.processURL("nmshd://qr#", runtime1Session.account); expect(result.isError).toBeDefined(); expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); @@ -72,9 +60,8 @@ describe("AppStringProcessor", function () { }); const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); - expect(result.isSuccess).toBeTruthy(); + expect(result).toBeSuccessful(); - verify(mockUiBridge.enterPassword(anyString(), anyNumber())).never(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); }); @@ -89,7 +76,6 @@ describe("AppStringProcessor", function () { }); const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); - expect(result.isSuccess).toBeFalsy(); - expect(result.error.code).toBe("error.appruntime.general.noAccountAvailableForIdentityTruncated"); + expect(result).toBeAnError("There is no account matching the given 'forIdentityTruncated'.", "error.appruntime.general.noAccountAvailableForIdentityTruncated"); }); }); From 16237d4ab4f0c53c8cdd516d1277566ae4a3c838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:37:56 +0100 Subject: [PATCH 21/37] feat: add password protection tests --- .../test/runtime/AppStringProcessor.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 798c2f4cc..e8568e4dd 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -78,4 +78,39 @@ describe("AppStringProcessor", function () { const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); expect(result).toBeAnError("There is no account matching the given 'forIdentityTruncated'.", "error.appruntime.general.noAccountAvailableForIdentityTruncated"); }); + + test("should properly handle a password protected RelationshipTemplate", async function () { + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + passwordProtection: { password: "password" } + }); + + mockUiBridge.passwordToReturn = "password"; + mockUiBridge.accountIdToReturn = runtime2SessionA.account.id; + + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result).toBeSuccessful(); + expect(result.value).toBeUndefined(); + + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + }); + + test("should properly handle a pin protected RelationshipTemplate", async function () { + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + passwordProtection: { password: "000000", passwordIsPin: true }, + forIdentity: runtime2SessionA.account.address! + }); + + mockUiBridge.passwordToReturn = "000000"; + mockUiBridge.accountIdToReturn = runtime2SessionA.account.id; + + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result).toBeSuccessful(); + expect(result.value).toBeUndefined(); + + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + }); }); From dcbd7272bcf8d7f18265a13ccc386d24f5f84cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:38:59 +0100 Subject: [PATCH 22/37] chore: remove forIdentity --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index e8568e4dd..98e4233fe 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -100,8 +100,7 @@ describe("AppStringProcessor", function () { const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ content: templateContent, expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), - passwordProtection: { password: "000000", passwordIsPin: true }, - forIdentity: runtime2SessionA.account.address! + passwordProtection: { password: "000000", passwordIsPin: true } }); mockUiBridge.passwordToReturn = "000000"; From c59a631d6403d82ad8718b9680e7e6149dceb5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:39:53 +0100 Subject: [PATCH 23/37] chore: add combinated test --- .../test/runtime/AppStringProcessor.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 98e4233fe..74676920a 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -112,4 +112,38 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); }); + + test("should properly handle a password protected personalized RelationshipTemplate", async function () { + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + passwordProtection: { password: "password" }, + forIdentity: runtime2SessionA.account.address! + }); + + mockUiBridge.passwordToReturn = "password"; + + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result).toBeSuccessful(); + expect(result.value).toBeUndefined(); + + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + }); + + test("should properly handle a pin protected personalized RelationshipTemplate", async function () { + const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: templateContent, + expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), + passwordProtection: { password: "000000", passwordIsPin: true }, + forIdentity: runtime2SessionA.account.address! + }); + + mockUiBridge.passwordToReturn = "000000"; + + const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); + expect(result).toBeSuccessful(); + expect(result.value).toBeUndefined(); + + await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + }); }); From 5c94b994514b65684b84d59fbb71dfbee586cc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 29 Nov 2024 17:43:50 +0100 Subject: [PATCH 24/37] chore: re-simplify uiBridge calls --- packages/app-runtime/src/AppRuntime.ts | 3 +-- packages/app-runtime/src/AppStringProcessor.ts | 6 ++---- .../app-runtime/src/modules/appEvents/AppLaunchModule.ts | 4 +--- .../app-runtime/src/modules/appEvents/MailReceivedModule.ts | 4 +--- .../src/modules/appEvents/OnboardingChangeReceivedModule.ts | 4 +--- .../appEvents/RelationshipTemplateProcessedModule.ts | 3 +-- 6 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 535dafea3..7588e6869 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -208,8 +208,7 @@ export class AppRuntime extends Runtime { } private async selectAccountViaBridge(accounts: LocalAccountDTO[], title: string, description: string): Promise> { - const promiseOrUiBridge = this.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + const uiBridge = await this.uiBridge(); const accountSelectionResult = await uiBridge.requestAccountSelection(accounts, title, description); if (accountSelectionResult.isError) { diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 8d97b5d57..2abf4fa19 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -72,8 +72,7 @@ export class AppStringProcessor { return UserfriendlyResult.fail(error); } - const promiseOrUiBridge = this.runtime.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + const uiBridge = await this.runtime.uiBridge(); let password: string | undefined; if (reference.passwordProtection) { @@ -122,8 +121,7 @@ export class AppStringProcessor { private async _handleReference(reference: Reference, account: LocalAccountDTO): Promise> { const services = await this.runtime.getServices(account.id); - const promiseOrUiBridge = this.runtime.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; + const uiBridge = await this.runtime.uiBridge(); let password: string | undefined; if (reference.passwordProtection) { diff --git a/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts b/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts index 200951167..e0d8aa933 100644 --- a/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts +++ b/packages/app-runtime/src/modules/appEvents/AppLaunchModule.ts @@ -19,9 +19,7 @@ export class AppLaunchModule extends AppRuntimeModule { const result = await this.runtime.stringProcessor.processURL(event.url); if (result.isSuccess) return; - const promiseOrUiBridge = this.runtime.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; - + const uiBridge = await this.runtime.uiBridge(); await uiBridge.showError(result.error); } diff --git a/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts b/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts index 467c68afb..480bc08d8 100644 --- a/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/MailReceivedModule.ts @@ -23,9 +23,7 @@ export class MailReceivedModule extends AppRuntimeModule { - const promiseOrUiBridge = this.runtime.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; - + const uiBridge = await this.runtime.uiBridge(); await uiBridge.showMessage(session.account, sender, mail); } }); diff --git a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts index c65996a12..7408b7c19 100644 --- a/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/OnboardingChangeReceivedModule.ts @@ -49,9 +49,7 @@ export class OnboardingChangeReceivedModule extends AppRuntimeModule { - const promiseOrUiBridge = this.runtime.uiBridge(); - const uiBridge = promiseOrUiBridge instanceof Promise ? await promiseOrUiBridge : promiseOrUiBridge; - + const uiBridge = await this.runtime.uiBridge(); await uiBridge.showRelationship(session.account, identity); } }); diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index e8149a8f1..be018d576 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -15,8 +15,7 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule Date: Mon, 2 Dec 2024 09:54:26 +0100 Subject: [PATCH 25/37] chore: wording --- packages/transport/src/core/Reference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transport/src/core/Reference.ts b/packages/transport/src/core/Reference.ts index 69d3b4cf2..944c6237d 100644 --- a/packages/transport/src/core/Reference.ts +++ b/packages/transport/src/core/Reference.ts @@ -50,7 +50,7 @@ export class Reference extends Serializable implements IReference { if (![3, 5].includes(splitted.length)) { throw TransportCoreErrors.general.invalidTruncatedReference( - `A TruncatedReference must consist of either exactly 3 or exactly 5 components, but it consists of '${splitted.length}'.` + `A TruncatedReference must consist of either exactly 3 or exactly 5 components, but it consists of '${splitted.length}' components.` ); } From 825cbb60339600dd2b78fd2dae8073d357ac7791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 10:21:30 +0100 Subject: [PATCH 26/37] feat: add passwordProtection to CreateDeviceOnboardingTokenRequest --- packages/runtime/src/useCases/common/Schemas.ts | 16 ++++++++++++++++ .../devices/CreateDeviceOnboardingToken.ts | 13 +++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 7dec71699..5fa6ec0c5 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -20933,6 +20933,22 @@ export const CreateDeviceOnboardingTokenRequest: any = { }, "profileName": { "type": "string" + }, + "passwordProtection": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "passwordIsPin": { + "type": "boolean", + "const": true + } + }, + "required": [ + "password" + ], + "additionalProperties": false } }, "required": [ diff --git a/packages/runtime/src/useCases/transport/devices/CreateDeviceOnboardingToken.ts b/packages/runtime/src/useCases/transport/devices/CreateDeviceOnboardingToken.ts index 4235e3ca0..0fcfc1bc4 100644 --- a/packages/runtime/src/useCases/transport/devices/CreateDeviceOnboardingToken.ts +++ b/packages/runtime/src/useCases/transport/devices/CreateDeviceOnboardingToken.ts @@ -1,18 +1,22 @@ import { Result } from "@js-soft/ts-utils"; import { CoreDate, CoreId } from "@nmshd/core-types"; -import { DevicesController, TokenContentDeviceSharedSecret, TokenController } from "@nmshd/transport"; +import { DevicesController, PasswordProtectionCreationParameters, TokenContentDeviceSharedSecret, TokenController } from "@nmshd/transport"; import { Inject } from "@nmshd/typescript-ioc"; import { TokenDTO } from "../../../types"; -import { DeviceIdString, ISO8601DateTimeString, SchemaRepository, SchemaValidator, UseCase } from "../../common"; +import { DeviceIdString, ISO8601DateTimeString, SchemaRepository, TokenAndTemplateCreationValidator, UseCase } from "../../common"; import { TokenMapper } from "../tokens/TokenMapper"; export interface CreateDeviceOnboardingTokenRequest { id: DeviceIdString; expiresAt?: ISO8601DateTimeString; profileName?: string; + passwordProtection?: { + password: string; + passwordIsPin?: true; + }; } -class Validator extends SchemaValidator { +class Validator extends TokenAndTemplateCreationValidator { public constructor(@Inject schemaRepository: SchemaRepository) { super(schemaRepository.getSchema("CreateDeviceOnboardingTokenRequest")); } @@ -35,7 +39,8 @@ export class CreateDeviceOnboardingTokenUseCase extends UseCase Date: Mon, 2 Dec 2024 10:21:48 +0100 Subject: [PATCH 27/37] test: test and assert more stuff --- packages/app-runtime/test/lib/MockUIBridge.ts | 45 +++++++++++++++-- .../test/runtime/AppStringProcessor.test.ts | 49 +++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/app-runtime/test/lib/MockUIBridge.ts b/packages/app-runtime/test/lib/MockUIBridge.ts index 2f8aa61dd..4b923b914 100644 --- a/packages/app-runtime/test/lib/MockUIBridge.ts +++ b/packages/app-runtime/test/lib/MockUIBridge.ts @@ -16,6 +16,10 @@ export class MockUIBridge implements IUIBridge { public reset(): void { this._passwordToReturn = undefined; this._accountIdToReturn = undefined; + + this._showDeviceOnboardingCalls = []; + this._requestAccountSelectionCalls = []; + this._enterPasswordCalls = []; } public showMessage(_account: LocalAccountDTO, _relationship: IdentityDVO, _message: MessageDVO | MailDVO | RequestMessageDVO): Promise> { @@ -30,8 +34,19 @@ export class MockUIBridge implements IUIBridge { throw new Error("Method not implemented."); } - public showDeviceOnboarding(_deviceOnboardingInfo: DeviceOnboardingInfoDTO): Promise> { - throw new Error("Method not implemented."); + public showDeviceOnboarding(deviceOnboardingInfo: DeviceOnboardingInfoDTO): Promise> { + this._showDeviceOnboardingCalls.push(deviceOnboardingInfo); + + return Promise.resolve(Result.ok(undefined)); + } + + private _showDeviceOnboardingCalls: DeviceOnboardingInfoDTO[] = []; + public showDeviceOnboardingCalled(deviceId: string): boolean { + return this._showDeviceOnboardingCalls.some((x) => x.id === deviceId); + } + + public showDeviceOnboardingNotCalled(): boolean { + return this._showDeviceOnboardingCalls.length === 0; } public showRequest(_account: LocalAccountDTO, _request: LocalRequestDVO): Promise> { @@ -42,7 +57,9 @@ export class MockUIBridge implements IUIBridge { throw new Error("Method not implemented."); } - public requestAccountSelection(possibleAccounts: LocalAccountDTO[], _title?: string, _description?: string): Promise> { + public requestAccountSelection(possibleAccounts: LocalAccountDTO[], title?: string, description?: string): Promise> { + this._requestAccountSelectionCalls.push({ possibleAccounts: possibleAccounts, title: title, description: description }); + if (!this._accountIdToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); const foundAccount = possibleAccounts.find((x) => x.id === this._accountIdToReturn); @@ -51,9 +68,29 @@ export class MockUIBridge implements IUIBridge { return Promise.resolve(Result.ok(foundAccount)); } - public enterPassword(_passwordType: "pw" | "pin", _pinLength?: number): Promise> { + private _requestAccountSelectionCalls: { possibleAccounts: LocalAccountDTO[]; title?: string; description?: string }[] = []; + public requestAccountSelectionCalled(possibleAccountsLength: number): boolean { + return this._requestAccountSelectionCalls.some((x) => x.possibleAccounts.length === possibleAccountsLength); + } + + public requestAccountSelectionNotCalled(): boolean { + return this._requestAccountSelectionCalls.length === 0; + } + + public enterPassword(passwordType: "pw" | "pin", pinLength?: number): Promise> { + this._enterPasswordCalls.push({ passwordType: passwordType, pinLength: pinLength }); + if (!this._passwordToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); return Promise.resolve(Result.ok(this._passwordToReturn)); } + + private _enterPasswordCalls: { passwordType: "pw" | "pin"; pinLength?: number }[] = []; + public enterPasswordCalled(passwordType: "pw" | "pin", pinLength?: number): boolean { + return this._enterPasswordCalls.some((x) => x.passwordType === passwordType && x.pinLength === pinLength); + } + + public enterPasswordNotCalled(): boolean { + return this._enterPasswordCalls.length === 0; + } } diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 74676920a..1a2951a2f 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -47,6 +47,9 @@ describe("AppStringProcessor", function () { expect(result.isError).toBeDefined(); expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); + + expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity available", async function () { @@ -63,6 +66,9 @@ describe("AppStringProcessor", function () { expect(result).toBeSuccessful(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + + expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { @@ -77,6 +83,9 @@ describe("AppStringProcessor", function () { const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); expect(result).toBeAnError("There is no account matching the given 'forIdentityTruncated'.", "error.appruntime.general.noAccountAvailableForIdentityTruncated"); + + expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); }); test("should properly handle a password protected RelationshipTemplate", async function () { @@ -94,6 +103,9 @@ describe("AppStringProcessor", function () { expect(result.value).toBeUndefined(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + + expect(mockUiBridge.enterPasswordCalled("pw")).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionCalled(2)).toBeTruthy(); }); test("should properly handle a pin protected RelationshipTemplate", async function () { @@ -111,6 +123,9 @@ describe("AppStringProcessor", function () { expect(result.value).toBeUndefined(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + + expect(mockUiBridge.enterPasswordCalled("pin", 6)).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionCalled(2)).toBeTruthy(); }); test("should properly handle a password protected personalized RelationshipTemplate", async function () { @@ -128,6 +143,9 @@ describe("AppStringProcessor", function () { expect(result.value).toBeUndefined(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + + expect(mockUiBridge.enterPasswordCalled("pw")).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); }); test("should properly handle a pin protected personalized RelationshipTemplate", async function () { @@ -145,5 +163,36 @@ describe("AppStringProcessor", function () { expect(result.value).toBeUndefined(); await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); + + expect(mockUiBridge.enterPasswordCalled("pin", 6)).toBeTruthy(); + expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + }); + + describe("onboarding", function () { + let runtime3: AppRuntime; + const runtime3MockUiBridge = new MockUIBridge(); + + beforeAll(async function () { + runtime3 = await TestUtil.createRuntime(undefined, runtime3MockUiBridge, eventBus); + await runtime3.start(); + }); + + afterAll(async () => await runtime3.stop()); + + test("device onboarding with a password protected Token", async function () { + const deviceResult = await runtime1Session.transportServices.devices.createDevice({}); + const tokenResult = await runtime1Session.transportServices.devices.getDeviceOnboardingToken({ + id: deviceResult.value.id, + passwordProtection: { password: "password" } + }); + + mockUiBridge.passwordToReturn = "password"; + + const result = await runtime2.stringProcessor.processTruncatedReference(tokenResult.value.truncatedReference); + expect(result).toBeSuccessful(); + expect(result.value).toBeUndefined(); + + expect(mockUiBridge.showDeviceOnboardingCalled(deviceResult.value.id)).toBeTruthy(); + }); }); }); From a4a87456b81887098d93a80ebdf8847d9fb0f5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 10:24:17 +0100 Subject: [PATCH 28/37] chore: remove todos --- packages/app-runtime/src/AppStringProcessor.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 2abf4fa19..d867a628f 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -88,10 +88,7 @@ export class AppStringProcessor { } const tokenResult = await this.runtime.anonymousServices.tokens.loadPeerToken({ reference: truncatedReference, password: password }); - if (tokenResult.isError) { - // TODO: should it be possible to ask for the password again when it was wrong? - return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(tokenResult.error)); - } + if (tokenResult.isError) return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(tokenResult.error)); const tokenDTO = tokenResult.value; const tokenContent = this.parseTokenContent(tokenDTO.content); @@ -144,7 +141,6 @@ export class AppStringProcessor { ); } - // TODO: should it be possible to ask for the password again when it was wrong? return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(result.error)); } From 4146ee2bafe7903549142b439336e907282f1b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 10:35:34 +0100 Subject: [PATCH 29/37] fix: make fully mockable --- packages/app-runtime/test/lib/MockUIBridge.ts | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/packages/app-runtime/test/lib/MockUIBridge.ts b/packages/app-runtime/test/lib/MockUIBridge.ts index 4b923b914..2c43d6bbb 100644 --- a/packages/app-runtime/test/lib/MockUIBridge.ts +++ b/packages/app-runtime/test/lib/MockUIBridge.ts @@ -2,6 +2,16 @@ import { ApplicationError, Result } from "@js-soft/ts-utils"; import { DeviceOnboardingInfoDTO, FileDVO, IdentityDVO, LocalRequestDVO, MailDVO, MessageDVO, RequestMessageDVO } from "@nmshd/runtime"; import { IUIBridge, LocalAccountDTO, UserfriendlyApplicationError } from "../../src"; +export type MockUIBridgeCall = + | { method: "showMessage"; account: LocalAccountDTO; relationship: IdentityDVO; message: MessageDVO | MailDVO | RequestMessageDVO } + | { method: "showRelationship"; account: LocalAccountDTO; relationship: IdentityDVO } + | { method: "showFile"; account: LocalAccountDTO; file: FileDVO } + | { method: "showDeviceOnboarding"; deviceOnboardingInfo: DeviceOnboardingInfoDTO } + | { method: "showRequest"; account: LocalAccountDTO; request: LocalRequestDVO } + | { method: "showError"; error: UserfriendlyApplicationError; account?: LocalAccountDTO } + | { method: "requestAccountSelection"; possibleAccounts: LocalAccountDTO[]; title?: string; description?: string } + | { method: "enterPassword"; passwordType: "pw" | "pin"; pinLength?: number }; + export class MockUIBridge implements IUIBridge { private _accountIdToReturn: string | undefined; public set accountIdToReturn(value: string | undefined) { @@ -13,52 +23,53 @@ export class MockUIBridge implements IUIBridge { this._passwordToReturn = value; } + private _calls: MockUIBridgeCall[] = []; + public reset(): void { this._passwordToReturn = undefined; this._accountIdToReturn = undefined; - this._showDeviceOnboardingCalls = []; - this._requestAccountSelectionCalls = []; - this._enterPasswordCalls = []; + this._calls = []; } public showMessage(_account: LocalAccountDTO, _relationship: IdentityDVO, _message: MessageDVO | MailDVO | RequestMessageDVO): Promise> { - throw new Error("Method not implemented."); + this._calls.push({ method: "showMessage", account: _account, relationship: _relationship, message: _message }); + + return Promise.resolve(Result.ok(undefined)); } - public showRelationship(_account: LocalAccountDTO, _relationship: IdentityDVO): Promise> { - throw new Error("Method not implemented."); + public showRelationship(account: LocalAccountDTO, relationship: IdentityDVO): Promise> { + this._calls.push({ method: "showRelationship", account, relationship }); + + return Promise.resolve(Result.ok(undefined)); } - public showFile(_account: LocalAccountDTO, _file: FileDVO): Promise> { - throw new Error("Method not implemented."); + public showFile(account: LocalAccountDTO, file: FileDVO): Promise> { + this._calls.push({ method: "showFile", account, file }); + + return Promise.resolve(Result.ok(undefined)); } public showDeviceOnboarding(deviceOnboardingInfo: DeviceOnboardingInfoDTO): Promise> { - this._showDeviceOnboardingCalls.push(deviceOnboardingInfo); + this._calls.push({ method: "showDeviceOnboarding", deviceOnboardingInfo }); return Promise.resolve(Result.ok(undefined)); } - private _showDeviceOnboardingCalls: DeviceOnboardingInfoDTO[] = []; - public showDeviceOnboardingCalled(deviceId: string): boolean { - return this._showDeviceOnboardingCalls.some((x) => x.id === deviceId); - } + public showRequest(account: LocalAccountDTO, request: LocalRequestDVO): Promise> { + this._calls.push({ method: "showRequest", account, request }); - public showDeviceOnboardingNotCalled(): boolean { - return this._showDeviceOnboardingCalls.length === 0; + return Promise.resolve(Result.ok(undefined)); } - public showRequest(_account: LocalAccountDTO, _request: LocalRequestDVO): Promise> { - throw new Error("Method not implemented."); - } + public showError(error: UserfriendlyApplicationError, account?: LocalAccountDTO): Promise> { + this._calls.push({ method: "showError", error, account }); - public showError(_error: UserfriendlyApplicationError, _account?: LocalAccountDTO): Promise> { - throw new Error("Method not implemented."); + return Promise.resolve(Result.ok(undefined)); } public requestAccountSelection(possibleAccounts: LocalAccountDTO[], title?: string, description?: string): Promise> { - this._requestAccountSelectionCalls.push({ possibleAccounts: possibleAccounts, title: title, description: description }); + this._calls.push({ method: "requestAccountSelection", possibleAccounts, title, description }); if (!this._accountIdToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); @@ -68,29 +79,41 @@ export class MockUIBridge implements IUIBridge { return Promise.resolve(Result.ok(foundAccount)); } - private _requestAccountSelectionCalls: { possibleAccounts: LocalAccountDTO[]; title?: string; description?: string }[] = []; - public requestAccountSelectionCalled(possibleAccountsLength: number): boolean { - return this._requestAccountSelectionCalls.some((x) => x.possibleAccounts.length === possibleAccountsLength); + public enterPassword(passwordType: "pw" | "pin", pinLength?: number): Promise> { + this._calls.push({ method: "enterPassword", passwordType, pinLength }); + + if (!this._passwordToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); + + return Promise.resolve(Result.ok(this._passwordToReturn)); } - public requestAccountSelectionNotCalled(): boolean { - return this._requestAccountSelectionCalls.length === 0; + public showDeviceOnboardingCalled(deviceId: string): boolean { + const showDeviceOnboardingCalls = this._calls.filter((x) => x.method === "showDeviceOnboarding").map((e) => e.deviceOnboardingInfo); + return showDeviceOnboardingCalls.some((x) => x.id === deviceId); } - public enterPassword(passwordType: "pw" | "pin", pinLength?: number): Promise> { - this._enterPasswordCalls.push({ passwordType: passwordType, pinLength: pinLength }); + public showDeviceOnboardingNotCalled(): boolean { + const showDeviceOnboardingCalls = this._calls.filter((x) => x.method === "showDeviceOnboarding"); + return showDeviceOnboardingCalls.length === 0; + } - if (!this._passwordToReturn) return Promise.resolve(Result.fail(new ApplicationError("code", "message"))); + public requestAccountSelectionCalled(possibleAccountsLength: number): boolean { + const requestAccountSelectionCalls = this._calls.filter((x) => x.method === "requestAccountSelection"); + return requestAccountSelectionCalls.some((x) => x.possibleAccounts.length === possibleAccountsLength); + } - return Promise.resolve(Result.ok(this._passwordToReturn)); + public requestAccountSelectionNotCalled(): boolean { + const requestAccountSelectionCalls = this._calls.filter((x) => x.method === "requestAccountSelection"); + return requestAccountSelectionCalls.length === 0; } - private _enterPasswordCalls: { passwordType: "pw" | "pin"; pinLength?: number }[] = []; public enterPasswordCalled(passwordType: "pw" | "pin", pinLength?: number): boolean { - return this._enterPasswordCalls.some((x) => x.passwordType === passwordType && x.pinLength === pinLength); + const enterPasswordCalls = this._calls.filter((x) => x.method === "enterPassword"); + return enterPasswordCalls.some((x) => x.passwordType === passwordType && x.pinLength === pinLength); } public enterPasswordNotCalled(): boolean { - return this._enterPasswordCalls.length === 0; + const enterPasswordCalls = this._calls.filter((x) => x.method === "enterPassword"); + return enterPasswordCalls.length === 0; } } From 7928394481e93de91a4a7052e9c1c969f1c59c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 10:55:12 +0100 Subject: [PATCH 30/37] refactor: migrate to custom matchers --- packages/app-runtime/test/customMatchers.ts | 2 + .../test/lib/MockUIBridge.matchers.ts | 140 ++++++++++++++++++ packages/app-runtime/test/lib/MockUIBridge.ts | 33 +---- .../test/runtime/AppStringProcessor.test.ts | 30 ++-- 4 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 packages/app-runtime/test/lib/MockUIBridge.matchers.ts diff --git a/packages/app-runtime/test/customMatchers.ts b/packages/app-runtime/test/customMatchers.ts index 7c003837a..a4ac01398 100644 --- a/packages/app-runtime/test/customMatchers.ts +++ b/packages/app-runtime/test/customMatchers.ts @@ -1,6 +1,8 @@ import { ApplicationError, EventConstructor, Result } from "@js-soft/ts-utils"; import { MockEventBus } from "./lib"; +import "./lib/MockUIBridge.matchers"; + expect.extend({ toBeSuccessful(actual: Result) { if (!(actual instanceof Result)) { diff --git a/packages/app-runtime/test/lib/MockUIBridge.matchers.ts b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts new file mode 100644 index 000000000..dbdba31bb --- /dev/null +++ b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts @@ -0,0 +1,140 @@ +import { MockUIBridge } from "./MockUIBridge"; + +expect.extend({ + showDeviceOnboardingCalled(mockUIBridge: unknown, deviceId: string) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "showDeviceOnboarding"); + if (calls.length === 0) { + return { + pass: false, + message: () => "The method showDeviceOnboarding was not called." + }; + } + + const matchingCalls = calls.filter((x) => x.deviceOnboardingInfo.id === deviceId); + if (matchingCalls.length === 0) { + return { + pass: false, + message: () => + `The method showDeviceOnboarding was called, but not with the specified device id '${deviceId}', instead with ids '${calls.map((e) => e.deviceOnboardingInfo.id).join(", ")}}'.` + }; + } + + return { pass: true, message: () => "" }; + }, + showDeviceOnboardingNotCalled(mockUIBridge: unknown) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "showDeviceOnboarding"); + if (calls.length > 0) { + return { + pass: false, + message: () => "The method showDeviceOnboarding was called." + }; + } + + return { pass: true, message: () => "" }; + }, + requestAccountSelectionCalled(mockUIBridge: unknown, possibleAccountsLength: number) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "requestAccountSelection"); + if (calls.length === 0) { + return { + pass: false, + message: () => "The method requestAccountSelection was not called." + }; + } + + const matchingCalls = calls.filter((x) => x.possibleAccounts.length === possibleAccountsLength); + if (matchingCalls.length === 0) { + return { + pass: false, + message: () => + `The method requestAccountSelection was called, but not with the specified possible accounts length '${possibleAccountsLength}', instead with lengths '${calls.map((e) => e.possibleAccounts.length).join(", ")}}'.` + }; + } + + return { pass: true, message: () => "" }; + }, + requestAccountSelectionNotCalled(mockUIBridge: unknown) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "requestAccountSelection"); + if (calls.length > 0) { + return { + pass: false, + message: () => "The method requestAccountSelection was called." + }; + } + + return { pass: true, message: () => "" }; + }, + enterPasswordCalled(mockUIBridge: unknown, passwordType: "pw" | "pin", pinLength?: number) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "enterPassword"); + if (calls.length === 0) { + return { + pass: false, + message: () => "The method enterPassword was not called." + }; + } + + const matchingCalls = calls.filter((x) => x.passwordType === passwordType && x.pinLength === pinLength); + if (matchingCalls.length === 0) { + const parameters = calls + .map((e) => { + return { passwordType: e.passwordType, pinLength: e.pinLength }; + }) + .join(", "); + + return { + pass: false, + message: () => + `The method enterPassword was called, but not with the specified password type '${passwordType}' and pin length '${pinLength}', instead with parameters '${parameters}'.` + }; + } + + return { pass: true, message: () => "" }; + }, + enterPasswordNotCalled(mockUIBridge: unknown) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const calls = mockUIBridge.calls.filter((x) => x.method === "enterPassword"); + if (calls.length > 0) { + return { + pass: false, + message: () => "The method enterPassword was called." + }; + } + + return { pass: true, message: () => "" }; + } +}); + +declare global { + namespace jest { + interface Matchers { + showDeviceOnboardingCalled(deviceId: string): R; + showDeviceOnboardingNotCalled(): R; + requestAccountSelectionCalled(possibleAccountsLength: number): R; + requestAccountSelectionNotCalled(): R; + enterPasswordCalled(passwordType: "pw" | "pin", pinLength?: number): R; + enterPasswordNotCalled(): R; + } + } +} diff --git a/packages/app-runtime/test/lib/MockUIBridge.ts b/packages/app-runtime/test/lib/MockUIBridge.ts index 2c43d6bbb..5e9d40982 100644 --- a/packages/app-runtime/test/lib/MockUIBridge.ts +++ b/packages/app-runtime/test/lib/MockUIBridge.ts @@ -24,6 +24,9 @@ export class MockUIBridge implements IUIBridge { } private _calls: MockUIBridgeCall[] = []; + public get calls(): MockUIBridgeCall[] { + return this._calls; + } public reset(): void { this._passwordToReturn = undefined; @@ -86,34 +89,4 @@ export class MockUIBridge implements IUIBridge { return Promise.resolve(Result.ok(this._passwordToReturn)); } - - public showDeviceOnboardingCalled(deviceId: string): boolean { - const showDeviceOnboardingCalls = this._calls.filter((x) => x.method === "showDeviceOnboarding").map((e) => e.deviceOnboardingInfo); - return showDeviceOnboardingCalls.some((x) => x.id === deviceId); - } - - public showDeviceOnboardingNotCalled(): boolean { - const showDeviceOnboardingCalls = this._calls.filter((x) => x.method === "showDeviceOnboarding"); - return showDeviceOnboardingCalls.length === 0; - } - - public requestAccountSelectionCalled(possibleAccountsLength: number): boolean { - const requestAccountSelectionCalls = this._calls.filter((x) => x.method === "requestAccountSelection"); - return requestAccountSelectionCalls.some((x) => x.possibleAccounts.length === possibleAccountsLength); - } - - public requestAccountSelectionNotCalled(): boolean { - const requestAccountSelectionCalls = this._calls.filter((x) => x.method === "requestAccountSelection"); - return requestAccountSelectionCalls.length === 0; - } - - public enterPasswordCalled(passwordType: "pw" | "pin", pinLength?: number): boolean { - const enterPasswordCalls = this._calls.filter((x) => x.method === "enterPassword"); - return enterPasswordCalls.some((x) => x.passwordType === passwordType && x.pinLength === pinLength); - } - - public enterPasswordNotCalled(): boolean { - const enterPasswordCalls = this._calls.filter((x) => x.method === "enterPassword"); - return enterPasswordCalls.length === 0; - } } diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 1a2951a2f..b62b80f94 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -48,8 +48,8 @@ describe("AppStringProcessor", function () { expect(result.error.code).toBe("error.appStringProcessor.truncatedReferenceInvalid"); - expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + expect(mockUiBridge).enterPasswordNotCalled(); + expect(mockUiBridge).requestAccountSelectionNotCalled(); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity available", async function () { @@ -67,8 +67,8 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); - expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + expect(mockUiBridge).enterPasswordNotCalled(); + expect(mockUiBridge).requestAccountSelectionNotCalled(); }); test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { @@ -84,8 +84,8 @@ describe("AppStringProcessor", function () { const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); expect(result).toBeAnError("There is no account matching the given 'forIdentityTruncated'.", "error.appruntime.general.noAccountAvailableForIdentityTruncated"); - expect(mockUiBridge.enterPasswordNotCalled()).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + expect(mockUiBridge).enterPasswordNotCalled(); + expect(mockUiBridge).requestAccountSelectionNotCalled(); }); test("should properly handle a password protected RelationshipTemplate", async function () { @@ -104,8 +104,8 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); - expect(mockUiBridge.enterPasswordCalled("pw")).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionCalled(2)).toBeTruthy(); + expect(mockUiBridge).enterPasswordCalled("pw"); + expect(mockUiBridge).requestAccountSelectionCalled(2); }); test("should properly handle a pin protected RelationshipTemplate", async function () { @@ -124,8 +124,8 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); - expect(mockUiBridge.enterPasswordCalled("pin", 6)).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionCalled(2)).toBeTruthy(); + expect(mockUiBridge).enterPasswordCalled("pin", 6); + expect(mockUiBridge).requestAccountSelectionCalled(2); }); test("should properly handle a password protected personalized RelationshipTemplate", async function () { @@ -144,8 +144,8 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); - expect(mockUiBridge.enterPasswordCalled("pw")).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + expect(mockUiBridge).enterPasswordCalled("pw"); + expect(mockUiBridge).requestAccountSelectionNotCalled(); }); test("should properly handle a pin protected personalized RelationshipTemplate", async function () { @@ -164,8 +164,8 @@ describe("AppStringProcessor", function () { await expect(eventBus).toHavePublished(PeerRelationshipTemplateLoadedEvent); - expect(mockUiBridge.enterPasswordCalled("pin", 6)).toBeTruthy(); - expect(mockUiBridge.requestAccountSelectionNotCalled()).toBeTruthy(); + expect(mockUiBridge).enterPasswordCalled("pin", 6); + expect(mockUiBridge).requestAccountSelectionNotCalled(); }); describe("onboarding", function () { @@ -192,7 +192,7 @@ describe("AppStringProcessor", function () { expect(result).toBeSuccessful(); expect(result.value).toBeUndefined(); - expect(mockUiBridge.showDeviceOnboardingCalled(deviceResult.value.id)).toBeTruthy(); + expect(mockUiBridge).showDeviceOnboardingCalled(deviceResult.value.id); }); }); }); From 3ce05ce6f48fdfe9ddb0412fc40cc9857d3fef31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 11:00:34 +0100 Subject: [PATCH 31/37] chore: move enterPassword to private method --- .../app-runtime/src/AppStringProcessor.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index d867a628f..81974412c 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -3,7 +3,7 @@ import { Serializable } from "@js-soft/ts-serval"; import { EventBus, Result } from "@js-soft/ts-utils"; import { ICoreAddress } from "@nmshd/core-types"; import { AnonymousServices, Base64ForIdPrefix, DeviceMapper } from "@nmshd/runtime"; -import { Reference, TokenContentDeviceSharedSecret } from "@nmshd/transport"; +import { Reference, SharedPasswordProtection, TokenContentDeviceSharedSecret } from "@nmshd/transport"; import { AppRuntimeErrors } from "./AppRuntimeErrors"; import { AppRuntimeServices } from "./AppRuntimeServices"; import { IUIBridge } from "./extensibility"; @@ -76,10 +76,7 @@ export class AppStringProcessor { let password: string | undefined; if (reference.passwordProtection) { - const passwordResult = await uiBridge.enterPassword( - reference.passwordProtection.passwordType === "pw" ? "pw" : "pin", - reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType.substring(3)) - ); + const passwordResult = await this.enterPassword(reference.passwordProtection); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); } @@ -122,10 +119,7 @@ export class AppStringProcessor { let password: string | undefined; if (reference.passwordProtection) { - const passwordResult = await uiBridge.enterPassword( - reference.passwordProtection.passwordType === "pw" ? "pw" : "pin", - reference.passwordProtection.passwordType === "pw" ? undefined : parseInt(reference.passwordProtection.passwordType.substring(3)) - ); + const passwordResult = await this.enterPassword(reference.passwordProtection); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); } @@ -134,15 +128,7 @@ export class AppStringProcessor { } const result = await services.transportServices.account.loadItemFromTruncatedReference({ reference: reference.truncate(), password: password }); - if (result.isError) { - if (result.error.code === "error.runtime.validation.invalidPropertyValue") { - return UserfriendlyResult.fail( - new UserfriendlyApplicationError("error.appStringProcessor.truncatedReferenceInvalid", "The given code does not contain a valid truncated reference.") - ); - } - - return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(result.error)); - } + if (result.isError) return UserfriendlyResult.fail(UserfriendlyApplicationError.fromError(result.error)); switch (result.value.type) { case "File": @@ -176,4 +162,14 @@ export class AppStringProcessor { return undefined; } } + + private async enterPassword(passwordProtection: SharedPasswordProtection): Promise> { + const uiBridge = await this.runtime.uiBridge(); + const passwordResult = await uiBridge.enterPassword( + passwordProtection.passwordType === "pw" ? "pw" : "pin", + passwordProtection.passwordType === "pw" ? undefined : parseInt(passwordProtection.passwordType.substring(3)) + ); + + return passwordResult; + } } From 6866435a3aebd6a2ce1d829323cea285ec4acfce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 12:13:17 +0100 Subject: [PATCH 32/37] chore: PR comments --- .../app-runtime/src/AppStringProcessor.ts | 4 +-- packages/app-runtime/test/customMatchers.ts | 25 ++++------------ packages/app-runtime/test/lib/MockEventBus.ts | 2 +- .../test/lib/MockUIBridge.matchers.ts | 30 ++++--------------- .../test/runtime/AppStringProcessor.test.ts | 13 ++++---- packages/runtime/test/lib/MockEventBus.ts | 2 +- 6 files changed, 22 insertions(+), 54 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 81974412c..92068e99e 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -78,7 +78,7 @@ export class AppStringProcessor { if (reference.passwordProtection) { const passwordResult = await this.enterPassword(reference.passwordProtection); if (passwordResult.isError) { - return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); + return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided.")); } password = passwordResult.value; @@ -121,7 +121,7 @@ export class AppStringProcessor { if (reference.passwordProtection) { const passwordResult = await this.enterPassword(reference.passwordProtection); if (passwordResult.isError) { - return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided")); + return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided.")); } password = passwordResult.value; diff --git a/packages/app-runtime/test/customMatchers.ts b/packages/app-runtime/test/customMatchers.ts index a4ac01398..dbae8a10b 100644 --- a/packages/app-runtime/test/customMatchers.ts +++ b/packages/app-runtime/test/customMatchers.ts @@ -9,39 +9,24 @@ expect.extend({ return { pass: false, message: () => "expected an instance of Result." }; } - return { - pass: actual.isSuccess, - message: () => `expected a successful result; got an error result with the error message '${actual.error.message}'.` - }; + return { pass: actual.isSuccess, message: () => `expected a successful result; got an error result with the error message '${actual.error.message}'.` }; }, toBeAnError(actual: Result, expectedMessage: string | RegExp, expectedCode: string | RegExp) { if (!(actual instanceof Result)) { - return { - pass: false, - message: () => "expected an instance of Result." - }; + return { pass: false, message: () => "expected an instance of Result." }; } if (!actual.isError) { - return { - pass: false, - message: () => "expected an error result, but it was successful." - }; + return { pass: false, message: () => "expected an error result, but it was successful." }; } if (actual.error.message.match(new RegExp(expectedMessage)) === null) { - return { - pass: false, - message: () => `expected the error message of the result to match '${expectedMessage}', but received '${actual.error.message}'.` - }; + return { pass: false, message: () => `expected the error message of the result to match '${expectedMessage}', but received '${actual.error.message}'.` }; } if (actual.error.code.match(new RegExp(expectedCode)) === null) { - return { - pass: false, - message: () => `expected the error code of the result to match '${expectedCode}', but received '${actual.error.code}'.` - }; + return { pass: false, message: () => `expected the error code of the result to match '${expectedCode}', but received '${actual.error.code}'.` }; } return { pass: true, message: () => "" }; diff --git a/packages/app-runtime/test/lib/MockEventBus.ts b/packages/app-runtime/test/lib/MockEventBus.ts index 7653e6f73..685db5efb 100644 --- a/packages/app-runtime/test/lib/MockEventBus.ts +++ b/packages/app-runtime/test/lib/MockEventBus.ts @@ -17,7 +17,7 @@ export class MockEventBus extends EventEmitter2EventBus { const namespace = getEventNamespaceFromObject(event); if (!namespace) { - throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for a event."); + throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for an event."); } const promise = this.emitter.emitAsync(namespace, event); diff --git a/packages/app-runtime/test/lib/MockUIBridge.matchers.ts b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts index dbdba31bb..e435e718a 100644 --- a/packages/app-runtime/test/lib/MockUIBridge.matchers.ts +++ b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts @@ -8,10 +8,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "showDeviceOnboarding"); if (calls.length === 0) { - return { - pass: false, - message: () => "The method showDeviceOnboarding was not called." - }; + return { pass: false, message: () => "The method showDeviceOnboarding was not called." }; } const matchingCalls = calls.filter((x) => x.deviceOnboardingInfo.id === deviceId); @@ -32,10 +29,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "showDeviceOnboarding"); if (calls.length > 0) { - return { - pass: false, - message: () => "The method showDeviceOnboarding was called." - }; + return { pass: false, message: () => "The method showDeviceOnboarding was called." }; } return { pass: true, message: () => "" }; @@ -47,10 +41,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "requestAccountSelection"); if (calls.length === 0) { - return { - pass: false, - message: () => "The method requestAccountSelection was not called." - }; + return { pass: false, message: () => "The method requestAccountSelection was not called." }; } const matchingCalls = calls.filter((x) => x.possibleAccounts.length === possibleAccountsLength); @@ -71,10 +62,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "requestAccountSelection"); if (calls.length > 0) { - return { - pass: false, - message: () => "The method requestAccountSelection was called." - }; + return { pass: false, message: () => "The method requestAccountSelection was called." }; } return { pass: true, message: () => "" }; @@ -86,10 +74,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "enterPassword"); if (calls.length === 0) { - return { - pass: false, - message: () => "The method enterPassword was not called." - }; + return { pass: false, message: () => "The method enterPassword was not called." }; } const matchingCalls = calls.filter((x) => x.passwordType === passwordType && x.pinLength === pinLength); @@ -116,10 +101,7 @@ expect.extend({ const calls = mockUIBridge.calls.filter((x) => x.method === "enterPassword"); if (calls.length > 0) { - return { - pass: false, - message: () => "The method enterPassword was called." - }; + return { pass: false, message: () => "The method enterPassword was called." }; } return { pass: true, message: () => "" }; diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index b62b80f94..9055b1d69 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -13,7 +13,6 @@ describe("AppStringProcessor", function () { let runtime1Session: LocalAccountSession; let runtime2: AppRuntime; - let runtime2SessionA: LocalAccountSession; const templateContent: ArbitraryRelationshipTemplateContentJSON = { "@type": "ArbitraryRelationshipTemplateContent", value: "value" }; @@ -22,14 +21,16 @@ describe("AppStringProcessor", function () { runtime1 = await TestUtil.createRuntime(); await runtime1.start(); - const account = await runtime1.accountServices.createAccount(Math.random().toString(36).substring(7)); - runtime1Session = await runtime1.selectAccount(account.id); + const account = await TestUtil.provideAccounts(runtime1, 1); + runtime1Session = await runtime1.selectAccount(account[0].id); runtime2 = await TestUtil.createRuntime(undefined, mockUiBridge, eventBus); await runtime2.start(); const accounts = await TestUtil.provideAccounts(runtime2, 2); runtime2SessionA = await runtime2.selectAccount(accounts[0].id); + + // second account to make sure everything works with multiple accounts await runtime2.selectAccount(accounts[1].id); }); @@ -72,13 +73,13 @@ describe("AppStringProcessor", function () { }); test("should properly handle a personalized RelationshipTemplate with the correct Identity not available", async function () { - const runtime2SessionAAddress = runtime1Session.account.address!; - assert(runtime2SessionAAddress); + const runtime1SessionAddress = runtime1Session.account.address!; + assert(runtime1SessionAddress); const templateResult = await runtime1Session.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ content: templateContent, expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), - forIdentity: runtime2SessionAAddress + forIdentity: runtime1SessionAddress }); const result = await runtime2.stringProcessor.processTruncatedReference(templateResult.value.truncatedReference); diff --git a/packages/runtime/test/lib/MockEventBus.ts b/packages/runtime/test/lib/MockEventBus.ts index d4a5fdd05..14c117e11 100644 --- a/packages/runtime/test/lib/MockEventBus.ts +++ b/packages/runtime/test/lib/MockEventBus.ts @@ -16,7 +16,7 @@ export class MockEventBus extends EventEmitter2EventBus { const namespace = getEventNamespaceFromObject(event); if (!namespace) { - throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for a event."); + throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for an event."); } this.publishPromises.push(this.emitter.emitAsync(namespace, event)); } From dca28d57dea24f4490c8b40c8bf461f7188c82f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 12:51:46 +0100 Subject: [PATCH 33/37] refactor: Thomas' PR comments --- packages/app-runtime/src/AppRuntime.ts | 32 +------------- .../app-runtime/src/AppStringProcessor.ts | 44 +++++++++++++++---- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/app-runtime/src/AppRuntime.ts b/packages/app-runtime/src/AppRuntime.ts index 7588e6869..62a409cd2 100644 --- a/packages/app-runtime/src/AppRuntime.ts +++ b/packages/app-runtime/src/AppRuntime.ts @@ -25,7 +25,7 @@ import { RelationshipChangedModule, RelationshipTemplateProcessedModule } from "./modules"; -import { AccountServices, LocalAccountDTO, LocalAccountMapper, LocalAccountSession, MultiAccountController } from "./multiAccount"; +import { AccountServices, LocalAccountMapper, LocalAccountSession, MultiAccountController } from "./multiAccount"; import { INativeBootstrapper, INativeEnvironment, INativeTranslationProvider } from "./natives"; import { SessionStorage } from "./SessionStorage"; import { UserfriendlyResult } from "./UserfriendlyResult"; @@ -189,36 +189,6 @@ export class AppRuntime extends Runtime { return session; } - public async requestAccountSelection( - title = "i18n://uibridge.accountSelection.title", - description = "i18n://uibridge.accountSelection.description", - forIdentityTruncated?: string - ): Promise> { - const accounts = await this.accountServices.getAccounts(); - - if (!forIdentityTruncated) return await this.selectAccountViaBridge(accounts, title, description); - - const accountsWithPostfix = accounts.filter((account) => account.address?.endsWith(forIdentityTruncated)); - if (accountsWithPostfix.length === 0) return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailableForIdentityTruncated()); - if (accountsWithPostfix.length === 1) return UserfriendlyResult.ok(accountsWithPostfix[0]); - - // This catches the extremely rare case where two accounts are available that have the same last 4 characters in their address. In that case - // the user will have to decide which account to use, which could not work because it is not the exactly same address specified when personalizing the object. - return await this.selectAccountViaBridge(accountsWithPostfix, title, description); - } - - private async selectAccountViaBridge(accounts: LocalAccountDTO[], title: string, description: string): Promise> { - const uiBridge = await this.uiBridge(); - - const accountSelectionResult = await uiBridge.requestAccountSelection(accounts, title, description); - if (accountSelectionResult.isError) { - return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailable(accountSelectionResult.error)); - } - - if (accountSelectionResult.value) await this.selectAccount(accountSelectionResult.value.id); - return UserfriendlyResult.ok(accountSelectionResult.value); - } - public getHealth(): Promise { const health = { isHealthy: true, diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 92068e99e..9f9337cef 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -7,7 +7,7 @@ import { Reference, SharedPasswordProtection, TokenContentDeviceSharedSecret } f import { AppRuntimeErrors } from "./AppRuntimeErrors"; import { AppRuntimeServices } from "./AppRuntimeServices"; import { IUIBridge } from "./extensibility"; -import { LocalAccountDTO } from "./multiAccount"; +import { AccountServices, LocalAccountDTO, LocalAccountSession } from "./multiAccount"; import { UserfriendlyApplicationError } from "./UserfriendlyApplicationError"; import { UserfriendlyResult } from "./UserfriendlyResult"; @@ -17,11 +17,12 @@ export class AppStringProcessor { public constructor( protected readonly runtime: { get anonymousServices(): AnonymousServices; - requestAccountSelection(title?: string, description?: string, forIdentityTruncated?: string): Promise>; + get accountServices(): AccountServices; uiBridge(): Promise | IUIBridge; getServices(accountReference: string | ICoreAddress): Promise; translate(key: string, ...values: any[]): Promise>; get eventBus(): EventBus; + selectAccount(accountReference: string): Promise; }, loggerFactory: ILoggerFactory ) { @@ -53,7 +54,7 @@ export class AppStringProcessor { // process Files and RelationshipTemplates and ask for an account if (truncatedReference.startsWith(Base64ForIdPrefix.File) || truncatedReference.startsWith(Base64ForIdPrefix.RelationshipTemplate)) { - const result = await this.runtime.requestAccountSelection(undefined, undefined, reference.forIdentityTruncated); + const result = await this.selectAccount(reference.forIdentityTruncated); if (result.isError) { this.logger.error("Could not query account", result.error); return UserfriendlyResult.fail(result.error); @@ -99,7 +100,7 @@ export class AppStringProcessor { return UserfriendlyResult.ok(undefined); } - const accountSelectionResult = await this.runtime.requestAccountSelection(); + const accountSelectionResult = await this.selectAccount(reference.forIdentityTruncated); if (accountSelectionResult.isError) { return UserfriendlyResult.fail(accountSelectionResult.error); } @@ -110,15 +111,15 @@ export class AppStringProcessor { return UserfriendlyResult.ok(undefined); } - return await this._handleReference(reference, selectedAccount); + return await this._handleReference(reference, selectedAccount, password); } - private async _handleReference(reference: Reference, account: LocalAccountDTO): Promise> { + private async _handleReference(reference: Reference, account: LocalAccountDTO, existingPassword?: string): Promise> { const services = await this.runtime.getServices(account.id); const uiBridge = await this.runtime.uiBridge(); - let password: string | undefined; - if (reference.passwordProtection) { + let password: string | undefined = existingPassword; + if (reference.passwordProtection && !password) { const passwordResult = await this.enterPassword(reference.passwordProtection); if (passwordResult.isError) { return UserfriendlyResult.fail(new UserfriendlyApplicationError("error.appStringProcessor.passwordNotProvided", "No password was provided.")); @@ -172,4 +173,31 @@ export class AppStringProcessor { return passwordResult; } + + private async selectAccount(forIdentityTruncated?: string): Promise> { + const accounts = await this.runtime.accountServices.getAccounts(); + + const title = "i18n://uibridge.accountSelection.title"; + const description = "i18n://uibridge.accountSelection.description"; + if (!forIdentityTruncated) return await this.requestManualAccountSelection(accounts, title, description); + + const accountsWithPostfix = accounts.filter((account) => account.address?.endsWith(forIdentityTruncated)); + if (accountsWithPostfix.length === 0) return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailableForIdentityTruncated()); + if (accountsWithPostfix.length === 1) return UserfriendlyResult.ok(accountsWithPostfix[0]); + + // This catches the extremely rare case where two accounts are available that have the same last 4 characters in their address. In that case + // the user will have to decide which account to use, which could not work because it is not the exactly same address specified when personalizing the object. + return await this.requestManualAccountSelection(accountsWithPostfix, title, description); + } + + private async requestManualAccountSelection(accounts: LocalAccountDTO[], title: string, description: string): Promise> { + const uiBridge = await this.runtime.uiBridge(); + const accountSelectionResult = await uiBridge.requestAccountSelection(accounts, title, description); + if (accountSelectionResult.isError) { + return UserfriendlyResult.fail(AppRuntimeErrors.general.noAccountAvailable(accountSelectionResult.error)); + } + + if (accountSelectionResult.value) await this.runtime.selectAccount(accountSelectionResult.value.id); + return UserfriendlyResult.ok(accountSelectionResult.value); + } } From c02b8dd77bf2e1b1e4112db48fc4be073a227801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 12:54:39 +0100 Subject: [PATCH 34/37] fix: bulletproof pin parsing --- packages/app-runtime/src/AppStringProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 9f9337cef..4629a5505 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -168,7 +168,7 @@ export class AppStringProcessor { const uiBridge = await this.runtime.uiBridge(); const passwordResult = await uiBridge.enterPassword( passwordProtection.passwordType === "pw" ? "pw" : "pin", - passwordProtection.passwordType === "pw" ? undefined : parseInt(passwordProtection.passwordType.substring(3)) + passwordProtection.passwordType.startsWith("pin") ? parseInt(passwordProtection.passwordType.substring(3)) : undefined ); return passwordResult; From 9e4df5af7614329a6164b17b44de40c35a818811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 15:05:00 +0100 Subject: [PATCH 35/37] chore: messages --- packages/app-runtime/test/lib/MockUIBridge.matchers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-runtime/test/lib/MockUIBridge.matchers.ts b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts index e435e718a..b3720888c 100644 --- a/packages/app-runtime/test/lib/MockUIBridge.matchers.ts +++ b/packages/app-runtime/test/lib/MockUIBridge.matchers.ts @@ -16,7 +16,7 @@ expect.extend({ return { pass: false, message: () => - `The method showDeviceOnboarding was called, but not with the specified device id '${deviceId}', instead with ids '${calls.map((e) => e.deviceOnboardingInfo.id).join(", ")}}'.` + `The method showDeviceOnboarding was called, but not with the specified device id '${deviceId}', instead with ids '${calls.map((e) => e.deviceOnboardingInfo.id).join(", ")}'.` }; } @@ -49,7 +49,7 @@ expect.extend({ return { pass: false, message: () => - `The method requestAccountSelection was called, but not with the specified possible accounts length '${possibleAccountsLength}', instead with lengths '${calls.map((e) => e.possibleAccounts.length).join(", ")}}'.` + `The method requestAccountSelection was called, but not with the specified possible accounts length '${possibleAccountsLength}', instead with lengths '${calls.map((e) => e.possibleAccounts.length).join(", ")}'.` }; } From 294ded3608f6eace9b0232ee9e13ced97d57753c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 15:08:52 +0100 Subject: [PATCH 36/37] chore: PR comments --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 2 +- packages/transport/src/core/Reference.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 9055b1d69..0ce989d7d 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -43,7 +43,7 @@ describe("AppStringProcessor", function () { mockUiBridge.reset(); }); - test("should process a URL", async function () { + test("should process an URL", async function () { const result = await runtime1.stringProcessor.processURL("nmshd://qr#", runtime1Session.account); expect(result.isError).toBeDefined(); diff --git a/packages/transport/src/core/Reference.ts b/packages/transport/src/core/Reference.ts index 944c6237d..b832f17cb 100644 --- a/packages/transport/src/core/Reference.ts +++ b/packages/transport/src/core/Reference.ts @@ -50,7 +50,7 @@ export class Reference extends Serializable implements IReference { if (![3, 5].includes(splitted.length)) { throw TransportCoreErrors.general.invalidTruncatedReference( - `A TruncatedReference must consist of either exactly 3 or exactly 5 components, but it consists of '${splitted.length}' components.` + `A truncated reference must consist of either exactly 3 or exactly 5 components, but it consists of '${splitted.length}' components.` ); } From ebd5a4cef7b394b58297b006ff2f5ef70c906d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 2 Dec 2024 15:13:06 +0100 Subject: [PATCH 37/37] chore: wording --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 0ce989d7d..9055b1d69 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -43,7 +43,7 @@ describe("AppStringProcessor", function () { mockUiBridge.reset(); }); - test("should process an URL", async function () { + test("should process a URL", async function () { const result = await runtime1.stringProcessor.processURL("nmshd://qr#", runtime1Session.account); expect(result.isError).toBeDefined();