diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeGiven.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeGiven.java index d5cb0bd1f0..2fb0ae93f3 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeGiven.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeGiven.java @@ -41,6 +41,7 @@ class CustomModeGiven implements Given, BodyMethodVisitor, CustomModeAcceptor { new CustomModeRequestGiven(blockBuilder, generatedClassMetaData))); this.bodyGivens.addAll( Arrays.asList(new CustomModeMethodWithUrlGiven(blockBuilder, bodyParser), + new CustomModeQueryParamsGiven(blockBuilder, bodyParser), new CustomModeSchemeProtocolGiven(blockBuilder), new CustomModeHeadersGiven(blockBuilder), new CustomModeCookiesGiven(blockBuilder), diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeQueryParamsGiven.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeQueryParamsGiven.java new file mode 100644 index 0000000000..bda3a939f5 --- /dev/null +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomModeQueryParamsGiven.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.builder; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.cloud.contract.spec.internal.MatchingStrategy; +import org.springframework.cloud.contract.spec.internal.QueryParameter; +import org.springframework.cloud.contract.spec.internal.Request; +import org.springframework.cloud.contract.spec.internal.Url; +import org.springframework.cloud.contract.verifier.file.SingleContractMetadata; +import org.springframework.cloud.contract.verifier.util.MapConverter; + +class CustomModeQueryParamsGiven + implements Given, CustomModeAcceptor, QueryParamsResolver { + + private final BlockBuilder blockBuilder; + + private final BodyParser bodyParser; + + CustomModeQueryParamsGiven(BlockBuilder blockBuilder, BodyParser bodyParser) { + this.blockBuilder = blockBuilder; + this.bodyParser = bodyParser; + } + + @Override + public MethodVisitor apply(SingleContractMetadata metadata) { + Request request = metadata.getContract().getRequest(); + Url url = getUrl(request); + addQueryParameters(url); + return this; + } + + private Url getUrl(Request request) { + if (request.getUrl() != null) { + return request.getUrl(); + } + if (request.getUrlPath() != null) { + return request.getUrlPath(); + } + throw new IllegalStateException("URL is not set!"); + } + + private void addQueryParameters(Url buildUrl) { + List queryParameters = buildUrl.getQueryParameters() + .getParameters().stream().filter(this::allowedQueryParameter) + .collect(Collectors.toList()); + Iterator iterator = queryParameters.iterator(); + while (iterator.hasNext()) { + QueryParameter parameter = iterator.next(); + String text = addQueryParameter(parameter); + if (iterator.hasNext()) { + this.blockBuilder.addLine(text); + } + else { + this.blockBuilder.addIndented(text); + } + } + } + + private boolean allowedQueryParameter(Object o) { + if (o instanceof QueryParameter) { + return allowedQueryParameter(((QueryParameter) o).getServerValue()); + } + else if (o instanceof MatchingStrategy) { + return !MatchingStrategy.Type.ABSENT.equals(((MatchingStrategy) o).getType()); + } + return true; + } + + private String addQueryParameter(QueryParameter queryParam) { + return "." + "queryParam(" + this.bodyParser.quotedLongText(queryParam.getName()) + + "," + this.bodyParser.quotedLongText(resolveParamValue( + MapConverter.getTestSideValuesForNonBody(queryParam))) + + ")"; + } + + @Override + public boolean accept(SingleContractMetadata metadata) { + return acceptType(metadata) && metadata.getContract().getRequest() != null + && getUrl(metadata.getContract().getRequest()) + .getQueryParameters() != null; + } + +} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomQueryParamsGiven.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomQueryParamsGiven.java deleted file mode 100644 index baa39ff651..0000000000 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/builder/CustomQueryParamsGiven.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2013-2020 the original author or authors. - * - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.contract.verifier.builder; - -import org.springframework.cloud.contract.spec.internal.Request; -import org.springframework.cloud.contract.spec.internal.Url; -import org.springframework.cloud.contract.verifier.file.SingleContractMetadata; - -class CustomQueryParamsGiven implements Given, CustomModeAcceptor, QueryParamsResolver { - - @Override - public MethodVisitor apply(SingleContractMetadata metadata) { - throw new UnsupportedOperationException("Query params are not supported"); - } - - private Url getUrl(Request request) { - if (request.getUrl() != null) { - return request.getUrl(); - } - if (request.getUrlPath() != null) { - return request.getUrlPath(); - } - throw new IllegalStateException("URL is not set!"); - } - - @Override - public boolean accept(SingleContractMetadata metadata) { - Request request = metadata.getContract().getRequest(); - Url url = getUrl(request); - return url.getQueryParameters() != null; - } - -} diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/OkHttpHttpVerifier.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/OkHttpHttpVerifier.java index 2ce4dd2681..38f8be1fc7 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/OkHttpHttpVerifier.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/OkHttpHttpVerifier.java @@ -17,11 +17,11 @@ package org.springframework.cloud.contract.verifier.http; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.AbstractMap; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -57,9 +57,7 @@ public class OkHttpHttpVerifier implements HttpVerifier { public Response exchange(Request request) { String requestContentType = request.contentType(); OkHttpClient client = new OkHttpClient.Builder() - .protocols(Collections - .singletonList(toProtocol(request.protocol().toString()))) - .build(); + .protocols(toProtocol(request.protocol().toString())).build(); Map headers = stringTyped(request.headers()); if (!request.cookies().isEmpty()) { headers.put("Set-Cookie", @@ -67,10 +65,7 @@ public class OkHttpHttpVerifier implements HttpVerifier { .map(e -> e.getKey() + "=" + e.getValue().toString()) .collect(Collectors.joining(";"))); } - okhttp3.Request req = new okhttp3.Request.Builder() - .url(request.scheme().name().toLowerCase() + ":" + this.hostAndPort - + (request.path().startsWith("/") ? request.path() - : "/" + request.path())) + okhttp3.Request req = new okhttp3.Request.Builder().url(url(request)) .method(request.method().name(), requestBody(request, requestContentType)) .headers(Headers.of(headers)).build(); try (okhttp3.Response res = client.newCall(req).execute()) { @@ -81,9 +76,33 @@ public class OkHttpHttpVerifier implements HttpVerifier { } } - private Protocol toProtocol(String string) { + private String url(Request request) { + String url = request.scheme().name().toLowerCase() + ":" + this.hostAndPort + + (request.path().startsWith("/") ? request.path() + : "/" + request.path()); + if (!request.queryParams().isEmpty()) { + return url + "?" + + request.queryParams().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining("&")); + } + return url; + } + + private List toProtocol(String string) { try { - return Protocol.get(string); + Protocol protocol = Protocol.get(string); + switch (protocol) { + case HTTP_1_0: + case HTTP_2: + case QUIC: + return Arrays.asList(protocol, Protocol.HTTP_1_1); + case HTTP_1_1: + return Collections.singletonList(Protocol.HTTP_1_1); + case H2_PRIOR_KNOWLEDGE: + return Collections.singletonList(Protocol.H2_PRIOR_KNOWLEDGE); + } + return Collections.emptyList(); } catch (IOException e) { throw new IllegalStateException(e); @@ -114,34 +133,6 @@ public class OkHttpHttpVerifier implements HttpVerifier { return RequestBody.create(MediaType.parse(requestContentType), bodyArray); } - private boolean isGrpc(String contentType) { - return contentType.startsWith("application/grpc"); - } - - // the encoded body should already have proper byte values - // TODO: This should be removed? - private byte[] grpcRequestBody(Request request) { - byte[] bodyArray; // TODO: Add compression support - byte compressedFlag = 0; - byte[] message = request.body().asByteArray(); - byte[] messageLength = ByteBuffer.allocate(4).putInt(message.length).array(); - bodyArray = ByteBuffer.allocate(1 + messageLength.length + message.length) - .put(compressedFlag).put(messageLength).put(message).array(); - return bodyArray; - } - - // TODO: This should be removed? - private byte[] grpcResponseBody(byte[] responseBody) { - // 5 value = 4th index - // 1 for compression, 4 for message size - int actualPayloadSize = responseBody.length - 5; - byte[] destination = new byte[actualPayloadSize]; - System.arraycopy(responseBody, 5, destination, 0, actualPayloadSize); - responseBody = destination; - return responseBody; - } - - @Nullable private byte[] responseBody(okhttp3.Response res) throws IOException { return res.body() != null ? res.body().bytes() : null; } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/Request.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/Request.java index 30e22b1d2a..9138549d8e 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/Request.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/http/Request.java @@ -18,6 +18,8 @@ package org.springframework.cloud.contract.verifier.http; import java.util.AbstractMap; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import org.springframework.cloud.contract.spec.internal.HttpMethods; @@ -40,6 +42,8 @@ public class Request { private final String path; + private final List> queryParameters; + private final Body body; private final Map headers; @@ -48,12 +52,14 @@ public class Request { Request(ContractVerifierHttpMetadata.Protocol protocol, ContractVerifierHttpMetadata.Scheme scheme, HttpMethods.HttpMethod method, - String path, Body body, Map headers, - Map cookies) { + String path, List> queryParameters, + Body body, Map headers, Map cookies) { this.protocol = protocol; this.scheme = scheme; this.method = method; this.path = path; + this.queryParameters = queryParameters == null ? new LinkedList<>() + : queryParameters; this.body = body; this.headers = headers == null ? new HashMap<>() : headers; this.cookies = cookies == null ? new HashMap<>() : cookies; @@ -121,6 +127,13 @@ public class Request { return this.cookies; } + /** + * @return query parameters + */ + public List> queryParams() { + return this.queryParameters; + } + /** * Factory method to pick the HTTP method. * @return method builder @@ -217,6 +230,8 @@ public class Request { final String path; + List> queryParameters = new LinkedList<>(); + ContractVerifierHttpMetadata.Protocol protocol = ContractVerifierHttpMetadata.Protocol.HTTP_1_1; ContractVerifierHttpMetadata.Scheme scheme = ContractVerifierHttpMetadata.Scheme.HTTP; @@ -259,6 +274,16 @@ public class Request { return this; } + /** + * @param name - query parameter name + * @param value - query parameter value + * @return builder + */ + public Request.Builder queryParam(String name, String value) { + this.queryParameters.add(new AbstractMap.SimpleEntry<>(name, value)); + return this; + } + /** * @param headers HTTP headers * @return builder @@ -292,7 +317,7 @@ public class Request { */ public Request build() { return new Request(this.protocol, this.scheme, this.method, this.path, - this.body, this.headers, this.cookies); + this.queryParameters, this.body, this.headers, this.cookies); } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy index 61a42632c4..5036361a86 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/YamlMockMvcMethodBodyBuilderSpec.groovy @@ -553,6 +553,7 @@ response: "spock" | { properties.testFramework = TestFramework.SPOCK } "mockmvc" | { properties.testMode = TestMode.MOCKMVC } "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + "custom" | { properties.testMode = TestMode.CUSTOM } } @Issue('#169') @@ -627,6 +628,7 @@ response: "spock" | { properties.testFramework = TestFramework.SPOCK } "mockmvc" | { properties.testMode = TestMode.MOCKMVC } "webclient" | { properties.testMode = TestMode.WEBTESTCLIENT } + "custom" | { properties.testMode = TestMode.CUSTOM } } def 'should generate test for empty body with #methodBuilderName'() {