diff --git a/pom.xml b/pom.xml index 4fa6be61dc..3a74749da9 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,8 @@ 2.5.8 3.5.4 - 1.4.1 + + 1.3.3 2.1.100 2.11.0 0.8.4 diff --git a/spring-cloud-contract-dependencies/pom.xml b/spring-cloud-contract-dependencies/pom.xml index 04efac5957..4c3265cf7c 100644 --- a/spring-cloud-contract-dependencies/pom.xml +++ b/spring-cloud-contract-dependencies/pom.xml @@ -15,7 +15,7 @@ spring-cloud-contract-dependencies Spring Cloud Contract Dependencies - 2.24.1 + 2.25.0 0.4.13 4.0.0 diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java index 1976050127..69e7a92d3a 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java @@ -21,6 +21,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,7 +31,6 @@ import java.util.Set; import groovy.json.JsonOutput; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import wiremock.org.eclipse.jetty.util.ConcurrentHashSet; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.spec.internal.DslProperty; @@ -47,7 +47,8 @@ import org.springframework.cloud.contract.verifier.util.BodyExtractor; */ class StubRunnerExecutor implements StubFinder { - static final Set STUB_SERVERS = new ConcurrentHashSet<>(); + static final Set STUB_SERVERS = Collections + .synchronizedSet(new HashSet<>()); private static final Log log = LogFactory.getLog(StubRunnerExecutor.class); diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml index 9c49f8d810..0b9c25f936 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/pom.xml @@ -22,6 +22,10 @@ org.springframework.cloud spring-cloud-contract-spec + + org.springframework.cloud + spring-cloud-contract-shade + org.springframework.cloud spring-cloud-contract-verifier diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java index e240d54b92..565a9c0b94 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/java/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.java @@ -33,7 +33,6 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import wiremock.com.google.common.collect.ListMultimap; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; @@ -42,6 +41,7 @@ import org.springframework.cloud.contract.verifier.file.ContractFileScannerBuild import org.springframework.cloud.contract.verifier.file.ContractMetadata; import org.springframework.cloud.contract.verifier.util.NamesUtil; import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -117,12 +117,12 @@ public class RecursiveFilesConverter { .baseDir(contractsDslDir).excluded(new HashSet<>(excludedFiles)) .ignored(new HashSet<>()).included(new HashSet<>()) .includeMatcher(includedContracts).build(); - ListMultimap contracts = scanner.findContracts(); + MultiValueMap contracts = scanner + .findContractsRecursively(); if (log.isDebugEnabled()) { log.debug("Found the following contracts " + contracts); } - for (Map.Entry> entry : contracts.asMap() - .entrySet()) { + for (Map.Entry> entry : contracts.entrySet()) { for (ContractMetadata contract : entry.getValue()) { if (log.isDebugEnabled()) { log.debug("Will create a stub for contract [" + contract + "]"); diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties index b5d38ac0d9..5a49fcf5f4 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/bootSimple/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -wiremockVersion=2.24.1 +wiremockVersion=2.25.0 jsonAssertVersion=0.4.13 verifierVersion=2.2.0.BUILD-SNAPSHOT groovyVersion=2.4.17 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle.properties b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle.properties index 964754f1e1..6bef4da25e 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle.properties +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -wiremockVersion=2.24.1 +wiremockVersion=2.25.0 jsonAssertVersion=0.4.13 verifierVersion=2.2.0.BUILD-SNAPSHOT bootVersion=2.2.0.BUILD-SNAPSHOT diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProjectKotlin/gradle.properties b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProjectKotlin/gradle.properties index 964754f1e1..6bef4da25e 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProjectKotlin/gradle.properties +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProjectKotlin/gradle.properties @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -wiremockVersion=2.24.1 +wiremockVersion=2.25.0 jsonAssertVersion=0.4.13 verifierVersion=2.2.0.BUILD-SNAPSHOT bootVersion=2.2.0.BUILD-SNAPSHOT diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 51eec90dd1..d95ae844ed 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -21,6 +21,10 @@ org.springframework.cloud spring-cloud-contract-spec + + org.springframework.cloud + spring-cloud-contract-shade + org.springframework.boot spring-boot-test-autoconfigure diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy index 71e7d93d73..024340ee1d 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy @@ -20,11 +20,11 @@ import java.nio.charset.StandardCharsets import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger +import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import groovy.transform.PackageScope import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory -import wiremock.com.google.common.collect.ListMultimap import org.springframework.cloud.contract.spec.ContractVerifierException import org.springframework.cloud.contract.verifier.builder.JavaTestGenerator @@ -35,6 +35,7 @@ import org.springframework.cloud.contract.verifier.file.ContractFileScannerBuild import org.springframework.cloud.contract.verifier.file.ContractMetadata import org.springframework.cloud.contract.verifier.util.NamesUtil import org.springframework.core.io.support.SpringFactoriesLoader +import org.springframework.util.MultiValueMap import org.springframework.util.StringUtils import static org.springframework.cloud.contract.verifier.util.NamesUtil.afterLast @@ -121,31 +122,38 @@ class TestGenerator { @PackageScope void generateTestClasses(final String basePackageName) { - ListMultimap contracts = contractFileScanner. - findContracts() + MultiValueMap contracts = contractFileScanner. + findContractsRecursively() if (log.isDebugEnabled()) { log.debug("Found the following contracts " + contracts.keySet()) } - Set>> inProgress = contracts.asMap().entrySet() - .findAll { Map.Entry> entry -> entry.value.any { it.anyInProgress() }} + Set>> inProgress = inProgress(contracts) if (!inProgress.isEmpty() && configProperties.failOnInProgress) { throw new IllegalStateException("In progress contracts found in paths [" + inProgress.collect { it.key.toString() }.join(",") + "] and the switch [failOnInProgress] is set to [true]. Either unmark those contracts as in progress, or set the switch to [false].") } processAllNotInProgress(contracts,basePackageName) } - @PackageScope Set>> processAllNotInProgress(ListMultimap contracts, String basePackageName) { - contracts.asMap().entrySet() - .findAll { Map.Entry> entry -> !entry.value.any { it.anyInProgress() }} + @CompileDynamic + private Set>> inProgress(MultiValueMap contracts) { + return contracts.entrySet() + .findAll { Map.Entry> entry -> entry.getValue().any { it.anyInProgress() }} + } + + @PackageScope + @CompileDynamic + Set>> processAllNotInProgress(MultiValueMap contracts, String basePackageName) { + contracts.entrySet() + .findAll { Map.Entry> entry -> !entry.value.any { it.anyInProgress() }} .each { - Map.Entry> entry -> + Map.Entry> entry -> processIncludedDirectory( relativizeContractPath(entry), (Collection) entry. getValue(), basePackageName) } } - private String relativizeContractPath(Map.Entry> entry) { + private String relativizeContractPath(Map.Entry> entry) { Path relativePath = configProperties.contractsDslDir.toPath(). relativize(entry.getKey()) if (StringUtils.isEmpty(relativePath.toString())) { diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.java index d9c7a34130..9700251855 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/TestMode.java @@ -40,6 +40,6 @@ public enum TestMode { /** * Uses Spring's reactive WebTestClient. */ - WEBTESTCLIENT; + WEBTESTCLIENT } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy index 91fa7b68ca..059aebfdb2 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy @@ -24,15 +24,18 @@ import java.util.regex.Pattern import groovy.transform.CompileStatic import groovy.util.logging.Commons -import wiremock.com.google.common.collect.ArrayListMultimap +import wiremock.com.google.common.collect.HashMultiset import wiremock.com.google.common.collect.ListMultimap +import wiremock.com.google.common.collect.Multimap +import wiremock.com.google.common.collect.Multiset import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractConverter import org.springframework.cloud.contract.verifier.converter.YamlContractConverter import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter import org.springframework.core.io.support.SpringFactoriesLoader - +import org.springframework.util.CollectionUtils +import org.springframework.util.MultiValueMap /** * Scans the provided file path for the DSLs. There's a possibility to provide * inclusion and exclusion filters. @@ -85,9 +88,136 @@ class ContractFileScanner { /** * @return for a map of paths for which a list of matching contracts has been found + * @deprecated use the {@link ContractFileScanner#findContractsRecursively} version */ + @Deprecated ListMultimap findContracts() { - ListMultimap result = ArrayListMultimap.create() + MultiValueMap contracts = findContractsRecursively(); + return new ListMultimap() { + @Override + List get(Path key) { + return contracts.get(key) + } + + @Override + List removeAll(Object key) { + return contracts.remove(key) + } + + @Override + List replaceValues(Path key, Iterable values) { + return contracts.put(key, asList(values)) + } + + private static List asList(Iterable self) { + if (self instanceof List) { + return (List) self; + } else { + return toList(self.iterator()); + } + } + + private static List toList(Iterator self) { + List answer = new ArrayList<>(); + while (self.hasNext()) { + answer.add(self.next()); + } + return answer; + } + + @Override + Map> asMap() { + return contracts.collectEntries { + [(it.key): (Collection) it.value] + } as Map> + } + + @Override + int size() { + return contracts.size() + } + + @Override + boolean isEmpty() { + return contracts.isEmpty() + } + + @Override + boolean containsKey(Object key) { + return contracts.containsKey(key) + } + + @Override + boolean containsValue(Object value) { + return contracts.findResult { it.value.contains(value) } + } + + @Override + boolean containsEntry(Object key, Object value) { + return contracts.findResult { it.key == key && it.value.contains(value) } + } + + @Override + boolean put(Path key, ContractMetadata value) { + return contracts.add(key, value) + } + + @Override + boolean remove(Object key, Object value) { + return contracts.getOrDefault(key, new ArrayList()).remove(value) + } + + @Override + boolean putAll(Path key, Iterable values) { + return contracts.getOrDefault(key, new ArrayList()).addAll(values) + } + + @Override + boolean putAll(Multimap multimap) { + multimap.entries().each { + contracts.add(it.key, it.value) + } + return true + } + + @Override + void clear() { + contracts.clear() + } + + @Override + Set keySet() { + return contracts.keySet() + } + + @Override + Multiset keys() { + return HashMultiset.create(contracts.keySet()) + } + + @Override + Collection values() { + return (Collection) contracts.values().flatten() + } + + @Override + Collection> entries() { + Collection> entries = new LinkedList<>() + contracts.each { + Path path = it.key + List list = it.value + list.each { + entries.add(new AbstractMap.SimpleEntry(path, it)) + } + } + return entries + } + } + } + + + MultiValueMap findContractsRecursively() { + MultiValueMap result = CollectionUtils.toMultiValueMap(new LinkedHashMap<>()); appendRecursively(baseDir, result) return result } @@ -96,7 +226,7 @@ class ContractFileScanner { * We iterate over found contracts, filter out those that should be excluded * and try to convert via pluggable Contract Converters any possible contracts */ - private void appendRecursively(File baseDir, ListMultimap result) { + private void appendRecursively(File baseDir, MultiValueMap result) { List converters = convertersWithYml() if (log.isTraceEnabled()) { log.trace("Found the following contract converters ${converters}") @@ -148,7 +278,7 @@ class ContractFileScanner { return SpringFactoriesLoader.loadFactories(ContractConverter, null) } - private void addContractToTestGeneration(List converters, ListMultimap result, + private void addContractToTestGeneration(List converters, MultiValueMap result, File[] files, File file, int index) { boolean converted = false if (!file.isDirectory()) { @@ -182,7 +312,7 @@ class ContractFileScanner { } } - private void addContractToTestGeneration(ListMultimap result, File[] files, File file, + private void addContractToTestGeneration(MultiValueMap result, File[] files, File file, int index, Collection convertedContract) { Path path = file.toPath() Integer order = null @@ -196,7 +326,7 @@ class ContractFileScanner { if (log.isDebugEnabled()) { log.debug("Creating a contract entry for path [" + path + "] and metadata [" + metadata + "]") } - result.put(parent, metadata) + result.add(parent, metadata) } private boolean hasScenarioFilenamePattern(Path path) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java index a0c0ac09b8..f048cc26a6 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/TestGeneratorTests.java @@ -18,21 +18,22 @@ package org.springframework.cloud.contract.verifier; import java.io.File; import java.nio.file.Path; -import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; import org.assertj.core.api.BDDAssertions; import org.junit.Test; import org.mockito.BDDMockito; -import wiremock.com.google.common.collect.ArrayListMultimap; -import wiremock.com.google.common.collect.ListMultimap; import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.verifier.builder.SingleTestGenerator; import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties; import org.springframework.cloud.contract.verifier.file.ContractFileScanner; import org.springframework.cloud.contract.verifier.file.ContractMetadata; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; public class TestGeneratorTests { @@ -46,9 +47,10 @@ public class TestGeneratorTests { .mock(SingleTestGenerator.class); FileSaver fileSaver = BDDMockito.mock(FileSaver.class); // and: - ArrayListMultimap multimap = ArrayListMultimap.create(); + MultiValueMap multimap = CollectionUtils + .toMultiValueMap(new LinkedHashMap<>()); Path path = new File(".").toPath(); - multimap.put(path, + multimap.add(path, new ContractMetadata(path, false, 0, null, Contract.make(it -> { it.inProgress(); it.request(r -> { @@ -61,7 +63,7 @@ public class TestGeneratorTests { }))); ContractFileScanner scanner = new ContractFileScanner(null, null, null) { @Override - public ListMultimap findContracts() { + public MultiValueMap findContractsRecursively() { return multimap; } }; @@ -87,9 +89,10 @@ public class TestGeneratorTests { .mock(SingleTestGenerator.class); FileSaver fileSaver = BDDMockito.mock(FileSaver.class); // and: - ArrayListMultimap multimap = ArrayListMultimap.create(); + MultiValueMap multimap = CollectionUtils + .toMultiValueMap(new LinkedHashMap<>()); Path path = new File(".").toPath(); - multimap.put(path, + multimap.add(path, new ContractMetadata(path, false, 0, null, Contract.make(it -> { it.inProgress(); it.request(r -> { @@ -102,7 +105,7 @@ public class TestGeneratorTests { }))); ContractFileScanner scanner = new ContractFileScanner(null, null, null) { @Override - public ListMultimap findContracts() { + public MultiValueMap findContractsRecursively() { return multimap; } }; @@ -110,8 +113,8 @@ public class TestGeneratorTests { TestGenerator testGenerator = new TestGenerator(properties, singleTestGenerator, fileSaver, scanner) { @Override - Set>> processAllNotInProgress( - ListMultimap contracts, + Set>> processAllNotInProgress( + MultiValueMap contracts, String basePackageName) { return null; } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy new file mode 100644 index 0000000000..841164e1b7 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerNewApiSpec.groovy @@ -0,0 +1,207 @@ +/* + * Copyright 2013-2019 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.file + + +import java.nio.file.Path + +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.util.FileSystemUtils +import org.springframework.util.MultiValueMap + +/** + * @author Jakub Kubrynski, codearte.io + */ +class ContractFileScannerNewApiSpec extends Specification { + + @Rule + TemporaryFolder tmp = new TemporaryFolder() + File tmpFolder + + def setup() { + tmpFolder = new File(tmp.newFolder(), "contracts") + } + + def "should find contract files"() { + given: + FileSystemUtils.copyRecursively( + new File(this.getClass().getResource("/directory/with/stubs").toURI()), + tmpFolder) + and: + File baseDir = tmpFolder + Set excluded = ["package/**"] as Set + Set ignored = ["other/different/**"] as Set + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + when: + MultiValueMap result = scanner.findContractsRecursively() + then: + result.keySet().size() == 3 + result.get(baseDir.toPath().resolve("different")).size() == 1 + result.get(baseDir.toPath().resolve("other")).size() == 2 + and: + Collection ignoredSet = result.get(baseDir.toPath().resolve("other").resolve("different")) + ignoredSet.size() == 1 + ignoredSet.ignored == [true] + } + + def "should find contract files in strange directories"() { + given: + File baseDir = new File(this.getClass().getResource("/strange_[3.3.3]_directory").toURI()) + Set excluded = ["foo/**"] as Set + Set ignored = ["bar/**"] as Set + ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set) + when: + MultiValueMap result = scanner.findContractsRecursively() + then: + result.entrySet().size() == 2 + and: + Collection ignoredSet = result.get(baseDir.toPath().resolve("bar")) + ignoredSet.size() == 1 + ignoredSet.ignored == [true] + } + + def "should find contracts group in scenario"() { + given: + File baseDir = new File(this.getClass().getResource("/directory/with/scenario").toURI()) + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set) + when: + MultiValueMap contracts = scanner.findContractsRecursively() + then: + contracts.values().size() == 1 + def firstEntry = contracts.values().first() + firstEntry.size() == 3 + firstEntry.find { + it.path.fileName.toString().startsWith('01') + }.groupSize == 3 + firstEntry.find { + it.path.fileName.toString().startsWith('01') + }.order == 0 + firstEntry.find { + it.path.fileName.toString().startsWith('02') + }.order == 1 + firstEntry.find { + it.path.fileName.toString().startsWith('03') + }.order == 2 + } + + def "should find contract files with converters"() { + given: + File baseDir = new File(this.getClass().getResource("/directory/with/mixed").toURI()) + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null) { + @Override + protected List converters() { + return [new ContractConverter() { + @Override + boolean isAccepted(File file) { + return file.name.endsWith(".json") + } + + @Override + Collection convertFrom(File file) { + throw new RuntimeException("boom") + } + + @Override + Object convertTo(Collection contract) { + throw new RuntimeException("boom") + } + }] + } + } + when: + scanner.findContractsRecursively() + then: + IllegalStateException e = thrown(IllegalStateException) + e.cause.message == "boom" + e.message.matches(".*Failed to convert file .*invalid.json.*") + } + + def "should prefer custom yaml converter over standard yaml converter"() { + given: + File baseDir = new File(this.getClass().getResource("/directory/with/custom/yml").toURI()) + ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null) { + @Override + protected List converters() { + return [new ContractConverter() { + @Override + boolean isAccepted(File file) { + if (!file.name.endsWith(".yml") && !file.name.endsWith(".yaml")) { + return false + } + String line + file.withReader { + line = it.readLine() + } + return line != null && line.startsWith("custom_format: 1.0") + } + + @Override + Collection convertFrom(File file) { + return Collections.singleton(Contract.newInstance()) + } + + @Override + Object convertTo(Collection contract) { + return new Object() + } + }] + } + } + when: + MultiValueMap result = scanner.findContractsRecursively() + then: + result.keySet().size() == 1 + result.entrySet().every { it.value.convertedContract } + } + + def "should find contracts for include pattern"() { + given: + FileSystemUtils.copyRecursively( + new File(this.getClass().getResource("/directory/with/common-messaging").toURI()), + tmpFolder) + and: + File baseDir = tmpFolder + Set included = ["social-service/**", "**/coupon-collected/**/*V1*"] as Set + ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included) + when: + MultiValueMap result = scanner.findContractsRecursively() + then: + result.keySet().size() == 3 + result.values().flatten().find { + (it.path.fileName.toString() == 'couponCollectedEventV1.groovy') + }.groupSize == 2 + result.values().flatten().find { + (it.convertedContract.first().label == 'couponCollectedV1') + } + result.values().flatten().findAll { + (it.path.fileName.toString() == 'couponCollectedEventV2.groovy') + }.isEmpty() + result.values().flatten().find { + (it.path.fileName.toString() == 'shouldUpdateUserInfo.groovy') + }.groupSize == 1 + result.values().flatten().find { + (it.path.fileName.toString() == 'shouldReturnEmptyFriendsWhenGetFriends.groovy') + }.groupSize == 1 + result.get(baseDir.toPath().resolve("coupon-sent")) == null + result.get(baseDir.toPath().resolve("reward-rules")) == null + } +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy index 41c78fbf1b..d12d98a7a9 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScannerSpec.groovy @@ -199,7 +199,7 @@ class ContractFileScannerSpec extends Specification { result.values().find { (it.path.fileName.toString() == 'shouldReturnEmptyFriendsWhenGetFriends.groovy') }.groupSize == 1 - result.get(baseDir.toPath().resolve("coupon-sent")).size() == 0 - result.get(baseDir.toPath().resolve("reward-rules")).size() == 0 + result.get(baseDir.toPath().resolve("coupon-sent")) == null + result.get(baseDir.toPath().resolve("reward-rules")) == null } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy index 070e3e1771..1e14bba67f 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/amqp/SpringAmqpStubMessagesSpec.groovy @@ -18,8 +18,8 @@ package org.springframework.cloud.contract.verifier.messaging.amqp import com.rabbitmq.client.Channel import org.mockito.exceptions.verification.WantedButNotInvoked +import shaded.com.google.common.collect.ImmutableMap import spock.lang.Specification -import wiremock.com.google.common.collect.ImmutableMap import org.springframework.amqp.core.Binding import org.springframework.amqp.core.BindingBuilder diff --git a/spring-cloud-contract-verifier/src/test/resources/logback.xml b/spring-cloud-contract-verifier/src/test/resources/logback.xml index 574bdb0622..38fb9e2a71 100644 --- a/spring-cloud-contract-verifier/src/test/resources/logback.xml +++ b/spring-cloud-contract-verifier/src/test/resources/logback.xml @@ -9,7 +9,7 @@ - + diff --git a/spring-cloud-contract-wiremock/pom.xml b/spring-cloud-contract-wiremock/pom.xml index a1d008dfd3..311ccd6701 100644 --- a/spring-cloud-contract-wiremock/pom.xml +++ b/spring-cloud-contract-wiremock/pom.xml @@ -81,6 +81,10 @@ javax.servlet-api true + + org.eclipse.jetty + jetty-server + org.apache.httpcomponents httpclient diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractExchangeHandler.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractExchangeHandler.java index 7eef5199f8..d8728e26f6 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractExchangeHandler.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractExchangeHandler.java @@ -38,7 +38,6 @@ import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.http.RequestMethod; import org.apache.commons.codec.binary.Base64; import wiremock.com.google.common.base.Optional; -import wiremock.org.eclipse.jetty.server.handler.ContextHandler; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -51,6 +50,8 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; +import static org.eclipse.jetty.server.handler.ContextHandler.StaticContext; + /** * @author Dave Syer * @@ -263,7 +264,7 @@ class WireMockHttpRequestAdapter implements Request { .request(this.result.getMethod(), this.result.getUriTemplate()) .contentType(this.result.getRequestHeaders().getContentType()) .content(this.result.getRequestBodyContent()) - .buildRequest(new ContextHandler.StaticContext()); + .buildRequest(new StaticContext()); try { return new StandardMultipartHttpServletRequest(request).getParts().stream() .map(part -> partFromServletPart(part)).collect(Collectors.toList()); diff --git a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractResultHandler.java b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractResultHandler.java index 3d6196aa19..e1bd65ad63 100644 --- a/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractResultHandler.java +++ b/spring-cloud-contract-wiremock/src/main/java/org/springframework/cloud/contract/wiremock/restdocs/ContractResultHandler.java @@ -18,21 +18,18 @@ package org.springframework.cloud.contract.wiremock.restdocs; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; +import java.util.stream.Collectors; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; -import com.github.tomakehurst.wiremock.common.Gzip; +import com.github.tomakehurst.wiremock.http.Body; import com.github.tomakehurst.wiremock.http.ContentTypeHeader; import com.github.tomakehurst.wiremock.http.Cookie; import com.github.tomakehurst.wiremock.http.HttpHeader; @@ -40,30 +37,15 @@ import com.github.tomakehurst.wiremock.http.HttpHeaders; import com.github.tomakehurst.wiremock.http.QueryParameter; import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.http.RequestMethod; -import com.github.tomakehurst.wiremock.servlet.WireMockHttpServletMultipartAdapter; -import wiremock.com.google.common.base.Function; import wiremock.com.google.common.base.Optional; -import wiremock.com.google.common.collect.ImmutableList; -import wiremock.com.google.common.collect.ImmutableMultimap; -import wiremock.com.google.common.collect.Maps; +import wiremock.org.apache.commons.io.IOUtils; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; - -import static com.github.tomakehurst.wiremock.common.Encoding.encodeBase64; -import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; -import static com.github.tomakehurst.wiremock.common.Strings.stringFromBytes; -import static com.github.tomakehurst.wiremock.common.Urls.splitQuery; -import static java.util.Collections.list; -import static wiremock.com.google.common.base.Charsets.UTF_8; -import static wiremock.com.google.common.base.MoreObjects.firstNonNull; -import static wiremock.com.google.common.base.Strings.isNullOrEmpty; -import static wiremock.com.google.common.collect.FluentIterable.from; -import static wiremock.com.google.common.collect.Lists.newArrayList; -import static wiremock.com.google.common.io.ByteStreams.toByteArray; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; /** * Converts a {@link MvcResult} to a WireMock response. @@ -117,7 +99,191 @@ public class ContractResultHandler extends @Override protected Request getWireMockRequest(MvcResult result) { - return new WireMockHttpServletRequestAdapter(result.getRequest()); + return new Request() { + @Override + public String getUrl() { + return result.getRequest().getRequestURI(); + } + + @Override + public String getAbsoluteUrl() { + return result.getRequest().getRequestURI(); + } + + @Override + public RequestMethod getMethod() { + return RequestMethod.fromString(result.getRequest().getMethod()); + } + + @Override + public String getScheme() { + return result.getRequest().getScheme(); + } + + @Override + public String getHost() { + return result.getRequest().getRemoteHost(); + } + + @Override + public int getPort() { + return result.getRequest().getServerPort(); + } + + @Override + public String getClientIp() { + return ""; + } + + @Override + public String getHeader(String key) { + return result.getRequest().getHeader(key); + } + + @Override + public HttpHeader header(String key) { + return new HttpHeader(key, getHeader(key)); + } + + @Override + public ContentTypeHeader contentTypeHeader() { + return new ContentTypeHeader(result.getRequest().getContentType()); + } + + @Override + public HttpHeaders getHeaders() { + List headers = new ArrayList<>(); + Enumeration headerNames = result.getRequest().getHeaderNames(); + while (headerNames.hasMoreElements()) { + String key = headerNames.nextElement(); + String value = getHeader(key); + headers.add(new HttpHeader(key, value)); + } + return new HttpHeaders(headers); + } + + @Override + public boolean containsHeader(String key) { + Enumeration headerNames = result.getRequest().getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + if (name.equals(key)) { + return true; + } + } + return false; + } + + @Override + public Set getAllHeaderKeys() { + return getHeaders().keys(); + } + + @Override + public Map getCookies() { + Map nameToCookie = new HashMap<>(); + if (result.getRequest().getCookies() == null) { + return nameToCookie; + } + for (javax.servlet.http.Cookie cookie : result.getRequest() + .getCookies()) { + nameToCookie.put(cookie.getName(), new Cookie(cookie.getValue())); + } + return nameToCookie; + } + + @Override + public QueryParameter queryParameter(String key) { + return new QueryParameter(key, + Collections.singletonList(result.getRequest().getParameter(key))); + } + + @Override + public byte[] getBody() { + return result.getRequest().getContentAsByteArray(); + } + + @Override + public String getBodyAsString() { + try { + return result.getRequest().getContentAsString(); + } + catch (Exception ex) { + return new String(result.getRequest().getContentAsByteArray()); + } + } + + @Override + public String getBodyAsBase64() { + return Base64Utils + .encodeToString(result.getRequest().getContentAsByteArray()); + } + + @Override + public boolean isMultipart() { + return StringUtils + .hasText(result.getRequest().getHeader("multipart/form-data")); + } + + @Override + public Collection getParts() { + try { + return result.getRequest().getParts().stream() + .map(part -> new Part() { + @Override + public String getName() { + return part.getName(); + } + + @Override + public HttpHeader getHeader(String name) { + String header = part.getHeader(name); + return new HttpHeader(name, header); + } + + @Override + public HttpHeaders getHeaders() { + List headers = new ArrayList<>(); + for (String headerName : part.getHeaderNames()) { + headers.add(new HttpHeader(headerName, + getHeader(headerName).values())); + } + return new HttpHeaders(headers); + } + + @Override + public Body getBody() { + try { + return new Body(IOUtils + .toByteArray(part.getInputStream())); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + }).collect(Collectors.toList()); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public Part getPart(String name) { + return getParts().stream().filter(part -> part.getName().equals(name)) + .findFirst().orElse(null); + } + + @Override + public boolean isBrowserProxyRequest() { + return false; + } + + @Override + public Optional getOriginalRequest() { + return Optional.absent(); + } + }; } @Override @@ -127,289 +293,8 @@ public class ContractResultHandler extends @Override protected byte[] getRequestBodyContent(MvcResult result) { - byte[] body = new WireMockHttpServletRequestAdapter(result.getRequest()) - .getBody(); + byte[] body = getWireMockRequest(result).getBody(); return body != null ? body : new byte[0]; } } - -// COPIED FROM WIREMOCK -class WireMockHttpServletRequestAdapter implements Request { - - private static final String ORIGINAL_REQUEST_KEY = "wiremock.ORIGINAL_REQUEST"; - - private final HttpServletRequest request; - - private byte[] cachedBody; - - private Collection cachedMultiparts; - - WireMockHttpServletRequestAdapter(HttpServletRequest request) { - this.request = request; - } - - @Override - public String getUrl() { - String url = this.request.getRequestURI(); - String contextPath = this.request.getContextPath(); - if (!isNullOrEmpty(contextPath) && url.startsWith(contextPath)) { - url = url.substring(contextPath.length()); - } - return withQueryStringIfPresent(url); - } - - @Override - public String getAbsoluteUrl() { - return withQueryStringIfPresent(this.request.getRequestURL().toString()); - } - - private String withQueryStringIfPresent(String url) { - return url + (isNullOrEmpty(this.request.getQueryString()) ? "" - : "?" + this.request.getQueryString()); - } - - @Override - public RequestMethod getMethod() { - return RequestMethod.fromString(this.request.getMethod().toUpperCase()); - } - - @Override - public String getScheme() { - return this.request.getScheme(); - } - - @Override - public String getHost() { - return this.request.getServerName(); - } - - @Override - public int getPort() { - return this.request.getServerPort(); - } - - @Override - public String getClientIp() { - String forwardedForHeader = this.getHeader("X-Forwarded-For"); - - if (forwardedForHeader != null && forwardedForHeader.length() > 0) { - return forwardedForHeader; - } - - return this.request.getRemoteAddr(); - } - - // Something's wrong with reading the body from request - @Override - public byte[] getBody() { - if (this.cachedBody == null || this.cachedBody.length == 0) { - try { - if (this.request instanceof MockHttpServletRequest) { - this.cachedBody = ((MockHttpServletRequest) this.request) - .getContentAsByteArray(); - return this.cachedBody; - } - byte[] body = toByteArray(this.request.getInputStream()); - boolean isGzipped = hasGzipEncoding() || Gzip.isGzipped(body); - this.cachedBody = isGzipped ? Gzip.unGzip(body) : body; - } - catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - return this.cachedBody; - } - - private Charset encodingFromContentTypeHeaderOrUtf8() { - ContentTypeHeader contentTypeHeader = contentTypeHeader(); - if (contentTypeHeader != null) { - return contentTypeHeader.charset(); - } - return UTF_8; - } - - private boolean hasGzipEncoding() { - String encodingHeader = this.request.getHeader("Content-Encoding"); - return encodingHeader != null && encodingHeader.contains("gzip"); - } - - @Override - public String getBodyAsString() { - return stringFromBytes(getBody(), encodingFromContentTypeHeaderOrUtf8()); - } - - @Override - public String getBodyAsBase64() { - return encodeBase64(getBody()); - } - - @Override - public String getHeader(String key) { - List headerNames = list(this.request.getHeaderNames()); - for (String currentKey : headerNames) { - if (currentKey.toLowerCase().equals(key.toLowerCase())) { - return this.request.getHeader(currentKey); - } - } - return null; - } - - @Override - public HttpHeader header(String key) { - List headerNames = list(this.request.getHeaderNames()); - for (String currentKey : headerNames) { - if (currentKey.toLowerCase().equals(key.toLowerCase())) { - List valueList = list(this.request.getHeaders(currentKey)); - if (valueList.isEmpty()) { - return HttpHeader.empty(key); - } - return new HttpHeader(key, valueList); - } - } - - return HttpHeader.absent(key); - } - - @Override - public ContentTypeHeader contentTypeHeader() { - return getHeaders().getContentTypeHeader(); - } - - @Override - public boolean containsHeader(String key) { - return header(key).isPresent(); - } - - @Override - public HttpHeaders getHeaders() { - List headerList = newArrayList(); - for (String key : getAllHeaderKeys()) { - headerList.add(header(key)); - } - - return new HttpHeaders(headerList); - } - - @Override - public Set getAllHeaderKeys() { - LinkedHashSet headerKeys = new LinkedHashSet<>(); - for (Enumeration headerNames = this.request.getHeaderNames(); headerNames - .hasMoreElements();) { - headerKeys.add(headerNames.nextElement()); - } - - return headerKeys; - } - - @Override - public Map getCookies() { - ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - javax.servlet.http.Cookie[] cookies = firstNonNull(this.request.getCookies(), - new javax.servlet.http.Cookie[0]); - for (javax.servlet.http.Cookie cookie : cookies) { - builder.put(cookie.getName(), cookie.getValue()); - } - return Maps.transformValues(builder.build().asMap(), - input -> new Cookie(null, ImmutableList.copyOf(input))); - } - - @Override - public QueryParameter queryParameter(String key) { - return firstNonNull((splitQuery(this.request.getQueryString()).get(key)), - QueryParameter.absent(key)); - } - - @Override - public boolean isBrowserProxyRequest() { - if (!isJetty()) { - return false; - } - return false; - } - - @Override - @SuppressWarnings("unchecked") - public Collection getParts() { - if (!isMultipart()) { - return null; - } - if (this.cachedMultiparts == null) { - try { - this.cachedMultiparts = from(safelyGetRequestParts()).transform( - (Function) WireMockHttpServletMultipartAdapter::from) - .toList(); - } - catch (IOException | ServletException exception) { - return throwUnchecked(exception, Collection.class); - } - } - return (this.cachedMultiparts.size() > 0) ? this.cachedMultiparts : null; - } - - private Collection safelyGetRequestParts() - throws IOException, ServletException { - try { - return this.request.getParts(); - } - catch (IOException ioe) { - if (ioe.getMessage().contains("Missing content for multipart")) { - return Collections.emptyList(); - } - - throw ioe; - } - } - - @Override - public boolean isMultipart() { - String header = getHeader("Content-Type"); - return (header != null && header.contains("multipart")); - } - - @Override - public Part getPart(final String name) { - if (name == null || name.length() == 0) { - return null; - } - if (this.cachedMultiparts == null) { - if (getParts() == null) { - return null; - } - } - return from(this.cachedMultiparts) - .firstMatch(input -> name.equals(input.getName())).get(); - } - - @Override - public Optional getOriginalRequest() { - Request originalRequest = (Request) this.request - .getAttribute(ORIGINAL_REQUEST_KEY); - return Optional.fromNullable(originalRequest); - } - - private boolean isJetty() { - try { - getClass("org.eclipse.jetty.server.Request"); - return true; - } - catch (Exception e) { - } - return false; - } - - private void getClass(String type) throws ClassNotFoundException { - ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); - ClassLoader loader = contextCL == null - ? com.github.tomakehurst.wiremock.servlet.WireMockHttpServletRequestAdapter.class - .getClassLoader() - : contextCL; - Class.forName(type, false, loader); - } - - @Override - public String toString() { - return this.request.toString() + getBodyAsString(); - } - -} diff --git a/tests/spring-cloud-contract-stub-runner-context-path/src/test/java/com/example/loan/FailFastLoanApplicationServiceTests.java b/tests/spring-cloud-contract-stub-runner-context-path/src/test/java/com/example/loan/FailFastLoanApplicationServiceTests.java index 98fdf5d154..868be7fd7e 100644 --- a/tests/spring-cloud-contract-stub-runner-context-path/src/test/java/com/example/loan/FailFastLoanApplicationServiceTests.java +++ b/tests/spring-cloud-contract-stub-runner-context-path/src/test/java/com/example/loan/FailFastLoanApplicationServiceTests.java @@ -17,7 +17,7 @@ package com.example.loan; import org.junit.Test; -import wiremock.com.google.common.collect.ImmutableMap; +import shaded.com.google.common.collect.ImmutableMap; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.factory.BeanCreationException;