diff --git a/packages/consumption/src/consumption/ConsumptionCoreErrors.ts b/packages/consumption/src/consumption/ConsumptionCoreErrors.ts index 73ace7303..7e3089abe 100644 --- a/packages/consumption/src/consumption/ConsumptionCoreErrors.ts +++ b/packages/consumption/src/consumption/ConsumptionCoreErrors.ts @@ -340,6 +340,13 @@ class Requests { return new CoreError("error.consumption.requests.cannotCreateRequestWithExpirationDateInPast", "You cannot create a Request with an expiration date that is in the past."); } + public canOnlyDeleteIncomingRequestThatIsExpired(id: string, status: string) { + return new CoreError( + "error.consumption.requests.canOnlyDeleteIncomingRequestThatIsExpired", + `The incoming Request '${id}' is in status '${status}'. At the moment, you can only delete incoming Requests that are expired.` + ); + } + private static readonly _decideValidation = class { public invalidNumberOfItems(message: string) { return new ApplicationError("error.consumption.requests.decide.validation.invalidNumberOfItems", message); diff --git a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts index d431de82f..f11642fec 100644 --- a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts +++ b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts @@ -451,6 +451,14 @@ export class IncomingRequestsController extends ConsumptionBaseController { await this.localRequests.update(requestDoc, request); } + public async delete(request: LocalRequest): Promise { + if (request.status !== LocalRequestStatus.Expired) { + throw ConsumptionCoreErrors.requests.canOnlyDeleteIncomingRequestThatIsExpired(request.id.toString(), request.status); + } + + await this.localRequests.delete(request); + } + public async deleteRequestsFromPeer(peer: CoreAddress): Promise { const requests = await this.getIncomingRequests({ peer: peer.toString() }); for (const request of requests) { diff --git a/packages/consumption/test/modules/requests/DeleteRequest.test.ts b/packages/consumption/test/modules/requests/DeleteRequest.test.ts index 846c9956e..6fae7dc00 100644 --- a/packages/consumption/test/modules/requests/DeleteRequest.test.ts +++ b/packages/consumption/test/modules/requests/DeleteRequest.test.ts @@ -1,23 +1,22 @@ import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions"; +import { sleep } from "@js-soft/ts-utils"; import { Request } from "@nmshd/content"; -import { AccountController, Message, Transport } from "@nmshd/transport"; -import { ConsumptionController, LocalRequest } from "../../../src"; +import { CoreDate } from "@nmshd/core-types"; +import { AccountController, Transport } from "@nmshd/transport"; +import { ConsumptionController } from "../../../src"; import { TestUtil } from "../../core/TestUtil"; import { TestRequestItem } from "./testHelpers/TestRequestItem"; import { TestRequestItemProcessor } from "./testHelpers/TestRequestItemProcessor"; let connection: IDatabaseConnection; let transport: Transport; + describe("Delete requests", function () { let sAccountController: AccountController; let sConsumptionController: ConsumptionController; let rAccountController: AccountController; let rConsumptionController: ConsumptionController; - let sLocalRequest: LocalRequest; - let rMessageWithRequest: Message; - let rLocalRequest: LocalRequest; - beforeAll(async function () { connection = await TestUtil.createConnection(); transport = TestUtil.createTransport(connection); @@ -29,32 +28,30 @@ describe("Delete requests", function () { sConsumptionController.incomingRequests["processorRegistry"].registerProcessor(TestRequestItem, TestRequestItemProcessor); ({ accountController: rAccountController, consumptionController: rConsumptionController } = accounts[1]); rConsumptionController.incomingRequests["processorRegistry"].registerProcessor(TestRequestItem, TestRequestItemProcessor); + }); + + beforeEach(async function () { + await TestUtil.ensureActiveRelationship(sAccountController, rAccountController); + }); - await TestUtil.addRelationship(sAccountController, rAccountController); - sLocalRequest = await sConsumptionController.outgoingRequests.create({ - content: Request.from({ - items: [TestRequestItem.from({ mustBeAccepted: false })] - }), + afterAll(async function () { + await connection.close(); + }); + + test("requests should be deleted after decomposing", async function () { + const sLocalRequest = await sConsumptionController.outgoingRequests.create({ + content: Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }), peer: rAccountController.identity.address }); - await sAccountController.messages.sendMessage({ - content: sLocalRequest.content, - recipients: [rAccountController.identity.address] - }); + await sAccountController.messages.sendMessage({ content: sLocalRequest.content, recipients: [rAccountController.identity.address] }); const messages = await TestUtil.syncUntilHasMessages(rAccountController); - rMessageWithRequest = messages[0]; - rLocalRequest = await rConsumptionController.incomingRequests.received({ + const rMessageWithRequest = messages[0]; + const rLocalRequest = await rConsumptionController.incomingRequests.received({ receivedRequest: rMessageWithRequest.cache!.content as Request, requestSourceObject: rMessageWithRequest }); - }); - - afterAll(async function () { - await connection.close(); - }); - test("requests should be deleted after decomposing", async function () { await TestUtil.terminateRelationship(sAccountController, rAccountController); await TestUtil.decomposeRelationship(sAccountController, sConsumptionController, rAccountController); await TestUtil.decomposeRelationship(rAccountController, rConsumptionController, sAccountController); @@ -63,4 +60,44 @@ describe("Delete requests", function () { expect(sRequest).toBeUndefined(); expect(rRequest).toBeUndefined(); }); + + test("should be possible to delete an expired request", async function () { + const sLocalRequest = await sConsumptionController.outgoingRequests.create({ + content: Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })], expiresAt: CoreDate.utc().add({ seconds: 3 }) }), + peer: rAccountController.identity.address + }); + + await sAccountController.messages.sendMessage({ content: sLocalRequest.content, recipients: [rAccountController.identity.address] }); + const messages = await TestUtil.syncUntilHasMessages(rAccountController); + const rMessageWithRequest = messages[0]; + const rLocalRequest = await rConsumptionController.incomingRequests.received({ + receivedRequest: rMessageWithRequest.cache!.content as Request, + requestSourceObject: rMessageWithRequest + }); + + await sleep(3000); + + const requestInDeletion = await rConsumptionController.incomingRequests.getIncomingRequest(rLocalRequest.id); + await rConsumptionController.incomingRequests.delete(requestInDeletion!); + + const rRequestAfterDeletion = await rConsumptionController.incomingRequests.getIncomingRequest(sLocalRequest.id); + expect(rRequestAfterDeletion).toBeUndefined(); + }); + + test("should not be possible to delete a non expired request", async function () { + const sLocalRequest = await sConsumptionController.outgoingRequests.create({ + content: Request.from({ items: [TestRequestItem.from({ mustBeAccepted: false })] }), + peer: rAccountController.identity.address + }); + + await sAccountController.messages.sendMessage({ content: sLocalRequest.content, recipients: [rAccountController.identity.address] }); + const messages = await TestUtil.syncUntilHasMessages(rAccountController); + const rMessageWithRequest = messages[0]; + const rLocalRequest = await rConsumptionController.incomingRequests.received({ + receivedRequest: rMessageWithRequest.cache!.content as Request, + requestSourceObject: rMessageWithRequest + }); + + await expect(rConsumptionController.incomingRequests.delete(rLocalRequest)).rejects.toThrow("error.consumption.requests.canOnlyDeleteIncomingRequestThatIsExpired"); + }); }); diff --git a/packages/runtime/src/extensibility/facades/consumption/IncomingRequestsFacade.ts b/packages/runtime/src/extensibility/facades/consumption/IncomingRequestsFacade.ts index 98bddc8ed..d28a0ba45 100644 --- a/packages/runtime/src/extensibility/facades/consumption/IncomingRequestsFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/IncomingRequestsFacade.ts @@ -10,6 +10,8 @@ import { CheckPrerequisitesOfIncomingRequestUseCase, CompleteIncomingRequestRequest, CompleteIncomingRequestUseCase, + DeleteIncomingRequestRequest, + DeleteIncomingRequestUseCase, GetIncomingRequestRequest, GetIncomingRequestsRequest, GetIncomingRequestsUseCase, @@ -33,7 +35,8 @@ export class IncomingRequestsFacade { @Inject private readonly rejectUseCase: RejectIncomingRequestUseCase, @Inject private readonly completeUseCase: CompleteIncomingRequestUseCase, @Inject private readonly getRequestUseCase: GetIncomingRequestUseCase, - @Inject private readonly getRequestsUseCase: GetIncomingRequestsUseCase + @Inject private readonly getRequestsUseCase: GetIncomingRequestsUseCase, + @Inject private readonly deleteUseCase: DeleteIncomingRequestUseCase ) {} public async received(request: ReceivedIncomingRequestRequest): Promise> { @@ -75,4 +78,8 @@ export class IncomingRequestsFacade { public async getRequests(request: GetIncomingRequestsRequest): Promise> { return await this.getRequestsUseCase.execute(request); } + + public async delete(request: DeleteIncomingRequestRequest): Promise> { + return await this.deleteUseCase.execute(request); + } } diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 13a4d977e..9d4825f1e 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -10863,6 +10863,25 @@ export const CreateOutgoingRequestRequest: any = { } } +export const DeleteIncomingRequestRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/DeleteIncomingRequestRequest", + "definitions": { + "DeleteIncomingRequestRequest": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + } + }, + "required": [ + "requestId" + ], + "additionalProperties": false + } + } +} + export const DiscardOutgoingRequestRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/DiscardOutgoingRequestRequest", diff --git a/packages/runtime/src/useCases/consumption/requests/DeleteIncomingRequest.ts b/packages/runtime/src/useCases/consumption/requests/DeleteIncomingRequest.ts new file mode 100644 index 000000000..a66b85268 --- /dev/null +++ b/packages/runtime/src/useCases/consumption/requests/DeleteIncomingRequest.ts @@ -0,0 +1,39 @@ +import { Result } from "@js-soft/ts-utils"; +import { IncomingRequestsController, LocalRequest } from "@nmshd/consumption"; +import { CoreId } from "@nmshd/core-types"; +import { AccountController } from "@nmshd/transport"; +import { Inject } from "@nmshd/typescript-ioc"; +import { RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common"; + +export interface DeleteIncomingRequestRequest { + requestId: string; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("DeleteIncomingRequestRequest")); + } +} + +export class DeleteIncomingRequestUseCase extends UseCase { + public constructor( + @Inject private readonly incomingRequestsController: IncomingRequestsController, + @Inject private readonly accountController: AccountController, + @Inject validator: Validator + ) { + super(validator); + } + + protected async executeInternal(request: DeleteIncomingRequestRequest): Promise> { + const localRequest = await this.incomingRequestsController.getIncomingRequest(CoreId.from(request.requestId)); + if (!localRequest) { + return Result.fail(RuntimeErrors.general.recordNotFound(LocalRequest)); + } + + await this.incomingRequestsController.delete(localRequest); + + await this.accountController.syncDatawallet(); + + return Result.ok(undefined); + } +} diff --git a/packages/runtime/src/useCases/consumption/requests/index.ts b/packages/runtime/src/useCases/consumption/requests/index.ts index 9ef35617b..aa5f6803d 100644 --- a/packages/runtime/src/useCases/consumption/requests/index.ts +++ b/packages/runtime/src/useCases/consumption/requests/index.ts @@ -7,6 +7,7 @@ export * from "./CompleteIncomingRequest"; export * from "./CompleteOutgoingRequest"; export * from "./CreateAndCompleteOutgoingRequestFromRelationshipTemplateResponse"; export * from "./CreateOutgoingRequest"; +export * from "./DeleteIncomingRequest"; export * from "./DiscardOutgoingRequest"; export * from "./GetIncomingRequest"; export * from "./GetIncomingRequests"; diff --git a/packages/runtime/test/consumption/requests.test.ts b/packages/runtime/test/consumption/requests.test.ts index 28cc0ead6..a04aa990f 100644 --- a/packages/runtime/test/consumption/requests.test.ts +++ b/packages/runtime/test/consumption/requests.test.ts @@ -673,6 +673,30 @@ describe("Requests", () => { expect(triggeredEvent).toBeUndefined(); }); + test("can not delete a Request when it is not expired", async () => { + const request = (await exchangeTemplateAndReceiverRequiresManualDecision(sRuntimeServices, rRuntimeServices, templateContent)).request; + + await expect(rConsumptionServices.incomingRequests.delete({ requestId: request.id })).resolves.toBeAnError( + "is in status 'ManualDecisionRequired'. At the moment, you can only delete incoming Requests that are expired.", + "error.consumption.requests.canOnlyDeleteIncomingRequestThatIsExpired" + ); + }); + + test("can delete a Request when it is expired", async () => { + const request = (await exchangeTemplateAndReceiverRequiresManualDecision(sRuntimeServices, rRuntimeServices, templateContent, DateTime.utc().plus({ seconds: 3 }))) + .request; + + await sleep(3000); + + const rLocalRequest = (await rConsumptionServices.incomingRequests.getRequest({ id: request.id })).value; + expect(rLocalRequest.status).toBe(LocalRequestStatus.Expired); + + await expect(rConsumptionServices.incomingRequests.delete({ requestId: request.id })).resolves.toBeSuccessful(); + + const rLocalRequestAfterDelete = await rConsumptionServices.incomingRequests.getRequest({ id: request.id }); + expect(rLocalRequestAfterDelete).toBeAnError("LocalRequest not found.", "error.runtime.recordNotFound"); + }); + describe.each([ { action: "Accept"