diff --git a/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/AbstractDynamicConstraintFunction.java b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/AbstractDynamicConstraintFunction.java new file mode 100644 index 000000000..9ffe3bbb5 --- /dev/null +++ b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/AbstractDynamicConstraintFunction.java @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.policy.cx.common; + +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.policy.engine.spi.DynamicAtomicConstraintFunction; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.edc.spi.result.Result; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * This is a base class for dynamically bound Tractus-X constraint evaluation functions that implements some basic common functionality and defines some + * common constants + */ +public abstract class AbstractDynamicConstraintFunction implements DynamicAtomicConstraintFunction { + public static final String VC_CLAIM = "vc"; + public static final String ACTIVE = "active"; + public static final String CREDENTIAL_LITERAL = "Credential"; + protected static final Collection EQUALITY_OPERATORS = List.of(Operator.EQ, Operator.NEQ); + + protected boolean checkOperator(Operator actual, PolicyContext context, Operator... expectedOperators) { + return checkOperator(actual, context, Arrays.asList(expectedOperators)); + } + + protected boolean checkOperator(Operator actual, PolicyContext context, Collection expectedOperators) { + if (!expectedOperators.contains(actual)) { + context.reportProblem("Invalid operator: this constraint only allows the following operators: %s, but received '%s'.".formatted(EQUALITY_OPERATORS, actual)); + return false; + } + return true; + } + + /** + * Extracts a {@link List} of {@link VerifiableCredential} objects from the {@link ParticipantAgent}. Credentials must be + * stored in the agent's claims map using the "vc" key. + */ + protected Result> getCredentialList(ParticipantAgent agent) { + var vcListClaim = agent.getClaims().get(VC_CLAIM); + + if (vcListClaim == null) { + return Result.failure("ParticipantAgent did not contain a '%s' claim.".formatted(VC_CLAIM)); + } + if (!(vcListClaim instanceof List)) { + return Result.failure("ParticipantAgent contains a '%s' claim, but the type is incorrect. Expected %s, received %s.".formatted(VC_CLAIM, List.class.getName(), vcListClaim.getClass().getName())); + } + var vcList = (List) vcListClaim; + if (vcList.isEmpty()) { + return Result.failure("ParticipantAgent contains a '%s' claim but it did not contain any VerifiableCredentials.".formatted(VC_CLAIM)); + } + return Result.success(vcList); + } + +} diff --git a/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/CredentialTypePredicate.java b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/CredentialTypePredicate.java new file mode 100644 index 000000000..0fc858efd --- /dev/null +++ b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/common/CredentialTypePredicate.java @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.policy.cx.common; + +import org.eclipse.edc.identitytrust.model.VerifiableCredential; + +import java.util.function.Predicate; + +public class CredentialTypePredicate implements Predicate { + private final String expectedType; + + public CredentialTypePredicate(String expectedType) { + this.expectedType = expectedType; + } + + @Override + public boolean test(VerifiableCredential credential) { + return credential.getTypes().contains(expectedType); + } +} diff --git a/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunction.java b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunction.java new file mode 100644 index 000000000..7013a4cc7 --- /dev/null +++ b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunction.java @@ -0,0 +1,169 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.policy.cx.dismantler; + +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.policy.model.Permission; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.tractusx.edc.policy.cx.common.AbstractDynamicConstraintFunction; +import org.eclipse.tractusx.edc.policy.cx.common.CredentialTypePredicate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.function.Predicate; + +import static org.eclipse.edc.policy.model.Operator.EQ; +import static org.eclipse.edc.policy.model.Operator.IN; +import static org.eclipse.edc.policy.model.Operator.IS_ANY_OF; +import static org.eclipse.edc.policy.model.Operator.IS_NONE_OF; +import static org.eclipse.edc.policy.model.Operator.NEQ; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CX_NS_1_0; + +/** + * Enforces a Dismantler constraint. This function can check for these properties: + *
    + *
  • presence: whether a Dismantler credential is present or not
  • + *
  • activityType: whether an existing DismantlerCredential permits the activity types required by the constraint
  • + *
  • allowedBrands: whether an existing DismantlerCredential permits the vehicle brands required by the constraint
  • + *
+ */ +public class DismantlerConstraintFunction extends AbstractDynamicConstraintFunction { + + public static final String ALLOWED_VEHICLE_BRANDS = CX_NS_1_0 + "allowedVehicleBrands"; + private static final String DISMANTLER_LITERAL = "Dismantler"; + private static final String ALLOWED_ACTIVITIES = CX_NS_1_0 + "activityType"; + + @Override + public boolean evaluate(Object leftOperand, Operator operator, Object rightOperand, Permission permission, PolicyContext context) { + Predicate predicate = c -> false; + + // make sure the ParticipantAgent is there + var participantAgent = context.getContextData(ParticipantAgent.class); + if (participantAgent == null) { + context.reportProblem("Required PolicyContext data not found: " + ParticipantAgent.class.getName()); + return false; + } + + // check if the participant agent contains the correct data + var vcListResult = getCredentialList(participantAgent); + if (vcListResult.failed()) { // couldn't extract credential list from agent + context.reportProblem(vcListResult.getFailureDetail()); + return false; + } + + if (leftOperand.equals(DISMANTLER_LITERAL)) { // only checks for presence + if (!checkOperator(operator, context, EQUALITY_OPERATORS)) { + return false; + } + if (!ACTIVE.equals(rightOperand)) { + context.reportProblem("Right-operand must be equal to '%s', but was '%s'".formatted(ACTIVE, rightOperand)); + return false; + } + predicate = new CredentialTypePredicate(DISMANTLER_LITERAL + CREDENTIAL_LITERAL); + if (operator == NEQ) { + predicate = predicate.negate(); + } + } else if (leftOperand.equals(DISMANTLER_LITERAL + ".activityType")) { + if (hasInvalidOperand(operator, rightOperand, context)) return false; + predicate = getCredentialPredicate(ALLOWED_ACTIVITIES, operator, rightOperand); + } else if (leftOperand.equals(DISMANTLER_LITERAL + ".allowedBrands")) { + if (hasInvalidOperand(operator, rightOperand, context)) return false; + predicate = getCredentialPredicate(ALLOWED_VEHICLE_BRANDS, operator, rightOperand); + } else { + context.reportProblem("Invalid left-operand: must be 'Dismantler[.activityType | .allowedBrands ], but was '%s'".formatted(leftOperand)); + return false; + } + + return !vcListResult.getContent().stream().filter(predicate) + .toList().isEmpty(); + } + + @Override + public boolean canHandle(Object leftOperand) { + return leftOperand instanceof String && ((String) leftOperand).startsWith(DISMANTLER_LITERAL); + } + + /** + * Creates a {@link Predicate} based on the {@code rightOperand} that tests whether whatever property is extracted from the {@link VerifiableCredential} + * is valid, according to the operator. For example {@link Operator#IS_ALL_OF} would check that the list from the constraint (= rightOperand) intersects with the list + * stored in the claim identified by {@code credentialSubjectProperty} in the {@link VerifiableCredential#getCredentialSubject()}. + * + * @param credentialSubjectProperty The name of the claim to be extracted from the {@link VerifiableCredential#getCredentialSubject()} + * @param operator the operator + * @param rightOperand The constraint value (i.e. policy expression right-operand) + * @return A predicate that tests a {@link VerifiableCredential} for the constraint + */ + @NotNull + private Predicate getCredentialPredicate(String credentialSubjectProperty, Operator operator, Object rightOperand) { + Predicate predicate; + var allowedValues = getList(rightOperand); + // the filter predicate is determined by the operator + predicate = credential -> credential.getCredentialSubject().stream().anyMatch(subject -> { + var claimsFromCredential = getList(subject.getClaims().getOrDefault(credentialSubjectProperty, List.of())); + return switch (operator) { + case EQ -> claimsFromCredential.equals(allowedValues); + case NEQ -> !claimsFromCredential.equals(allowedValues); + case IN -> + new HashSet<>(allowedValues).containsAll(claimsFromCredential); //IntelliJ says Hashset has better performance + case IS_ANY_OF -> !intersect(allowedValues, claimsFromCredential).isEmpty(); + case IS_NONE_OF -> intersect(allowedValues, claimsFromCredential).isEmpty(); + default -> false; + }; + }); + return predicate; + } + + /** + * Checks whether {@code operator} is valid in the context of {@code rightOperand}. In practice, this means that if {@code rightOperand} is a String, it checks for {@link AbstractDynamicConstraintFunction#EQUALITY_OPERATORS}, + * and if its list type, it checks for {@code List.of(EQ, NEQ, IN, IS_ANY_OF, IS_NONE_OF)} + */ + private boolean hasInvalidOperand(Operator operator, Object rightOperand, PolicyContext context) { + if (rightOperand instanceof String) { + return !checkOperator(operator, context, EQUALITY_OPERATORS); + } else if (rightOperand instanceof Iterable) { + return !checkOperator(operator, context, List.of(EQ, NEQ, IN, IS_ANY_OF, IS_NONE_OF)); + } else { + context.reportProblem("Invalid right-operand type: expected String or List, but received: %s".formatted(rightOperand.getClass().getName())); + return true; + } + } + + /** + * Checks whether two lists "intersect", i.e. that at least one element is found in both lists. + * Caution: {@code list1} may be modified! + */ + private List intersect(List list1, List list2) { + list1.retainAll(list2); + return list1; + } + + private List getList(Object object) { + if (object instanceof Iterable iterable) { + var list = new ArrayList<>(); + iterable.iterator().forEachRemaining(list::add); + return list; + } + return List.of(object); + } +} diff --git a/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunction.java b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunction.java index d7a13ee5b..f005add2f 100644 --- a/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunction.java +++ b/edc-extensions/cx-policy/src/main/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunction.java @@ -20,17 +20,17 @@ package org.eclipse.tractusx.edc.policy.cx.framework; import org.eclipse.edc.identitytrust.model.VerifiableCredential; -import org.eclipse.edc.policy.engine.spi.DynamicAtomicConstraintFunction; import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.model.Operator; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.policy.cx.common.AbstractDynamicConstraintFunction; +import org.eclipse.tractusx.edc.policy.cx.common.CredentialTypePredicate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.function.Predicate; @@ -52,13 +52,9 @@ * policy is considered not fulfilled. Note that if the {@code version} is specified, it must be satisfied by the same * credential that satisfies the {@code subtype} requirement. */ -public class FrameworkAgreementConstraintFunction implements DynamicAtomicConstraintFunction { +public class FrameworkAgreementConstraintFunction extends AbstractDynamicConstraintFunction { public static final String CONTRACT_VERSION_PROPERTY = CX_NS_1_0 + "contractVersion"; - private static final String VC_CLAIM = "vc"; - private static final String ACTIVE = "active"; private static final String FRAMEWORK_AGREEMENT_LITERAL = "FrameworkAgreement"; - private static final String CREDENTIAL_LITERAL = "Credential"; - private static final Collection ALLOWED_OPERATORS = List.of(Operator.EQ, Operator.NEQ); public FrameworkAgreementConstraintFunction() { } @@ -77,8 +73,7 @@ public FrameworkAgreementConstraintFunction() { @Override public boolean evaluate(Object leftValue, Operator operator, Object rightValue, Permission rule, PolicyContext context) { - if (!ALLOWED_OPERATORS.contains(operator)) { - context.reportProblem("Invalid operator: allowed operators are %s, got '%s'.".formatted(ALLOWED_OPERATORS, operator)); + if (!checkOperator(operator, context, EQUALITY_OPERATORS)) { return false; } @@ -131,7 +126,7 @@ public boolean evaluate(Object leftValue, Operator operator, Object rightValue, } /** - * Returns {@code true} if the left-operand starts with {@link FrameworkAgreementConstraintFunction#FRAMEWORK_AGREEMENT_LITERAL}, {@link false} otherwise. + * Returns {@code true} if the left-operand starts with {@link FrameworkAgreementConstraintFunction#FRAMEWORK_AGREEMENT_LITERAL}, {@code false} otherwise. */ @Override public boolean canHandle(Object leftValue) { @@ -145,26 +140,6 @@ private Predicate reducePredicates(List true); } - /** - * Extracts a {@link List} of {@link VerifiableCredential} objects from the {@link ParticipantAgent}. Credentials must be - * stored in the agent's claims map using the "vc" key. - */ - private Result> getCredentialList(ParticipantAgent agent) { - var vcListClaim = agent.getClaims().get(VC_CLAIM); - - if (vcListClaim == null) { - return Result.failure("ParticipantAgent did not contain a '%s' claim.".formatted(VC_CLAIM)); - } - if (!(vcListClaim instanceof List)) { - return Result.failure("ParticipantAgent contains a '%s' claim, but the type is incorrect. Expected %s, got %s.".formatted(VC_CLAIM, List.class.getName(), vcListClaim.getClass().getName())); - } - var vcList = (List) vcListClaim; - if (vcList.isEmpty()) { - return Result.failure("ParticipantAgent contains a '%s' claim but it did not contain any VerifiableCredentials.".formatted(VC_CLAIM)); - } - return Result.success(vcList); - } - /** * Converts the right-operand (new notation) into either 1 or 2 predicates, depending on whether the version was encoded or not. */ @@ -201,7 +176,7 @@ private Result>> getFilterPredicateLegacy(S @NotNull private List> createPredicates(String subtype, @Nullable String version) { var list = new ArrayList>(); - list.add(credential -> credential.getTypes().contains(capitalize(subtype) + CREDENTIAL_LITERAL)); + list.add(new CredentialTypePredicate(capitalize(subtype) + CREDENTIAL_LITERAL)); if (version != null) { list.add(credential -> credential.getCredentialSubject().stream().anyMatch(cs -> cs.getClaims().get(CONTRACT_VERSION_PROPERTY).equals(version))); } diff --git a/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/CredentialFunctions.java b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/CredentialFunctions.java new file mode 100644 index 000000000..1d60aa3a1 --- /dev/null +++ b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/CredentialFunctions.java @@ -0,0 +1,75 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.policy.cx; + +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class CredentialFunctions { + + public static VerifiableCredential.Builder createCredential(String type, String version) { + return VerifiableCredential.Builder.newInstance() + .types(List.of("VerifiableCredential", type)) + .id(UUID.randomUUID().toString()) + .issuer(new Issuer(UUID.randomUUID().toString(), Map.of("prop1", "val1"))) + .expirationDate(Instant.now().plus(365, ChronoUnit.DAYS)) + .issuanceDate(Instant.now()) + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("subject-id") + .claim("https://w3id.org/catenax/credentials/v1.0.0/holderIdentifier", "did:web:holder") + .claim("https://w3id.org/catenax/credentials/v1.0.0/contractVersion", version) + .claim("https://w3id.org/catenax/credentials/v1.0.0/contractTemplate", "https://public.catena-x.org/contracts/pcf.v1.pdf") + .build()); + } + + public static VerifiableCredential.Builder createPcfCredential() { + return createCredential("PcfCredential", "1.0.0"); + } + + public static VerifiableCredential.Builder createDismantlerCredential(String... brands) { + return createDismantlerCredential(Arrays.asList(brands), "vehicleDismantle"); + } + + public static VerifiableCredential.Builder createDismantlerCredential(Collection brands, String... activityType) { + var at = activityType.length == 1 ? activityType[0] : List.of(activityType); + return VerifiableCredential.Builder.newInstance() + .types(List.of("VerifiableCredential", "DismantlerCredential")) + .id(UUID.randomUUID().toString()) + .issuer(new Issuer(UUID.randomUUID().toString(), Map.of("prop1", "val1"))) + .expirationDate(Instant.now().plus(365, ChronoUnit.DAYS)) + .issuanceDate(Instant.now()) + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("subject-id") + .claim("https://w3id.org/catenax/credentials/v1.0.0/holderIdentifier", "did:web:holder") + .claim("https://w3id.org/catenax/credentials/v1.0.0/allowedVehicleBrands", brands) + .claim("https://w3id.org/catenax/credentials/v1.0.0/activityType", at) + .build()); + } + +} diff --git a/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunctionTest.java b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunctionTest.java new file mode 100644 index 000000000..8462b40b8 --- /dev/null +++ b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/dismantler/DismantlerConstraintFunctionTest.java @@ -0,0 +1,440 @@ +/******************************************************************************** + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +package org.eclipse.tractusx.edc.policy.cx.dismantler; + +import org.eclipse.edc.policy.engine.spi.PolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.policy.model.Operator.IN; +import static org.eclipse.tractusx.edc.policy.cx.CredentialFunctions.createDismantlerCredential; +import static org.eclipse.tractusx.edc.policy.cx.CredentialFunctions.createPcfCredential; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DismantlerConstraintFunctionTest { + + private final DismantlerConstraintFunction function = new DismantlerConstraintFunction(); + private final PolicyContext context = mock(); + private ParticipantAgent participantAgent; + + @BeforeEach + void setup() { + participantAgent = mock(ParticipantAgent.class); + when(context.getContextData(eq(ParticipantAgent.class))) + .thenReturn(participantAgent); + } + + @Test + void evaluate_leftOperandInvalid() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich").build()))); + assertThat(function.evaluate("foobar", Operator.EQ, "active", null, context)).isFalse(); + verify(context).reportProblem(eq("Invalid left-operand: must be 'Dismantler[.activityType | .allowedBrands ], but was 'foobar'")); + } + + @Test + void evaluate_noParticipantAgentOnContext() { + when(context.getContextData(eq(ParticipantAgent.class))).thenReturn(null); + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isFalse(); + verify(context).reportProblem("Required PolicyContext data not found: org.eclipse.edc.spi.agent.ParticipantAgent"); + } + + @Test + void evaluate_noVcClaimOnParticipantAgent() { + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isFalse(); + verify(context).reportProblem(eq("ParticipantAgent did not contain a 'vc' claim.")); + } + + @Test + void evaluate_vcClaimEmpty() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of())); + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isFalse(); + verify(context).reportProblem(eq("ParticipantAgent contains a 'vc' claim but it did not contain any VerifiableCredentials.")); + } + + @Test + void evaluate_vcClaimNotList() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", new Object())); + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isFalse(); + verify(context).reportProblem(eq("ParticipantAgent contains a 'vc' claim, but the type is incorrect. Expected java.util.List, received java.lang.Object.")); + } + + @Nested + class Active { + @Test + void evaluate_eq_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich").build()))); + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isTrue(); + } + + @Test + void evaluate_eq_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createPcfCredential().build()))); + assertThat(function.evaluate("Dismantler", Operator.EQ, "active", null, context)).isFalse(); + } + + @Test + void evaluate_neq_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createPcfCredential().build()))); + assertThat(function.evaluate("Dismantler", Operator.NEQ, "active", null, context)).isTrue(); + } + + @Test + void evaluate_neq_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Yugo", "Tatra").build()))); + assertThat(function.evaluate("Dismantler", Operator.NEQ, "active", null, context)).isFalse(); + } + + @Test + void evaluate_invalidOperators() { + var invalidOperators = new ArrayList<>(Arrays.asList(Operator.values())); + invalidOperators.remove(Operator.EQ); + invalidOperators.remove(Operator.NEQ); + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createPcfCredential().build()))); + + assertThat(invalidOperators).allSatisfy(invalidOperator -> assertThat(function.evaluate("Dismantler", invalidOperator, "active", null, context)).isFalse()); + + } + + @Test + void evaluate_rightOperandInvalid() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createPcfCredential().build()))); + assertThat(function.evaluate("Dismantler", Operator.EQ, "invalid", null, context)).isFalse(); + verify(context).reportProblem("Right-operand must be equal to 'active', but was 'invalid'"); + } + } + + @Nested + class AllowedBrands { + + @DisplayName("Constraint (list) must match credential EXACTLY") + @Test + void evaluate_eq_list() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.EQ, List.of("Tatra", "Moskvich"), null, context)).isTrue(); + } + + @DisplayName("Constraint (list) must credential EXACTLY - failure") + @Test + void evaluate_eq_list_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich", "Lada").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.EQ, List.of("Tatra", "Moskvich"), null, context)).isFalse(); + verify(context, never()).reportProblem(anyString()); + } + + @DisplayName("Constraint (scalar) must match credential EXACTLY") + @Test + void evaluate_eq_scalar() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.EQ, "Yugo", null, context)).isTrue(); + } + + @DisplayName("Constraint (scalar) must credential EXACTLY - failure") + @Test + void evaluate_eq_scalar_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.EQ, "Yugo", null, context)).isFalse(); + verify(context, never()).reportProblem(anyString()); + } + + @DisplayName("Constraint and credential must be DISJOINT") + @Test + void evaluate_neq_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.NEQ, List.of("Lada", "Yugo"), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Tatra", "Moskvich", "Yugo", "Lada").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.NEQ, List.of("Lada", "Yugo"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must be DISJOINT - failure") + @Test + void evaluate_neq_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Lada", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.NEQ, List.of("Lada", "Yugo"), null, context)).isFalse(); + } + + @DisplayName("Constraint and credential must INTERSECT") + @Test + void evaluate_isAnyOf_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Lada").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Moskvich", "Tatra").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of("Lada", "Moskvich"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must INTERSECT - failure") + @Test + void evaluate_isAnyOf_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential().build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Moskvich", "Tatra").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_ANY_OF, List.of(), null, context)).isFalse(); + } + + @DisplayName("Constraint and credential must NOT INTERSECT") + @Test + void evaluate_isNoneOf_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_NONE_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_NONE_OF, List.of(), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential().build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_NONE_OF, List.of("Tatra", "Moskvich", "Lada"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must NOT INTERSECT - failure") + @Test + void evaluate_isNoneOf_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", Operator.IS_NONE_OF, List.of("Tatra", "Moskvich", "Yugo"), null, context)).isFalse(); + + } + + @DisplayName("Brand list from credential must be FULLY CONTAINED within constraint") + @Test + void evaluate_in_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", IN, List.of("Gaz", "Moskvich", "Yugo", "Lada"), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", IN, List.of("Gaz"), null, context)).isTrue(); + } + + @DisplayName("Brand list from credential must be FULLY CONTAINED within constraint - failure") + @Test + void evaluate_in_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", IN, List.of("Gaz", "Moskvich", "Yugo", "Lada"), null, context)).isTrue(); + } + + @DisplayName("Illegal operator when constraint contains a list value") + @Test + void evaluate_illegalOperator_constraintIsList() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + var illegalOp = List.of(Operator.HAS_PART, Operator.GEQ, Operator.LEQ, Operator.GT, Operator.LT, Operator.IS_ALL_OF); + + assertThat(illegalOp).allSatisfy(op -> { + assertThat(function.evaluate("Dismantler.allowedBrands", op, List.of("Gaz", "Moskvich"), null, context)).isFalse(); + verify(context).reportProblem("Invalid operator: this constraint only allows the following operators: [EQ, NEQ], but received '%s'.".formatted(op)); + }); + } + + @DisplayName("Illegal operator when constraint contains a scalar value") + @Test + void evaluate_illegalOperator_constraintIsScalar() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + + var invalidOperators = new ArrayList<>(Arrays.asList(Operator.values())); + invalidOperators.remove(Operator.EQ); + invalidOperators.remove(Operator.NEQ); + + assertThat(invalidOperators).allSatisfy(op -> { + assertThat(function.evaluate("Dismantler.allowedBrands", op, "Moskvich", null, context)).isFalse(); + verify(context).reportProblem("Invalid operator: this constraint only allows the following operators: [EQ, NEQ], but received '%s'.".formatted(op)); + }); + } + + @DisplayName("Constraint right-operand has an invalid type") + @Test + void evaluate_righOpInvalidType() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential("Gaz", "Yugo").build()))); + assertThat(function.evaluate("Dismantler.allowedBrands", IN, Map.of("foo", "bar"), null, context)).isFalse(); + verify(context).reportProblem(startsWith("Invalid right-operand type: expected String or List, but received:")); + } + } + + @Nested + class ActivityType { + private final Collection brands = List.of("Tatra", "Yugo"); + + @DisplayName("Constraint (list) must match credential EXACTLY") + @Test + void evaluate_eq_list() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.EQ, List.of("vehicleDismantle", "vehicleScrap"), null, context)).isTrue(); + } + + @DisplayName("Constraint (list) must credential EXACTLY - failure") + @Test + void evaluate_eq_list_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.EQ, List.of("vehicleRefurbish"), null, context)).isFalse(); + verify(context, never()).reportProblem(anyString()); + } + + @DisplayName("Constraint (scalar) must match credential EXACTLY") + @Test + void evaluate_eq_scalar() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.EQ, "vehicleDismantle", null, context)).isTrue(); + } + + @DisplayName("Constraint (scalar) must credential EXACTLY - failure") + @Test + void evaluate_eq_scalar_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.EQ, "vehicleScrap", null, context)).isFalse(); + verify(context, never()).reportProblem(anyString()); + } + + @DisplayName("Constraint and credential must be DISJOINT") + @Test + void evaluate_neq_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.NEQ, "vehicleScrap", null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.NEQ, List.of("vehicleScrap", "vehicleRefurbish"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must be DISJOINT - failure") + @Test + void evaluate_neq_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.NEQ, "vehicleDismantle", null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.NEQ, List.of("vehicleDismantle", "vehicleRefurbish"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must INTERSECT") + @Test + void evaluate_isAnyOf_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_ANY_OF, List.of("vehicleDismantle", "vehicleRefurbish"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must INTERSECT - failure") + @Test + void evaluate_isAnyOf_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_ANY_OF, List.of("vehicleCrush", "vehicleRefurbish"), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_ANY_OF, List.of(), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands).build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_ANY_OF, List.of("vehicleCrush", "vehicleRefurbish"), null, context)).isFalse(); + } + + @DisplayName("Constraint and credential must NOT INTERSECT") + @Test + void evaluate_isNoneOf_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_NONE_OF, List.of("vehicleRefurbish"), null, context)).isTrue(); + + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_NONE_OF, List.of(), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands).build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_NONE_OF, List.of("vehicleRefurbish"), null, context)).isTrue(); + } + + @DisplayName("Constraint and credential must NOT INTERSECT - failure") + @Test + void evaluate_isNoneOf_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle", "vehicleRefurbish").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IS_NONE_OF, List.of("vehicleRefurbish"), null, context)).isFalse(); + } + + @DisplayName("Activity list from credential must be FULLY CONTAINED within constraint") + @Test + void evaluate_in_satisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleRefurbish").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IN, List.of("vehicleRefurbish", "vehicleDismantle"), null, context)).isTrue(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IN, List.of("vehicleDismantle"), null, context)).isTrue(); + } + + @DisplayName("Activity list from credential must be FULLY CONTAINED within constraint - failure") + @Test + void evaluate_in_notSatisfied() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleRefurbish", "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IN, List.of("vehicleRefurbish", "vehicleDismantle"), null, context)).isFalse(); + + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleScrap").build()))); + assertThat(function.evaluate("Dismantler.activityType", Operator.IN, List.of("vehicleRefurbish", "vehicleDismantle"), null, context)).isFalse(); + + } + + @DisplayName("Illegal operator when constraint contains a list value") + @Test + void evaluate_illegalOperator_constraintIsList() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + var illegalOp = List.of(Operator.HAS_PART, Operator.GEQ, Operator.LEQ, Operator.GT, Operator.LT, Operator.IS_ALL_OF); + + assertThat(illegalOp).allSatisfy(op -> { + assertThat(function.evaluate("Dismantler.activityType", op, List.of("vehicleDismantle"), null, context)).isFalse(); + }); + } + + @DisplayName("Illegal operator when constraint contains a scalar value") + @Test + void evaluate_illegalOperator_constraintIsScalar() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + + var invalidOperators = new ArrayList<>(Arrays.asList(Operator.values())); + invalidOperators.remove(Operator.EQ); + invalidOperators.remove(Operator.NEQ); + + assertThat(invalidOperators).allSatisfy(op -> { + assertThat(function.evaluate("Dismantler.activityType", op, "vehicleDismantle", null, context)).isFalse(); + verify(context).reportProblem("Invalid operator: this constraint only allows the following operators: [EQ, NEQ], but received '%s'.".formatted(op)); + }); + } + + @DisplayName("Constraint right-operand has an invalid type") + @Test + void evaluate_righOpInvalidType() { + when(participantAgent.getClaims()).thenReturn(Map.of("vc", List.of(createDismantlerCredential(brands, "vehicleDismantle").build()))); + assertThat(function.evaluate("Dismantler.activityType", IN, Map.of("foo", "bar"), null, context)).isFalse(); + verify(context).reportProblem(startsWith("Invalid right-operand type: expected String or List, but received:")); + } + } +} \ No newline at end of file diff --git a/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunctionTest.java b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunctionTest.java index 9d9c0ceef..bfcd2513d 100644 --- a/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunctionTest.java +++ b/edc-extensions/cx-policy/src/test/java/org/eclipse/tractusx/edc/policy/cx/framework/FrameworkAgreementConstraintFunctionTest.java @@ -19,8 +19,6 @@ package org.eclipse.tractusx.edc.policy.cx.framework; -import org.eclipse.edc.identitytrust.model.CredentialSubject; -import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.model.Operator; @@ -30,14 +28,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.policy.cx.CredentialFunctions.createCredential; +import static org.eclipse.tractusx.edc.policy.cx.CredentialFunctions.createPcfCredential; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; @@ -67,7 +64,7 @@ void evaluate_leftOperandInvalid() { @Test void evaluate_invalidOperator() { assertThat(function.evaluate("FrameworkAgreement.foobar", Operator.HAS_PART, "irrelevant", permission, context)).isFalse(); - verify(context).reportProblem(eq("Invalid operator: allowed operators are [EQ, NEQ], got 'HAS_PART'.")); + verify(context).reportProblem(eq("Invalid operator: this constraint only allows the following operators: [EQ, NEQ], but received 'HAS_PART'.")); verifyNoMoreInteractions(context); } @@ -93,7 +90,7 @@ void evaluate_vcClaimNotListOfCredentials() { "vc", new Object() )); assertThat(function.evaluate("FrameworkAgreement.foobar", Operator.EQ, "active:0.0.1", permission, context)).isFalse(); - verify(context).reportProblem(eq("ParticipantAgent contains a 'vc' claim, but the type is incorrect. Expected java.util.List, got java.lang.Object.")); + verify(context).reportProblem(eq("ParticipantAgent contains a 'vc' claim, but the type is incorrect. Expected java.util.List, received java.lang.Object.")); } @Test @@ -189,7 +186,7 @@ void evaluate_neq_oneViolates() { )); assertThat(function.evaluate("FrameworkAgreement", Operator.NEQ, "sustainability", permission, context)).isFalse(); } - + @Test void evaluate_neq_requiredCredentialFound_withCorrectVersion() { when(participantAgent.getClaims()).thenReturn(Map.of( @@ -202,25 +199,6 @@ void evaluate_neq_requiredCredentialFound_withCorrectVersion() { } - private VerifiableCredential.Builder createCredential(String type, String version) { - return VerifiableCredential.Builder.newInstance() - .types(List.of("VerifiableCredential", type)) - .id(UUID.randomUUID().toString()) - .issuer(new Issuer(UUID.randomUUID().toString(), Map.of("prop1", "val1"))) - .expirationDate(Instant.now().plus(365, ChronoUnit.DAYS)) - .issuanceDate(Instant.now()) - .credentialSubject(CredentialSubject.Builder.newInstance() - .id("subject-id") - .claim("https://w3id.org/catenax/credentials/v1.0.0/holderIdentifier", "did:web:holder") - .claim("https://w3id.org/catenax/credentials/v1.0.0/contractVersion", version) - .claim("https://w3id.org/catenax/credentials/v1.0.0/contractTemplate", "https://public.catena-x.org/contracts/pcf.v1.pdf") - .build()); - } - - private VerifiableCredential.Builder createPcfCredential() { - return createCredential("PcfCredential", "1.0.0"); - } - @Nested class LegacyLeftOperand { @Test