From 261f18eba516f6375a532bb36a3f220783be5ba2 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Mon, 25 Sep 2023 17:37:44 +0200 Subject: [PATCH] Open api param validation (#745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added referenced query param * added tests for referenced param * New list concat, tests * added header params to yaml * added header to openapi request * refactoring * working on HeaderParam Validation WIP * added header param validation + test * updated OAS3 * added test header * Refactoring * optimized code * added status code test * Renamed getErrorsAndRemoveParam --------- Co-authored-by: Christian Gördes Co-authored-by: T. Burch --- .../core/openapi/OpenAPIValidator.java | 4 + .../membrane/core/openapi/model/Message.java | 17 +- .../membrane/core/openapi/model/Request.java | 4 +- .../core/openapi/util/OpenAPIUtil.java | 4 +- .../membrane/core/openapi/util/Utils.java | 6 + .../AbstractParameterValidator.java | 97 ++ .../validators/HeaderParameterValidator.java | 39 + .../validators/OperationValidator.java | 1 + .../validators/QueryParameterValidator.java | 91 +- .../openapi/validators/ValidationContext.java | 9 +- .../openapi/validators/ValidationErrors.java | 14 +- .../membrane/core/util/CollectionsUtil.java | 11 + .../com/predic8/membrane/core/UnitTests.java | 3 +- .../membrane/core/openapi/util/UtilsTest.java | 11 +- .../AbstractParameterValidatorTest.java | 36 + .../validators/AbstractValidatorTest.java | 2 +- .../validators/HeaderParameterTest.java | 75 ++ .../QueryParameterValidatorTest.java | 46 + .../openapi/validators/QueryParamsTest.java | 46 +- .../core/util/CollectionsUtilTest.java | 30 + .../specs/fruitshop-api-v2-openapi-3.yml | 1041 +++++++++++++++++ .../resources/openapi/specs/header-params.yml | 32 + .../resources/openapi/specs/query-params.yml | 21 +- .../examples/tests/PaddingHeaderTest.java | 10 +- 24 files changed, 1545 insertions(+), 105 deletions(-) create mode 100644 core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidator.java create mode 100644 core/src/main/java/com/predic8/membrane/core/openapi/validators/HeaderParameterValidator.java create mode 100644 core/src/main/java/com/predic8/membrane/core/util/CollectionsUtil.java create mode 100644 core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidatorTest.java create mode 100644 core/src/test/java/com/predic8/membrane/core/openapi/validators/HeaderParameterTest.java create mode 100644 core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidatorTest.java create mode 100644 core/src/test/java/com/predic8/membrane/core/util/CollectionsUtilTest.java create mode 100644 core/src/test/resources/openapi/specs/fruitshop-api-v2-openapi-3.yml create mode 100644 core/src/test/resources/openapi/specs/header-params.yml diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/OpenAPIValidator.java b/core/src/main/java/com/predic8/membrane/core/openapi/OpenAPIValidator.java index 342776f98..50be36530 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/OpenAPIValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/OpenAPIValidator.java @@ -119,4 +119,8 @@ private ValidationErrors validateMethods(ValidationContext ctx, Request req, Res .entityType(METHOD), format("Method %s is not allowed", req.getMethod())); } } + + public OpenAPI getApi() { + return api; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/model/Message.java b/core/src/main/java/com/predic8/membrane/core/openapi/model/Message.java index 562f4b6f1..c4f12a9da 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/model/Message.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/model/Message.java @@ -21,6 +21,7 @@ import org.slf4j.*; import java.io.*; +import java.util.Map; import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON_CONTENT_TYPE; @@ -30,13 +31,21 @@ public abstract class Message { protected Body body = new NoBody(); protected ContentType mediaType; + private Map headers; + protected Message() { + } - protected Message(String mediaType) throws ParseException { - this.mediaType = new ContentType(mediaType); + public Map getHeaders() { + return headers; } - protected Message() { + public void setHeaders(Map headers) { + this.headers = headers; + } + + protected Message(String mediaType) throws ParseException { + this.mediaType = new ContentType(mediaType); } public Body getBody() { @@ -101,6 +110,4 @@ public T json() { public ContentType getMediaType() { return mediaType; } - - } diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/model/Request.java b/core/src/main/java/com/predic8/membrane/core/openapi/model/Request.java index ccd37dc2e..ee09ed23f 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/model/Request.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/model/Request.java @@ -27,6 +27,7 @@ public class Request extends Message { private final UriTemplateMatcher uriTemplateMatcher = new UriTemplateMatcher(); private Map pathParameters; + public Request(String method) { this.method = method; } @@ -85,5 +86,4 @@ public String toString() { ", pathParameters=" + pathParameters + '}'; } - -} +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/util/OpenAPIUtil.java b/core/src/main/java/com/predic8/membrane/core/openapi/util/OpenAPIUtil.java index f56b6d06d..ccd999f2b 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/util/OpenAPIUtil.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/util/OpenAPIUtil.java @@ -23,8 +23,8 @@ import java.util.regex.*; -import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.X_MEMBRANE_ID; -import static com.predic8.membrane.core.openapi.util.Utils.normalizeForId; +import static com.predic8.membrane.core.openapi.serviceproxy.APIProxy.*; +import static com.predic8.membrane.core.openapi.util.Utils.*; public class OpenAPIUtil { diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java b/core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java index 6cf644300..f428b6b41 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java @@ -17,6 +17,7 @@ package com.predic8.membrane.core.openapi.util; import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.http.HeaderField; import com.predic8.membrane.core.openapi.model.*; import com.predic8.membrane.core.openapi.validators.*; import jakarta.mail.internet.*; @@ -136,6 +137,11 @@ public static boolean areThereErrors(ValidationErrors ve) { public static Request getOpenapiValidatorRequest(Exchange exc) throws IOException, ParseException { Request request = new Request(exc.getRequest().getMethod()).path(exc.getRequestURI()); + Map headers = new HashMap<>(); + for (HeaderField header : exc.getRequest().getHeader().getAllHeaderFields()) { + headers.put(header.getHeaderName().toString(), header.getValue()); + } + request.setHeaders(headers); if (exc.getRequest().getHeader().getContentType() != null) { request.mediaType(exc.getRequest().getHeader().getContentType()); } diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidator.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidator.java new file mode 100644 index 000000000..c71672baa --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidator.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022 predic8 GmbH, www.predic8.com + * + * 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 com.predic8.membrane.core.openapi.validators; + +import com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.parameters.Parameter; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static com.predic8.membrane.core.openapi.util.Utils.getComponentLocalNameFromRef; +import static com.predic8.membrane.core.util.CollectionsUtil.concat; +import static java.lang.String.format; + +public abstract class AbstractParameterValidator { + OpenAPI api; + PathItem pathItem; + + public AbstractParameterValidator(OpenAPI api, PathItem pathItem) { + this.api = api; + this.pathItem = pathItem; + } + + public Stream getParametersOfType(Operation operation, Class paramClazz) { + return getAllParameterSchemas(operation).stream().filter(p -> isTypeOf(p, paramClazz)); + } + + public List getAllParameterSchemas(Operation operation) { + return concat(resolveRefs(pathItem.getParameters()), resolveRefs(operation.getParameters())); + } + + boolean isTypeOf(Parameter p, Class clazz) { + return p.getClass().equals(clazz); + } + + private List resolveRefs(List parameters) { + if (parameters == null) + return null; + + return parameters.stream().map(this::resolveParamIfNeeded).toList(); + } + + private Parameter resolveParamIfNeeded(Parameter p ) { + if (p.get$ref() != null) + return resolveReferencedParameter(p); + return p; + } + + public ValidationErrors getValidationErrors(ValidationContext ctx, Map parameters, Parameter param, ValidatedEntityType type) { + return validateParameter(getCtx(ctx, param, type), parameters, param, type); + } + + private static ValidationContext getCtx(ValidationContext ctx, Parameter param, ValidatedEntityType type) { + return ctx.entity(param.getName()) + .entityType(type) + .statusCode(400); + } + + public Parameter resolveReferencedParameter(Parameter p) { + return api.getComponents().getParameters().get(getComponentLocalNameFromRef(p.get$ref())); + } + + public ValidationErrors validateParameter(ValidationContext ctx, Map params, Parameter param, ValidatedEntityType type) { + ValidationErrors errors = new ValidationErrors(); + String value = params.get(param.getName()); + + if (value != null) { + errors.add(new SchemaValidator(api, param.getSchema()).validate(ctx + .statusCode(400) + .entity(param.getName()) + .entityType(type) + , value)); + } else if (param.getRequired()) { + errors.add(ctx, format("Missing required %s %s.", type.name, param.getName())); + } + return errors; + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/HeaderParameterValidator.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/HeaderParameterValidator.java new file mode 100644 index 000000000..1585aa632 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/HeaderParameterValidator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 predic8 GmbH, www.predic8.com + * + * 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 com.predic8.membrane.core.openapi.validators; + +import com.predic8.membrane.core.openapi.model.Request; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.parameters.HeaderParameter; + +import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.HEADER_PARAMETER; + +public class HeaderParameterValidator extends AbstractParameterValidator{ + + public HeaderParameterValidator(OpenAPI api, PathItem pathItem) { + super(api, pathItem); + } + + ValidationErrors validateHeaderParameters(ValidationContext ctx, Request request, Operation operation) { + return getParametersOfType(operation, HeaderParameter.class) + .map(param -> getValidationErrors(ctx, request.getHeaders(), param, HEADER_PARAMETER)) + .reduce(ValidationErrors::add) + .orElse(new ValidationErrors()); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/OperationValidator.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/OperationValidator.java index 9c617aa28..d268e9a7c 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/validators/OperationValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/OperationValidator.java @@ -48,6 +48,7 @@ public ValidationErrors validateOperation(ValidationContext ctx, Request req, Re validatePathParameters(ctx, req, operation.getParameters()); errors.add(new QueryParameterValidator(api,pathItem).validateQueryParameters(ctx, req, operation)); + errors.add(new HeaderParameterValidator(api,pathItem).validateHeaderParameters(ctx, req, operation)); return errors.add(new RequestBodyValidator(api).validateRequestBody(ctx.entityType(BODY).entity("REQUEST"), operation, req)); } else { diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidator.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidator.java index 8969ddfb3..5bfb6a4d2 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidator.java @@ -16,50 +16,44 @@ package com.predic8.membrane.core.openapi.validators; -import com.predic8.membrane.core.openapi.model.*; -import com.predic8.membrane.core.util.*; -import io.swagger.v3.oas.models.*; -import io.swagger.v3.oas.models.parameters.*; - -import java.util.*; +import com.predic8.membrane.core.openapi.model.Request; +import com.predic8.membrane.core.util.URIFactory; +import com.predic8.membrane.core.util.URLParamUtil; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.QueryParameter; + +import java.util.HashMap; +import java.util.Map; import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.QUERY_PARAMETER; import static com.predic8.membrane.core.util.URLParamUtil.DuplicateKeyOrInvalidFormStrategy.ERROR; -import static java.lang.String.format; -import static java.util.Objects.requireNonNullElseGet; - -public class QueryParameterValidator { - OpenAPI api; - PathItem pathItem; +public class QueryParameterValidator extends AbstractParameterValidator{ public QueryParameterValidator(OpenAPI api, PathItem pathItem) { - this.api = api; - this.pathItem = pathItem; + super(api, pathItem); } ValidationErrors validateQueryParameters(ValidationContext ctx, Request request, Operation operation) { + Map queryParams = getQueryParams(getQueryString(request)); + return getParametersOfType(operation, QueryParameter.class) + .map(param -> getErrors(ctx, queryParams, param)) + .reduce(ValidationErrors::add) + .orElse(new ValidationErrors()) + .add(checkForAdditionalQueryParameters(ctx, queryParams)); + } - ValidationErrors errors = new ValidationErrors(); - -// Map qparams = request.getQueryParams(); - - // TODO - // Router? - String query = (new URIFactory().createWithoutException(request.getPath())).getQuery(); - Map qparams = getQueryParams(query); - - getAllParameterSchemas(operation).forEach(param -> { - if (!(param instanceof QueryParameter)) { - return; - } - errors.add(validateQueryParameter(ctx.entity(param.getName()).entityType(QUERY_PARAMETER), qparams, param)); - qparams.remove(param.getName()); // Delete param so there should't be any parameter left - }); - - errors.add(checkForAdditionalQueryParameters(ctx, qparams)); + private ValidationErrors getErrors(ValidationContext ctx, Map queryParams, Parameter param) { + ValidationErrors err = getValidationErrors(ctx, queryParams, param, QUERY_PARAMETER); + queryParams.remove(param.getName()); + return err; + } - return errors; + private static String getQueryString(Request request) { + return (new URIFactory().createWithoutException(request.getPath())).getQuery(); } private Map getQueryParams(String query) { @@ -68,37 +62,8 @@ private Map getQueryParams(String query) { return new HashMap<>(); } - private List getAllParameterSchemas(Operation operation) { - return concat(pathItem.getParameters(), operation.getParameters()); - } - - private static List concat(List l1, List l2) { - if (l1 == null) { - return requireNonNullElseGet(l2, ArrayList::new); - } - if (l2!=null) - l1.addAll(l2); - return l1; - } - - private ValidationErrors validateQueryParameter(ValidationContext ctx, Map qparams, Parameter param) { - ValidationErrors errors = new ValidationErrors(); - String value = qparams.get(param.getName()); - - if (value != null) { - errors.add(new SchemaValidator(api, param.getSchema()).validate(ctx - .statusCode(400) - .entity(param.getName()) - .entityType(QUERY_PARAMETER) - , value)); - } else if (param.getRequired()) { - errors.add(ctx, format("Missing required query parameter %s.", param.getName())); - } - return errors; - } - private ValidationError checkForAdditionalQueryParameters(ValidationContext ctx, Map qparams) { - if (qparams.size() > 0) { + if (!qparams.isEmpty()) { return new ValidationError(ctx.entityType(QUERY_PARAMETER), "There are query parameters that are not supported by the API: " + qparams.keySet()); } return null; diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationContext.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationContext.java index 795332da4..ec8849e25 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationContext.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationContext.java @@ -109,7 +109,7 @@ private String getLocation(String message) { sb.append("HEADER/Content-Type"); } else { sb.append(validatedEntityType.name()); - if (jsonPointer.length() > 0) { + if (!jsonPointer.isEmpty()) { sb.append("#"); sb.append(getJSONpointer()); } @@ -192,7 +192,12 @@ public ValidationContext addJSONpointerSegment(String segment) { } public enum ValidatedEntityType { - PATH, METHOD, PATH_PARAMETER, QUERY_PARAMETER, BODY, FIELD, PROPERTY, MEDIA_TYPE + PATH("path"), METHOD("method"), PATH_PARAMETER("path parameter"), QUERY_PARAMETER("query parameter"), HEADER_PARAMETER("header parameter"), BODY("body"), FIELD("field"), PROPERTY("property"), MEDIA_TYPE("media type"); + + public final String name; + ValidatedEntityType(String s) { + this.name = s; + } } @Override diff --git a/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationErrors.java b/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationErrors.java index 83a5933cb..854e11333 100644 --- a/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationErrors.java +++ b/core/src/main/java/com/predic8/membrane/core/openapi/validators/ValidationErrors.java @@ -39,6 +39,10 @@ public static ValidationErrors create(ValidationContext ctx, String message) { return ve; } + public List getErrors() { + return errors; + } + public ValidationErrors add(ValidationError error) { if (error != null) errors.add(error); @@ -76,16 +80,6 @@ public Stream stream() { return errors.stream(); } - /** - * Call with 400 or 500. Returns a more specifiy status code if there is any. - */ - public int getConsolidatedStatusCode(int defaultValue) { - return errors.stream().map(e -> e.getContext().getStatusCode()).reduce((code, acc) -> { - if (acc == defaultValue) return code; - return acc; - }).orElse(defaultValue); - } - public byte[] getErrorMessage(Direction direction) { if (errors.size() == 0) diff --git a/core/src/main/java/com/predic8/membrane/core/util/CollectionsUtil.java b/core/src/main/java/com/predic8/membrane/core/util/CollectionsUtil.java new file mode 100644 index 000000000..ea0e5d242 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/util/CollectionsUtil.java @@ -0,0 +1,11 @@ +package com.predic8.membrane.core.util; + +import java.util.*; +import java.util.stream.*; + +public class CollectionsUtil { + + public static List concat(List l1, List l2) { + return Stream.of(l1,l2).filter(Objects::nonNull).flatMap(Collection::stream).toList(); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/UnitTests.java b/core/src/test/java/com/predic8/membrane/core/UnitTests.java index b3ff85f23..6052d6384 100644 --- a/core/src/test/java/com/predic8/membrane/core/UnitTests.java +++ b/core/src/test/java/com/predic8/membrane/core/UnitTests.java @@ -132,7 +132,8 @@ JsonProtectionInterceptorTest.class, BeautifierInterceptorTest.class, ExchangeEvaluationContextTest.class, - PaddingHeaderInterceptorTest.class + PaddingHeaderInterceptorTest.class, + CollectionsUtilTest.class }) @SelectPackages({"com.predic8.membrane.core.openapi"}) public class UnitTests { diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/util/UtilsTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/util/UtilsTest.java index 009848a86..3c71f5ccf 100644 --- a/core/src/test/java/com/predic8/membrane/core/openapi/util/UtilsTest.java +++ b/core/src/test/java/com/predic8/membrane/core/openapi/util/UtilsTest.java @@ -17,6 +17,7 @@ package com.predic8.membrane.core.openapi.util; import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.http.Header; import com.predic8.membrane.core.openapi.model.*; import com.predic8.membrane.core.util.*; import jakarta.mail.internet.*; @@ -24,6 +25,8 @@ import java.io.*; import java.net.*; +import java.util.HashMap; +import java.util.Map; import static com.predic8.membrane.core.http.MimeType.APPLICATION_JSON; import static com.predic8.membrane.core.openapi.util.Utils.*; @@ -202,10 +205,16 @@ public void normalizeForId() { @Test void getOpenapiValidatorRequestFromExchange() throws IOException, ParseException { Exchange exc = new Exchange(null); + Header header = new Header(); + header.setValue("X-Padding", "V0hQCMkJV4mKigp"); exc.setOriginalRequestUri("/foo"); - exc.setRequest(new com.predic8.membrane.core.http.Request.Builder().method("POST").build()); + exc.setRequest(new com.predic8.membrane.core.http.Request.Builder().method("POST").header(header).build()); Request request = Utils.getOpenapiValidatorRequest(exc); assertEquals("/foo",request.getPath()); assertEquals("POST", request.getMethod()); + Map expectedHeaders = new HashMap<>(); + expectedHeaders.put("X-Padding", "V0hQCMkJV4mKigp"); + assertEquals(request.getHeaders(), expectedHeaders); } + } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidatorTest.java new file mode 100644 index 000000000..2c5132faf --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractParameterValidatorTest.java @@ -0,0 +1,36 @@ +package com.predic8.membrane.core.openapi.validators; + +import com.predic8.membrane.core.interceptor.rest.QueryParameter; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.parameters.HeaderParameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractParameterValidatorTest { + + ParameterValidatorMock validatorMock; + + static class ParameterValidatorMock extends AbstractParameterValidator{ + public ParameterValidatorMock(OpenAPI api, PathItem pathItem) { + super(api, pathItem); + } + } + + @BeforeEach + void setup() { + validatorMock = new ParameterValidatorMock(null, null); + } + + @Test + void isNotTypeOfTest() { + assertFalse(validatorMock.isTypeOf(new HeaderParameter(), QueryParameter.class)); + } + + @Test + void isTypeOfTest() { + assertTrue(validatorMock.isTypeOf(new HeaderParameter(), HeaderParameter.class)); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractValidatorTest.java index a89a3de06..7d20ecb94 100644 --- a/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractValidatorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/openapi/validators/AbstractValidatorTest.java @@ -39,4 +39,4 @@ public void setUp() { public InputStream getResourceAsStream(String fileName) { return this.getClass().getResourceAsStream(fileName); } -} +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/validators/HeaderParameterTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/validators/HeaderParameterTest.java new file mode 100644 index 000000000..688a4c18d --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/openapi/validators/HeaderParameterTest.java @@ -0,0 +1,75 @@ +package com.predic8.membrane.core.openapi.validators; + +import com.predic8.membrane.core.exchange.Exchange; +import com.predic8.membrane.core.http.Header; +import com.predic8.membrane.core.http.Request; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.*; +import java.util.stream.Stream; + +import static com.predic8.membrane.core.openapi.util.Utils.getOpenapiValidatorRequest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +public class HeaderParameterTest extends AbstractValidatorTest { + + Exchange exc = new Exchange(null); + Request request; + + @Override + String getOpenAPIFileName() { + return "/openapi/specs/header-params.yml"; + } + + @BeforeEach + public void setUp() { + try { + request = Request.get("/cities").build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + exc.setOriginalRequestUri("/cities"); + super.setUp(); + } + + static Stream headerDataProvider() { + return Stream.of( + arguments(new Header() {{ + setValue("X-Padding", "V0hQCMkJV4mKigp"); + setValue("X-Token", "122"); // Must be Integer + }}, 0), + arguments(new Header() {{ + setValue("X-Padding", "V0hQCMkJV4mKigp"); + setValue("X-Token", "foo"); // Shoud be Integer + }}, 1), + arguments(new Header() {{ + setValue("X-Padding", "V0hQCMkJV4mKigp"); + }}, 1), + arguments(new Header() {{ + setValue("X-Token", "1222"); + setValue("X-Test", "V0hQCMkJV4mKigp"); // Should be ignored by validation + }}, 0) + ); + } + + @ParameterizedTest + @MethodSource("headerDataProvider") + public void headerParamTest(Header header, int expected) throws Exception { + request.setHeader(header); + exc.setRequest(request); + + ValidationErrors errors = validator.validate(getOpenapiValidatorRequest(exc)); + + assertEquals(expected, errors.size()); + + if (errors.isEmpty()) + return; + + ValidationError ve = errors.get(0); + assertEquals(400,ve.getContext().getStatusCode()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidatorTest.java new file mode 100644 index 000000000..30b3db353 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParameterValidatorTest.java @@ -0,0 +1,46 @@ +package com.predic8.membrane.core.openapi.validators; + +import io.swagger.v3.oas.models.parameters.*; +import org.junit.jupiter.api.*; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class QueryParameterValidatorTest extends AbstractValidatorTest{ + + QueryParameterValidator queryParameterValidator; + + @Override + String getOpenAPIFileName() { + return "/openapi/specs/query-params.yml"; + } + + @BeforeEach + public void setUp() { + super.setUp(); + queryParameterValidator = new QueryParameterValidator(validator.getApi(),validator.getApi().getPaths().get("/cities")); + } + + @Test + void getPathAndOperationParameters() { + + List parameterSchemas = getParameterSchemas(queryParameterValidator); + + assertEquals(6,parameterSchemas.size()); + + // All Parameters must have a name. Referenced params do not have a name. + assertFalse(parameterSchemas.stream().anyMatch(param -> param.getName() == null)); + } + + private List getParameterSchemas(QueryParameterValidator val) { + return val.getAllParameterSchemas(validator.getApi().getPaths().get("/cities").getGet()); + } + + @Test + void resolveReferencedParameter() { + Parameter referencingParam = validator.getApi().getPaths().get("/cities").getParameters().get(1); + Parameter resolvedParam = queryParameterValidator.resolveReferencedParameter(referencingParam); + assertEquals("bar",resolvedParam.getName()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParamsTest.java b/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParamsTest.java index 2ab00d20b..04ea41f5f 100644 --- a/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParamsTest.java +++ b/core/src/test/java/com/predic8/membrane/core/openapi/validators/QueryParamsTest.java @@ -16,11 +16,11 @@ package com.predic8.membrane.core.openapi.validators; -import com.predic8.membrane.core.openapi.model.*; -import org.junit.jupiter.api.*; +import com.predic8.membrane.core.openapi.model.Request; +import org.junit.jupiter.api.Test; -import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.*; -import static org.junit.jupiter.api.Assertions.*; +import static com.predic8.membrane.core.openapi.validators.ValidationContext.ValidatedEntityType.QUERY_PARAMETER; +import static org.junit.jupiter.api.Assertions.assertEquals; public class QueryParamsTest extends AbstractValidatorTest { @@ -78,19 +78,41 @@ public void queryParamAtPathLevel() { assertEquals("REQUEST/QUERY_PARAMETER/foo", e.getContext().getLocationForRequest()); } -// @Test -// public void escapedTest() { -// ValidationErrors errors = validator.validate(Request.get().path("/cities?name=Bad%20Godesberg&limit=10")); + @Test + public void escapedTest() { + ValidationErrors errors = validator.validate(Request.get().path("/cities?name=Bad%20Godesberg&limit=10")); // System.out.println("errors = " + errors); -// assertEquals(1,errors.size()); -// ValidationError e = errors.get(0); -// assertEquals("REQUEST/QUERY_PARAMETER", e.getContext().getLocationForRequest()); -// } + assertEquals(1,errors.size()); + ValidationError e = errors.get(0); + assertEquals("REQUEST/QUERY_PARAMETER/name", e.getContext().getLocationForRequest()); + } @Test public void utf8Test() { ValidationErrors errors = validator.validate(Request.get().path("/cities?name=K%C3%B6%C3%B6%C3%B6ln&limit=10")); - System.out.println("errors = " + errors); +// System.out.println("errors = " + errors); assertEquals(0,errors.size()); } + + @Test + public void referencedParamTest() { + ValidationErrors errors = validator.validate(Request.get().path("/cities?limit=1&page=10")); +// System.out.println("errors = " + errors); + assertEquals(0,errors.size()); + } + + @Test + public void referencedParamValueTest() { + ValidationErrors errors = validator.validate(Request.get().path("/cities?limit=1&page=-1")); +// System.out.println("errors = " + errors); + assertEquals(1,errors.size()); + ValidationError e = errors.get(0); + assertEquals("page",e.getContext().getValidatedEntity()); + assertEquals(QUERY_PARAMETER,e.getContext().getValidatedEntityType()); + assertEquals("REQUEST/QUERY_PARAMETER/page", e.getContext().getLocationForRequest()); + assertEquals(400,e.getContext().getStatusCode()); + } + + + } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/util/CollectionsUtilTest.java b/core/src/test/java/com/predic8/membrane/core/util/CollectionsUtilTest.java new file mode 100644 index 000000000..59e11e7b5 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/util/CollectionsUtilTest.java @@ -0,0 +1,30 @@ +package com.predic8.membrane.core.util; + +import org.junit.jupiter.api.*; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class CollectionsUtilTest { + + @Test + void normal() { + assertEquals(List.of(1,2,3), CollectionsUtil.concat(List.of(1),List.of(2,3))); + } + + @Test + void l1Null() { + assertEquals(List.of(2,3), CollectionsUtil.concat(null,List.of(2,3))); + } + + @Test + void l2Null() { + assertEquals(List.of(1), CollectionsUtil.concat(List.of(1),null)); + } + + @Test + void allNull() { + assertEquals(List.of(), CollectionsUtil.concat(null,null)); + } +} \ No newline at end of file diff --git a/core/src/test/resources/openapi/specs/fruitshop-api-v2-openapi-3.yml b/core/src/test/resources/openapi/specs/fruitshop-api-v2-openapi-3.yml new file mode 100644 index 000000000..629712bec --- /dev/null +++ b/core/src/test/resources/openapi/specs/fruitshop-api-v2-openapi-3.yml @@ -0,0 +1,1041 @@ +openapi: 3.0.3 +info: + title: Fruit Shop API + description: | + ![Logo](https://www.predic8.de/logo6.png) + + Showcases REST API design and serves as a public API for + educational usage. Feel free to use this API even by using the POST, PUT and DELETE methods. You + cannot do any harm, the API will be reset automatically. + contact: + name: Predic8 + url: https://www.predic8.de + email: info@predic8.de + version: 2.0.0 +servers: + - url: https://api.predic8.de/shop/v2 +tags: + - name: Root + - name: Products + - name: Vendors + - name: Orders + - name: Customers +paths: + /: + get: + tags: + - Root + summary: Get an overview of the api + description: Shows the paths for Products, Vendors, Orders, Customers and the oas3 + responses: + '200': + description: OK + content: + application/json: + example: + description: + openapi: "/shop/v2/api-docs" + swagger_ui: "/shop/v2/swagger-ui" + link: + products_link: "/shop/v2/products" + vendors_link: "/shop/v2/vendors" + orders_link: "/shop/v2/orders" + customer_link: "/shop/v2/customers" + /api-docs: + get: + tags: + - Root + summary: Get OpenAPI documentation + description: Get the OpenAPI documentation in YAML format. + operationId: getOpenAPI + responses: + '200': + description: OK + content: + application/yaml: + example: | + /swagger-ui: + get: + tags: + - Root + summary: Open Swagger UI + description: Open the Swagger UI for interactive API documentation. + operationId: openSwaggerUI + responses: + '200': + description: OK + + /products: + get: + tags: + - Products + summary: Get all products + description: Get a list of all the products available over the Fruitshop API. + operationId: getProducts + parameters: + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + - name: search + in: query + description: Search for products containing this String + schema: + type: string + responses: + '200': + $ref: "#/components/responses/Products" + post: + tags: + - Products + summary: Create a product + description: Create a new product for the store. + operationId: createProduct + requestBody: + $ref: "#/components/requestBodies/Product" + responses: + '201': + $ref: "#/components/responses/ProductCreated" + '5XX': + $ref: "#/components/responses/ServerError" + /products/{id}: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Products + summary: Get product by id + description: Get detailed information about the product. + operationId: getProduct + responses: + '200': + $ref: "#/components/responses/Product" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + put: + tags: + - Products + summary: Update a product + description: Update a product description with new data. + operationId: updateProduct + requestBody: + $ref: "#/components/requestBodies/Product" + responses: + '200': + $ref: "#/components/responses/Product" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + delete: + tags: + - Products + summary: Delete a product + description: Delete a single product. + operationId: deleteProduct + responses: + '200': + $ref: "#/components/responses/Success" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + patch: + tags: + - Products + summary: Update properties of a product + description: Update one or more properties of a product + operationId: patchProduct + requestBody: + $ref: "#/components/requestBodies/ProductPatch" + responses: + '200': + $ref: "#/components/responses/Product" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /products/{id}/image: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Products + summary: Get image + description: Get image of the product + operationId: getProductImage + responses: + '200': + $ref: "#/components/responses/Image" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /vendors: + get: + tags: + - Vendors + summary: Get all vendors + description: Get a list of all the vendors registered the Fruitshop API. + operationId: getVendors + parameters: + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + responses: + '200': + $ref: "#/components/responses/Vendors" + post: + tags: + - Vendors + summary: Create a vendor + description: Register a new vendor offering products with the **Fruitshop API**. + operationId: createVendor + requestBody: + $ref: "#/components/requestBodies/Vendor" + responses: + '201': + $ref: "#/components/responses/VendorCreated" + '5XX': + $ref: "#/components/responses/ServerError" + /vendors/{id}: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Vendors + summary: Get a vendor by id + description: Get detailed information about the vendor. + operationId: getVendor + responses: + '200': + $ref: "#/components/responses/Vendor" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + put: + tags: + - Vendors + summary: Update a vendor + description: Update a vendor description with new data. + operationId: updateVendor + requestBody: + $ref: "#/components/requestBodies/Vendor" + responses: + '200': + $ref: "#/components/responses/Vendor" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /vendors/{id}/products: + get: + tags: + - Vendors + summary: Get products + description: Get the products offered by this vendor + operationId: getProductsOfVendor + parameters: + - $ref: "#/components/parameters/Id" + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + responses: + '200': + $ref: "#/components/responses/Products" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /vendors/{id}/products/{pid}: + parameters: + - $ref: "#/components/parameters/Id" + - in: path + name: pid + description: The id of the product to add + schema: + $ref: "#/components/schemas/Id" + required: true + example: 44 + put: + tags: + - Vendors + summary: Add a product to a vendor + description: Extend the list of products a vendor sells with the **Fruitshop API**. + operationId: addProductToVendor + responses: + '200': + $ref: "#/components/responses/Success" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /orders: + get: + tags: + - Orders + summary: Get all orders + description: Get a list of all the orders the **Fruitshop API** processed. + operationId: getOrders + parameters: + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + - $ref: "#/components/parameters/State" + responses: + "200": + $ref: "#/components/responses/Orders" + /orders/{id}: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Orders + summary: Get an order by id + description: Get detailed information about the order. + operationId: getOrder + responses: + "200": + $ref: "#/components/responses/Order" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /orders/{id}/items: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Orders + summary: Get the items + description: Get the items of an order + operationId: getItems + responses: + '200': + $ref: "#/components/responses/Items" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /orders/{id}/actions/purchase: + parameters: + - $ref: "#/components/parameters/Id" + put: + tags: + - Orders + summary: Purchase an order + description: Purchase an order that was in the created state. + operationId: purchaseOrder + responses: + '200': + $ref: "#/components/responses/OrderPurchase" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /orders/{id}/actions/cancel: + parameters: + - $ref: "#/components/parameters/Id" + put: + tags: + - Orders + summary: Cancel + description: Cancel an order + operationId: cancelOrder + responses: + '200': + $ref: "#/components/responses/OrderCancel" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /customers: + get: + tags: + - Customers + summary: Get all customers + description: Get a list of all the customers doing commerce with the Fruitshop API. + operationId: getCustomers + parameters: + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + responses: + "200": + $ref: "#/components/responses/Customers" + /customers/{id}: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Customers + summary: Get a customer by id + description: Get detailed information about the customer. + operationId: getCustomer + responses: + "200": + $ref: "#/components/responses/Customer" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + /customers/{id}/orders: + parameters: + - $ref: "#/components/parameters/Id" + get: + tags: + - Customers + summary: Get the orders + description: Get the orders of a customer + operationId: getOrdersOfCustomer + parameters: + - $ref: "#/components/parameters/Start" + - $ref: "#/components/parameters/Limit" + responses: + '200': + $ref: "#/components/responses/Orders" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + post: + tags: + - Customers + summary: Create an order for a customer + description: Create a new order for the customer. + operationId: createOrderForCustomer + requestBody: + $ref: "#/components/requestBodies/Order" + responses: + '201': + $ref: "#/components/responses/OrderCreated" + '4XX': + $ref: "#/components/responses/NotFound" + '5XX': + $ref: "#/components/responses/ServerError" + +components: + parameters: + Id: + name: id + in: path + description: Id of the object + required: true + schema: + $ref: "#/components/schemas/Id" + Start: + name: start + in: query + description: Starting entry of the result list + schema: + type: number + minimum: 1 + default: 1 + example: 7 + Limit: + name: limit + in: query + description: Limits the number of result entries + schema: + minimum: 1 + type: number + default: 10 + example: 100 + State: + name: state + in: query + description: State of an order + schema: + $ref: "#/components/schemas/State" + requestBodies: + Product: + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + example: + name: Mangos, + price: 2.79 + ProductPatch: + content: + application/json: + schema: + $ref: '#/components/schemas/ProductPatch' + Vendor: + content: + application/json: + schema: + $ref: "#/components/schemas/Vendor" + + Image: + content: + image/jpeg: + schema: + $ref: '#/components/schemas/Image' + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/OrderInput" + responses: + ServerError: + description: Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + example: + type: "https://api.predic8.de/shop/v2/validation" + title: "Server Error" + status: 500 + detail: Internal Server Error + + NotFound: + description: Client Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + example: + type: "https://api.predic8.de/shop/v2/validation" + title: "Not Found" + status: 404 + detail: An entry with this id does not exist + OrderCreated: + description: Created + headers: + location: + schema: + type: string + format: uri + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + Order: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + OrderPurchase: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + example: + id: 8 + state: ORDERED + actions: + cancel: + link: /shop/v2/orders/8/actions/cancel + method: PUT + customer: 2 + customer_link: /shop/v2/customer/2 + items_link: /shop/v2/orders/8/items + total: 45.78 + createdAt: 2023-02-26T16:44:36+02:00 + updatedAt: 2023-02-26T18:14:22+02:00 + + OrderCancel: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + example: + id: 8 + state: CANCELED + actions: {} + customer: 2 + customer_link: /shop/v2/customer/2 + items_link: /shop/v2/orders/8/items + total: 45.78 + createdAt: 2023-02-26T16:44:36+02:00 + updatedAt: 2023-02-26T18:14:22+02:00 + Orders: + description: OK + content: + application/json: + schema: + type: object + properties: + meta: + $ref: "#/components/schemas/Meta" + orders: + type: array + items: + type: object + properties: + id: + $ref: "#/components/schemas/Id" + state: + $ref: "#/components/schemas/State" + self_link: + $ref: "#/components/schemas/SelfLink" + example: + meta: + count: 14 + start: 1 + limit: 10 + next_link: "/shop/v2/orders/?start=11&limit=10" + orders: + - id: 22 + state: ORDERED + self_link: /shop/v2/orders/22 + Items: + description: List of items + content: + application/json: + schema: + $ref: '#/components/schemas/Items' + VendorCreated: + description: Created + headers: + location: + schema: + type: string + format: uri + content: + application/json: + schema: + $ref: '#/components/schemas/Vendor' + example: + id: 4 + name: Fresh Fruits from France Ltd. + self_link: /shop/v2/vendors/4 + Vendor: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Vendor' + example: + id: 8 + name: Fresh Fruits from France Ltd. + Vendors: + description: OK + content: + application/json: + schema: + type: object + properties: + meta: + $ref: "#/components/schemas/Meta" + vendors: + type: array + items: + type: object + properties: + id: + $ref: "#/components/schemas/Id" + name: + type: string + description: Name of the vendor + example: Exotic Fruits LLC + self_link: + $ref: "#/components/schemas/SelfLink" + example: + meta: + count: 22 + start: 11 + limit: 10 + previous_link: /shop/v2/vendors/?start=1&limit=10 + next_link: /shop/v2/vendors/?start=21&limit=10 + vendors: + - id: 42 + name: Exotic Fruits LLC + self_link: /shop/v2/vendors/42 + Product: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + example: + id: 8 + name: Mangos, + price: 2.79 + ProductCreated: + description: Created + headers: + location: + schema: + type: string + format: uri + content: + application/json: + schema: + $ref: "#/components/schemas/Product" + example: + id: 8 + name: Mangos, + price: 2.79 + self_link: /shop/v2/products/8 + Products: + description: OK + content: + application/json: + schema: + type: object + properties: + meta: + $ref: "#/components/schemas/Meta" + products: + type: array + items: + type: object + properties: + id: + $ref: "#/components/schemas/Id" + name: + type: string + description: Name of the product + example: Cherries + self_link: + $ref: "#/components/schemas/SelfLink" + example: + id: 1 + name: Banana + self_link: /shop/v2/products/1 + Image: + description: OK + content: + image/jpeg: + schema: + $ref: "#/components/schemas/Image" + Customer: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + Customers: + description: OK + content: + application/json: + schema: + type: object + properties: + meta: + $ref: "#/components/schemas/Meta" + customers: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + description: "First- and lastname" + example: "Victor Hugo" + self_link: + $ref: "#/components/schemas/SelfLink" + example: + meta: + count: 4 + start: 1 + limit: 10 + customers: + - id: 2 + name: Max Mustermann + self_link: /shop/v2/customers/2 + Success: + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: string + example: added + schemas: + Id: + type: integer + description: Id of an object + example: 8 + minimum: 1 + readOnly: true + SelfLink: + type: string + format: url + readOnly: true + example: /shop/v2/... + description: Link to the resource itself + Meta: + required: + - count + type: object + properties: + count: + type: integer + description: Number of resources + example: 22 + start: + type: integer + description: Starting entry of the result list + example: 11 + limit: + type: integer + description: How many entries are delivered in one response + example: 10 + previous_link: + type: string + description: URL to the previous page of results + example: /shop/v2/products/?start=1&limit=10 + next_link: + type: string + description: URL to the next page of results + example: /shop/v2/products/?start=21&limit=10 + description: Metadata about a collection of resources. + State: + type: string + description: Current state of an order + example: CREATED + enum: + - CREATED + - ORDERED + - DELIVERED + - CANCELED + Customer: + required: + - firstname + - lastname + type: object + properties: + firstname: + type: string + example: Fred + lastname: + type: string + example: Meyers + orders_url: + type: string + description: Link to the orders of the customer + customer_url: + type: string + description: Link to the customer resource + description: Customer details + example: + firstname: Freddy + lastname: Meyers + ActionDescription: + type: object + properties: + link: + type: string + example: /shop/v2/orders/8/actions/purchase + method: + type: string + enum: + - PUT + description: Description of a possible action on the resource + Actions: + type: object + properties: + purchase: + $ref: '#/components/schemas/ActionDescription' + cancel: + $ref: '#/components/schemas/ActionDescription' + description: Actions that the order supports + example: + purchase: + url: /shop/v2/orders/8/actions/purchase + method: PUT + cancel: + url: /shop/v2/orders/8/actions/cancel + method: PUT + OrderInput: + type: object + description: Order structure for input + properties: + items: + type: array + items: + $ref: "#/components/schemas/Item" + example: + items: + - product: 44 + quantity: 5 + - product: 60 + quantity: 1 + - product: 72 + quantity: 3 + Order: + type: object + description: Order details + required: + - actions + properties: + id: + $ref: "#/components/schemas/Id" + state: + $ref: "#/components/schemas/State" + actions: + $ref: '#/components/schemas/Actions' + customer: + $ref: "#/components/schemas/Id" + customer_link: + type: string + example: /shop/v2/customers/342 + items_link: + type: string + example: /shop/v2/orders/3142/items + total: + type: number + description: Total price of the order + example: 45.78 + createdAt: + type: string + format: datetime + example: 2023-02-26T16:44:36+02:00 + updatedAt: + type: string + format: datetime + example: 2023-02-26T18:14:22+02:00 + Product: + type: object + description: Description of a product + required: + - name + - price + properties: + id: + $ref: "#/components/schemas/Id" + name: + type: string + description: Name of a product + example: Berries + maxLength: 30 + price: + type: number + description: Price of a good + example: 4.5 + minimum: 0 + maximum: 1000 + vendors: + type: array + readOnly: true + items: + type: object + properties: + id: + $ref: "#/components/schemas/Id" + name: + type: string + description: Name of the vendor + example: Exotic Fruits LLC + maxLength: 30 + self_link: + $ref: "#/components/schemas/SelfLink" + image_link: + type: string + readOnly: true + self_link: + $ref: "#/components/schemas/SelfLink" + example: + name: Wildberries + price: 4.99 + ProductPatch: + type: object + description: Structure to patch a product. All the properties are optional. + properties: + name: + type: string + description: Name of a product + example: Berries + maxLength: 30 + price: + type: number + description: Price of a good + example: 4.5 + minimum: 0 + maximum: 1000 + vendor: + $ref: "#/components/schemas/Id" + example: + price: 2.79 + Vendor: + required: + - name + type: object + properties: + id: + $ref: "#/components/schemas/Id" + name: + type: string + description: Name of the vendor + example: foo + maxLength: 30 + products_link: + type: string + description: URL to the products of this vendor + readOnly: true + self_link: + type: string + description: URL of the vendor + description: Vendor of products + example: + name: Fresh Fruits from France Ltd. + Item: + description: Item details + required: + - quantity + - product + type: object + properties: + quantity: + type: number + example: 5 + minimum: 0 + maximum: 1000 + price: + type: number + description: Price of a good + example: 4.5 + minimum: 0 + maximum: 1000 + product: + $ref: "#/components/schemas/Id" + product_link: + type: string + readOnly: true + example: + quantity: 5 + price: 0.9 + product: 3 + product_link: /shop/v2/products/3 + ItemList: + type: array + description: Collection of items + items: + $ref: '#/components/schemas/Item' + Items: + required: + - items + type: object + properties: + order_link: + type: string + example: /shop/v2/orders/1432 + items: + $ref: '#/components/schemas/ItemList' + description: Collection of items + Image: + type: string + format: binary + description: Image as binary + ProblemDetails: + type: object + properties: + type: + type: string + format: url + title: + type: string + status: + type: integer + minimum: 200 + detail: + type: string + maxLength: 500 \ No newline at end of file diff --git a/core/src/test/resources/openapi/specs/header-params.yml b/core/src/test/resources/openapi/specs/header-params.yml new file mode 100644 index 000000000..ffe560580 --- /dev/null +++ b/core/src/test/resources/openapi/specs/header-params.yml @@ -0,0 +1,32 @@ +openapi: '3.0.2' +info: + title: Header Params Test API + version: '1.0' +paths: + /cities: + parameters: + - $ref: '#/components/parameters/X-Token' + + get: + parameters: + - in: header + name: X-Padding + schema: + type: string + required: false + - in: query # Should be ignored by the HeaderParameterValidator + name: 'ignore-me' + schema: + type: integer + responses: + '200': + description: OK + +components: + parameters: + X-Token: + in: header + name: X-Token + schema: + type: integer + required: true diff --git a/core/src/test/resources/openapi/specs/query-params.yml b/core/src/test/resources/openapi/specs/query-params.yml index cf24949ed..acc7c9d80 100644 --- a/core/src/test/resources/openapi/specs/query-params.yml +++ b/core/src/test/resources/openapi/specs/query-params.yml @@ -7,12 +7,14 @@ paths: parameters: - in: query name: foo - required: false schema: type: integer minimum: 0 + required: false + - $ref: '#/components/parameters/BarParam' get: parameters: + - $ref: '#/components/parameters/PageQueryParam' - in: query name: limit schema: @@ -30,6 +32,23 @@ paths: schema: type: string maxLength: 10 + responses: '200': description: OK + +components: + parameters: + PageQueryParam: + in: query + name: page + schema: + type: integer + minimum: 1 + required: false + BarParam: + in: query + name: bar + schema: + type: integer + required: false \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/tests/PaddingHeaderTest.java b/distribution/src/test/java/com/predic8/membrane/examples/tests/PaddingHeaderTest.java index f2bfff658..fabea6829 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/tests/PaddingHeaderTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/tests/PaddingHeaderTest.java @@ -35,10 +35,10 @@ protected String getExampleDirName() { @RepeatedTest(10) public void testHeader() throws Exception { given() - .when() - .get("http://localhost:2000/") - .then() - .header("X-Padding", matchesPattern(PATTERN)) - .statusCode(200); + .when() + .get("http://localhost:2000/") + .then() + .header("X-Padding", matchesPattern(PATTERN)) + .statusCode(200); } }