From 238151060f587deb2c59615417b0449c34809443 Mon Sep 17 00:00:00 2001 From: Milena-Czierlinski Date: Tue, 3 Feb 2026 16:26:06 +0100 Subject: [PATCH 1/3] fix: owner of incoming RelationshipAttributeQuery is incorrectly expanded --- .../runtime/src/dataViews/DataViewExpander.ts | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 6f1b879b7..6a9066d47 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -459,7 +459,7 @@ export class DataViewExpander { case "ReadAttributeRequestItem": const readAttributeRequestItem = requestItem as ReadAttributeRequestItemJSON; if (isDecidable) { - const processedQuery = await this.processAttributeQuery(readAttributeRequestItem.query); + const processedQuery = await this.processAttributeQuery(readAttributeRequestItem.query, localRequestDTO?.isOwn); // ThirdPartyAttributeQueries without results cannot be changed. if (processedQuery.type === "ProcessedThirdPartyRelationshipAttributeQueryDVO" && processedQuery.results.length === 0) { error = { @@ -497,7 +497,7 @@ export class DataViewExpander { type: "ReadAttributeRequestItemDVO", id: "", name: this.generateRequestItemName(requestItem["@type"], isDecidable), - query: await this.expandAttributeQuery(readAttributeRequestItem.query), + query: await this.expandAttributeQuery(readAttributeRequestItem.query, localRequestDTO?.isOwn), isDecidable, response: responseItemDVO } as ReadAttributeRequestItemDVO; @@ -557,7 +557,9 @@ export class DataViewExpander { id: "", name: this.generateRequestItemName(requestItem["@type"], isDecidable), attribute: await this.expandAttribute(proposeAttributeRequestItem.attribute), - query: isDecidable ? await this.processAttributeQuery(proposeAttributeRequestItem.query) : await this.expandAttributeQuery(proposeAttributeRequestItem.query), + query: isDecidable + ? await this.processAttributeQuery(proposeAttributeRequestItem.query, localRequestDTO?.isOwn) + : await this.expandAttributeQuery(proposeAttributeRequestItem.query, localRequestDTO?.isOwn), isDecidable, response: responseItemDVO, proposedValueOverruled @@ -1179,13 +1181,14 @@ export class DataViewExpander { } public async expandAttributeQuery( - query: IdentityAttributeQueryJSON | RelationshipAttributeQueryJSON | ThirdPartyRelationshipAttributeQueryJSON | IQLQueryJSON + query: IdentityAttributeQueryJSON | RelationshipAttributeQueryJSON | ThirdPartyRelationshipAttributeQueryJSON | IQLQueryJSON, + isOwnRequest?: boolean ): Promise { switch (query["@type"]) { case "IdentityAttributeQuery": return this.expandIdentityAttributeQuery(query); case "RelationshipAttributeQuery": - return await this.expandRelationshipAttributeQuery(query); + return await this.expandRelationshipAttributeQuery(query, isOwnRequest); case "ThirdPartyRelationshipAttributeQuery": return await this.expandThirdPartyRelationshipAttributeQuery(query); case "IQLQuery": @@ -1214,7 +1217,7 @@ export class DataViewExpander { }; } - public async expandRelationshipAttributeQuery(query: RelationshipAttributeQueryJSON): Promise { + public async expandRelationshipAttributeQuery(query: RelationshipAttributeQueryJSON, isOwnRequest?: boolean): Promise { const valueType = query.attributeCreationHints.valueType; let name = "i18n://dvo.attributeQuery.name.RelationshipAttributeQuery"; let description = "i18n://dvo.attributeQuery.description.RelationshipAttributeQuery"; @@ -1230,12 +1233,14 @@ export class DataViewExpander { hints.valueHints = query.attributeCreationHints.valueHints; } + const emptyOwnerIsSelf = query.owner === "" && isOwnRequest === false; + return { type: "RelationshipAttributeQueryDVO", id: "", name, description, - owner: await this.expandAddress(query.owner), + owner: emptyOwnerIsSelf ? this.expandSelf() : await this.expandAddress(query.owner), key: query.key, attributeCreationHints: query.attributeCreationHints, renderHints: hints.renderHints, @@ -1324,13 +1329,14 @@ export class DataViewExpander { } public async processAttributeQuery( - attributeQuery: IdentityAttributeQueryJSON | RelationshipAttributeQueryJSON | ThirdPartyRelationshipAttributeQueryJSON | IQLQueryJSON + attributeQuery: IdentityAttributeQueryJSON | RelationshipAttributeQueryJSON | ThirdPartyRelationshipAttributeQueryJSON | IQLQueryJSON, + isOwnRequest?: boolean ): Promise { switch (attributeQuery["@type"]) { case "IdentityAttributeQuery": return await this.processIdentityAttributeQuery(attributeQuery); case "RelationshipAttributeQuery": - return await this.processRelationshipAttributeQuery(attributeQuery); + return await this.processRelationshipAttributeQuery(attributeQuery, isOwnRequest); case "ThirdPartyRelationshipAttributeQuery": return await this.processThirdPartyRelationshipAttributeQuery(attributeQuery); case "IQLQuery": @@ -1359,24 +1365,23 @@ export class DataViewExpander { }; } - public async processRelationshipAttributeQuery(query: RelationshipAttributeQueryJSON): Promise { - const matchedAttributeDTOResult = await this.consumption.attributes.executeRelationshipAttributeQuery({ - query - }); + public async processRelationshipAttributeQuery(query: RelationshipAttributeQueryJSON, isOwnRequest?: boolean): Promise { + const matchedAttributeDTOResult = await this.consumption.attributes.executeRelationshipAttributeQuery({ query }); + if (matchedAttributeDTOResult.isError) { if (matchedAttributeDTOResult.error.code !== "error.runtime.recordNotFound") throw matchedAttributeDTOResult.error; return { - ...(await this.expandRelationshipAttributeQuery(query)), + ...(await this.expandRelationshipAttributeQuery(query, isOwnRequest)), type: "ProcessedRelationshipAttributeQueryDVO", results: [], isProcessed: true }; } - const matchedAttributeDVOs = await this.expandLocalAttributeDTO(matchedAttributeDTOResult.value); + const matchedAttributeDVOs = await this.expandLocalAttributeDTO(matchedAttributeDTOResult.value); return { - ...(await this.expandRelationshipAttributeQuery(query)), + ...(await this.expandRelationshipAttributeQuery(query, isOwnRequest)), type: "ProcessedRelationshipAttributeQueryDVO", results: [matchedAttributeDVOs as OwnRelationshipAttributeDVO | PeerRelationshipAttributeDVO], isProcessed: true From 4576ac01add92ab8936db64b355301fde9413c8e Mon Sep 17 00:00:00 2001 From: Milena-Czierlinski Date: Tue, 3 Feb 2026 16:26:38 +0100 Subject: [PATCH 2/3] test: expansion of empty string as owner of RelationshipAttributeQuery --- .../ReadAttributeRequestItemDVO.test.ts | 220 +++++++++++++++++- 1 file changed, 219 insertions(+), 1 deletion(-) diff --git a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts index ddefd0a44..f30ade607 100644 --- a/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts +++ b/packages/runtime/test/dataViews/requestItems/ReadAttributeRequestItemDVO.test.ts @@ -3,7 +3,18 @@ import { AcceptReadAttributeRequestItemParametersWithNewAttributeJSON, DecideRequestItemParametersJSON } from "@nmshd/consumption"; -import { GivenNameJSON, IdentityAttributeQuery, IQLQuery, ReadAttributeAcceptResponseItemJSON, ReadAttributeRequestItem, RequestJSON, SurnameJSON } from "@nmshd/content"; +import { + GivenNameJSON, + IdentityAttributeQuery, + IQLQuery, + ReadAttributeAcceptResponseItemJSON, + ReadAttributeRequestItem, + RelationshipAttributeConfidentiality, + RelationshipAttributeJSON, + RelationshipAttributeQuery, + RequestJSON, + SurnameJSON +} from "@nmshd/content"; import { CoreId } from "@nmshd/core-types"; import { AttributeAlreadySharedAcceptResponseItemDVO, @@ -18,8 +29,10 @@ import { OutgoingRequestStatusChangedEvent, ProcessedIdentityAttributeQueryDVO, ProcessedIQLQueryDVO, + ProcessedRelationshipAttributeQueryDVO, ReadAttributeAcceptResponseItemDVO, ReadAttributeRequestItemDVO, + RelationshipAttributeQueryDVO, RequestMessageDVO, TransportServices } from "../../../src"; @@ -1179,3 +1192,208 @@ describe("AttributeAlreadySharedAcceptResponseItemDVO with IdentityAttributeQuer expect(requestItemDVO.response).toStrictEqual(responseItem); }); }); + +describe("ReadAttributeRequestItemDVO with RelationshipAttributeQuery", () => { + beforeAll(async () => { + const runtimeServices = await serviceProvider.launch(2, { enableRequestModule: true }); + runtimeServices1 = runtimeServices[0]; + runtimeServices2 = runtimeServices[1]; + transportServices1 = runtimeServices1.transport; + transportServices2 = runtimeServices2.transport; + expander1 = runtimeServices1.expander; + expander2 = runtimeServices2.expander; + consumptionServices1 = runtimeServices1.consumption; + consumptionServices2 = runtimeServices2.consumption; + eventBus1 = runtimeServices1.eventBus; + eventBus2 = runtimeServices2.eventBus; + + await establishRelationship(transportServices1, transportServices2); + address2 = (await transportServices2.account.getIdentityInfo()).value.address; + + requestContent = { + content: { + items: [ + ReadAttributeRequestItem.from({ + mustBeAccepted: true, + query: RelationshipAttributeQuery.from({ + key: "aKey", + owner: "", + attributeCreationHints: { + title: "aTitle", + valueType: "ProprietaryString", + confidentiality: RelationshipAttributeConfidentiality.Public + } + }) + }).toJSON() + ] + }, + peer: address2 + }; + + const relationshipAttribute: RelationshipAttributeJSON = { + "@type": "RelationshipAttribute", + value: { + "@type": "ProprietaryString", + value: "aString", + title: "aTitle" + }, + owner: address2, + key: "aKey", + confidentiality: RelationshipAttributeConfidentiality.Public + }; + + responseItems = [{ accept: true, newAttribute: relationshipAttribute } as AcceptReadAttributeRequestItemParametersWithNewAttributeJSON]; + }, 30000); + + beforeEach(async () => await cleanupAttributes([runtimeServices2])); + + test("check the MessageDVO for the sender", async () => { + const senderMessage = await sendMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await syncUntilHasMessageWithRequest(transportServices2, senderMessage.content.id!); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Open"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Open"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.mustBeAccepted).toBe(true); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("RelationshipAttributeQueryDVO"); + const relationshipAttributeQueryDVO = requestItemDVO.query as RelationshipAttributeQueryDVO; + expect(relationshipAttributeQueryDVO.owner.id).toBe(""); + }); + + test("check the MessageDVO for the recipient", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(dvo.request.status).toBe("DecisionRequired"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.DecisionRequired"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(true); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(true); + expect(requestItemDVO.mustBeAccepted).toBe(true); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("ProcessedRelationshipAttributeQueryDVO"); + const relationshipAttributeQueryDVO = requestItemDVO.query as ProcessedRelationshipAttributeQueryDVO; + expect(relationshipAttributeQueryDVO.owner.id).toBe(address2); + }); + + test("check the MessageDVO for the recipient after acceptance", async () => { + const recipientMessage = await exchangeMessageWithRequest(runtimeServices1, runtimeServices2, requestContent); + await eventBus2.waitForEvent(IncomingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.DecisionRequired); + const acceptResult = await consumptionServices2.incomingRequests.accept({ + requestId: recipientMessage.content.id!, + items: responseItems + }); + expect(acceptResult).toBeSuccessful(); + + const dto = recipientMessage; + const dvo = (await expander2.expandMessageDTO(recipientMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(false); + expect(dvo.request.status).toBe("Decided"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Decided"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.mustBeAccepted).toBe(true); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("RelationshipAttributeQueryDVO"); + const relationshipAttributeQueryDVO = requestItemDVO.query as ProcessedRelationshipAttributeQueryDVO; + expect(relationshipAttributeQueryDVO.owner.id).toBe(address2); + + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as ReadAttributeAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("ReadAttributeAcceptResponseItemDVO"); + expect(responseItem.attribute).toBeDefined(); + const recipientAddress = (await transportServices2.account.getIdentityInfo()).value.address; + expect(responseItem.attribute.owner).toBe(recipientAddress); + expect(responseItem.attribute.type).toBe("OwnRelationshipAttributeDVO"); + expect(responseItem.attribute.content.value["@type"]).toBe("ProprietaryString"); + expect((responseItem.attribute.content.value as GivenNameJSON).value).toBe("aString"); + expect(requestItemDVO.response).toStrictEqual(responseItem); + + await syncUntilHasMessageWithResponse(transportServices1, recipientMessage.content.id!); + await eventBus1.waitForEvent(OutgoingRequestStatusChangedEvent, (e) => e.data.newStatus === LocalRequestStatus.Completed); + }); + + test("check the MessageDVO for the sender after acceptance", async () => { + const senderMessage = await exchangeAndAcceptRequestByMessage(runtimeServices1, runtimeServices2, requestContent, responseItems); + const dto = senderMessage; + const dvo = (await expander1.expandMessageDTO(senderMessage)) as RequestMessageDVO; + expect(dvo).toBeDefined(); + expect(dvo.id).toBe(dto.id); + expect(dvo.name).toBe("i18n://dvo.message.name"); + expect(dvo.type).toBe("RequestMessageDVO"); + expect(dvo.date).toBe(dto.createdAt); + expect(dvo.request).toBeDefined(); + expect(dvo.request.isOwn).toBe(true); + expect(dvo.request.status).toBe("Completed"); + expect(dvo.request.statusText).toBe("i18n://dvo.localRequest.status.Completed"); + expect(dvo.request.type).toBe("LocalRequestDVO"); + expect(dvo.request.content.type).toBe("RequestDVO"); + expect(dvo.request.content.items).toHaveLength(1); + expect(dvo.request.isDecidable).toBe(false); + const requestItemDVO = dvo.request.content.items[0] as ReadAttributeRequestItemDVO; + expect(requestItemDVO.type).toBe("ReadAttributeRequestItemDVO"); + expect(requestItemDVO.isDecidable).toBe(false); + expect(requestItemDVO.mustBeAccepted).toBe(true); + expect(requestItemDVO.query).toBeDefined(); + expect(requestItemDVO.query.type).toBe("RelationshipAttributeQueryDVO"); + const relationshipAttributeQueryDVO = requestItemDVO.query as ProcessedRelationshipAttributeQueryDVO; + expect(relationshipAttributeQueryDVO.owner.id).toBe(""); + const response = dvo.request.response; + expect(response).toBeDefined(); + expect(response!.type).toBe("LocalResponseDVO"); + expect(response!.name).toBe("i18n://dvo.localResponse.name"); + expect(response!.date).toBeDefined(); + expect(response!.content.result).toBe("Accepted"); + expect(response!.content.items).toHaveLength(1); + const responseItem = response!.content.items[0] as ReadAttributeAcceptResponseItemDVO; + expect(responseItem.result).toBe("Accepted"); + expect(responseItem.type).toBe("ReadAttributeAcceptResponseItemDVO"); + expect(responseItem.attributeId).toBeDefined(); + expect(requestItemDVO.response).toStrictEqual(responseItem); + }); +}); From 529de19f6788a101fc7fb4f54b7bb8dd23310482 Mon Sep 17 00:00:00 2001 From: Milena-Czierlinski Date: Tue, 3 Feb 2026 16:39:17 +0100 Subject: [PATCH 3/3] fix: expandUnknown if Request is own --- packages/runtime/src/dataViews/DataViewExpander.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/dataViews/DataViewExpander.ts b/packages/runtime/src/dataViews/DataViewExpander.ts index 6a9066d47..5bbefe665 100644 --- a/packages/runtime/src/dataViews/DataViewExpander.ts +++ b/packages/runtime/src/dataViews/DataViewExpander.ts @@ -1233,14 +1233,14 @@ export class DataViewExpander { hints.valueHints = query.attributeCreationHints.valueHints; } - const emptyOwnerIsSelf = query.owner === "" && isOwnRequest === false; + const owner = query.owner !== "" ? await this.expandAddress(query.owner) : isOwnRequest ? this.expandUnknown("") : this.expandSelf(); return { type: "RelationshipAttributeQueryDVO", id: "", name, description, - owner: emptyOwnerIsSelf ? this.expandSelf() : await this.expandAddress(query.owner), + owner, key: query.key, attributeCreationHints: query.attributeCreationHints, renderHints: hints.renderHints,