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 0a192b2aa99..b6553e3d79a 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 @@ -95,6 +95,7 @@ public Response createResou var token = request.getToken(); if (token == null) { + monitor.severe("DSP: No auth token provided - returning 401"); return unauthorized(request); } diff --git a/extensions/tck-extension/build.gradle.kts b/extensions/tck-extension/build.gradle.kts new file mode 100644 index 00000000000..782c40c3389 --- /dev/null +++ b/extensions/tck-extension/build.gradle.kts @@ -0,0 +1,33 @@ +/* + * 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 + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(project(":spi:common:core-spi")) + implementation(project(":spi:control-plane:contract-spi")) + implementation(project(":spi:control-plane:asset-spi")) + implementation(project(":spi:control-plane:control-plane-spi")) + implementation(project(":spi:common:web-spi")) + implementation(libs.jakarta.rsApi) +} + +// If the EDC Build Plugin is used, every module gets visited during Publishing by default. +// Single modules can be excluded by setting the "publish" flag to false: + +edcBuild { + publish.set(false) +} \ No newline at end of file diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java new file mode 100644 index 00000000000..4bbd6291c76 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Initiates a negotiation request + */ +public record ContractNegotiationRequest(@JsonProperty("providerId") String providerId, + @JsonProperty("connectorAddress") String connectorAddress, + @JsonProperty("offerId") String offerId) { +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java new file mode 100644 index 00000000000..1c9162a3950 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java @@ -0,0 +1,52 @@ +package org.eclipse.edc.tck.dsp.controller; + +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebServer; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer; +import org.eclipse.edc.web.spi.configuration.WebServiceSettings; + +/** + * Bootstraps the TCK web hook. + */ +public class TckControllerExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Controller"; + private static final String PROTOCOL = "tck"; + + private static final WebServiceSettings SETTINGS = WebServiceSettings.Builder.newInstance() + .apiConfigKey(PROTOCOL) + .contextAlias("tck") + .defaultPath("/tck") + .defaultPort(8687) + .useDefaultContext(false) + .name("Tck API") + .build(); + + + @Inject + private WebServiceConfigurer configurator; + + @Inject + private WebService webService; + + @Inject + private WebServer webServer; + + @Inject + private ContractNegotiationService negotiationService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var config = context.getConfig(PROTOCOL); + configurator.configure(config, webServer, SETTINGS); + webService.registerResource(PROTOCOL, new TckWebhookController(negotiationService)); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java new file mode 100644 index 00000000000..8067289ca8a --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.controller; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequest; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.types.domain.callback.CallbackAddress; + +import java.util.List; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +/** + * Implements TCK web hooks. + */ +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/negotiations") +public class TckWebhookController { + + private ContractNegotiationService negotiationService; + + public TckWebhookController(ContractNegotiationService negotiationService) { + this.negotiationService = negotiationService; + } + + @POST + @Path("requests") + public void startNegotiation(ContractNegotiationRequest request) { + + var contractOffer = ContractOffer.Builder.newInstance() + .id(request.offerId()) + .assetId(request.offerId()) + .policy(Policy.Builder.newInstance().assigner(request.providerId()).build()) + .build(); + var contractRequest = ContractRequest.Builder.newInstance() + .callbackAddresses(List.of(CallbackAddress.Builder.newInstance().uri(request.connectorAddress()).build())) + .counterPartyAddress(request.connectorAddress()) + .contractOffer(contractOffer) + .protocol("dataspace-protocol-http") + .build(); + negotiationService.initiateNegotiation(contractRequest); + System.out.println("Negotiation"); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java new file mode 100644 index 00000000000..3c77f079c47 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.data; + +import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationAccepted; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationAgreed; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationOffered; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.tck.dsp.guard.Trigger; +import org.eclipse.edc.tck.dsp.recorder.StepRecorder; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import static java.util.stream.Collectors.toSet; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTED; + +/** + * Assembles data for the TCK scenarios. + */ +public class DataAssembly { + private static final Set ASSET_IDS = Set.of("ACN0101", "ACN0102", "ACN0103", "ACN0104", + "ACN0201", "ACN0202", "ACN0203", "ACN0204", "ACN0205", "ACN0206", "ACN0207", + "ACN0301", "ACN0302", "ACN0303", "ACN0304"); + + private static final String POLICY_ID = "P123"; + private static final String CONTRACT_DEFINITION_ID = "CD123"; + + public static Set createAssets() { + return ASSET_IDS.stream().map(DataAssembly::createAsset).collect(toSet()); + } + + public static Set createPolicyDefinitions() { + return Set.of(PolicyDefinition.Builder.newInstance() + .id(POLICY_ID) + .policy(Policy.Builder.newInstance().build()) + .build()); + } + + public static Set createContractDefinitions() { + return Set.of(ContractDefinition.Builder.newInstance() + .id(CONTRACT_DEFINITION_ID) + .accessPolicyId(POLICY_ID) + .contractPolicyId(POLICY_ID) + .build()); + } + + public static StepRecorder createNegotiationRecorder() { + var recorder = new StepRecorder(); + + record01NegotiationSequences(recorder); + record02NegotiationSequences(recorder); + record03NegotiationSequences(recorder); + + recordC01NegotiationSequences(recorder); + + return recorder.repeat(); + } + + private static void recordC01NegotiationSequences(StepRecorder recorder) { + } + + private static void record01NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0101", ContractNegotiation::transitionOffering); + + recorder.record("ACN0102", ContractNegotiation::transitionOffering) + .record("ACN0102", ContractNegotiation::transitionTerminating); + + recorder.record("ACN0103", ContractNegotiation::transitionOffering) + .record("ACN0103", ContractNegotiation::transitionAgreeing) + .record("ACN0103", ContractNegotiation::transitionFinalizing); + + recorder.record("ACN0104", ContractNegotiation::transitionAgreeing) + .record("ACN0104", ContractNegotiation::transitionFinalizing); + } + + private static void record02NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0201", ContractNegotiation::transitionTerminating); + + recorder.record("ACN0202", ContractNegotiation::transitionRequested); + + recorder.record("ACN0203", ContractNegotiation::transitionAgreeing); + + recorder.record("ACN0204", ContractNegotiation::transitionOffering); + + recorder.record("ACN0205", ContractNegotiation::transitionOffering); + + recorder.record("ACN0206", contractNegotiation -> { + // only transition if in requested + if (contractNegotiation.getState() == REQUESTED.code()) { + contractNegotiation.transitionOffering(); + } + }); + + recorder.record("ACN0207", ContractNegotiation::transitionAgreeing) + .record("ACN0207", ContractNegotiation::transitionTerminating); + } + + private static void record03NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0301", ContractNegotiation::transitionAgreeing) + .record("ACN0301", ContractNegotiation::transitionFinalizing); + + recorder.record("ACN0302", ContractNegotiation::transitionOffering); + recorder.record("ACN0303", ContractNegotiation::transitionOffering) + .record("ACN0303", ContractNegotiation::transitionAccepting); + + recorder.record("ACN0304", ContractNegotiation::transitionOffering); + } + + public static List> createNegotiationTriggers() { + return List.of( + createTrigger(ContractNegotiationOffered.class, "ACN0205", ContractNegotiation::transitionTerminating), + createTrigger(ContractNegotiationAccepted.class, "ACN0206", ContractNegotiation::transitionTerminating), + createTrigger(ContractNegotiationOffered.class, "C0101", contractNegotiation -> { + contractNegotiation.transitionAccepting(); + contractNegotiation.setPending(false); + }), + createTrigger(ContractNegotiationAgreed.class, "C0101", contractNegotiation -> { + contractNegotiation.transitionVerifying(); + contractNegotiation.setPending(false); + }) + + ); + } + + private static Trigger createTrigger(Class type, + String assetId, + Consumer action) { + return new Trigger<>(event -> { + if (event.getClass().equals(type)) { + return assetId.equals(((ContractNegotiationEvent) event).getLastContractOffer().getAssetId()); + } + return false; + }, action); + } + + private DataAssembly() { + } + + private static Asset createAsset(String id) { + return Asset.Builder.newInstance() + .id(id) + .dataAddress(DataAddress.Builder.newInstance().type("HTTP").build()) + .build(); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java new file mode 100644 index 00000000000..544af77b272 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.ContractNegotiationPendingGuard; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.spi.persistence.StateEntityStore; + +import java.util.Set; +import java.util.function.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.ACCEPTING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.AGREEING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.FINALIZING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.INITIAL; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.OFFERING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.TERMINATING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.VERIFYING; + +/** + * Contract negotiation guard for TCK testcases. + */ +public class ContractNegotiationGuard extends DelayedActionGuard implements ContractNegotiationPendingGuard { + // the states to not apply the guard to - i.e. to allow automatic transitions by the contract negotiation manager + private static final Set PROVIDER_AUTOMATIC_STATES = Set.of( + OFFERING.code(), + AGREEING.code(), + TERMINATING.code(), + FINALIZING.code()); + + private static final Set CONSUMER_AUTOMATIC_STATES = Set.of( + INITIAL.code(), + REQUESTING.code(), + ACCEPTING.code(), + VERIFYING.code() + ); + + public ContractNegotiationGuard(Consumer action, StateEntityStore store) { + super(cn -> cn.getType() == PROVIDER ? + !PROVIDER_AUTOMATIC_STATES.contains(cn.getState()) : !CONSUMER_AUTOMATIC_STATES.contains(cn.getState()), action, store); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java new file mode 100644 index 00000000000..d7e5e910fc6 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; + +/** + * Registers contract negotiation triggers. + */ +public interface ContractNegotiationTriggerRegistry { + + void register(Trigger trigger); + +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java new file mode 100644 index 00000000000..3d1dbea1c52 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.spi.event.Event; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventSubscriber; +import org.eclipse.edc.spi.persistence.StateEntityStore; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fires triggers based on negotiation events. + */ +public class ContractNegotiationTriggerSubscriber implements EventSubscriber, ContractNegotiationTriggerRegistry { + private StateEntityStore store; + + private List> triggers = new ArrayList<>(); + + public ContractNegotiationTriggerSubscriber(StateEntityStore store) { + this.store = store; + } + + @Override + public void register(Trigger trigger) { + triggers.add(trigger); + } + + @Override + public void on(EventEnvelope envelope) { + triggers.stream() + .filter(trigger -> trigger.predicate().test(envelope.getPayload())) + .forEach(trigger -> { + var event = (ContractNegotiationEvent) envelope.getPayload(); + var negotiation = store.findByIdAndLease(event.getContractNegotiationId()).getContent(); + trigger.action().accept(negotiation); + store.save(negotiation); + }); + } + +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java new file mode 100644 index 00000000000..838350bf031 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import org.eclipse.edc.spi.entity.PendingGuard; +import org.eclipse.edc.spi.entity.StatefulEntity; +import org.eclipse.edc.spi.persistence.StateEntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * A guard that performs actions on a stateful entity. + *

+ * Note this implementation is not safe to use in a clustered environment since transitions are not performed in the context of + * a command handler. + */ +public class DelayedActionGuard> implements PendingGuard { + private final Predicate filter; + private final Consumer action; + private final StateEntityStore store; + private final DelayQueue queue; + private final AtomicBoolean active = new AtomicBoolean(); + + private final ExecutorService executor = Executors.newFixedThreadPool(1); + + public DelayedActionGuard(Predicate filter, + Consumer action, + StateEntityStore store) { + this.filter = filter; + this.action = action; + this.store = store; + queue = new DelayQueue<>(); + } + + public void start() { + active.set(true); + executor.submit(() -> { + while (active.get()) { + try { + var entry = queue.poll(10, MILLISECONDS); + if (entry != null) { + action.accept(entry.entity); + entry.entity.setPending(false); + store.save(entry.entity); + } + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.interrupted(); + break; + } + } + }); + } + + public void stop() { + active.set(false); + } + + @Override + public boolean test(T entity) { + if (filter.test(entity)) { + queue.put(new GuardDelay(entity)); + return true; + } + return false; + } + + protected class GuardDelay implements Delayed { + T entity; + private final long start; + + GuardDelay(T entity) { + this.entity = entity; + start = System.currentTimeMillis(); + } + + @Override + public int compareTo(@NotNull Delayed delayed) { + var millis = getDelay(MILLISECONDS) - delayed.getDelay(MILLISECONDS); + millis = Math.min(millis, 1); + millis = Math.max(millis, -1); + return (int) millis; + } + + @Override + public long getDelay(@NotNull TimeUnit timeUnit) { + return timeUnit.convert(500 - (System.currentTimeMillis() - start), MILLISECONDS); + } + + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java new file mode 100644 index 00000000000..b5779b829d3 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.ContractNegotiationPendingGuard; +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createNegotiationRecorder; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createNegotiationTriggers; + +/** + * Loads the transition guard. + */ +public class TckGuardExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Guard"; + + private ContractNegotiationGuard negotiationGuard; + + @Inject + private ContractNegotiationStore store; + + @Inject + private EventRouter router; + + @Override + public String name() { + return NAME; + } + + @Provider + public ContractNegotiationPendingGuard negotiationGuard() { + var recorder = createNegotiationRecorder(); + + var registry = new ContractNegotiationTriggerSubscriber(store); + createNegotiationTriggers().forEach(registry::register); + router.register(ContractNegotiationEvent.class, registry); + + negotiationGuard = new ContractNegotiationGuard(cn -> recorder.playNext(cn.getContractOffers().get(0).getAssetId(), cn), store); + return negotiationGuard; + } + + @Override + public void prepare() { + if (negotiationGuard != null) { + negotiationGuard.start(); + } + } + + @Override + public void shutdown() { + if (negotiationGuard != null) { + negotiationGuard.stop(); + } + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java new file mode 100644 index 00000000000..5aa8ebe3124 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.guard; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An action that is triggered when the predicate matches a condition. + */ +public record Trigger(Predicate predicate, Consumer action) { +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java new file mode 100644 index 00000000000..f6b51f93316 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.identity; + +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.iam.VerificationContext; +import org.eclipse.edc.spi.result.Result; + +/** + * No-op service + */ +public class NoopIdentityService implements IdentityService { + private static final String TCK_PARTICIPANT_ID = "TCK_PARTICIPANT"; // the official TCK id + + @Override + public Result obtainClientCredentials(TokenParameters tokenParameters) { + return Result.success(TokenRepresentation.Builder.newInstance().token("1234").expiresIn(Long.MAX_VALUE).build()); + } + + @Override + public Result verifyJwtToken(TokenRepresentation tokenRepresentation, VerificationContext verificationContext) { + return Result.success(ClaimToken.Builder.newInstance().claim("client_id", TCK_PARTICIPANT_ID).build()); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java new file mode 100644 index 00000000000..32ce17d20e1 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.identity; + +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.iam.AudienceResolver; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.system.ServiceExtension; + +/** + * Loads a no-op identity service. + */ +public class TckIdentityExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Identity"; + + @Override + public String name() { + return NAME; + } + + @Provider + public IdentityService identityService() { + return new NoopIdentityService(); + } + + @Provider + public AudienceResolver audienceResolver() { + return m -> Result.success(m.getCounterPartyId()); + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java new file mode 100644 index 00000000000..0a29567c21e --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc + * + * 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: + * Metaform Systems, Inc - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.recorder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Records and plays a sequence of steps. Sequences may be repeated if {@link #repeat()} is enabled. + */ +public class StepRecorder { + private boolean repeat; + private int playIndex = 0; + private Map>> sequences = new HashMap<>(); + + public synchronized StepRecorder playNext(String key, T entity) { + var sequence = sequences.get(key); + if (sequence == null) { + throw new AssertionError("No sequence found for key " + key); + } + if (sequence.isEmpty()) { + throw new IllegalStateException("No replay steps"); + } + if (playIndex >= sequence.size()) { + throw new IllegalStateException("Exceeded replay steps"); + } + sequence.get(playIndex).accept(entity); + if (repeat && playIndex == sequence.size() - 1) { + playIndex = 0; + } else { + playIndex++; + } + return this; + } + + public synchronized StepRecorder record(String key, Consumer step) { + var sequence = sequences.computeIfAbsent(key, k -> new ArrayList<>()); + sequence.add(step); + return this; + } + + public synchronized StepRecorder repeat() { + repeat = true; + return this; + } +} diff --git a/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java new file mode 100644 index 00000000000..44259c598c2 --- /dev/null +++ b/extensions/tck-extension/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java @@ -0,0 +1,57 @@ +/* + * 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.tck.dsp.setup; + +import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; +import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createAssets; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createContractDefinitions; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createPolicyDefinitions; +import static org.eclipse.edc.tck.dsp.setup.TckSetupExtension.NAME; + +/** + * Loads customizations and seed data for the TCK. + */ +@Extension(NAME) +public class TckSetupExtension implements ServiceExtension { + public static final String NAME = "DSP TCK Setup"; + + @Inject + private AssetIndex assetIndex; + + @Inject + private PolicyDefinitionService policyDefinitionService; + + @Inject + private ContractDefinitionService contractDefinitionService; + + @Override + public String name() { + return NAME; + } + + @Override + public void prepare() { + createAssets().forEach(asset -> assetIndex.create(asset)); + createPolicyDefinitions().forEach(definition -> policyDefinitionService.create(definition)); + createContractDefinitions().forEach(definition -> contractDefinitionService.create(definition)); + } + +} diff --git a/extensions/tck-extension/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/tck-extension/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000000..84672e50a18 --- /dev/null +++ b/extensions/tck-extension/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,17 @@ +# +# 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 +# +# +org.eclipse.edc.tck.dsp.guard.TckGuardExtension +org.eclipse.edc.tck.dsp.setup.TckSetupExtension +org.eclipse.edc.tck.dsp.identity.TckIdentityExtension +org.eclipse.edc.tck.dsp.controller.TckControllerExtension \ No newline at end of file diff --git a/extensions/tck-extension/src/test/java/org/eclipse/edc/tck/dsp/recorder/StepRecorderTest.java b/extensions/tck-extension/src/test/java/org/eclipse/edc/tck/dsp/recorder/StepRecorderTest.java new file mode 100644 index 00000000000..e6fab18c3df --- /dev/null +++ b/extensions/tck-extension/src/test/java/org/eclipse/edc/tck/dsp/recorder/StepRecorderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc + * + * 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: + * Metaform Systems, Inc - initial API and implementation + * + */ + +package org.eclipse.edc.tck.dsp.recorder; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class StepRecorderTest { + private static final String KEY = "test"; + + @Test + void verifyRecordReplay() { + var recorder = new StepRecorder(); + var counter = new int[1]; + recorder.record(KEY, i -> assertThat(counter[0]++).isEqualTo(0)); + recorder.record(KEY, i -> assertThat(counter[0]++).isEqualTo(1)); + + recorder.playNext(KEY, null); + recorder.playNext(KEY, null); + assertThat(counter[0]).isEqualTo(2); + } + + @Test + void verifyRepeat() { + var recorder = new StepRecorder(); + var counter = new int[1]; + recorder.record(KEY, i -> counter[0]++); + recorder.repeat(); + + for (int i = 0; i < 4; i++) { + recorder.playNext(KEY, null); + } + assertThat(counter[0]).isEqualTo(4); + } + + @Test + void verifyNoRepeat() { + var recorder = new StepRecorder(); + var counter = new int[1]; + recorder.record(KEY, i -> counter[0]++); + + recorder.playNext(KEY, null); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> recorder.playNext(KEY, null)); + assertThat(counter[0]).isEqualTo(1); + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0852091a239..b8de6ee48fe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -221,6 +221,7 @@ include(":extensions:data-plane-selector:store:sql:data-plane-instance-store-sql include(":extensions:policy-monitor:store:sql:policy-monitor-store-sql") +include(":extensions:tck-extension") // modules for launchers, i.e. runnable compositions of the app ------------------------------------ include(":launchers:dpf-selector") @@ -291,6 +292,8 @@ include(":system-tests:sts-api:sts-api-test-runtime") include(":system-tests:telemetry:telemetry-test-runner") include(":system-tests:telemetry:telemetry-test-runtime") include(":system-tests:bom-tests") +include(":system-tests:dsp-compatibility-tests:connector-under-test") +include(":system-tests:dsp-compatibility-tests:compatibility-test-runner") // BOM modules ---------------------------------------------------------------- include(":dist:bom:controlplane-base-bom") diff --git a/system-tests/dsp-compatibility-tests/compatibility-test-runner/build.gradle.kts b/system-tests/dsp-compatibility-tests/compatibility-test-runner/build.gradle.kts new file mode 100644 index 00000000000..32548fe1d78 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/compatibility-test-runner/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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 + * + */ + +plugins { + java +} + +dependencies { + testImplementation(project(":core:common:junit")) + testImplementation(libs.assertj) + testImplementation(libs.awaitility) +} + +edcBuild { + publish.set(false) +} diff --git a/system-tests/dsp-compatibility-tests/compatibility-test-runner/src/test/java/org/eclipse/edc/tck/dsp/EdcCompatibilityTest.java b/system-tests/dsp-compatibility-tests/compatibility-test-runner/src/test/java/org/eclipse/edc/tck/dsp/EdcCompatibilityTest.java new file mode 100644 index 00000000000..5de8758d308 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/compatibility-test-runner/src/test/java/org/eclipse/edc/tck/dsp/EdcCompatibilityTest.java @@ -0,0 +1,60 @@ +/* + * 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.tck.dsp; + +import org.eclipse.edc.junit.extensions.EmbeddedRuntime; +import org.eclipse.edc.junit.extensions.RuntimeExtension; +import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; + +import static org.eclipse.edc.util.io.Ports.getFreePort; + +public class EdcCompatibilityTest { + + @RegisterExtension + protected RuntimeExtension runtime = + new RuntimePerClassExtension(new EmbeddedRuntime("CUnT", + new HashMap<>() { + { + put("edc.participant.id", "CONNECTOR_UNDER_TEST"); + put("web.http.port", "8080"); + put("web.http.path", "/api"); + put("web.http.version.port", String.valueOf(getFreePort())); + put("web.http.version.path", "/api/version"); + put("web.http.control.port", String.valueOf(getFreePort())); + put("web.http.control.path", "/api/control"); + put("web.http.management.port", "8081"); + put("web.http.management.path", "/api/management"); + put("web.http.protocol.port", "8282"); + put("web.http.protocol.path", "/api/v1/dsp"); // expected by TCK + put("web.api.auth.key", "password"); + put("edc.dsp.callback.address", "http://localhost:8282/api/v1/dsp"); + put("edc.management.context.enabled", "true"); + } + }, + ":system-tests:dsp-compatibility-tests:connector-under-test" + )); + + @Test + void assertRuntimeReady() throws InterruptedException { + var l = new CountDownLatch(1); + l.await(); + } +} + diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/build.gradle.kts b/system-tests/dsp-compatibility-tests/connector-under-test/build.gradle.kts new file mode 100644 index 00000000000..569bab553c6 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * 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 + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":dist:bom:controlplane-base-bom")) + runtimeOnly(project(":extensions:tck-extension")) +} \ No newline at end of file diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java new file mode 100644 index 00000000000..6a0f4173167 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/ContractNegotiationRequest.java @@ -0,0 +1,25 @@ +/* + * 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.tck.dsp.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Initiates a negotiation request + */ +public record ContractNegotiationRequest(@JsonProperty("providerId") String providerId, + @JsonProperty("connectorAddress") String connectorAddress, + @JsonProperty("offerId") String offerId) { +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java new file mode 100644 index 00000000000..c4506b2b0f7 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckControllerExtension.java @@ -0,0 +1,66 @@ +/* + * 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.tck.dsp.controller; + +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebServer; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.edc.web.spi.configuration.WebServiceConfigurer; +import org.eclipse.edc.web.spi.configuration.WebServiceSettings; + +/** + * Bootstraps the TCK web hook. + */ +public class TckControllerExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Controller"; + private static final String PROTOCOL = "tck"; + + private static final WebServiceSettings SETTINGS = WebServiceSettings.Builder.newInstance() + .apiConfigKey(PROTOCOL) + .contextAlias("tck") + .defaultPath("/tck") + .defaultPort(8687) + .useDefaultContext(false) + .name("Tck API") + .build(); + + + @Inject + private WebServiceConfigurer configurator; + + @Inject + private WebService webService; + + @Inject + private WebServer webServer; + + @Inject + private ContractNegotiationService negotiationService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var config = context.getConfig(PROTOCOL); + configurator.configure(config, webServer, SETTINGS); + webService.registerResource(PROTOCOL, new TckWebhookController(negotiationService)); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java new file mode 100644 index 00000000000..96851691343 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/controller/TckWebhookController.java @@ -0,0 +1,63 @@ +/* + * 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.tck.dsp.controller; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequest; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; +import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationService; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.types.domain.callback.CallbackAddress; + +import java.util.List; + +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +/** + * Implements TCK web hooks. + */ +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +@Path("/negotiations") +public class TckWebhookController { + + private final ContractNegotiationService negotiationService; + + public TckWebhookController(ContractNegotiationService negotiationService) { + this.negotiationService = negotiationService; + } + + @POST + @Path("requests") + public void startNegotiation(ContractNegotiationRequest request) { + + var contractOffer = ContractOffer.Builder.newInstance() + .id(request.offerId()) + .assetId(request.offerId()) + .policy(Policy.Builder.newInstance().assigner(request.providerId()).build()) + .build(); + var contractRequest = ContractRequest.Builder.newInstance() + .callbackAddresses(List.of(CallbackAddress.Builder.newInstance().uri(request.connectorAddress()).build())) + .counterPartyAddress(request.connectorAddress()) + .contractOffer(contractOffer) + .protocol("dataspace-protocol-http") + .build(); + negotiationService.initiateNegotiation(contractRequest); + System.out.println("Negotiation"); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java new file mode 100644 index 00000000000..0530a3e3569 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/data/DataAssembly.java @@ -0,0 +1,165 @@ +/* + * 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.tck.dsp.data; + +import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationAccepted; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationAgreed; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationOffered; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.tck.dsp.guard.Trigger; +import org.eclipse.edc.tck.dsp.recorder.StepRecorder; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import static java.util.stream.Collectors.toSet; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTED; + +/** + * Assembles data for the TCK scenarios. + */ +public class DataAssembly { + private static final Set ASSET_IDS = Set.of("ACN0101", "ACN0102", "ACN0103", "ACN0104", + "ACN0201", "ACN0202", "ACN0203", "ACN0204", "ACN0205", "ACN0206", "ACN0207", + "ACN0301", "ACN0302", "ACN0303", "ACN0304"); + + private static final String POLICY_ID = "P123"; + private static final String CONTRACT_DEFINITION_ID = "CD123"; + + public static Set createAssets() { + return ASSET_IDS.stream().map(DataAssembly::createAsset).collect(toSet()); + } + + public static Set createPolicyDefinitions() { + return Set.of(PolicyDefinition.Builder.newInstance() + .id(POLICY_ID) + .policy(Policy.Builder.newInstance().build()) + .build()); + } + + public static Set createContractDefinitions() { + return Set.of(ContractDefinition.Builder.newInstance() + .id(CONTRACT_DEFINITION_ID) + .accessPolicyId(POLICY_ID) + .contractPolicyId(POLICY_ID) + .build()); + } + + public static StepRecorder createNegotiationRecorder() { + var recorder = new StepRecorder(); + + record01NegotiationSequences(recorder); + record02NegotiationSequences(recorder); + record03NegotiationSequences(recorder); + + recordC01NegotiationSequences(recorder); + + return recorder.repeat(); + } + + private static void recordC01NegotiationSequences(StepRecorder recorder) { + } + + private static void record01NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0101", ContractNegotiation::transitionOffering); + + recorder.record("ACN0102", ContractNegotiation::transitionOffering) + .record("ACN0102", ContractNegotiation::transitionTerminating); + + recorder.record("ACN0103", ContractNegotiation::transitionOffering) + .record("ACN0103", ContractNegotiation::transitionAgreeing) + .record("ACN0103", ContractNegotiation::transitionFinalizing); + + recorder.record("ACN0104", ContractNegotiation::transitionAgreeing) + .record("ACN0104", ContractNegotiation::transitionFinalizing); + } + + private static void record02NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0201", ContractNegotiation::transitionTerminating); + + recorder.record("ACN0202", ContractNegotiation::transitionRequested); + + recorder.record("ACN0203", ContractNegotiation::transitionAgreeing); + + recorder.record("ACN0204", ContractNegotiation::transitionOffering); + + recorder.record("ACN0205", ContractNegotiation::transitionOffering); + + recorder.record("ACN0206", contractNegotiation -> { + // only transition if in requested + if (contractNegotiation.getState() == REQUESTED.code()) { + contractNegotiation.transitionOffering(); + } + }); + + recorder.record("ACN0207", ContractNegotiation::transitionAgreeing) + .record("ACN0207", ContractNegotiation::transitionTerminating); + } + + private static void record03NegotiationSequences(StepRecorder recorder) { + recorder.record("ACN0301", ContractNegotiation::transitionAgreeing) + .record("ACN0301", ContractNegotiation::transitionFinalizing); + + recorder.record("ACN0302", ContractNegotiation::transitionOffering); + recorder.record("ACN0303", ContractNegotiation::transitionOffering) + .record("ACN0303", ContractNegotiation::transitionAccepting); + + recorder.record("ACN0304", ContractNegotiation::transitionOffering); + } + + public static List> createNegotiationTriggers() { + return List.of( + createTrigger(ContractNegotiationOffered.class, "ACN0205", ContractNegotiation::transitionTerminating), + createTrigger(ContractNegotiationAccepted.class, "ACN0206", ContractNegotiation::transitionTerminating), + createTrigger(ContractNegotiationOffered.class, "C0101", contractNegotiation -> { + contractNegotiation.transitionAccepting(); + contractNegotiation.setPending(false); + }), + createTrigger(ContractNegotiationAgreed.class, "C0101", contractNegotiation -> { + contractNegotiation.transitionVerifying(); + contractNegotiation.setPending(false); + }) + + ); + } + + private static Trigger createTrigger(Class type, + String assetId, + Consumer action) { + return new Trigger<>(event -> { + if (event.getClass().equals(type)) { + return assetId.equals(((ContractNegotiationEvent) event).getLastContractOffer().getAssetId()); + } + return false; + }, action); + } + + private DataAssembly() { + } + + private static Asset createAsset(String id) { + return Asset.Builder.newInstance() + .id(id) + .dataAddress(DataAddress.Builder.newInstance().type("HTTP").build()) + .build(); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java new file mode 100644 index 00000000000..027513fc920 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationGuard.java @@ -0,0 +1,56 @@ +/* + * 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.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.ContractNegotiationPendingGuard; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.spi.persistence.StateEntityStore; + +import java.util.Set; +import java.util.function.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.ACCEPTING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.AGREEING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.FINALIZING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.INITIAL; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.OFFERING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.TERMINATING; +import static org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationStates.VERIFYING; + +/** + * Contract negotiation guard for TCK testcases. + */ +public class ContractNegotiationGuard extends DelayedActionGuard implements ContractNegotiationPendingGuard { + // the states to not apply the guard to - i.e. to allow automatic transitions by the contract negotiation manager + private static final Set PROVIDER_AUTOMATIC_STATES = Set.of( + OFFERING.code(), + AGREEING.code(), + TERMINATING.code(), + FINALIZING.code()); + + private static final Set CONSUMER_AUTOMATIC_STATES = Set.of( + INITIAL.code(), + REQUESTING.code(), + ACCEPTING.code(), + VERIFYING.code() + ); + + public ContractNegotiationGuard(Consumer action, StateEntityStore store) { + super(cn -> cn.getType() == PROVIDER ? + !PROVIDER_AUTOMATIC_STATES.contains(cn.getState()) : !CONSUMER_AUTOMATIC_STATES.contains(cn.getState()), action, store); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java new file mode 100644 index 00000000000..e516813104c --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerRegistry.java @@ -0,0 +1,26 @@ +/* + * 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.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; + +/** + * Registers contract negotiation triggers. + */ +public interface ContractNegotiationTriggerRegistry { + + void register(Trigger trigger); + +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java new file mode 100644 index 00000000000..b508c547720 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/ContractNegotiationTriggerSubscriber.java @@ -0,0 +1,56 @@ +/* + * 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.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.spi.event.Event; +import org.eclipse.edc.spi.event.EventEnvelope; +import org.eclipse.edc.spi.event.EventSubscriber; +import org.eclipse.edc.spi.persistence.StateEntityStore; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fires triggers based on negotiation events. + */ +public class ContractNegotiationTriggerSubscriber implements EventSubscriber, ContractNegotiationTriggerRegistry { + private final StateEntityStore store; + + private final List> triggers = new ArrayList<>(); + + public ContractNegotiationTriggerSubscriber(StateEntityStore store) { + this.store = store; + } + + @Override + public void register(Trigger trigger) { + triggers.add(trigger); + } + + @Override + public void on(EventEnvelope envelope) { + triggers.stream() + .filter(trigger -> trigger.predicate().test(envelope.getPayload())) + .forEach(trigger -> { + var event = (ContractNegotiationEvent) envelope.getPayload(); + var negotiation = store.findByIdAndLease(event.getContractNegotiationId()).getContent(); + trigger.action().accept(negotiation); + store.save(negotiation); + }); + } + +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java new file mode 100644 index 00000000000..e9e9a89dec2 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/DelayedActionGuard.java @@ -0,0 +1,113 @@ +/* + * 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.tck.dsp.guard; + +import org.eclipse.edc.spi.entity.PendingGuard; +import org.eclipse.edc.spi.entity.StatefulEntity; +import org.eclipse.edc.spi.persistence.StateEntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +/** + * A guard that performs actions on a stateful entity. + *

+ * Note this implementation is not safe to use in a clustered environment since transitions are not performed in the context of + * a command handler. + */ +public class DelayedActionGuard> implements PendingGuard { + private final Predicate filter; + private final Consumer action; + private final StateEntityStore store; + private final DelayQueue queue; + private final AtomicBoolean active = new AtomicBoolean(); + + private final ExecutorService executor = Executors.newFixedThreadPool(1); + + public DelayedActionGuard(Predicate filter, + Consumer action, + StateEntityStore store) { + this.filter = filter; + this.action = action; + this.store = store; + queue = new DelayQueue<>(); + } + + public void start() { + active.set(true); + executor.submit(() -> { + while (active.get()) { + try { + var entry = queue.poll(10, MILLISECONDS); + if (entry != null) { + action.accept(entry.entity); + entry.entity.setPending(false); + store.save(entry.entity); + } + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.interrupted(); + break; + } + } + }); + } + + public void stop() { + active.set(false); + } + + @Override + public boolean test(T entity) { + if (filter.test(entity)) { + queue.put(new GuardDelay(entity)); + return true; + } + return false; + } + + protected class GuardDelay implements Delayed { + T entity; + private final long start; + + GuardDelay(T entity) { + this.entity = entity; + start = System.currentTimeMillis(); + } + + @Override + public int compareTo(@NotNull Delayed delayed) { + var millis = getDelay(MILLISECONDS) - delayed.getDelay(MILLISECONDS); + millis = Math.min(millis, 1); + millis = Math.max(millis, -1); + return (int) millis; + } + + @Override + public long getDelay(@NotNull TimeUnit timeUnit) { + return timeUnit.convert(500 - (System.currentTimeMillis() - start), MILLISECONDS); + } + + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java new file mode 100644 index 00000000000..3a2f1a00169 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/TckGuardExtension.java @@ -0,0 +1,72 @@ +/* + * 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.tck.dsp.guard; + +import org.eclipse.edc.connector.controlplane.contract.spi.event.contractnegotiation.ContractNegotiationEvent; +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.ContractNegotiationPendingGuard; +import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.event.EventRouter; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createNegotiationRecorder; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createNegotiationTriggers; + +/** + * Loads the transition guard. + */ +public class TckGuardExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Guard"; + + private org.eclipse.edc.tck.dsp.guard.ContractNegotiationGuard negotiationGuard; + + @Inject + private ContractNegotiationStore store; + + @Inject + private EventRouter router; + + @Override + public String name() { + return NAME; + } + + @Provider + public ContractNegotiationPendingGuard negotiationGuard() { + var recorder = createNegotiationRecorder(); + + var registry = new org.eclipse.edc.tck.dsp.guard.ContractNegotiationTriggerSubscriber(store); + createNegotiationTriggers().forEach(registry::register); + router.register(ContractNegotiationEvent.class, registry); + + negotiationGuard = new org.eclipse.edc.tck.dsp.guard.ContractNegotiationGuard(cn -> recorder.playNext(cn.getContractOffers().get(0).getAssetId(), cn), store); + return negotiationGuard; + } + + @Override + public void prepare() { + if (negotiationGuard != null) { + negotiationGuard.start(); + } + } + + @Override + public void shutdown() { + if (negotiationGuard != null) { + negotiationGuard.stop(); + } + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java new file mode 100644 index 00000000000..2dfbad828d2 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/guard/Trigger.java @@ -0,0 +1,24 @@ +/* + * 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.tck.dsp.guard; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * An action that is triggered when the predicate matches a condition. + */ +public record Trigger(Predicate predicate, Consumer action) { +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java new file mode 100644 index 00000000000..134375156ca --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/NoopIdentityService.java @@ -0,0 +1,39 @@ +/* + * 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.tck.dsp.identity; + +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.iam.VerificationContext; +import org.eclipse.edc.spi.result.Result; + +/** + * No-op service + */ +public class NoopIdentityService implements IdentityService { + private static final String TCK_PARTICIPANT_ID = "TCK_PARTICIPANT"; // the official TCK id + + @Override + public Result obtainClientCredentials(TokenParameters tokenParameters) { + return Result.success(TokenRepresentation.Builder.newInstance().token("1234").expiresIn(Long.MAX_VALUE).build()); + } + + @Override + public Result verifyJwtToken(TokenRepresentation tokenRepresentation, VerificationContext verificationContext) { + return Result.success(ClaimToken.Builder.newInstance().claim("client_id", TCK_PARTICIPANT_ID).build()); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java new file mode 100644 index 00000000000..88ed10b1797 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/identity/TckIdentityExtension.java @@ -0,0 +1,43 @@ +/* + * 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.tck.dsp.identity; + +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.iam.AudienceResolver; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.system.ServiceExtension; + +/** + * Loads a no-op identity service. + */ +public class TckIdentityExtension implements ServiceExtension { + private static final String NAME = "DSP TCK Identity"; + + @Override + public String name() { + return NAME; + } + + @Provider + public IdentityService identityService() { + return new NoopIdentityService(); + } + + @Provider + public AudienceResolver audienceResolver() { + return m -> Result.success(m.getCounterPartyId()); + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java new file mode 100644 index 00000000000..41d860befe8 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/recorder/StepRecorder.java @@ -0,0 +1,61 @@ +/* + * 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.tck.dsp.recorder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Records and plays a sequence of steps. Sequences may be repeated if {@link #repeat()} is enabled. + */ +public class StepRecorder { + private boolean repeat; + private int playIndex = 0; + private final Map>> sequences = new HashMap<>(); + + public synchronized StepRecorder playNext(String key, T entity) { + var sequence = sequences.get(key); + if (sequence == null) { + throw new AssertionError("No sequence found for key " + key); + } + if (sequence.isEmpty()) { + throw new IllegalStateException("No replay steps"); + } + if (playIndex >= sequence.size()) { + throw new IllegalStateException("Exceeded replay steps"); + } + sequence.get(playIndex).accept(entity); + if (repeat && playIndex == sequence.size() - 1) { + playIndex = 0; + } else { + playIndex++; + } + return this; + } + + public synchronized StepRecorder record(String key, Consumer step) { + var sequence = sequences.computeIfAbsent(key, k -> new ArrayList<>()); + sequence.add(step); + return this; + } + + public synchronized StepRecorder repeat() { + repeat = true; + return this; + } +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java new file mode 100644 index 00000000000..44259c598c2 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/java/org/eclipse/edc/tck/dsp/setup/TckSetupExtension.java @@ -0,0 +1,57 @@ +/* + * 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.tck.dsp.setup; + +import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; +import org.eclipse.edc.connector.controlplane.services.spi.contractdefinition.ContractDefinitionService; +import org.eclipse.edc.connector.controlplane.services.spi.policydefinition.PolicyDefinitionService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createAssets; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createContractDefinitions; +import static org.eclipse.edc.tck.dsp.data.DataAssembly.createPolicyDefinitions; +import static org.eclipse.edc.tck.dsp.setup.TckSetupExtension.NAME; + +/** + * Loads customizations and seed data for the TCK. + */ +@Extension(NAME) +public class TckSetupExtension implements ServiceExtension { + public static final String NAME = "DSP TCK Setup"; + + @Inject + private AssetIndex assetIndex; + + @Inject + private PolicyDefinitionService policyDefinitionService; + + @Inject + private ContractDefinitionService contractDefinitionService; + + @Override + public String name() { + return NAME; + } + + @Override + public void prepare() { + createAssets().forEach(asset -> assetIndex.create(asset)); + createPolicyDefinitions().forEach(definition -> policyDefinitionService.create(definition)); + createContractDefinitions().forEach(definition -> contractDefinitionService.create(definition)); + } + +} diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 00000000000..e7ca6341b1c --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,18 @@ +# +# Copyright (c) 2023 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 +# +# + +org.eclipse.edc.tck.dsp.guard.TckGuardExtension +org.eclipse.edc.tck.dsp.setup.TckSetupExtension +org.eclipse.edc.tck.dsp.identity.TckIdentityExtension +org.eclipse.edc.tck.dsp.controller.TckControllerExtension \ No newline at end of file diff --git a/system-tests/dsp-compatibility-tests/connector-under-test/tck-runtime.env b/system-tests/dsp-compatibility-tests/connector-under-test/tck-runtime.env new file mode 100644 index 00000000000..28b5a5d4ee7 --- /dev/null +++ b/system-tests/dsp-compatibility-tests/connector-under-test/tck-runtime.env @@ -0,0 +1,26 @@ +# control plane specific config +WEB_HTTP_PORT=8080 +WEB_HTTP_PATH="/api" +WEB_HTTP_MANAGEMENT_PORT=8081 +WEB_HTTP_MANAGEMENT_PATH="/api/management/" +WEB_HTTP_PROTOCOL_PORT=8282 +WEB_HTTP_PROTOCOL_PATH="/api/v1/dsp" +WEB_HTTP_CONTROL_PORT=8083 +WEB_HTTP_CONTROL_PATH="/api/control" +WEB_HTTP_CATALOG_PORT=8084 +WEB_HTTP_CATALOG_PATH="/api/catalog" +WEB_HTTP_VERSION_PORT=8085 +WEB_HTTP_VERSION_PATH="/api/version" +EDC_API_AUTH_KEY="password" +EDC_IAM_DID_WEB_USE_HTTPS="false" +EDC_DSP_CALLBACK_ADDRESS="http://localhost:8082/api/dsp" +EDC_PARTICIPANT_ID=CONNECTOR_UNDER_TEST" +EDC_MANAGEMENT_CONTEXT_ENABLED=true + +# dataplane specific config +EDC_RUNTIME_ID="consumer-embedded-runtime" +EDC_TRANSFER_PROXY_TOKEN_VERIFIER_PUBLICKEY_ALIAS="did:web:localhost%3A7083#key-1" +EDC_TRANSFER_PROXY_TOKEN_SIGNER_PRIVATEKEY_ALIAS="did:web:localhost%3A7083-alias" +EDC_DPF_SELECTOR_URL="http://localhost:8083/api/control/v1/dataplanes" +WEB_HTTP_PUBLIC_PORT=11001 +WEB_HTTP_PUBLIC_PATH="/api/public"