Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/validate the message content when it is a Request #46

Merged
merged 5 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/runtime/package.json
@@ -1,6 +1,6 @@
{
"name": "@nmshd/runtime",
"version": "4.2.0",
"version": "4.3.0",
"description": "The enmeshed client runtime.",
"homepage": "https://enmeshed.eu",
"repository": {
Expand Down
31 changes: 31 additions & 0 deletions packages/runtime/src/useCases/transport/messages/SendMessage.ts
@@ -1,5 +1,9 @@
import { Serializable } from "@js-soft/ts-serval";
import { Result } from "@js-soft/ts-utils";
import { OutgoingRequestsController } from "@nmshd/consumption";
import { Request } from "@nmshd/content";
import { AccountController, CoreAddress, CoreId, File, FileController, MessageController } from "@nmshd/transport";
import _ from "lodash";
import { Inject } from "typescript-ioc";
import { MessageDTO } from "../../../types";
import { AddressString, FileIdString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common";
Expand All @@ -26,12 +30,16 @@ export class SendMessageUseCase extends UseCase<SendMessageRequest, MessageDTO>
@Inject private readonly messageController: MessageController,
@Inject private readonly fileController: FileController,
@Inject private readonly accountController: AccountController,
@Inject private readonly outgoingRequestsController: OutgoingRequestsController,
@Inject validator: Validator
) {
super(validator);
}

protected async executeInternal(request: SendMessageRequest): Promise<Result<MessageDTO>> {
const validationError = await this.validateMessageContent(request.content, request.recipients);
if (validationError) return Result.fail(validationError);

const transformAttachmentsResult = await this.transformAttachments(request.attachments);
if (transformAttachmentsResult.isError) {
return Result.fail(transformAttachmentsResult.error);
Expand All @@ -48,6 +56,29 @@ export class SendMessageUseCase extends UseCase<SendMessageRequest, MessageDTO>
return Result.ok(MessageMapper.toMessageDTO(result));
}

private async validateMessageContent(content: any, recipients: string[]) {
const transformedContent = Serializable.fromUnknown(content);
if (!(transformedContent instanceof Request)) return;

if (typeof transformedContent.id === "undefined") {
return RuntimeErrors.general.invalidPropertyValue("The Request must have an id.");
}

const localRequest = await this.outgoingRequestsController.getOutgoingRequest(transformedContent.id);
if (!localRequest) return RuntimeErrors.general.recordNotFound(Request);

if (!_.isEqual(transformedContent.toJSON(), localRequest.content.toJSON())) {
return RuntimeErrors.general.invalidPropertyValue("The sent Request must have the same content as the LocalRequest.");
}

if (recipients.length > 1) return RuntimeErrors.general.invalidPropertyValue("Only one recipient is allowed for sending Requests.");

const recipient = CoreAddress.from(recipients[0]);
if (!recipient.equals(localRequest.peer)) return RuntimeErrors.general.invalidPropertyValue("The recipient does not match the Request's peer.");

return;
}

private async transformAttachments(attachmentsIds?: string[]): Promise<Result<File[]>> {
if (!attachmentsIds || attachmentsIds.length === 0) {
return Result.ok([]);
Expand Down
114 changes: 108 additions & 6 deletions packages/runtime/test/transport/messages.test.ts
@@ -1,5 +1,6 @@
import { CoreDate } from "@nmshd/transport";
import { GetMessagesQuery, MessageSentEvent, MessageWasReadAtChangedEvent } from "../../src";
import { ConsentRequestItemJSON } from "@nmshd/content";
import { CoreDate, CoreId } from "@nmshd/transport";
import { GetMessagesQuery, MessageReceivedEvent, MessageSentEvent, MessageWasReadAtChangedEvent } from "../../src";
import {
ensureActiveRelationship,
establishRelationship,
Expand All @@ -15,12 +16,15 @@ import {
const serviceProvider = new RuntimeServiceProvider();
let client1: TestRuntimeServices;
let client2: TestRuntimeServices;
let client3: TestRuntimeServices;

beforeAll(async () => {
const runtimeServices = await serviceProvider.launch(2);
const runtimeServices = await serviceProvider.launch(3);
client1 = runtimeServices[0];
client2 = runtimeServices[1];
client3 = runtimeServices[2];
await ensureActiveRelationship(client1.transport, client2.transport);
await ensureActiveRelationship(client1.transport, client3.transport);
}, 30000);

beforeEach(() => {
Expand Down Expand Up @@ -62,6 +66,7 @@ describe("Messaging", () => {
expect(messageId).toBeDefined();

const messages = await syncUntilHasMessages(client2.transport);
await expect(client2.eventBus).toHavePublished(MessageReceivedEvent, (m) => m.data.id === messageId);
expect(messages).toHaveLength(1);

const message = messages[0];
Expand Down Expand Up @@ -99,13 +104,47 @@ describe("Messaging", () => {
const response = await client2.transport.messages.getMessage({ id: messageId });
expect(response).toBeSuccessful();
});

test("send a Message to multiple recipients", async () => {
expect(fileId).toBeDefined();

const result = await client1.transport.messages.sendMessage({
recipients: [client2.address, client3.address],
content: {
"@type": "Mail",
body: "b",
cc: [client3.address],
subject: "a",
to: [client2.address]
},
attachments: [fileId]
});
expect(result).toBeSuccessful();
await expect(client1.eventBus).toHavePublished(MessageSentEvent, (m) => m.data.id === result.value.id);
messageId = result.value.id;
});
});

describe("Message errors", () => {
const fakeAddress = "id1PNvUP4jHD74qo6usnWNoaFGFf33MXZi6c";
let requestItem: ConsentRequestItemJSON;
let requestId: string;
beforeAll(async () => {
requestItem = {
"@type": "ConsentRequestItem",
consent: "I consent to this RequestItem",
mustBeAccepted: true
};
const createRequestResult = await client1.consumption.outgoingRequests.create({
content: {
items: [requestItem]
},
peer: client2.address
});
requestId = createRequestResult.value.id;
});
test("should throw correct error for empty 'to' in the Message", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [fakeAddress],
recipients: [client2.address],
content: {
"@type": "Mail",
to: [],
Expand All @@ -118,7 +157,7 @@ describe("Message errors", () => {

test("should throw correct error for missing 'to' in the Message", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [fakeAddress],
recipients: [client2.address],
content: {
"@type": "Mail",
subject: "A Subject",
Expand All @@ -127,6 +166,69 @@ describe("Message errors", () => {
});
expect(result).toBeAnError("Mail.to :: Value is not defined", "error.runtime.requestDeserialization");
});

test("should throw correct error for missing Request ID in a Message with Request content", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [client2.address],
content: {
"@type": "Request",
items: [requestItem]
}
});
expect(result).toBeAnError("The Request must have an id.", "error.runtime.validation.invalidPropertyValue");
});

test("should throw correct error for missing LocalRequest trying to send a Message with Request content", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [client2.address],
content: {
"@type": "Request",
id: CoreId.from("REQxxxxxxxxxxxxxxxxx"),
items: [requestItem]
}
});
expect(result).toBeAnError(/.*/, "error.runtime.recordNotFound");
});

test("should throw correct error for trying to send a Message with Request content to multiple recipients", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [client2.address, client3.address],
content: {
"@type": "Request",
id: requestId,
items: [requestItem]
}
});
expect(result).toBeAnError("Only one recipient is allowed for sending Requests.", "error.runtime.validation.invalidPropertyValue");
});

test("should throw correct error for trying to send a Message with a Request content that doesn't match the content of the LocalRequest", async () => {
const wrongRequestItem = {
"@type": "AuthenticationRequestItem",
mustBeAccepted: true
};
const result = await client1.transport.messages.sendMessage({
recipients: [client2.address],
content: {
"@type": "Request",
id: requestId,
items: [wrongRequestItem]
}
});
expect(result).toBeAnError("The sent Request must have the same content as the LocalRequest.", "error.runtime.validation.invalidPropertyValue");
});

test("should throw correct error if Message's recipient doesn't match Request's peer", async () => {
const result = await client1.transport.messages.sendMessage({
recipients: [client3.address],
content: {
"@type": "Request",
id: requestId,
items: [requestItem]
}
});
expect(result).toBeAnError("The recipient does not match the Request's peer.", "error.runtime.validation.invalidPropertyValue");
});
});

describe("Mark Message as un-/read", () => {
Expand Down