Skip to content

Commit

Permalink
The RequestModule creates expired requests in the `PeerRelationshipTe…
Browse files Browse the repository at this point in the history
…mplateLoaded` 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
  • Loading branch information
jkoenig134 authored Feb 19, 2025
1 parent 7dadb23 commit 40345c6
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export class RelationshipTemplateProcessedModule extends AppRuntimeModule<Relati
break;
}

case RelationshipTemplateProcessedResult.RequestExpired: {
await uiBridge.showError(
new UserfriendlyApplicationError(
"error.relationshipTemplateProcessedModule.requestExpired",
"No incoming Request could be created because the Request in the RelationshipTemplate is already expired."
)
);
break;
}

case RelationshipTemplateProcessedResult.RequestAutomaticallyDecided: {
break;
}
Expand Down
47 changes: 47 additions & 0 deletions packages/app-runtime/test/lib/MockUIBridge.matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,50 @@ expect.extend({
return { pass: false, message: () => "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: () => "" };
}
});
Expand All @@ -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;
}
}
}
12 changes: 1 addition & 11 deletions packages/app-runtime/test/lib/TestUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export enum RelationshipTemplateProcessedResult {
NonCompletedRequestExists = "NonCompletedRequestExists",
RelationshipExists = "RelationshipExists",
NoRequest = "NoRequest",
Error = "Error"
Error = "Error",
RequestExpired = "RequestExpired"
}

export type RelationshipTemplateProcessedEventData =
Expand Down Expand Up @@ -48,4 +49,8 @@ export type RelationshipTemplateProcessedEventData =
| {
template: RelationshipTemplateDTO;
result: RelationshipTemplateProcessedResult.Error;
}
| {
template: RelationshipTemplateDTO;
result: RelationshipTemplateProcessedResult.RequestExpired;
};
17 changes: 17 additions & 0 deletions packages/runtime/src/modules/RequestModule.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 40345c6

Please sign in to comment.