From 8685910f301f09459f231921dda7ffb210f16c46 Mon Sep 17 00:00:00 2001 From: Milena Czierlinski <146972016+Milena-Czierlinski@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:14:55 +0100 Subject: [PATCH] Return all CanDecide errors simultaneously (#421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor validate * test: adjust to split function * feat: add mergeResults * test: mergeResults * refactor: wording * refactor: simplify recursion --------- Co-authored-by: Julian König --- .../consumption/src/modules/common/index.ts | 1 + .../src/modules/common/mergeResults.ts | 10 + .../DecideRequestParametersValidator.ts | 6 +- .../incoming/IncomingRequestsController.ts | 19 +- .../DecideRequestParametersValidator.test.ts | 571 ++++++++++-------- .../IncomingRequestsController.test.ts | 101 ++++ 6 files changed, 447 insertions(+), 261 deletions(-) create mode 100644 packages/consumption/src/modules/common/mergeResults.ts diff --git a/packages/consumption/src/modules/common/index.ts b/packages/consumption/src/modules/common/index.ts index 5bc477615..48c65a565 100644 --- a/packages/consumption/src/modules/common/index.ts +++ b/packages/consumption/src/modules/common/index.ts @@ -1,2 +1,3 @@ export * from "./flattenObject"; +export * from "./mergeResults"; export * from "./ValidationResult"; diff --git a/packages/consumption/src/modules/common/mergeResults.ts b/packages/consumption/src/modules/common/mergeResults.ts new file mode 100644 index 000000000..878fddb01 --- /dev/null +++ b/packages/consumption/src/modules/common/mergeResults.ts @@ -0,0 +1,10 @@ +import { ValidationResult } from "./ValidationResult"; + +export function mergeResults(result1: ValidationResult, result2: ValidationResult): ValidationResult { + if (result1.items.length !== result2.items.length) throw new Error("The dimensions of the ValidationResults do not match."); + + if (result1.items.length === 0) return result1.isError() ? result1 : result2; + + const mergedValidationResults = result1.items.map((item, index) => mergeResults(item, result2.items[index])); + return ValidationResult.fromItems(mergedValidationResults); +} diff --git a/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts b/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts index 74b970f7b..b167ba6dd 100644 --- a/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts +++ b/packages/consumption/src/modules/requests/incoming/DecideRequestParametersValidator.ts @@ -8,7 +8,7 @@ import { DecideRequestItemParametersJSON, isDecideRequestItemParametersJSON } fr import { InternalDecideRequestParametersJSON } from "./decide/InternalDecideRequestParameters"; export class DecideRequestParametersValidator { - public validate(params: InternalDecideRequestParametersJSON, request: LocalRequest): ValidationResult { + public validateRequest(params: InternalDecideRequestParametersJSON, request: LocalRequest): ValidationResult { if (!request.id.equals(CoreId.from(params.requestId))) { throw new Error("The response is invalid because the id of the Request does not match the id of the Response."); } @@ -19,6 +19,10 @@ export class DecideRequestParametersValidator { ); } + return ValidationResult.success(); + } + + public validateItems(params: InternalDecideRequestParametersJSON, request: LocalRequest): ValidationResult { const validationResults = request.content.items.map((requestItem, index) => this.checkItemOrGroup(requestItem, params.items[index], params.accept)); return ValidationResult.fromItems(validationResults); } diff --git a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts index 6245e9f9d..d431de82f 100644 --- a/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts +++ b/packages/consumption/src/modules/requests/incoming/IncomingRequestsController.ts @@ -9,6 +9,7 @@ import { ConsumptionControllerName } from "../../../consumption/ConsumptionContr import { ConsumptionCoreErrors } from "../../../consumption/ConsumptionCoreErrors"; import { ConsumptionError } from "../../../consumption/ConsumptionError"; import { ConsumptionIds } from "../../../consumption/ConsumptionIds"; +import { mergeResults } from "../../common"; import { ValidationResult } from "../../common/ValidationResult"; import { IncomingRequestReceivedEvent, IncomingRequestStatusChangedEvent } from "../events"; import { RequestItemProcessorRegistry } from "../itemProcessors/RequestItemProcessorRegistry"; @@ -214,12 +215,22 @@ export class IncomingRequestsController extends ConsumptionBaseController { ); } - const validationResult = this.decideRequestParamsValidator.validate(params, request); - if (validationResult.isError()) return validationResult; + const validateRequestResult = this.decideRequestParamsValidator.validateRequest(params, request); + if (validateRequestResult.isError()) return validateRequestResult; - const itemResults = await this.canDecideItems(params.items, request.content.items, request); + const validateItemsResult = this.decideRequestParamsValidator.validateItems(params, request); - return ValidationResult.fromItems(itemResults); + const canDecideItemsResults = await this.canDecideItems(params.items, request.content.items, request); + const canDecideItemsResult = ValidationResult.fromItems(canDecideItemsResults); + + try { + return mergeResults(validateItemsResult, canDecideItemsResult); + } catch (_) { + this._log.error( + `Merging '${JSON.stringify(validateItemsResult)}' and '${JSON.stringify(canDecideItemsResult)}' was not possible because their dimensions don't match.` + ); + return validateItemsResult.isError() ? validateItemsResult : canDecideItemsResult; + } } private async canDecideGroup(params: DecideRequestItemGroupParametersJSON, requestItemGroup: RequestItemGroup, request: LocalRequest) { diff --git a/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts b/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts index e1fee13cd..04284b1c6 100644 --- a/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts +++ b/packages/consumption/test/modules/requests/DecideRequestParametersValidator.test.ts @@ -31,302 +31,361 @@ interface TestParam { let validator: DecideRequestParametersValidator; +const requestId = "requestId"; + beforeEach(function () { validator = new DecideRequestParametersValidator(); }); describe("DecideRequestParametersValidator", function () { - const requestId = "requestId"; - - const successParams: TestParam[] = [ - { - description: "(1) success: accept Request with one RequestItem and accept the RequestItem", - input: { - request: TestObjectFactory.createRequestWithOneItem(), - response: { - accept: true, - items: [{ accept: true }], - requestId + describe("validateRequest", function () { + const errorParams: TestParam[] = [ + { + description: "(1) error: Request with two RequestItems is answered with one item", + input: { + request: TestObjectFactory.createRequestWithTwoItems(), + response: { + accept: true, + items: [{ accept: true }], + requestId + } + }, + expectedError: { + code: "error.consumption.requests.decide.validation.invalidNumberOfItems", + message: "The number of items in the Request and the Response do not match." } - } - }, - { - description: "(2) success: accept Request with RequestItemGroup and accept the contained RequestItem", - input: { - request: TestObjectFactory.createRequestWithOneItemGroup(), - response: { - accept: true, - items: [{ items: [{ accept: true }] }], - requestId + }, + { + description: "(2) error: Request with one RequestItem is answered with two items", + input: { + request: TestObjectFactory.createRequestWithOneItem(), + response: { + accept: true, + items: [{ accept: true }, { accept: true }], + requestId + } + }, + expectedError: { + code: "error.consumption.requests.decide.validation.invalidNumberOfItems", + message: "The number of items in the Request and the Response do not match." } } - }, - { - description: "(3) success: accept Request with one RequestItem and reject the RequestItem", - input: { - request: TestObjectFactory.createRequestWithOneItem(), - response: { - accept: true, - items: [{ accept: false }], - requestId - } + ]; + + test.each(errorParams)("$description", async function (data) { + const localRequest = LocalRequest.from({ + id: CoreId.from(requestId), + content: data.input.request, + createdAt: CoreDate.utc(), + isOwn: true, + peer: CoreAddress.from("did:e:a-domain:dids:anidentity"), + source: { reference: await CoreIdHelper.notPrefixed.generate(), type: "Message" }, + status: LocalRequestStatus.Open, + statusLog: [] + }); + + const validationResult = validator.validateRequest(data.input.response, localRequest); + + if (!data.expectedError) { + // eslint-disable-next-line jest/no-conditional-expect + expect( + validationResult.isError(), + `expected success, but received the error '${ + validationResult.isError() ? validationResult.error.code : "" + } - ${validationResult.isError() ? validationResult.error.message : ""}'` + ).toBe(false); + return; } - }, - { - description: "(4) success: accept Request with RequestItemGroup and reject the contained RequestItem", - input: { - request: TestObjectFactory.createRequestWithOneItemGroup(), - response: { - accept: true, - items: [{ items: [{ accept: false }] }], - requestId - } + + expect(validationResult.isError(), "expected an error, but received success").toBe(true); + if (!validationResult.isError()) throw new Error(); + + const errorIndexPath = data.expectedError.indexPath; + if (!errorIndexPath) { + // no error path provided, so we expect the error to be at the root + // eslint-disable-next-line jest/no-conditional-expect + expect(validationResult.error.code).toStrictEqual(data.expectedError.code); + // eslint-disable-next-line jest/no-conditional-expect + expect(validationResult.error.message).toStrictEqual(data.expectedError.message); + return; } - }, - { - description: "(5) success: accept a Request without accepting any RequestItem (no RequestItems mustBeAccepted)", - input: { - request: Request.from({ - items: [TestRequestItem.from({ mustBeAccepted: false })] - }), - response: { - accept: true, - items: [{ accept: false }], - requestId + + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.validation.inheritedFromItem" + }); + + let childResult = validationResult; + for (const index of errorIndexPath) childResult = childResult.items[index] as ErrorValidationResult; + + expect(childResult.isError(), "expected an error, but received success").toBe(true); + if (!childResult.isError()) throw new Error(); + + expect(childResult.error.code).toStrictEqual(data.expectedError.code); + expect(childResult.error.message).toStrictEqual(data.expectedError.message); + }); + }); + + describe("validateItems", function () { + const successParams: TestParam[] = [ + { + description: "(1) success: accept Request with one RequestItem and accept the RequestItem", + input: { + request: TestObjectFactory.createRequestWithOneItem(), + response: { + accept: true, + items: [{ accept: true }], + requestId + } } - } - }, - { - description: "(6) success: RequestItems that must not be accepted in a RequestItemGroup are rejected", - input: { - request: Request.from({ - items: [ - RequestItemGroup.from({ - items: [TestRequestItem.from({ mustBeAccepted: false }), TestRequestItem.from({ mustBeAccepted: true })] - }) - ] - }), - response: { - accept: true, - items: [ - { - items: [{ accept: false }, { accept: true }] - } - ], - requestId + }, + { + description: "(2) success: accept Request with RequestItemGroup and accept the contained RequestItem", + input: { + request: TestObjectFactory.createRequestWithOneItemGroup(), + response: { + accept: true, + items: [{ items: [{ accept: true }] }], + requestId + } } - } - } - ]; - - const errorParams: TestParam[] = [ - { - description: "(1) error: Request with two RequestItems is answered with one item", - input: { - request: TestObjectFactory.createRequestWithTwoItems(), - response: { - accept: true, - items: [{ accept: true }], - requestId + }, + { + description: "(3) success: accept Request with one RequestItem and reject the RequestItem", + input: { + request: TestObjectFactory.createRequestWithOneItem(), + response: { + accept: true, + items: [{ accept: false }], + requestId + } } }, - expectedError: { - code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "The number of items in the Request and the Response do not match." - } - }, - { - description: "(2) error: Request with one RequestItem is answered with two items", - input: { - request: TestObjectFactory.createRequestWithOneItem(), - response: { - accept: true, - items: [{ accept: true }, { accept: true }], - requestId + { + description: "(4) success: accept Request with RequestItemGroup and reject the contained RequestItem", + input: { + request: TestObjectFactory.createRequestWithOneItemGroup(), + response: { + accept: true, + items: [{ items: [{ accept: false }] }], + requestId + } } }, - expectedError: { - code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "The number of items in the Request and the Response do not match." - } - }, - { - description: "(3) error: Request with one RequestItemGroup is answered as a RequestItem", - input: { - request: TestObjectFactory.createRequestWithOneItemGroup(), - response: { - accept: true, - items: [{ accept: true }], - requestId + { + description: "(5) success: accept a Request without accepting any RequestItem (no RequestItems mustBeAccepted)", + input: { + request: Request.from({ + items: [TestRequestItem.from({ mustBeAccepted: false })] + }), + response: { + accept: true, + items: [{ accept: false }], + requestId + } } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.decide.validation.requestItemGroupAnsweredAsRequestItem", - message: "The RequestItemGroup was answered as a RequestItem." + { + description: "(6) success: RequestItems that must not be accepted in a RequestItemGroup are rejected", + input: { + request: Request.from({ + items: [ + RequestItemGroup.from({ + items: [TestRequestItem.from({ mustBeAccepted: false }), TestRequestItem.from({ mustBeAccepted: true })] + }) + ] + }), + response: { + accept: true, + items: [ + { + items: [{ accept: false }, { accept: true }] + } + ], + requestId + } + } } - }, - { - description: "(4) error: Request with one RequestItem is answered as a RequestItemGroup", - input: { - request: TestObjectFactory.createRequestWithOneItem(), - response: { - accept: true, - items: [{ items: [{ accept: true }] }], - requestId + ]; + + const errorParams: TestParam[] = [ + { + description: "(1) error: Request with one RequestItemGroup is answered as a RequestItem", + input: { + request: TestObjectFactory.createRequestWithOneItemGroup(), + response: { + accept: true, + items: [{ accept: true }], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.decide.validation.requestItemGroupAnsweredAsRequestItem", + message: "The RequestItemGroup was answered as a RequestItem." } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.decide.validation.requestItemAnsweredAsRequestItemGroup", - message: "The RequestItem was answered as a RequestItemGroup." - } - }, - { - description: "(5) error: RequestItemGroup and ResponseItemGroup have different number of items", - input: { - request: TestObjectFactory.createRequestWithOneItemGroup(), - response: { - accept: true, - items: [ - { - items: [{ accept: true }, { accept: true }] - } - ], - requestId + { + description: "(2) error: Request with one RequestItem is answered as a RequestItemGroup", + input: { + request: TestObjectFactory.createRequestWithOneItem(), + response: { + accept: true, + items: [{ items: [{ accept: true }] }], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.decide.validation.requestItemAnsweredAsRequestItemGroup", + message: "The RequestItem was answered as a RequestItemGroup." } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.decide.validation.invalidNumberOfItems", - message: "The number of items in the RequestItemGroup and the ResponseItemGroup do not match." - } - }, - { - description: "(6) error: item that must be accepted was rejected", - input: { - request: TestObjectFactory.createRequestWithOneItem(undefined, true), - response: { - accept: true, - items: [{ accept: false }], - requestId + { + description: "(3) error: RequestItemGroup and ResponseItemGroup have different number of items", + input: { + request: TestObjectFactory.createRequestWithOneItemGroup(), + response: { + accept: true, + items: [ + { + items: [{ accept: true }, { accept: true }] + } + ], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.decide.validation.invalidNumberOfItems", + message: "The number of items in the RequestItemGroup and the ResponseItemGroup do not match." } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", - message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." - } - }, - { - description: "(7) error: RequestItem contained within a RequestItemGroup that must be accepted was rejected", - input: { - request: Request.from({ - items: [ - RequestItemGroup.from({ - items: [TestRequestItem.from({ mustBeAccepted: true }), TestRequestItem.from({ mustBeAccepted: true })] - }) - ] - }), - response: { - accept: true, - items: [ - { - items: [{ accept: true }, { accept: false }] - } - ], - requestId + { + description: "(4) error: item that must be accepted was rejected", + input: { + request: TestObjectFactory.createRequestWithOneItem(undefined, true), + response: { + accept: true, + items: [{ accept: false }], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", + message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." } }, - expectedError: { - indexPath: [0, 1], - code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", - message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." - } - }, - { - description: "(8) error: when the Request is rejected no RequestItem may be accepted", - input: { - request: TestObjectFactory.createRequestWithOneItem(), - response: { - accept: false, - items: [{ accept: true }], - requestId + { + description: "(5) error: RequestItem contained within a RequestItemGroup that must be accepted was rejected", + input: { + request: Request.from({ + items: [ + RequestItemGroup.from({ + items: [TestRequestItem.from({ mustBeAccepted: true }), TestRequestItem.from({ mustBeAccepted: true })] + }) + ] + }), + response: { + accept: true, + items: [ + { + items: [{ accept: true }, { accept: false }] + } + ], + requestId + } + }, + expectedError: { + indexPath: [0, 1], + code: "error.consumption.requests.decide.validation.mustBeAcceptedItemNotAccepted", + message: "The RequestItem is flagged as 'mustBeAccepted', but it was not accepted." } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.decide.validation.itemAcceptedButRequestNotAccepted", - message: "The RequestItem was accepted, but the Request was not accepted." - } - }, - { - description: "(9) error: when the Request is rejected no RequestItem contained within a RequestItemGroup may be accepted", - input: { - request: TestObjectFactory.createRequestWithOneItemGroup(), - response: { - accept: false, - items: [{ items: [{ accept: true }] }], - requestId + { + description: "(6) error: when the Request is rejected no RequestItem may be accepted", + input: { + request: TestObjectFactory.createRequestWithOneItem(), + response: { + accept: false, + items: [{ accept: true }], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.decide.validation.itemAcceptedButRequestNotAccepted", + message: "The RequestItem was accepted, but the Request was not accepted." } }, - expectedError: { - indexPath: [0], - code: "error.consumption.requests.validation.inheritedFromItem", - message: "Some child items have errors." + { + description: "(7) error: when the Request is rejected no RequestItem contained within a RequestItemGroup may be accepted", + input: { + request: TestObjectFactory.createRequestWithOneItemGroup(), + response: { + accept: false, + items: [{ items: [{ accept: true }] }], + requestId + } + }, + expectedError: { + indexPath: [0], + code: "error.consumption.requests.validation.inheritedFromItem", + message: "Some child items have errors." + } } - } - ]; + ]; - test.each([...successParams, ...errorParams])("$description", async function (data) { - const localRequest = LocalRequest.from({ - id: CoreId.from(requestId), - content: data.input.request, - createdAt: CoreDate.utc(), - isOwn: true, - peer: CoreAddress.from("did:e:a-domain:dids:anidentity"), - source: { reference: await CoreIdHelper.notPrefixed.generate(), type: "Message" }, - status: LocalRequestStatus.Open, - statusLog: [] - }); + test.each([...successParams, ...errorParams])("$description", async function (data) { + const localRequest = LocalRequest.from({ + id: CoreId.from(requestId), + content: data.input.request, + createdAt: CoreDate.utc(), + isOwn: true, + peer: CoreAddress.from("did:e:a-domain:dids:anidentity"), + source: { reference: await CoreIdHelper.notPrefixed.generate(), type: "Message" }, + status: LocalRequestStatus.Open, + statusLog: [] + }); - const validationResult = validator.validate(data.input.response, localRequest); + const validationResult = validator.validateItems(data.input.response, localRequest); - if (!data.expectedError) { - // eslint-disable-next-line jest/no-conditional-expect - expect( - validationResult.isError(), - `expected success, but received the error '${ - validationResult.isError() ? validationResult.error.code : "" - } - ${validationResult.isError() ? validationResult.error.message : ""}'` - ).toBe(false); - return; - } + if (!data.expectedError) { + // eslint-disable-next-line jest/no-conditional-expect + expect( + validationResult.isError(), + `expected success, but received the error '${ + validationResult.isError() ? validationResult.error.code : "" + } - ${validationResult.isError() ? validationResult.error.message : ""}'` + ).toBe(false); + return; + } - expect(validationResult.isError(), "expected an error, but received success").toBe(true); - if (!validationResult.isError()) throw new Error(); + expect(validationResult.isError(), "expected an error, but received success").toBe(true); + if (!validationResult.isError()) throw new Error(); - const errorIndexPath = data.expectedError.indexPath; - if (!errorIndexPath) { - // no error path provided, so we expect the error to be at the root - // eslint-disable-next-line jest/no-conditional-expect - expect(validationResult.error.code).toStrictEqual(data.expectedError.code); - // eslint-disable-next-line jest/no-conditional-expect - expect(validationResult.error.message).toStrictEqual(data.expectedError.message); - return; - } + const errorIndexPath = data.expectedError.indexPath; + if (!errorIndexPath) { + // no error path provided, so we expect the error to be at the root + // eslint-disable-next-line jest/no-conditional-expect + expect(validationResult.error.code).toStrictEqual(data.expectedError.code); + // eslint-disable-next-line jest/no-conditional-expect + expect(validationResult.error.message).toStrictEqual(data.expectedError.message); + return; + } - expect(validationResult).errorValidationResult({ - code: "error.consumption.requests.validation.inheritedFromItem" - }); + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.validation.inheritedFromItem" + }); - let childResult = validationResult; - for (const index of errorIndexPath) childResult = childResult.items[index] as ErrorValidationResult; + let childResult = validationResult; + for (const index of errorIndexPath) childResult = childResult.items[index] as ErrorValidationResult; - expect(childResult.isError(), "expected an error, but received success").toBe(true); - if (!childResult.isError()) throw new Error(); + expect(childResult.isError(), "expected an error, but received success").toBe(true); + if (!childResult.isError()) throw new Error(); - expect(childResult.error.code).toStrictEqual(data.expectedError.code); - expect(childResult.error.message).toStrictEqual(data.expectedError.message); + expect(childResult.error.code).toStrictEqual(data.expectedError.code); + expect(childResult.error.message).toStrictEqual(data.expectedError.message); + }); }); }); diff --git a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts index 7c7647148..06f382abf 100644 --- a/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts +++ b/packages/consumption/test/modules/requests/IncomingRequestsController.test.ts @@ -424,6 +424,107 @@ describe("IncomingRequestsController", function () { expect(validationResult.items[1].items[2].isError()).toBe(true); }); + test("returns a validation result that merges the results from decideRequestParamsValidator and the RequestItemProcessor", async function () { + const request = { + items: [ + TestRequestItem.from({ + mustBeAccepted: true + }), + TestRequestItem.from({ + mustBeAccepted: true + }), + TestRequestItem.from({ + mustBeAccepted: true, + shouldFailAtCanAccept: true + }), + TestRequestItem.from({ + mustBeAccepted: true, + shouldFailAtCanReject: true + }), + RequestItemGroup.from({ + items: [ + TestRequestItem.from({ + mustBeAccepted: true + }), + TestRequestItem.from({ + mustBeAccepted: true + }), + TestRequestItem.from({ + mustBeAccepted: true, + shouldFailAtCanAccept: true + }), + TestRequestItem.from({ + mustBeAccepted: true, + shouldFailAtCanReject: true + }) + ] + }) + ] + } as IRequest; + + const acceptParams = { + items: [ + { + accept: true + }, + { + accept: false + }, + { + accept: true + }, + { + accept: false + }, + { + items: [ + { + accept: true + }, + { + accept: false + }, + { + accept: true + }, + { + accept: false + } + ] + } + ] + } as Omit; + + await Given.anIncomingRequestWith({ + content: request, + status: LocalRequestStatus.DecisionRequired + }); + + const validationResult = await When.iCallCanAcceptWith({ + items: acceptParams.items + }); + + expect(validationResult).errorValidationResult({ + code: "error.consumption.requests.validation.inheritedFromItem", + message: "Some child items have errors." + }); + expect(validationResult.items).toHaveLength(5); + + expect(validationResult.items[0].isError()).toBe(false); + expect(validationResult.items[1].isError()).toBe(true); + expect(validationResult.items[2].isError()).toBe(true); + expect(validationResult.items[3].isError()).toBe(true); + + expect(validationResult.items[4].isError()).toBe(true); + expect(validationResult.items[4]).errorValidationResult({ code: "error.consumption.requests.validation.inheritedFromItem" }); + + expect(validationResult.items[4].items).toHaveLength(4); + expect(validationResult.items[4].items[0].isError()).toBe(false); + expect(validationResult.items[4].items[1].isError()).toBe(true); + expect(validationResult.items[4].items[2].isError()).toBe(true); + expect(validationResult.items[4].items[3].isError()).toBe(true); + }); + test("throws error for requests whose acceptance always would lead to the creation of more than one RelationshipAttribute with the same key", async function () { await Given.anIncomingRequestWith({ content: {