diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java index 44738abb26a..5351e6fdd6a 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainService.java @@ -26,12 +26,12 @@ import io.gravitee.definition.model.v4.flow.Flow; import io.gravitee.definition.model.v4.flow.selector.ChannelSelector; import io.gravitee.definition.model.v4.flow.selector.HttpSelector; -import io.gravitee.definition.model.v4.flow.selector.McpSelector; import io.gravitee.definition.model.v4.flow.selector.Selector; import io.gravitee.definition.model.v4.flow.selector.SelectorType; import io.gravitee.definition.model.v4.flow.step.Step; import io.gravitee.definition.model.v4.nativeapi.NativeFlow; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -40,7 +40,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -227,7 +226,7 @@ public void validatePathParameters(ApiType apiType, Stream apiFlows, Strea planFlows = planFlows == null ? Stream.empty() : planFlows; // group all flows in one stream final Stream flowsWithPathParam = filterFlowsWithPathParam(apiType, apiFlows, planFlows); - validatePathParamOverlapping(apiType, flowsWithPathParam); + checkOverlappingPaths(apiType, flowsWithPathParam); } private Stream filterFlowsWithPathParam(ApiType apiType, Stream apiFlows, Stream planFlows) { @@ -236,57 +235,120 @@ private Stream filterFlowsWithPathParam(ApiType apiType, Stream apiF .filter(flow -> containsPathParam(apiType, flow)); } - private void validatePathParamOverlapping(ApiType apiType, Stream flows) { - Map paramWithPosition = new HashMap<>(); - Map> pathsByParam = new HashMap<>(); - final AtomicBoolean hasOverlap = new AtomicBoolean(false); - - flows.forEach(flow -> { - final String path = extractPath(apiType, flow); - String[] branches = SEPARATOR_SPLITTER.split(path); - for (int i = 0; i < branches.length; i++) { - final String currentBranch = branches[i]; - if (currentBranch.startsWith(PATH_PARAM_PREFIX)) { - // Store every path for a path param in a map - prepareOverlapsMap(pathsByParam, path, currentBranch); - if (isOverlapping(paramWithPosition, currentBranch, i)) { - // Exception is thrown later to be able to provide every overlapping case to the end user - hasOverlap.set(true); - } else { - paramWithPosition.put(currentBranch, i); - } + private void checkOverlappingPaths(ApiType apiType, Stream flows) { + // Extract unique, non-empty paths from enabled flows + List uniquePaths = flows + .map(flow -> extractPath(apiType, flow)) + .map(this::normalizePath) // normalize to avoid ambiguity due to slashes/case + .filter(path -> !path.isEmpty()) + .distinct() + .toList(); + + Map> overlappingPaths = new HashMap<>(); + int pathCount = uniquePaths.size(); + + for (int i = 0; i < pathCount; i++) { + String path1 = uniquePaths.get(i); + String[] segments1 = splitPathSegments(path1); + + for (int j = i + 1; j < pathCount; j++) { + String path2 = uniquePaths.get(j); + String[] segments2 = splitPathSegments(path2); + + if (segments1.length != segments2.length) continue; + + if (arePathsAmbiguous(segments1, segments2)) { + // Use a deterministic grouping key to avoid merging unrelated conflicts + String key = buildAmbiguitySignature(segments1); + Set paths = overlappingPaths.computeIfAbsent(key, k -> new HashSet<>()); + paths.add(path1); + paths.add(path2); } } - }); + } - if (hasOverlap.get()) { - throw new ValidationDomainException( - "Some path parameters are used at different position across different flows.", - pathsByParam - .entrySet() - .stream() - // Only keep params with overlap - .filter(entry -> entry.getValue().size() > 1) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().toString())) - ); + if (!overlappingPaths.isEmpty()) { + // Sort lists for stable output + Map payload = overlappingPaths + .entrySet() + .stream() + .collect( + Collectors.toMap(Map.Entry::getKey, entry -> { + List sortedPaths = new ArrayList<>(entry.getValue()); + sortedPaths.sort(String::compareTo); + return sortedPaths.toString(); + }) + ); + + throw new ValidationDomainException("Invalid path parameters", payload); } } - private static void prepareOverlapsMap(Map> pathsByParam, String path, String branches) { - pathsByParam.compute(branches, (key, value) -> { - if (value == null) { - value = new ArrayList<>(); - } - // Add the path only once to the error message - if (!value.contains(path)) { - value.add(path); - } - return value; - }); + /** + * Returns true if the two paths (split into segments) are ambiguous per OpenAPI 3.0: + * - Same number of segments + * - For each segment: both are parameters, or both are static and equal + */ + private boolean arePathsAmbiguous(String[] segments1, String[] segments2) { + for (int i = 0; i < segments1.length; i++) { + boolean isParam1 = segments1[i].startsWith(PATH_PARAM_PREFIX); + boolean isParam2 = segments2[i].startsWith(PATH_PARAM_PREFIX); + + if (isParam1 && isParam2) continue; + + if (!isParam1 && !isParam2 && segments1[i].equals(segments2[i])) continue; + + return false; + } + + return true; + } + + /** + * Normalize path: + * - Collapse multiple slashes + * - Remove trailing slash (except root "/") + * - Lowercase literals if routing is case-insensitive; keeping case as-is here + */ + private String normalizePath(String raw) { + if (raw == null) return ""; + String p = raw.trim(); + + if (p.isEmpty()) return ""; + // Collapse multiple slashes + p = p.replaceAll("/{2,}", PATH_SEPARATOR); + // Remove trailing slash except root + if (p.length() > 1 && p.endsWith(PATH_SEPARATOR)) { + p = p.substring(0, p.length() - 1); + } + // Ensure leading slash for consistency + if (!p.startsWith(PATH_SEPARATOR)) { + p = PATH_SEPARATOR + p; + } + + return p; + } + + /** + * Split path into non-empty segments after normalization. + */ + private String[] splitPathSegments(String path) { + return Arrays.stream(SEPARATOR_SPLITTER.split(path)) + .filter(s -> !s.isEmpty()) + .toArray(String[]::new); } - private static boolean isOverlapping(Map paramWithPosition, String param, Integer i) { - return paramWithPosition.containsKey(param) && !paramWithPosition.get(param).equals(i); + /** + * Build a deterministic ambiguity signature by replacing any parameter segment with ":" and keeping literals. + * Example: /users/:id/orders -> /users/:/orders + */ + private String buildAmbiguitySignature(String[] segments) { + return ( + PATH_SEPARATOR + + Arrays.stream(segments) + .map(s -> s.startsWith(PATH_PARAM_PREFIX) ? PATH_PARAM_PREFIX : s) + .collect(Collectors.joining(PATH_SEPARATOR)) + ); } private static Boolean containsPathParam(ApiType apiType, Flow flow) { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/exception/PathParameterOverlapValidationException.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/exception/PathParameterOverlapValidationException.java deleted file mode 100644 index 4c0e9e6d402..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/exception/PathParameterOverlapValidationException.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright © 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ -package io.gravitee.rest.api.service.v4.exception; - -import io.gravitee.rest.api.service.exceptions.AbstractValidationException; -import java.util.HashMap; -import java.util.Map; - -/** - * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) - * @author GraviteeSource Team - */ -public class PathParameterOverlapValidationException extends AbstractValidationException { - - private final Map overlaps; - - public PathParameterOverlapValidationException(Map overlaps) { - super(); - this.overlaps = overlaps; - } - - @Override - public String getMessage() { - return "Some path parameters are used at different position across different flows."; - } - - @Override - public String getTechnicalCode() { - return "api.pathparams.overlap"; - } - - @Override - public Map getParameters() { - return new HashMap<>(); - } - - @Override - public String getDetailMessage() { - return "There is a path parameter overlap"; - } - - @Override - public Map getConstraints() { - return overlaps; - } -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/PlanServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/PlanServiceImpl.java index 74ebfe24cfb..8b94cf4095a 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/PlanServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/PlanServiceImpl.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.gravitee.apim.core.audit.model.AuditInfo; import io.gravitee.apim.core.flow.crud_service.FlowCrudService; +import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService; import io.gravitee.apim.core.subscription.domain_service.CloseSubscriptionDomainService; import io.gravitee.definition.model.v4.ApiType; import io.gravitee.definition.model.v4.flow.Flow; @@ -83,7 +84,6 @@ import io.gravitee.rest.api.service.v4.mapper.GenericPlanMapper; import io.gravitee.rest.api.service.v4.mapper.PlanMapper; import io.gravitee.rest.api.service.v4.validation.FlowValidationService; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; import io.gravitee.rest.api.service.v4.validation.TagsValidationService; import java.util.Arrays; import java.util.Collection; @@ -176,7 +176,7 @@ public class PlanServiceImpl extends AbstractService implements PlanService { private FlowValidationService flowValidationService; @Autowired - private PathParametersValidationService pathParametersValidationService; + private FlowValidationDomainService flowValidationDomainService; @Autowired private GroupService groupService; @@ -265,7 +265,7 @@ private void validatePathParameters(Api api, List newPlanFlows) throws Tec .flatMap(Collection::stream); planFlows = Stream.concat(planFlows, newPlanFlows.stream()); - pathParametersValidationService.validate(api.getType(), apiFlows, planFlows); + flowValidationDomainService.validatePathParameters(api.getType(), apiFlows, planFlows); } private void validateTags(Set tags, Api api) { @@ -435,7 +435,7 @@ private void validatePathParameters(Api api, UpdatePlanEntity updatePlan) throws }) .flatMap(Collection::stream); - pathParametersValidationService.validate(api.getType(), apiFlows, planFlows); + flowValidationDomainService.validatePathParameters(api.getType(), apiFlows, planFlows); } private void checkStatusOfGeneralConditions(Plan plan) { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java index 8afa2ccadb8..2649a4f6c2d 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImpl.java @@ -21,6 +21,7 @@ import static io.gravitee.rest.api.model.api.ApiLifecycleState.UNPUBLISHED; import static org.apache.commons.lang3.StringUtils.isBlank; +import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService; import io.gravitee.definition.model.DefinitionVersion; import io.gravitee.definition.model.v4.ApiType; import io.gravitee.definition.model.v4.flow.Flow; @@ -51,7 +52,6 @@ import io.gravitee.rest.api.service.v4.validation.FlowValidationService; import io.gravitee.rest.api.service.v4.validation.GroupValidationService; import io.gravitee.rest.api.service.v4.validation.ListenerValidationService; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; import io.gravitee.rest.api.service.v4.validation.PlanValidationService; import io.gravitee.rest.api.service.v4.validation.ResourcesValidationService; import io.gravitee.rest.api.service.v4.validation.TagsValidationService; @@ -78,8 +78,8 @@ public class ApiValidationServiceImpl extends TransactionalService implements Ap private final AnalyticsValidationService analyticsValidationService; private final PlanSearchService planSearchService; private final PlanValidationService planValidationService; - private final PathParametersValidationService pathParametersValidationService; private final ApiServicePluginService apiServicePluginService; + private final FlowValidationDomainService flowValidationDomainService; public ApiValidationServiceImpl( final TagsValidationService tagsValidationService, @@ -91,8 +91,8 @@ public ApiValidationServiceImpl( final AnalyticsValidationService loggingValidationService, final PlanSearchService planSearchService, final PlanValidationService planValidationService, - final PathParametersValidationService pathParametersValidationService, - ApiServicePluginService apiServicePluginService + ApiServicePluginService apiServicePluginService, + FlowValidationDomainService flowValidationDomainService ) { this.tagsValidationService = tagsValidationService; this.groupValidationService = groupValidationService; @@ -103,8 +103,8 @@ public ApiValidationServiceImpl( this.analyticsValidationService = loggingValidationService; this.planSearchService = planSearchService; this.planValidationService = planValidationService; - this.pathParametersValidationService = pathParametersValidationService; this.apiServicePluginService = apiServicePluginService; + this.flowValidationDomainService = flowValidationDomainService; } @Override @@ -143,7 +143,7 @@ public void validateAndSanitizeNewApi( // Validate and clean flow newApiEntity.setFlows(flowValidationService.validateAndSanitize(newApiEntity.getType(), newApiEntity.getFlows())); - pathParametersValidationService.validate( + flowValidationDomainService.validatePathParameters( newApiEntity.getType(), (newApiEntity.getFlows() != null ? newApiEntity.getFlows().stream() : Stream.empty()), Stream.empty() @@ -204,7 +204,7 @@ public void validateAndSanitizeUpdateApi( updateApiEntity.setPlans(planValidationService.validateAndSanitize(updateApiEntity.getType(), updateApiEntity.getPlans())); // Validate path parameters - pathParametersValidationService.validate( + flowValidationDomainService.validatePathParameters( updateApiEntity.getType(), (updateApiEntity.getFlows() != null ? updateApiEntity.getFlows().stream() : Stream.empty()), getPlansFlows(updateApiEntity.getPlans()) @@ -258,7 +258,7 @@ public void validateAndSanitizeImportApiForCreation( apiEntity.setPlans(planValidationService.validateAndSanitize(apiEntity.getType(), apiEntity.getPlans())); // Validate path parameters - pathParametersValidationService.validate( + flowValidationDomainService.validatePathParameters( apiEntity.getType(), (apiEntity.getFlows() != null ? apiEntity.getFlows().stream() : Stream.empty()), getPlansFlows(apiEntity.getPlans()) diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImpl.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImpl.java deleted file mode 100644 index f1d7aac8ab6..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImpl.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright © 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ -package io.gravitee.rest.api.service.v4.impl.validation; - -import io.gravitee.definition.model.v4.Api; -import io.gravitee.definition.model.v4.ApiType; -import io.gravitee.definition.model.v4.flow.Flow; -import io.gravitee.definition.model.v4.flow.selector.ChannelSelector; -import io.gravitee.definition.model.v4.flow.selector.HttpSelector; -import io.gravitee.definition.model.v4.flow.selector.SelectorType; -import io.gravitee.rest.api.service.v4.exception.PathParameterOverlapValidationException; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; -import jakarta.validation.constraints.NotNull; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.springframework.stereotype.Component; - -/** - * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) - * @author GraviteeSource Team - * @deprecated Use {@link io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService} instead - */ -@Component -@Deprecated -public class PathParametersValidationServiceImpl implements PathParametersValidationService { - - private static final String PATH_PARAM_PREFIX = ":"; - private static final String PATH_SEPARATOR = "/"; - private static final Pattern SEPARATOR_SPLITTER = Pattern.compile(PATH_SEPARATOR); - private static final Pattern PARAM_PATTERN = Pattern.compile(":\\w*"); - - private static final Map>> PATH_EXTRACTOR = Map.of( - ApiType.PROXY, - flow -> - flow - .selectorByType(SelectorType.HTTP) - .stream() - .map(selector -> ((HttpSelector) selector).getPath()) - .findFirst(), - ApiType.MESSAGE, - flow -> - flow - .selectorByType(SelectorType.CHANNEL) - .stream() - .map(selector -> ((ChannelSelector) selector).getChannel()) - .findFirst(), - ApiType.LLM_PROXY, - flow -> - flow - .selectorByType(SelectorType.HTTP) - .stream() - .map(selector -> ((HttpSelector) selector).getPath()) - .findFirst() - ); - - @Override - public void validate(ApiType apiType, Stream apiFlows, Stream planFlows) { - apiFlows = apiFlows == null ? Stream.empty() : apiFlows; - planFlows = planFlows == null ? Stream.empty() : planFlows; - // group all flows in one stream - final Stream flowsWithPathParam = filterFlowsWithPathParam(apiType, apiFlows, planFlows); - // foreach flow, reprendre PathParameters.extractPathParamsAndPAttern - validatePathParamOverlapping(apiType, flowsWithPathParam); - } - - private Stream filterFlowsWithPathParam(ApiType apiType, Stream apiFlows, Stream planFlows) { - return Stream.concat(apiFlows, planFlows) - .filter(Flow::isEnabled) - .filter(flow -> containsPathParam(apiType, flow)); - } - - private void validatePathParamOverlapping(ApiType apiType, Stream flows) { - Map paramWithPosition = new HashMap<>(); - Map> pathsByParam = new HashMap<>(); - final AtomicBoolean hasOverlap = new AtomicBoolean(false); - - flows.forEach(flow -> { - final String path = extractPath(apiType, flow); - String[] branches = SEPARATOR_SPLITTER.split(path); - for (int i = 0; i < branches.length; i++) { - final String currentBranch = branches[i]; - if (currentBranch.startsWith(PATH_PARAM_PREFIX)) { - // Store every path for a path param in a map - prepareOverlapsMap(pathsByParam, path, currentBranch); - if (isOverlapping(paramWithPosition, currentBranch, i)) { - // Exception is thrown later to be able to provide every overlapping case to the end user - hasOverlap.set(true); - } else { - paramWithPosition.put(currentBranch, i); - } - } - } - }); - - if (hasOverlap.get()) { - throw new PathParameterOverlapValidationException( - pathsByParam - .entrySet() - .stream() - // Only keep params with overlap - .filter(entry -> entry.getValue().size() > 1) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().toString())) - ); - } - } - - private static void prepareOverlapsMap(Map> pathsByParam, String path, String branches) { - pathsByParam.compute(branches, (key, value) -> { - if (value == null) { - value = new ArrayList<>(); - } - // Add the path only once to the error message - if (!value.contains(path)) { - value.add(path); - } - return value; - }); - } - - private static boolean isOverlapping(Map paramWithPosition, String param, Integer i) { - return paramWithPosition.containsKey(param) && !paramWithPosition.get(param).equals(i); - } - - private static Boolean containsPathParam(ApiType apiType, Flow flow) { - final String path = extractPath(apiType, flow); - return PARAM_PATTERN.asPredicate().test(path); - } - - private static String extractPath(ApiType apiType, Flow flow) { - return PATH_EXTRACTOR.get(apiType).apply(flow).orElse(""); - } -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/validation/PathParametersValidationService.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/validation/PathParametersValidationService.java deleted file mode 100644 index cacfb2151e7..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/main/java/io/gravitee/rest/api/service/v4/validation/PathParametersValidationService.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ -package io.gravitee.rest.api.service.v4.validation; - -import io.gravitee.definition.model.v4.ApiType; -import io.gravitee.definition.model.v4.flow.Flow; -import java.util.stream.Stream; - -/** - * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) - * @author GraviteeSource Team - * @deprecated Use {@link io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService} instead - */ -@Deprecated -public interface PathParametersValidationService { - void validate(ApiType apiType, Stream apiFlows, Stream planFlows); -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java index 6fd3f321dbd..ffb96779ea9 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/flow/domain_service/FlowValidationDomainServiceTest.java @@ -18,6 +18,8 @@ import static inmemory.EntrypointPluginQueryServiceInMemory.SSE_CONNECTOR_ID; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -44,6 +46,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Stream; import org.assertj.core.api.Condition; @@ -60,7 +63,6 @@ public class FlowValidationDomainServiceTest { PolicyValidationDomainService policyValidationDomainService; EntrypointPluginQueryService entrypointPluginQueryService = new EntrypointPluginQueryServiceInMemory(); - FlowValidationDomainService service; @BeforeEach @@ -231,6 +233,116 @@ public void should_throw_exception_with_invalid_entrypoints() { .asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class)) .contains(entry("flowName", "bad_flow"), entry("invalidEntrypoints", "unknown2,unknown")); } + + @Test + void should_not_throw_on_valid_openapi_with_same_hierarchy_different_param_names() { + // Simulate flows for the OpenAPI spec in the problem statement + Flow flow1 = new Flow(); + flow1.setEnabled(true); + HttpSelector selector1 = new HttpSelector(); + selector1.setPath("/factFiles/:activityId"); + flow1.setSelectors(List.of(selector1)); + + Flow flow2 = new Flow(); + flow2.setEnabled(true); + HttpSelector selector2 = new HttpSelector(); + selector2.setPath("/factFiles/:activityId/:id"); + flow2.setSelectors(List.of(selector2)); + + Flow flow3 = new Flow(); + flow3.setEnabled(true); + HttpSelector selector3 = new HttpSelector(); + selector3.setPath("/facts/:locationNodeId/:timeNodeId/:versionNodeId"); + flow3.setSelectors(List.of(selector3)); + + Flow flow4 = new Flow(); + flow4.setEnabled(true); + HttpSelector selector4 = new HttpSelector(); + selector4.setPath("/facts/:locationNodeId/:timeNodeId/:versionNodeId/:id"); + flow4.setSelectors(List.of(selector4)); + + // Add all flows to a stream + Stream flows = Stream.of(flow1, flow2, flow3, flow4); + + // Should not throw + assertThatNoException().isThrownBy(() -> service.validatePathParameters(ApiType.PROXY, flows, Stream.empty())); + } + + @Test + void should_not_throw_on_different_segment_counts() { + Flow flow1 = new Flow(); + flow1.setEnabled(true); + HttpSelector selector1 = new HttpSelector(); + selector1.setPath("/products/:productId/items/:itemId"); + flow1.setSelectors(List.of(selector1)); + + Flow flow2 = new Flow(); + flow2.setEnabled(true); + HttpSelector selector2 = new HttpSelector(); + selector2.setPath("/:productId"); + flow2.setSelectors(List.of(selector2)); + + assertThatNoException().isThrownBy(() -> + service.validatePathParameters(ApiType.PROXY, Stream.of(flow1, flow2), Stream.empty()) + ); + } + + @Test + void should_not_throw_on_static_vs_param_segment() { + Flow flow1 = new Flow(); + flow1.setEnabled(true); + HttpSelector selector1 = new HttpSelector(); + selector1.setPath("/products/:productId/items/:itemId"); + flow1.setSelectors(List.of(selector1)); + + Flow flow2 = new Flow(); + flow2.setEnabled(true); + HttpSelector selector2 = new HttpSelector(); + selector2.setPath("/products/:productId/items/static"); + flow2.setSelectors(List.of(selector2)); + + assertThatNoException().isThrownBy(() -> + service.validatePathParameters(ApiType.PROXY, Stream.of(flow1, flow2), Stream.empty()) + ); + } + + @Test + void should_throw_on_same_structure_all_params() { + Flow flow1 = new Flow(); + flow1.setEnabled(true); + HttpSelector selector1 = new HttpSelector(); + selector1.setPath("/products/:productId/items/:itemId"); + flow1.setSelectors(List.of(selector1)); + + Flow flow2 = new Flow(); + flow2.setEnabled(true); + HttpSelector selector2 = new HttpSelector(); + selector2.setPath("/products/:id/items/:itemId"); + flow2.setSelectors(List.of(selector2)); + + assertThatThrownBy(() -> service.validatePathParameters(ApiType.PROXY, Stream.of(flow1, flow2), Stream.empty())).isInstanceOf( + ValidationDomainException.class + ); + } + + @Test + void should_not_throw_on_duplicate_paths() { + Flow flow1 = new Flow(); + flow1.setEnabled(true); + HttpSelector selector1 = new HttpSelector(); + selector1.setPath("/products/:productId/items/:itemId"); + flow1.setSelectors(List.of(selector1)); + + Flow flow2 = new Flow(); + flow2.setEnabled(true); + HttpSelector selector2 = new HttpSelector(); + selector2.setPath("/products/:productId/items/:itemId"); + flow2.setSelectors(List.of(selector2)); + + assertThatNoException().isThrownBy(() -> + service.validatePathParameters(ApiType.PROXY, Stream.of(flow1, flow2), Stream.empty()) + ); + } } @Nested @@ -319,26 +431,8 @@ void should_test_overlapping_cases(String apiName, Map> exp public static Stream provideParameters() { return Stream.of( - Arguments.of("api-proxy-flows-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of("api-proxy-plans-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of( - "api-proxy-plans-and-flows-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), Arguments.of("api-proxy-no-overlap", Map.of()), Arguments.of("api-proxy-no-flows", Map.of()), - Arguments.of( - "api-message-flows-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), - Arguments.of( - "api-message-plans-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), - Arguments.of( - "api-message-plans-and-flows-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), Arguments.of("api-message-no-overlap", Map.of()), Arguments.of("api-message-no-flows", Map.of()), Arguments.of("api-mcp-proxy-no-flows", Map.of()), @@ -357,8 +451,7 @@ private static Api readApi(String name) throws IOException { @NotNull private static Stream getPlanFlows(Api api) { - return api - .getPlans() + return Objects.requireNonNull(api.getPlans()) .stream() .flatMap(plan -> plan.getFlows() == null ? Stream.empty() : plan.getFlows().stream()); } diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/CreatePlanDomainServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/CreatePlanDomainServiceTest.java index df3ec1ebbe2..4f095f576a1 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/CreatePlanDomainServiceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/CreatePlanDomainServiceTest.java @@ -52,8 +52,6 @@ import io.gravitee.apim.core.policy.domain_service.PolicyValidationDomainService; import io.gravitee.apim.infra.json.jackson.JacksonJsonDiffProcessor; import io.gravitee.common.utils.TimeProvider; -import io.gravitee.definition.model.flow.Operator; -import io.gravitee.definition.model.v4.ApiType; import io.gravitee.definition.model.v4.flow.AbstractFlow; import io.gravitee.definition.model.v4.flow.Flow; import io.gravitee.definition.model.v4.flow.selector.ChannelSelector; @@ -303,30 +301,6 @@ void should_throw_when_general_conditions_page_is_not_published_while_updating_a .hasMessage("Plan references a non published page as general conditions"); } - @ParameterizedTest - @MethodSource("httpPlans") - void should_throw_when_flows_contains_overlapped_path_parameters(Api api, Plan plan) { - // Given - var selector1 = api.getType() == ApiType.PROXY - ? HttpSelector.builder().path("/products/:productId/items/:itemId").pathOperator(Operator.STARTS_WITH).build() - : ChannelSelector.builder().channel("/products/:productId/items/:itemId").channelOperator(Operator.STARTS_WITH).build(); - var selector2 = api.getType() == ApiType.PROXY - ? HttpSelector.builder().path("/:productId").pathOperator(Operator.STARTS_WITH).build() - : ChannelSelector.builder().channel("/:productId").channelOperator(Operator.STARTS_WITH).build(); - var invalidFlows = List.of( - Flow.builder().name("flow1").selectors(List.of(selector1)).build(), - Flow.builder().name("flow2").selectors(List.of(selector2)).build() - ); - - // When - var throwable = Assertions.catchThrowable(() -> service.create(plan, invalidFlows, api, AUDIT_INFO)); - - // Then - assertThat(throwable) - .isInstanceOf(ValidationDomainException.class) - .hasMessage("Some path parameters are used at different position across different flows."); - } - @ParameterizedTest @MethodSource("plans") void should_create_plan(Api api, Plan plan, List flows) { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/UpdatePlanDomainServiceTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/UpdatePlanDomainServiceTest.java index 1e444718432..7d8838fec8a 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/UpdatePlanDomainServiceTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/apim/core/plan/domain_service/UpdatePlanDomainServiceTest.java @@ -51,8 +51,6 @@ import io.gravitee.apim.infra.json.jackson.JacksonJsonDiffProcessor; import io.gravitee.common.utils.TimeProvider; import io.gravitee.definition.model.DefinitionVersion; -import io.gravitee.definition.model.flow.Operator; -import io.gravitee.definition.model.v4.ApiType; import io.gravitee.definition.model.v4.flow.Flow; import io.gravitee.definition.model.v4.flow.selector.ChannelSelector; import io.gravitee.definition.model.v4.flow.selector.HttpSelector; @@ -393,31 +391,6 @@ void should_throw_when_flows_are_invalid(Api api, Plan plan) { .hasMessageContaining("The flow [invalid] contains selectors that couldn't apply"); } - @ParameterizedTest - @MethodSource("io.gravitee.apim.core.plan.domain_service.UpdatePlanDomainServiceTest#v4testData") - void should_throw_when_flows_contains_overlapped_path_parameters(Api api, Plan plan) { - // Given - givenExistingPlan(plan); - var selector1 = api.getType() == ApiType.PROXY - ? HttpSelector.builder().path("/products/:productId/items/:itemId").pathOperator(Operator.STARTS_WITH).build() - : ChannelSelector.builder().channel("/products/:productId/items/:itemId").channelOperator(Operator.STARTS_WITH).build(); - var selector2 = api.getType() == ApiType.PROXY - ? HttpSelector.builder().path("/:productId").pathOperator(Operator.STARTS_WITH).build() - : ChannelSelector.builder().channel("/:productId").channelOperator(Operator.STARTS_WITH).build(); - var invalidFlows = List.of( - Flow.builder().name("flow1").selectors(List.of(selector1)).build(), - Flow.builder().name("flow2").selectors(List.of(selector2)).build() - ); - - // When - var throwable = Assertions.catchThrowable(() -> service.update(plan, invalidFlows, null, api, AUDIT_INFO)); - - // Then - assertThat(throwable) - .isInstanceOf(ValidationDomainException.class) - .hasMessage("Some path parameters are used at different position across different flows."); - } - @ParameterizedTest @MethodSource("io.gravitee.apim.core.plan.domain_service.UpdatePlanDomainServiceTest#v4testData") void should_throw_when_general_conditions_page_is_not_published_while_updating_a_published_plan( diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/PlanService_CreateOrUpdateTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/PlanService_CreateOrUpdateTest.java index 509c36b91dd..ddb65bf75d2 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/PlanService_CreateOrUpdateTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/PlanService_CreateOrUpdateTest.java @@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.gravitee.apim.core.flow.crud_service.FlowCrudService; +import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService; import io.gravitee.definition.model.v4.flow.Flow; import io.gravitee.definition.model.v4.plan.PlanMode; import io.gravitee.definition.model.v4.plan.PlanSecurity; @@ -53,7 +54,6 @@ import io.gravitee.rest.api.service.v4.PlanService; import io.gravitee.rest.api.service.v4.mapper.PlanMapper; import io.gravitee.rest.api.service.v4.validation.FlowValidationService; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; import io.gravitee.rest.api.service.v4.validation.TagsValidationService; import java.util.List; import java.util.Optional; @@ -75,7 +75,6 @@ public class PlanService_CreateOrUpdateTest { private static final String PLAN_ID = UUID.randomUUID().toString(); - private static final String ENVIRONMENT_ID = "my-environment"; public static final String API_ID = "api-id"; @Spy @@ -116,7 +115,7 @@ public class PlanService_CreateOrUpdateTest { private ApiRepository apiRepository; @Mock - private PathParametersValidationService pathParametersValidationService; + private FlowValidationDomainService flowValidationDomainService; @Mock private TagsValidationService tagsValidationService; @@ -153,7 +152,7 @@ public void shouldUpdateAndHaveId() throws TechnicalException { assertThat(actual.getId()).isEqualTo(expected.getId()); verify(planSearchService, times(1)).findById(GraviteeContext.getExecutionContext(), PLAN_ID); - verify(pathParametersValidationService, never()).validate(any(), any(), any()); + verify(flowValidationDomainService, never()).validatePathParameters(any(), any(), any()); } @Test @@ -169,7 +168,7 @@ public void shouldUpdateAndHaveNoId() throws TechnicalException { final PlanEntity actual = planService.createOrUpdatePlan(GraviteeContext.getExecutionContext(), planEntity); assertThat(actual.getId()).isEqualTo(expected.getId()); - verify(pathParametersValidationService, never()).validate(any(), any(), any()); + verify(flowValidationDomainService, never()).validatePathParameters(any(), any(), any()); } @Test @@ -188,7 +187,7 @@ public void shouldCreateAndHaveId() throws TechnicalException { assertThat(actual.getId()).isEqualTo(expected.getId()); verify(planSearchService, times(1)).findById(GraviteeContext.getExecutionContext(), PLAN_ID); - verify(pathParametersValidationService, never()).validate(any(), any(), any()); + verify(flowValidationDomainService, never()).validatePathParameters(any(), any(), any()); } @Test @@ -204,7 +203,7 @@ public void shouldCreateAndHaveNoId() throws TechnicalException { final PlanEntity actual = planService.createOrUpdatePlan(GraviteeContext.getExecutionContext(), planEntity); assertThat(actual.getId()).isEqualTo(expected.getId()); - verify(pathParametersValidationService, never()).validate(any(), any(), any()); + verify(flowValidationDomainService, never()).validatePathParameters(any(), any(), any()); } private void mockPrivateUpdate(PlanEntity expected) throws TechnicalException { diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java index cd4de0c9355..2be5220f1fd 100644 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java +++ b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/ApiValidationServiceImplTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.gravitee.apim.core.flow.domain_service.FlowValidationDomainService; import io.gravitee.definition.model.DefinitionVersion; import io.gravitee.definition.model.v4.ApiType; import io.gravitee.definition.model.v4.plan.PlanStatus; @@ -56,13 +57,11 @@ import io.gravitee.rest.api.service.v4.validation.FlowValidationService; import io.gravitee.rest.api.service.v4.validation.GroupValidationService; import io.gravitee.rest.api.service.v4.validation.ListenerValidationService; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; import io.gravitee.rest.api.service.v4.validation.PlanValidationService; import io.gravitee.rest.api.service.v4.validation.ResourcesValidationService; import io.gravitee.rest.api.service.v4.validation.TagsValidationService; import java.util.List; import java.util.Set; -import java.util.stream.Stream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -103,14 +102,14 @@ public class ApiValidationServiceImplTest { @Mock private PlanValidationService planValidationService; - @Mock - private PathParametersValidationService pathParametersValidationService; - @Mock private ApiServicePluginService apiServicePluginService; private ApiValidationService apiValidationService; + @Mock + private FlowValidationDomainService flowValidationDomainService; + @Before public void setUp() throws Exception { apiValidationService = new ApiValidationServiceImpl( @@ -123,8 +122,8 @@ public void setUp() throws Exception { loggingValidationService, planSearchService, planValidationService, - pathParametersValidationService, - apiServicePluginService + apiServicePluginService, + flowValidationDomainService ); } @@ -150,7 +149,7 @@ public void shouldCallOtherServicesWhenValidatingNewApiEntity() { verify(flowValidationService, times(1)).validateAndSanitize(newApiEntity.getType(), null); verify(resourcesValidationService, never()).validateAndSanitize(any()); verify(planValidationService, never()).validateAndSanitize(any(), any()); - verify(pathParametersValidationService, times(1)).validate(any(), any(), any()); + verify(flowValidationDomainService, times(1)).validatePathParameters(any(), any(), any()); } @Test @@ -179,7 +178,7 @@ public void shouldCallOtherServicesWhenValidatingImportApiForCreation() { verify(flowValidationService, times(1)).validateAndSanitize(apiEntity.getType(), null); verify(resourcesValidationService, times(1)).validateAndSanitize(List.of()); verify(planValidationService, times(1)).validateAndSanitize(apiEntity.getType(), Set.of()); - verify(pathParametersValidationService, times(1)).validate(eq(apiEntity.getType()), any(Stream.class), any(Stream.class)); + verify(flowValidationDomainService, times(1)).validatePathParameters(eq(apiEntity.getType()), any(), any()); } @Test(expected = InvalidDataException.class) diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImplTest.java b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImplTest.java deleted file mode 100644 index e30bea1832a..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/java/io/gravitee/rest/api/service/v4/impl/validation/PathParametersValidationServiceImplTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright © 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://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. - */ -package io.gravitee.rest.api.service.v4.impl.validation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.gravitee.definition.model.v4.Api; -import io.gravitee.definition.model.v4.flow.Flow; -import io.gravitee.rest.api.service.v4.exception.PathParameterOverlapValidationException; -import io.gravitee.rest.api.service.v4.validation.PathParametersValidationService; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import org.assertj.core.api.Condition; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -/** - * @author Yann TAVERNIER (yann.tavernier at graviteesource.com) - * @author GraviteeSource Team - */ -@Deprecated -class PathParametersValidationServiceImplTest { - - private PathParametersValidationService cut; - - @BeforeEach - void setUp() { - cut = new PathParametersValidationServiceImpl(); - } - - @ParameterizedTest - @MethodSource("provideParameters") - void should_test_overlaping_cases(String apiName, Map> expectedOverlaps) throws IOException { - final Api api = readApi(apiName); - - if (expectedOverlaps.isEmpty()) { - assertThatNoException().isThrownBy(() -> cut.validate(api.getType(), api.getFlows().stream(), getPlanFlows(api))); - } else { - assertThatThrownBy(() -> cut.validate(api.getType(), api.getFlows().stream(), getPlanFlows(api))) - .isInstanceOf(PathParameterOverlapValidationException.class) - .hasMessage("Some path parameters are used at different position across different flows.") - .is( - new Condition<>( - error -> { - final PathParameterOverlapValidationException pathParamException = - (PathParameterOverlapValidationException) error; - assertThat(pathParamException.getDetailMessage()).isEqualTo("There is a path parameter overlap"); - assertThat(pathParamException.getConstraints()).containsOnlyKeys(expectedOverlaps.keySet()); - expectedOverlaps.forEach((key, value) -> { - value.forEach(expectedPath -> { - assertThat(pathParamException.getConstraints().get(key)).contains(expectedPath); - }); - }); - return true; - }, - "" - ) - ); - } - } - - public static Stream provideParameters() { - return Stream.of( - Arguments.of("api-proxy-flows-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of("api-proxy-plans-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of( - "api-proxy-plans-and-flows-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), - Arguments.of("api-proxy-no-overlap", Map.of()), - Arguments.of("api-proxy-no-flows", Map.of()), - Arguments.of("api-message-flows-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of("api-message-plans-overlap", Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId"))), - Arguments.of( - "api-message-plans-and-flows-overlap", - Map.of(":productId", List.of("/products/:productId/items/:itemId", "/:productId")) - ), - Arguments.of("api-message-no-overlap", Map.of()), - Arguments.of("api-message-no-flows", Map.of()), - Arguments.of("api-llm-proxy-no-flows", Map.of()), - Arguments.of("api-llm-proxy-with-flows", Map.of()) - ); - } - - private static Api readApi(String name) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue( - PathParametersValidationServiceImplTest.class.getClassLoader().getResourceAsStream("apis/v4/pathparams/" + name + ".json"), - Api.class - ); - } - - @NotNull - private static Stream getPlanFlows(Api api) { - return api - .getPlans() - .stream() - .flatMap(plan -> plan.getFlows() == null ? Stream.empty() : plan.getFlows().stream()); - } -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-flows-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-flows-overlap.json deleted file mode 100644 index a87333ed8bb..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-flows-overlap.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "type": "message", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - - ] - } - ], - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "channel", - "channel": "/products/:productId/items/:itemId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "channel", - "channel": "/:productId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ], - "resources": [] -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-and-flows-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-and-flows-overlap.json deleted file mode 100644 index e6bae40e225..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-and-flows-overlap.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "type": "message", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "channel", - "channel": "/products/:productId/items/:itemId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "channel", - "channel": "/:productId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ] - } - ], - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "channel", - "channel": "/products/:productId/items/:itemId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "channel", - "channel": "/:productId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product from products", - "selectors": [ - { - "type": "channel", - "channel": "/products/:productId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ], - "resources": [] -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-overlap.json deleted file mode 100644 index edd8c1b198f..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-message-plans-overlap.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "message", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "channel", - "channel": "/products/:productId/items/:itemId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "channel", - "channel": "/:productId", - "channelOperator": "STARTS_WITH", - "operations": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ] - } - ], - "flows": [], - "resources": [] -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-flows-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-flows-overlap.json deleted file mode 100644 index efe440105b0..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-flows-overlap.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "type": "proxy", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - - ] - } - ], - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "http", - "path": "/products/:productId/items/:itemId", - "pathOperator": "STARTS_WITH", - "methods": ["GET"] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "http", - "path": "/:productId", - "pathOperator": "STARTS_WITH", - "methods": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ], - "resources": [] -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-and-flows-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-and-flows-overlap.json deleted file mode 100644 index a1ddeb26ec3..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-and-flows-overlap.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "type": "proxy", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "http", - "path": "/products/:productId/items/:itemId", - "pathOperator": "STARTS_WITH", - "methods": ["GET"] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "http", - "path": "/:productId", - "pathOperator": "STARTS_WITH", - "methods": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ] - } - ], - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "http", - "path": "/products/:productId/items/:itemId", - "pathOperator": "STARTS_WITH", - "methods": ["GET"] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "http", - "path": "/:productId", - "pathOperator": "STARTS_WITH", - "methods": [] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product from products", - "selectors": [ - { - "type": "http", - "path": "/products/:productId", - "pathOperator": "STARTS_WITH", - "methods": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ], - "resources": [] -} diff --git a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-overlap.json b/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-overlap.json deleted file mode 100644 index 4c90de95aae..00000000000 --- a/gravitee-apim-rest-api/gravitee-apim-rest-api-service/src/test/resources/apis/v4/pathparams/api-proxy-plans-overlap.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "type": "proxy", - "plans": [ - { - "id": "keyless", - "name": "Keyless", - "security": { - "type": "key-less" - }, - "flows": [ - { - "name": "Item", - "selectors": [ - { - "type": "http", - "path": "/products/:productId/items/:itemId", - "pathOperator": "STARTS_WITH", - "methods": ["GET"] - } - ], - "request": [], - "response": [], - "enabled": true - }, - { - "name": "Product", - "selectors": [ - { - "type": "http", - "path": "/:productId", - "pathOperator": "STARTS_WITH", - "methods": [] - } - ], - "request": [], - "response": [], - "enabled": true - } - ] - } - ], - "flows": [], - "resources": [] -}