diff --git a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java index 2510f9a07f5..4ef4df25abb 100644 --- a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java +++ b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java @@ -488,6 +488,7 @@ public Stream provideArguments(ExtensionContext extensionCo .counterPartyAddress("http://any") .consumerPid("consumerPid") .providerPid("providerPid") + .policy(Policy.Builder.newInstance().build()) .build(), PROVIDER, OFFERED), Arguments.of(verified, ContractAgreementVerificationMessage.Builder.newInstance() .protocol("protocol") diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImpl.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImpl.java index a60061e2827..08e465b77b4 100644 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImpl.java +++ b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImpl.java @@ -126,17 +126,15 @@ private boolean processRequesting(ContractNegotiation negotiation) { } /** - * Processes {@link ContractNegotiation} in state ACCEPTING. Tries to send a dummy contract agreement to - * the respective provider in order to approve the last offer sent by the provider. If this succeeds, the - * ContractNegotiation is transitioned to state ACCEPTED. Else, it is transitioned to ACCEPTING - * for a retry. + * Processes {@link ContractNegotiation} in state ACCEPTING. If the dispatch succeeds, the + * ContractNegotiation is transitioned to state ACCEPTED. Else, it is transitioned to ACCEPTING for a retry. * * @return true if processed, false otherwise */ @WithSpan private boolean processAccepting(ContractNegotiation negotiation) { var messageBuilder = ContractNegotiationEventMessage.Builder.newInstance().type(ACCEPTED); - + messageBuilder.policy(negotiation.getLastContractOffer().getPolicy()); return dispatch(messageBuilder, negotiation, Object.class) .onSuccess((n, result) -> transitionToAccepted(n)) .onFailure((n, throwable) -> transitionToAccepting(n)) diff --git a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java index a8f9bcf462f..4da57db523a 100644 --- a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java +++ b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java @@ -21,6 +21,7 @@ import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreementVerificationMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequest; @@ -210,12 +211,16 @@ void requesting_shouldSendMessageWithId_whenCorrelationIdIsNull_toSupportOldProt void accepting_shouldSendAcceptedMessageAndTransitionToApproved() { var negotiation = contractNegotiationBuilder().state(ACCEPTING.code()).contractOffer(contractOffer()).build(); when(store.nextNotLeased(anyInt(), stateIs(ACCEPTING.code()))).thenReturn(List.of(negotiation)).thenReturn(emptyList()); - when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); + + var captor = ArgumentCaptor.forClass(ContractNegotiationEventMessage.class); + when(dispatcherRegistry.dispatch(any(), captor.capture())).thenReturn(completedFuture(StatusResult.success("any"))); when(store.findById(negotiation.getId())).thenReturn(negotiation); manager.start(); await().untilAsserted(() -> { + var message = captor.getValue(); + assertThat(message.getPolicy()).isNotNull(); verify(store).save(argThat(p -> p.getState() == ACCEPTED.code())); verify(dispatcherRegistry, only()).dispatch(any(), any()); verify(listener).accepted(any()); diff --git a/core/control-plane/control-plane-transform/src/main/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformer.java b/core/control-plane/control-plane-transform/src/main/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformer.java index 9c62d5286be..6ffb55e2117 100644 --- a/core/control-plane/control-plane-transform/src/main/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformer.java +++ b/core/control-plane/control-plane-transform/src/main/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformer.java @@ -85,6 +85,22 @@ public JsonObjectToPolicyTransformer(ParticipantIdMapper participantIdMapper) { .error("Invalid type for ODRL policy, should be one of [%s, %s, %s]".formatted(ODRL_POLICY_TYPE_SET, ODRL_POLICY_TYPE_OFFER, ODRL_POLICY_TYPE_AGREEMENT)) .report(); return null; + } else if (policyType == PolicyType.CONTRACT) { + if (object.get(ODRL_ASSIGNEE_ATTRIBUTE) == null) { + context.problem() + .missingProperty() + .property(ODRL_ASSIGNEE_ATTRIBUTE) + .report(); + return null; + } + + if (object.get(ODRL_ASSIGNER_ATTRIBUTE) == null) { + context.problem() + .missingProperty() + .property(ODRL_ASSIGNER_ATTRIBUTE) + .report(); + return null; + } } builder.type(policyType); @@ -94,8 +110,10 @@ public JsonObjectToPolicyTransformer(ParticipantIdMapper participantIdMapper) { case ODRL_PROHIBITION_ATTRIBUTE -> v -> builder.prohibitions(transformArray(v, Prohibition.class, context)); case ODRL_OBLIGATION_ATTRIBUTE -> v -> builder.duties(transformArray(v, Duty.class, context)); case ODRL_TARGET_ATTRIBUTE -> v -> builder.target(transformString(v, context)); - case ODRL_ASSIGNER_ATTRIBUTE -> v -> builder.assigner(participantIdMapper.fromIri(transformString(v, context))); - case ODRL_ASSIGNEE_ATTRIBUTE -> v -> builder.assignee(participantIdMapper.fromIri(transformString(v, context))); + case ODRL_ASSIGNER_ATTRIBUTE -> + v -> builder.assigner(participantIdMapper.fromIri(transformString(v, context))); + case ODRL_ASSIGNEE_ATTRIBUTE -> + v -> builder.assignee(participantIdMapper.fromIri(transformString(v, context))); case ODRL_PROFILE_ATTRIBUTE -> v -> builder.profiles(transformProfile(v)); default -> v -> builder.extensibleProperty(key, transformGenericProperty(v, context)); }); diff --git a/core/control-plane/control-plane-transform/src/test/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformerTest.java b/core/control-plane/control-plane-transform/src/test/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformerTest.java index b4471a381c0..d816dc65737 100644 --- a/core/control-plane/control-plane-transform/src/test/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformerTest.java +++ b/core/control-plane/control-plane-transform/src/test/java/org/eclipse/edc/connector/controlplane/transform/odrl/to/JsonObjectToPolicyTransformerTest.java @@ -151,6 +151,8 @@ void transform_differentPolicyTypes_returnPolicy(String type, PolicyType policyT var policy = jsonFactory.createObjectBuilder() .add(CONTEXT, JsonObject.EMPTY_JSON_OBJECT) .add(TYPE, type) + .add(ODRL_ASSIGNEE_ATTRIBUTE, "assignee") + .add(ODRL_ASSIGNER_ATTRIBUTE, "assigner") .build(); var result = transformer.transform(TestInput.getExpanded(policy), context); @@ -179,6 +181,8 @@ void shouldGetTypeFromContext_whenSet() { var policy = jsonFactory.createObjectBuilder() .add(ODRL_TARGET_ATTRIBUTE, TARGET) + .add(ODRL_ASSIGNEE_ATTRIBUTE, "assignee") + .add(ODRL_ASSIGNER_ATTRIBUTE, "assigner") .build(); var result = transformer.transform(TestInput.getExpanded(policy), context); diff --git a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/dispatcher/DspHttpRemoteMessageDispatcherImpl.java b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/dispatcher/DspHttpRemoteMessageDispatcherImpl.java index b5138a4a124..a84bf6fd6ed 100644 --- a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/dispatcher/DspHttpRemoteMessageDispatcherImpl.java +++ b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/dispatcher/DspHttpRemoteMessageDispatcherImpl.java @@ -152,7 +152,7 @@ private StatusResult handleResponse(Response response, String protocol, C } else { var stringBody = Optional.ofNullable(responseBody) .map(this::asString) - .orElse("Response body is null"); + .orElse("Response body is null. Error code: " + response.code()); var status = response.code() >= 400 && response.code() < 500 ? FATAL_ERROR : ERROR_RETRY; diff --git a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/message/DspRequestHandlerImpl.java b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/message/DspRequestHandlerImpl.java index ff6e7f9a574..067c0f4d82d 100644 --- a/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/message/DspRequestHandlerImpl.java +++ b/data-protocols/dsp/dsp-http-core/src/main/java/org/eclipse/edc/protocol/dsp/http/message/DspRequestHandlerImpl.java @@ -189,7 +189,7 @@ public Response updateResource(PostDspRequest }); if (inputTransformation.failed()) { - monitor.debug(() -> "DSP: Transformation failed: %s".formatted(validation.getFailureMessages())); + monitor.debug(() -> "DSP: Transformation failed: %s".formatted(inputTransformation.getFailureMessages())); return type(request.getErrorType()).processId(request.getProcessId()).badRequest(); } diff --git a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-transform/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/transform/from/JsonObjectFromContractNegotiationEventMessageTransformerTest.java b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-transform/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/transform/from/JsonObjectFromContractNegotiationEventMessageTransformerTest.java index 9b7c00600c3..59b60bafa2e 100644 --- a/data-protocols/dsp/dsp-negotiation/dsp-negotiation-transform/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/transform/from/JsonObjectFromContractNegotiationEventMessageTransformerTest.java +++ b/data-protocols/dsp/dsp-negotiation/dsp-negotiation-transform/src/test/java/org/eclipse/edc/protocol/dsp/negotiation/transform/from/JsonObjectFromContractNegotiationEventMessageTransformerTest.java @@ -17,6 +17,7 @@ import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractNegotiationEventMessage; +import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.transform.spi.TransformerContext; import org.junit.jupiter.api.Test; @@ -52,6 +53,7 @@ void transform() { .consumerPid("consumerPid") .providerPid("providerPid") .counterPartyAddress("https://test.com") + .policy(Policy.Builder.newInstance().build()) .type(ACCEPTED) .build(); diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiation.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiation.java index 84d77ae7cdd..53f350b4969 100644 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiation.java +++ b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiation.java @@ -38,6 +38,7 @@ import static java.lang.String.format; import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation.Type.PROVIDER; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.FINALIZED; import static org.eclipse.edc.spi.constants.CoreConstants.EDC_NAMESPACE; /** @@ -190,7 +191,7 @@ public void transitionRequesting() { if (Type.PROVIDER == type) { throw new IllegalStateException("Provider processes have no REQUESTING state"); } - transition(ContractNegotiationStates.REQUESTING, ContractNegotiationStates.REQUESTING, ContractNegotiationStates.INITIAL); + transition(ContractNegotiationStates.REQUESTING, ContractNegotiationStates.REQUESTING, ContractNegotiationStates.OFFERED, ContractNegotiationStates.INITIAL); } /** @@ -209,7 +210,7 @@ public void transitionRequested() { */ public void transitionOffering() { if (CONSUMER == type) { - throw new IllegalStateException("Provider processes have no OFFERING state"); + throw new IllegalStateException("Consumer processes have no OFFERING state"); } transition(ContractNegotiationStates.OFFERING, ContractNegotiationStates.OFFERING, ContractNegotiationStates.OFFERED, ContractNegotiationStates.REQUESTED); @@ -233,7 +234,7 @@ public void transitionAccepting() { if (Type.PROVIDER == type) { throw new IllegalStateException("Provider processes have no ACCEPTING state"); } - transition(ContractNegotiationStates.ACCEPTING, ContractNegotiationStates.ACCEPTING, ContractNegotiationStates.REQUESTED); + transition(ContractNegotiationStates.ACCEPTING, ContractNegotiationStates.ACCEPTING, ContractNegotiationStates.REQUESTED, ContractNegotiationStates.OFFERED); } /** @@ -301,7 +302,7 @@ public void transitionFinalizing() { * Transition to state FINALIZED. */ public void transitionFinalized() { - transition(ContractNegotiationStates.FINALIZED, ContractNegotiationStates.FINALIZED, ContractNegotiationStates.FINALIZING, ContractNegotiationStates.AGREED, ContractNegotiationStates.VERIFIED); + transition(FINALIZED, FINALIZED, ContractNegotiationStates.FINALIZING, ContractNegotiationStates.AGREED, ContractNegotiationStates.VERIFIED); } /** @@ -310,7 +311,7 @@ public void transitionFinalized() { * @return true if the negotiation can be terminated, false otherwise */ public boolean canBeTerminated() { - return true; + return FINALIZED.code() != state; } /** diff --git a/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiationTest.java b/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiationTest.java new file mode 100644 index 00000000000..97c6407bc91 --- /dev/null +++ b/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/controlplane/contract/spi/types/negotiation/ContractNegotiationTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation.Type.PROVIDER; + +public class ContractNegotiationTest { + + @Test + void verify_consumerComplete() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionAccepting(); + negotiation.transitionAccepted(); + negotiation.transitionAgreed(); + negotiation.transitionVerifying(); + negotiation.transitionVerified(); + negotiation.transitionFinalized(); + } + + @Test + void verify_consumerRequestedAgreed() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionAgreed(); + } + + @Test + void verify_consumerRequestedTerminated() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionTerminated(); + } + + @Test + void verify_consumerOfferedTerminated() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionTerminated(); + } + + @Test + void verify_consumerAcceptedTerminated() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionAccepting(); + negotiation.transitionAccepted(); + negotiation.transitionTerminated(); + } + + @Test + void verify_consumerAgreedTerminated() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionAccepting(); + negotiation.transitionAccepted(); + negotiation.transitionAgreed(); + negotiation.transitionTerminated(); + } + + @Test + void verify_consumerVerifiedTerminated() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionAccepting(); + negotiation.transitionAccepted(); + negotiation.transitionAgreed(); + negotiation.transitionVerifying(); + negotiation.transitionVerified(); + negotiation.transitionTerminated(); + } + + @Test + void verify_consumerFinalizedTerminal() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionAgreed(); + negotiation.transitionVerifying(); + negotiation.transitionVerified(); + negotiation.transitionFinalized(); + assertThatThrownBy(negotiation::transitionTerminated).isInstanceOf(IllegalStateException.class); + } + + @Test + void verify_consumerCounterOffer() { + var negotiation = createNegotiation(CONSUMER); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + negotiation.transitionRequesting(); + negotiation.transitionRequested(); + negotiation.transitionOffered(); + } + + @Test + void verify_providerRequetedAgreedComplete() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionAgreeing(); + negotiation.transitionAgreed(); + negotiation.transitionVerified(); + negotiation.transitionFinalizing(); + negotiation.transitionFinalized(); + assertThatThrownBy(negotiation::transitionTerminated).isInstanceOf(IllegalStateException.class); + } + + @Test + void verify_providerComplete() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionOffering(); + negotiation.transitionOffered(); + negotiation.transitionAccepted(); + negotiation.transitionAgreeing(); + negotiation.transitionAgreed(); + negotiation.transitionVerified(); + negotiation.transitionFinalizing(); + negotiation.transitionFinalized(); + assertThatThrownBy(negotiation::transitionTerminated).isInstanceOf(IllegalStateException.class); + } + + @Test + void verify_providerRequestedTerminated() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionTerminated(); + } + + @Test + void verify_providerOfferedTerminated() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionOffering(); + negotiation.transitionOffered(); + negotiation.transitionTerminated(); + } + + @Test + void verify_providerAcceptedTerminated() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionTerminated(); + } + + @Test + void verify_providerAgreedTerminated() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionOffering(); + negotiation.transitionOffered(); + negotiation.transitionAccepted(); + negotiation.transitionAgreeing(); + negotiation.transitionAgreed(); + negotiation.transitionTerminated(); + } + + @Test + void verify_providerVerifiedTerminated() { + var negotiation = createNegotiation(PROVIDER); + negotiation.transitionRequested(); + negotiation.transitionOffering(); + negotiation.transitionOffered(); + negotiation.transitionAccepted(); + negotiation.transitionAgreeing(); + negotiation.transitionAgreed(); + negotiation.transitionVerified(); + negotiation.transitionTerminated(); + } + + private ContractNegotiation createNegotiation(ContractNegotiation.Type type) { + return ContractNegotiation.Builder.newInstance() + .counterPartyId("counterpartyId") + .counterPartyAddress("https://test.com") + .type(type) + .protocol("test") + .build(); + } +}