Skip to content

Commit

Permalink
Request can be created and sent via a message with an expiration date…
Browse files Browse the repository at this point in the history
… that has passed (#369)

* chore: respect expireation date of request when creating request and sending message

* chore: white space

* Update packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts

Co-authored-by: Julian König <[email protected]>

* Update packages/consumption/src/consumption/ConsumptionCoreErrors.ts

Co-authored-by: Milena Czierlinski <[email protected]>

* chore: fix test misstake

* chore: fix tests

* chore: PR comments

* chore: revert Date changes

* chore: pr comments

* chore: test

* chore: fix flaky test

* Update packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts

Co-authored-by: Milena Czierlinski <[email protected]>

* Update packages/consumption/test/modules/requests/OutgoingRequestsController.test.ts

Co-authored-by: Milena Czierlinski <[email protected]>

* chore: improve test performance

* chore: improve test performance

* chore: PR comments

* refactor: add empty lines

* Uniqueness of `key` for RelationshipAttributes (#319)

* refactor: preapare CreateAttributeRequestItemProcessor to add more validation

* feat: add validation for CreateAttributeRequestItemProcessor

* test: CreateAttributeRequestItemProcessor key validation

* fix: uniqueness for key only instead of pair of owner and key

* fix: failing test

* test: ensure key uniqueness in attributes.test

* test: ensure key uniqueness in CreateRelationshipAttributeRequestItemDVO.test

* feat: use database language every database understands

* test: ensure key uniqueness only for constant ownership

* fix: too late clean up of Attributes

* feat: ensure key uniqueness only for RelationshipAttributes that are not in deletion in any way

* refactor: extract function call as auxiliary function

* refactor: put afterEach block inside describe block

* feat: ensure key uniqueness for Recipient within CreateAttributeRequestItemProcessor

* refactor: auxiliary function can be applied to queries as well

* feat: ensure key uniqueness for Sender within ReadAttributeRequestItemProcessor

* feat: ensure key uniqueness only for constant value type

* fix: failing tests with async canCreateOutgoingRequestItem method

* feat: key uniqueness for Recipient within ReadAttributeRequestItemProcessor

* feat: ensure key uniquess for empty owner as placeholder as well

* refactor: simplify error message

* refactor: move validation to canAccept method

* refactor: standardize error messages

* test: ensure key uniqueness with empty owner

* test: move tests to canAccept block

* fix: call accept instead of canAccept

* test: refactor canAccept test with SuccessfulValidationResult

* test: make tests independent of each other

* feat: ensure key uniqueness for empty owner queried with ReadAttributeRequestItem

* feat: ensure key uniqueness on Sender site of ProposeAttributeRequestItemProcessor

* feat: ensure key uniqueness on Recipient site of ProposeAttributeRequestItemProcessor

* refactor: standardize error messages

* feat: prohibit Requests that create more than one RelationshipAttribute with same key

* fix: title type

* test: key uniqueness of incomingRequestsController

* test: key uniqueness of OutgoingRequestsController

* refactor: remove recursion of IncomingRequestsController

* refactor: keep RequestItemGroup in mind

* refactor: use auxiliary method for clearness

* feat: handle empty owner in IncomingRequestsController and OutgoingRequestsController

* test: ensure key uniqueness for empty owner

* refactor: move validation from canDecide to canAccept

* fix: failing IncomingRequestsController test

* test: ensure key uniqueness with RequestItemGroups

* fix: wrong DecideRequestParameters for RequestItemGroups

* feat: use already known separation sequence

* feat: give better validation function name

* refactor: do not break existing code blocks anymore

* fix: error due to incorrect merging

* test: ensure key uniqueness with ProposeAttributeRequestItem

* chore: descriptive values should not start with capital letter

* refactor: rename error code

* feat: distinguish between user mistake and deformed Request

* feat: throwing instead of returning error if Request is deformed

* feat: reuse key uniqueness error

* feat: distinguish between deformed Requests and wrong accept parameters

* test: RequestItem cannot be accepted due to incorrect params of User

* fix: failing test due to void return

* feat: self-explanatory variable names

* refactor: more renaming of variables

* refactor: unify renaming

* feat: simplify error message for unknown recipient of Request

* feat: incorporate some review comments

* fix: failing test due to wrong error format

* feat: incorporate review comment

* refactor: prepare reusability of code

* fix: error due to merge

* feat: use JSON.stringify() instead of own method for extracting identifiers

* refactor: reduce redundancy by move auxiliary functions to different file

* feat: inform user what key would be redundantly in use

* feat: incorporate some review comments

* feat: incoporate review comments

* refactor: change order of thrown errors

* test: can propose and query RelationshipAttribute with same key but different value type

* feat: incorporate review comments

* refactor: simplify extraction of fragments of mustBeAcceptedItems

* refactor: further simplification of extraction of fragments of mustBeAcceptedItems

* refactor: simplify extraction of fragments of accepte items

* fix: returned instead of continued

* test: RequestItem cannot be accepted if key uniqueness is violated regardless of value of mustBeAccepted

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Milena Czierlinski <[email protected]>

* chore: prettier

* chore: pr comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <[email protected]>
Co-authored-by: Milena Czierlinski <[email protected]>
Co-authored-by: Milena Czierlinski <[email protected]>
Co-authored-by: Britta Stallknecht <[email protected]>
  • Loading branch information
6 people authored Dec 18, 2024
1 parent 6bd9be2 commit 919eefc
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 7 deletions.
4 changes: 4 additions & 0 deletions packages/consumption/src/consumption/ConsumptionCoreErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ class Requests {
return new CoreError("error.consumption.requests.cannotShareRequestWithYourself", "You cannot share a Request with yourself.");
}

public cannotCreateRequestWithExpirationDateInPast() {
return new CoreError("error.consumption.requests.cannotCreateRequestWithExpirationDateInPast", "You cannot create a Request with an expiration date that is in the past.");
}

private static readonly _decideValidation = class {
public invalidNumberOfItems(message: string) {
return new ApplicationError("error.consumption.requests.decide.validation.invalidNumberOfItems", message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export class OutgoingRequestsController extends ConsumptionBaseController {
return ValidationResult.error(ConsumptionCoreErrors.requests.cannotShareRequestWithYourself());
}

if (parsedParams.content.expiresAt?.isBefore(CoreDate.utc())) {
return ValidationResult.error(ConsumptionCoreErrors.requests.cannotCreateRequestWithExpirationDateInPast());
}

if (parsedParams.peer) {
const relationship = await this.relationshipResolver.getRelationshipToIdentity(parsedParams.peer);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions";
import { ApplicationError } from "@js-soft/ts-utils";
import { ApplicationError, sleep } from "@js-soft/ts-utils";
import {
CreateAttributeRequestItem,
IAcceptResponseItem,
Expand Down Expand Up @@ -231,6 +231,24 @@ describe("OutgoingRequestsController", function () {
});
});

test("returns a validation result that contains an error for requests that are expired", async function () {
const validationResult = await When.iCallCanCreateForAnOutgoingRequest({
content: {
expiresAt: CoreDate.utc().subtract({ days: 1 }),
items: [
TestRequestItem.from({
mustBeAccepted: false
})
]
}
});

expect(validationResult).errorValidationResult({
code: "error.consumption.requests.cannotCreateRequestWithExpirationDateInPast",
message: "You cannot create a Request with an expiration date that is in the past."
});
});

test("returns a validation result that contains an error for requests that would lead to the creation of more than one RelationshipAttribute with the same key", async function () {
const validationResult = await When.iCallCanCreateForAnOutgoingRequest({
content: {
Expand Down Expand Up @@ -745,25 +763,27 @@ describe("OutgoingRequestsController", function () {
await Given.anOutgoingRequestWith({
status: LocalRequestStatus.Expired,
content: {
expiresAt: CoreDate.utc().subtract({ days: 1 }),
expiresAt: CoreDate.utc().add({ millisecond: 100 }),
items: [TestRequestItem.from({ mustBeAccepted: false })]
}
});
await sleep(150);

await When.iCompleteTheOutgoingRequestWith({ responseSourceObject: incomingMessage });
await Then.theRequestMovesToStatus(LocalRequestStatus.Completed);
});

test("throws when trying to complete an expired Request with a Message that was created after the expiry date", async function () {
const incomingMessage = TestObjectFactory.createIncomingIMessage(context.currentIdentity);
await Given.anOutgoingRequestWith({
status: LocalRequestStatus.Expired,
content: {
expiresAt: CoreDate.utc().subtract({ days: 1 }),
expiresAt: CoreDate.utc().add({ millisecond: 100 }),
items: [TestRequestItem.from({ mustBeAccepted: false })]
}
});
await sleep(150);

const incomingMessage = TestObjectFactory.createIncomingIMessage(context.currentIdentity);
await When.iTryToCompleteTheOutgoingRequestWith({ responseSourceObject: incomingMessage });
await Then.itThrowsAnErrorWithTheErrorMessage("*Cannot complete an expired request with a response that was created before the expiration date*");
});
Expand All @@ -784,10 +804,11 @@ describe("OutgoingRequestsController", function () {
await Given.anOutgoingRequestWith({
status: LocalRequestStatus.Draft,
content: {
expiresAt: CoreDate.utc().subtract({ days: 1 }),
expiresAt: CoreDate.utc().add({ millisecond: 100 }),
items: [TestRequestItem.from({ mustBeAccepted: false })]
}
});
await sleep(150);

await When.iGetTheOutgoingRequest();
await Then.theRequestIsInStatus(LocalRequestStatus.Expired);
Expand Down Expand Up @@ -863,10 +884,11 @@ describe("OutgoingRequestsController", function () {
const outgoingRequest = await Given.anOutgoingRequestWith({
status: LocalRequestStatus.Draft,
content: {
expiresAt: CoreDate.utc().subtract({ days: 1 }),
expiresAt: CoreDate.utc().add({ millisecond: 100 }),
items: [TestRequestItem.from({ mustBeAccepted: false })]
}
});
await sleep(150);

await When.iGetOutgoingRequestsWithTheQuery({ id: outgoingRequest.id.toString() });
await Then.theOnlyReturnedRequestIsInStatus(LocalRequestStatus.Expired);
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime/src/useCases/common/RuntimeErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ class Messages {
);
}

public cannotSendMessageWithExpiredRequest() {
return new ApplicationError(
"error.runtime.messages.cannotSendMessageWithExpiredRequest",
"The Message cannot be sent as the contained Request is already expired. Please create a new Request and try again."
);
}

public peerIsInDeletion(addresses: string[]) {
return new ApplicationError(
"error.runtime.messages.peerIsInDeletion",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Serializable } from "@js-soft/ts-serval";
import { ApplicationError, Result } from "@js-soft/ts-utils";
import { OutgoingRequestsController } from "@nmshd/consumption";
import { ArbitraryMessageContent, Mail, Notification, Request, ResponseWrapper } from "@nmshd/content";
import { CoreAddress, CoreError, CoreId } from "@nmshd/core-types";
import { CoreAddress, CoreDate, CoreError, CoreId } from "@nmshd/core-types";
import { AccountController, File, FileController, MessageController, PeerDeletionStatus, RelationshipsController, RelationshipStatus, TransportCoreErrors } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
import _ from "lodash";
Expand Down Expand Up @@ -143,6 +143,10 @@ export class SendMessageUseCase extends UseCase<SendMessageRequest, MessageDTO>
return RuntimeErrors.general.invalidPropertyValue("The sent Request must have the same content as the LocalRequest.");
}

if (request.expiresAt?.isBefore(CoreDate.utc())) {
return RuntimeErrors.messages.cannotSendMessageWithExpiredRequest();
}

if (!recipient.equals(localRequest.peer)) return RuntimeErrors.general.invalidPropertyValue("The recipient does not match the Request's peer.");

return;
Expand Down
58 changes: 58 additions & 0 deletions packages/runtime/test/transport/messages.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { sleep } from "@js-soft/ts-utils";
import { ConsumptionIds } from "@nmshd/consumption";
import { ConsentRequestItemJSON, Notification } from "@nmshd/content";
import { CoreDate } from "@nmshd/core-types";
Expand All @@ -6,8 +7,10 @@ import {
AttributeDeletedEvent,
GetMessagesQuery,
IdentityDeletionProcessStatus,
IncomingRequestReceivedEvent,
LocalAttributeDeletionStatus,
LocalRequestDTO,
LocalRequestStatus,
MessageReceivedEvent,
MessageSentEvent,
MessageWasReadAtChangedEvent,
Expand Down Expand Up @@ -272,6 +275,61 @@ describe("Message errors", () => {
expect(result).toBeAnError("The recipient does not match the Request's peer.", "error.runtime.validation.invalidPropertyValue");
});

test("should throw correct error for trying to send a Message with an expired Request", async () => {
const expiresAt = CoreDate.utc().add({ milliseconds: 100 }).toString();
const createRequestResult = (
await client1.consumption.outgoingRequests.create({
content: {
expiresAt,
items: [requestItem]
},
peer: client2.address
})
).value;

await sleep(150);

const result = await client1.transport.messages.sendMessage({
recipients: [client2.address],
content: createRequestResult.content
});

expect(result).toBeAnError(
"The Message cannot be sent as the contained Request is already expired. Please create a new Request and try again.",
"error.runtime.messages.cannotSendMessageWithExpiredRequest"
);
});

test("should mark a Request as expired when synced after the expiration date", async () => {
const expiresAt = CoreDate.utc().add({ milliseconds: 100 }).toString();
const createRequestResult = (
await client1.consumption.outgoingRequests.create({
content: {
expiresAt,
items: [requestItem]
},
peer: client2.address
})
).value;

const result = await client1.transport.messages.sendMessage({
recipients: [client2.address],
content: createRequestResult.content
});

expect(result).toBeSuccessful();

await sleep(150);
const client1ExpiredRequestResult = await client1.consumption.outgoingRequests.getRequest({ id: createRequestResult.id });
expect(client1ExpiredRequestResult.value.status).toBe(LocalRequestStatus.Expired);

await client2.transport.account.syncEverything();
await client2.eventBus.waitForEvent(IncomingRequestReceivedEvent, (event) => event.data.id === createRequestResult.id);

const client2ExpiredRequestResult = await client2.consumption.incomingRequests.getRequest({ id: createRequestResult.id });
expect(client2ExpiredRequestResult.value.status).toBe(LocalRequestStatus.Expired);
});

describe("Message errors for Relationships that are not active", () => {
test("should throw correct error for trying to send a Message if there are recipients to which no Relationship exists", async () => {
const result = await client1.transport.messages.sendMessage({
Expand Down

0 comments on commit 919eefc

Please sign in to comment.