From 40345c6a1dd217ccb171fe00539104b910588fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= <33655937+jkoenig134@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:07:26 +0100 Subject: [PATCH] The RequestModule creates expired requests in the `PeerRelationshipTemplateLoaded` event handler (#427) * fix: add new event type * fix: trigger new event type * fix: handle new event type * chore: simplify testutil * test: add matchers * test: add tests for RelationshipTemplateProcessedModule * chore: wording --- .../RelationshipTemplateProcessedModule.ts | 10 +++ .../test/lib/MockUIBridge.matchers.ts | 47 ++++++++++++ packages/app-runtime/test/lib/TestUtil.ts | 12 +--- ...elationshipTemplateProcessedModule.test.ts | 72 +++++++++++++++++++ .../RelationshipTemplateProcessedEvent.ts | 7 +- packages/runtime/src/modules/RequestModule.ts | 17 +++++ 6 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 packages/app-runtime/test/modules/appEvents/RelationshipTemplateProcessedModule.test.ts diff --git a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts index 3b0024108..3e24de180 100644 --- a/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts +++ b/packages/app-runtime/src/modules/appEvents/RelationshipTemplateProcessedModule.ts @@ -70,6 +70,16 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule "The method enterPassword was called." }; } + return { pass: true, message: () => "" }; + }, + showRequestCalled(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 === "showRequest"); + if (calls.length === 0) { + return { pass: false, message: () => "The method showRequest was not called." }; + } + + return { pass: true, message: () => "" }; + }, + showRequestNotCalled(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 === "showRequest"); + if (calls.length > 0) { + return { pass: false, message: () => `The method showRequest called: ${calls.map((c) => `'account id: ${c.account.id} - requestId: ${c.request.id}'`)}` }; + } + + return { pass: true, message: () => "" }; + }, + showErrorCalled(mockUIBridge: unknown, code: string) { + if (!(mockUIBridge instanceof MockUIBridge)) { + throw new Error("This method can only be used with expect(MockUIBridge)."); + } + + const errorCalls = mockUIBridge.calls.filter((x) => x.method === "showError"); + if (errorCalls.length === 0) { + return { pass: false, message: () => "The method showError was not called." }; + } + + const errorCallsWithCode = errorCalls.filter((x) => x.error.code === code); + if (errorCallsWithCode.length === 0) { + return { + pass: false, + message: () => `The method showRequest was called but not with the code '${code}' instead with the codes: ${errorCalls.map((c) => `'${c.error.code}'`).join(", ")}` + }; + } + return { pass: true, message: () => "" }; } }); @@ -119,6 +163,9 @@ declare global { requestAccountSelectionNotCalled(): R; enterPasswordCalled(passwordType: "pw" | "pin", pinLength?: number, attempt?: number): R; enterPasswordNotCalled(): R; + showRequestCalled(): R; + showRequestNotCalled(): R; + showErrorCalled(code: string): R; } } } diff --git a/packages/app-runtime/test/lib/TestUtil.ts b/packages/app-runtime/test/lib/TestUtil.ts index 2bc57acca..909979bc0 100644 --- a/packages/app-runtime/test/lib/TestUtil.ts +++ b/packages/app-runtime/test/lib/TestUtil.ts @@ -129,17 +129,7 @@ export class TestUtil { }) ).value; - const tokenFrom = ( - await from.transportServices.relationshipTemplates.createTokenForOwnTemplate({ - templateId: templateFrom.id, - ephemeral: true, - expiresAt: CoreDate.utc().add({ minutes: 5 }).toString() - }) - ).value; - - const templateTo = await to.transportServices.relationshipTemplates.loadPeerRelationshipTemplate({ - reference: tokenFrom.truncatedReference - }); + const templateTo = await to.transportServices.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templateFrom.truncatedReference }); return templateTo.value; } diff --git a/packages/app-runtime/test/modules/appEvents/RelationshipTemplateProcessedModule.test.ts b/packages/app-runtime/test/modules/appEvents/RelationshipTemplateProcessedModule.test.ts new file mode 100644 index 000000000..589bf339d --- /dev/null +++ b/packages/app-runtime/test/modules/appEvents/RelationshipTemplateProcessedModule.test.ts @@ -0,0 +1,72 @@ +import { sleep } from "@js-soft/ts-utils"; +import { AuthenticationRequestItem, RelationshipTemplateContent } from "@nmshd/content"; +import { CoreDate } from "@nmshd/core-types"; +import assert from "assert"; +import { AppRuntime, LocalAccountSession } from "../../../src"; +import { MockEventBus, MockUIBridge, TestUtil } from "../../lib"; + +describe("RelationshipTemplateProcessedModule", function () { + const uiBridge = new MockUIBridge(); + const eventBus = new MockEventBus(); + + let runtime1: AppRuntime; + let session1: LocalAccountSession; + let session2: LocalAccountSession; + + beforeAll(async function () { + runtime1 = await TestUtil.createRuntime(undefined, uiBridge, eventBus); + await runtime1.start(); + + const [localAccount1, localAccount2] = await TestUtil.provideAccounts(runtime1, 2); + + session1 = await runtime1.selectAccount(localAccount1.id); + session2 = await runtime1.selectAccount(localAccount2.id); + }); + + afterAll(async function () { + await runtime1.stop(); + }); + + afterEach(async function () { + uiBridge.reset(); + eventBus.reset(); + + const incomingRequests = await session2.consumptionServices.incomingRequests.getRequests({ query: { status: ["Open", "DecisionRequired", "ManualDecisionRequired"] } }); + for (const request of incomingRequests.value) { + const response = await session2.consumptionServices.incomingRequests.reject({ requestId: request.id, items: [{ accept: false }] }); + assert(response.isSuccess); + } + + await eventBus.waitForRunningEventHandlers(); + }); + + test("should show request when RelationshipTemplateProcessedEvent is received with ManualRequestDecisionRequired", async function () { + await TestUtil.createAndLoadPeerTemplate( + session1, + session2, + RelationshipTemplateContent.from({ onNewRelationship: { items: [AuthenticationRequestItem.from({ mustBeAccepted: false })] } }).toJSON() + ); + await eventBus.waitForRunningEventHandlers(); + + expect(uiBridge).showRequestCalled(); + }); + + test("should show an error when RelationshipTemplateProcessedEvent is received with an expired Request", async function () { + const templateFrom = ( + await session1.transportServices.relationshipTemplates.createOwnRelationshipTemplate({ + content: RelationshipTemplateContent.from({ + onNewRelationship: { expiresAt: CoreDate.utc().add({ seconds: 2 }), items: [AuthenticationRequestItem.from({ mustBeAccepted: false })] } + }).toJSON(), + expiresAt: CoreDate.utc().add({ minutes: 5 }).toString(), + maxNumberOfAllocations: 1 + }) + ).value; + + await sleep(3000); + await session2.transportServices.relationshipTemplates.loadPeerRelationshipTemplate({ reference: templateFrom.truncatedReference }); + await eventBus.waitForRunningEventHandlers(); + + expect(uiBridge).showRequestNotCalled(); + expect(uiBridge).showErrorCalled("error.relationshipTemplateProcessedModule.requestExpired"); + }); +}); diff --git a/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts b/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts index 649a4b607..d986d29d8 100644 --- a/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts +++ b/packages/runtime/src/events/consumption/RelationshipTemplateProcessedEvent.ts @@ -17,7 +17,8 @@ export enum RelationshipTemplateProcessedResult { NonCompletedRequestExists = "NonCompletedRequestExists", RelationshipExists = "RelationshipExists", NoRequest = "NoRequest", - Error = "Error" + Error = "Error", + RequestExpired = "RequestExpired" } export type RelationshipTemplateProcessedEventData = @@ -48,4 +49,8 @@ export type RelationshipTemplateProcessedEventData = | { template: RelationshipTemplateDTO; result: RelationshipTemplateProcessedResult.Error; + } + | { + template: RelationshipTemplateDTO; + result: RelationshipTemplateProcessedResult.RequestExpired; }; diff --git a/packages/runtime/src/modules/RequestModule.ts b/packages/runtime/src/modules/RequestModule.ts index eabcb4812..fa28c74a3 100644 --- a/packages/runtime/src/modules/RequestModule.ts +++ b/packages/runtime/src/modules/RequestModule.ts @@ -1,5 +1,6 @@ import { LocalRequestStatus } from "@nmshd/consumption"; import { RelationshipCreationContent, RequestJSON, ResponseJSON, ResponseResult, ResponseWrapper } from "@nmshd/content"; +import { CoreDate } from "@nmshd/core-types"; import { IncomingRequestStatusChangedEvent, MessageProcessedEvent, @@ -92,6 +93,14 @@ export class RequestModule extends RuntimeModule { const activeRelationships = relationshipsToPeer.filter((r) => r.status === RelationshipStatus.Active); if (activeRelationships.length !== 0) { if (body.onExistingRelationship) { + if (body.onExistingRelationship.expiresAt && CoreDate.from(body.onExistingRelationship.expiresAt).isExpired()) { + this.runtime.eventBus.publish( + new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { template, result: RelationshipTemplateProcessedResult.RequestExpired }) + ); + + return; + } + const requestCreated = await this.createIncomingRequest(services, body.onExistingRelationship, template.id); if (!requestCreated) { this.runtime.eventBus.publish( @@ -137,6 +146,14 @@ export class RequestModule extends RuntimeModule { return; } + if (body.onNewRelationship.expiresAt && CoreDate.from(body.onNewRelationship.expiresAt).isExpired()) { + this.runtime.eventBus.publish( + new RelationshipTemplateProcessedEvent(event.eventTargetAddress, { template, result: RelationshipTemplateProcessedResult.RequestExpired }) + ); + + return; + } + const requestCreated = await this.createIncomingRequest(services, body.onNewRelationship, template.id); if (!requestCreated) {