From c9538548fdeb3c799c6b6cab10b49a1442cd46ac Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 Apr 2017 17:13:15 +0200 Subject: [PATCH] Adds an option to use StubRunner from classpath (#284) without this change there's no option to use Stub Runner to pick stubs from classpath with this change we're adding that option. It's enough for the user not to provide neither the `repositoryRoot` nor `workOffline`. If that's the case then Classpath scanning will take place. fixes #282 --- docs/src/main/asciidoc/verifier/contract.adoc | 42 +++- .../restdocs/http-client/build.gradle | 1 + .../standalone/restdocs/http-client/pom.xml | 5 + ...pplicationServiceusingStubRunnerTests.java | 58 +++++ spring-cloud-contract-stub-runner/README.adoc | 137 +++++++++++- .../stubrunner/BatchStubRunnerFactory.java | 2 +- .../stubrunner/ClasspathStubProvider.java | 203 ++++++++++++++++++ .../StubDownloaderBuilderProvider.java | 26 ++- .../spring/StubRunnerConfiguration.java | 4 +- .../verifier/MavenContractsDownloader.java | 4 + .../messaging/stream/StreamStubMessages.java | 2 +- tests/pom.xml | 1 + .../FailFastLoanApplicationServiceTests.java | 8 +- .../pom.xml | 39 ++++ .../1.0.0/contracts/nested/contract2.groovy | 27 +++ .../1.0.0/mappings/nested/shouldSayBye.json | 12 ++ .../1.0.0/mappings}/shouldSayHello.json | 0 .../nested/contract3.groovy | 27 +++ .../nested/shouldSayBye2.json | 12 ++ .../resources/mappings/shouldSayHello2.json | 12 ++ .../pom.xml | 16 ++ .../moco/ClasspathStubProvider.groovy | 96 --------- .../moco/MocoHttpServerStubSpec.groovy | 58 ++++- .../test/resources/META-INF/spring.factories | 4 - .../src/test/resources/application-test.yaml | 2 + 25 files changed, 673 insertions(+), 125 deletions(-) create mode 100644 samples/standalone/restdocs/http-client/src/test/java/com/example/loan/LoanApplicationServiceusingStubRunnerTests.java create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ClasspathStubProvider.java create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/pom.xml create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/contracts/nested/contract2.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/nested/shouldSayBye.json rename tests/{spring-cloud-contract-stub-runner-moco/src/test/resources/unpacked/com/example/fraudDetectionServerMoco/someFolder => spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings}/shouldSayHello.json (100%) create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/contracts/com.example/fraudDetectionServerMoco/nested/contract3.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/com.example/fraudDetectionServerMoco/nested/shouldSayBye2.json create mode 100644 tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/shouldSayHello2.json delete mode 100644 tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy create mode 100644 tests/spring-cloud-contract-stub-runner-moco/src/test/resources/application-test.yaml diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index ea6e1e0f2e..09904660b5 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -1007,7 +1007,8 @@ and just register it in your `spring.factories` file [source] ---- -include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories[indent=0,lines=1..4] +org.springframework.cloud.contract.stubrunner.HttpServerStub=\ +org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub ---- that way you'll be able to run stubs using Moco. @@ -1017,25 +1018,46 @@ will be picked. If you provide more than one then the first one on the list will ==== Custom Stub Downloader -You can customize the way your stubs are downloaded. If you don't want to download the JARs -from Nexus / Artifactory in the way we do by default you can set your own implementation. -Below you can find an example of a Stub Downloader Provider that takes `json` files from the test resources -from classpath, copies them to a temp file and then passes that temporary folder - as a root for the stubs. +You can customize the way your stubs are downloaded. It's enough to create an +implementation of the `StubDownloaderBuilder` [source,java] ---- -include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy[indent=0,lines=16..-1] +package com.example; + +class CustomStubDownloaderBuilder implements StubDownloaderBuilder { + + @Override + public StubDownloader build(final StubRunnerOptions stubRunnerOptions) { + return new StubDownloader() { + @Override + public Map.Entry downloadAndUnpackStubJar( + StubConfiguration config) { + File unpackedStubs = retrieveStubs(); + return new AbstractMap.SimpleEntry<>( + new StubConfiguration(config.getGroupId(), config.getArtifactId(), version, + config.getClassifier()), unpackedStubs); + } + + File retrieveStubs() { + // here goes your custom logic to provide a folder where all the stubs reside + } +} ---- and just register it in your `spring.factories` file [source] ---- -include::{tests_path}/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories[indent=0,lines=5..-1] +# Example of a custom Stub Downloader Provider +org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ +com.example.CustomStubDownloaderBuilder ---- that way you'll be able to pick a folder with the source of your stubs. -IMPORTANT: If you don't provide any implementation then the default one - Aether based that will download stubs from a remote repo -will be picked. If you provide more than one then the first one on the list will be picked. \ No newline at end of file +IMPORTANT: If you don't provide any implementation then the default one will be picked. + If you provide `repositoryRoot` property or `workOffline` flag then Aether based + that will download stubs from a remote repo will be picked. If you don't provide these + values then the `ClasspathStubProvider` will be picked that will scan the classpath. + If you provide more than one, then the first one on the list will be picked. \ No newline at end of file diff --git a/samples/standalone/restdocs/http-client/build.gradle b/samples/standalone/restdocs/http-client/build.gradle index b14b0359d5..a13f009b68 100644 --- a/samples/standalone/restdocs/http-client/build.gradle +++ b/samples/standalone/restdocs/http-client/build.gradle @@ -38,6 +38,7 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-actuator") testCompile 'org.springframework.cloud:spring-cloud-contract-wiremock' + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' testCompile "org.springframework.boot:spring-boot-starter-test" testCompile "com.example:http-server-restdocs:0.0.1-SNAPSHOT:stubs" } diff --git a/samples/standalone/restdocs/http-client/pom.xml b/samples/standalone/restdocs/http-client/pom.xml index 4e74d158d8..6b19223ed4 100644 --- a/samples/standalone/restdocs/http-client/pom.xml +++ b/samples/standalone/restdocs/http-client/pom.xml @@ -49,6 +49,11 @@ spring-cloud-contract-wiremock test + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner + test + com.example http-server-restdocs diff --git a/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/LoanApplicationServiceusingStubRunnerTests.java b/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/LoanApplicationServiceusingStubRunnerTests.java new file mode 100644 index 0000000000..3c7403298f --- /dev/null +++ b/samples/standalone/restdocs/http-client/src/test/java/com/example/loan/LoanApplicationServiceusingStubRunnerTests.java @@ -0,0 +1,58 @@ +package com.example.loan; + +import com.example.loan.model.Client; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureStubRunner(ids = "com.example:http-server-restdocs") +public class LoanApplicationServiceusingStubRunnerTests { + + @Autowired LoanApplicationService service; + @Value("${stubrunner.runningstubs.http-server-restdocs.port}") int port; + + @Before + public void setup() { + this.service.setPort(this.port); + } + + @Test + public void shouldSuccessfullyApplyForLoan() throws Exception { + // given + LoanApplication application = new LoanApplication(new Client("1234567890"), + 123.123); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLIED); + assertThat(loanApplication.getRejectionReason()).isNull(); + } + + @Test + public void shouldBeRejectedDueToAbnormalLoanAmount() throws Exception { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 99999); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED); + assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); + } + +} diff --git a/spring-cloud-contract-stub-runner/README.adoc b/spring-cloud-contract-stub-runner/README.adoc index 20f498470f..0d3a0a113c 100644 --- a/spring-cloud-contract-stub-runner/README.adoc +++ b/spring-cloud-contract-stub-runner/README.adoc @@ -3,9 +3,144 @@ Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of http://martinfowler.com/articles/consumerDrivenContracts.html[Consumer Driven Contracts]. -Stub Runner allows you to automatically download the stubs of the provided dependencies, start WireMock servers for them and feed them with proper stub definitions. +Stub Runner allows you to automatically download the stubs of the provided dependencies (or pick those from the classpath), start WireMock servers for them and feed them with proper stub definitions. For messaging, special stub routes are defined. +==== Retrieving stubs + +You can pick the following options of acquiring stubs + +- Aether based solution that downloads JARs with stubs from Artifactory / Nexus +- Classpath scanning solution that searches classpath via pattern to retrieve stubs +- Write your own implementation of the `org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder` for full customization + +The latter example is described in the <> section. + +===== Stub downloading + +If you provide the `stubrunner.repositoryRoot` or `stubrunner.workOffline` flag will be set +to `true` then Stub Runner will connect to the given server and download the required jars. +It will then unpack the JAR to a temporary folder and reference those files in further +contract processing. + +Example: + +[source,java] +---- +@AutoConfigureStubRunner(repositoryRoot="http://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095") +---- + +===== Classpath scanning + +If you *DON'T* provide the `stubrunner.repositoryRoot` or `stubrunner.workOffline` flag will +be set to `false` (that's the default) then classpath will get scanned. Let's look at the +following example: + +[source,java] +---- +@AutoConfigureStubRunner(ids = { + "com.example:beer-api-producer:+:stubs:8095", + "com.example.foo:bar:1.0.0:superstubs:8096" +}) +---- + +If you've added the dependencies to your classpath + +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven +---- + + com.example + beer-api-producer-restdocs + stubs + 0.0.1-SNAPSHOT + test + + + * + * + + + + + com.example.foo + bar + superstubs + 1.0.0 + test + + + * + * + + + +---- + +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- +testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") { + transitive = false +} +testCompile("com.example.foo:bar:1.0.0:superstubs") { + transitive = false +} +---- + +Then the following locations on your classpath will get scanned. For `com.example:beer-api-producer-restdocs` + +- /META-INF/com.example/beer-api-producer-restdocs/**/*.* +- /contracts/com.example/beer-api-producer-restdocs/**/*.* +- /mappings/com.example/beer-api-producer-restdocs/**/*.* + +and `com.example.foo:bar` + +- /META-INF/com.example.foo/bar/**/*.* +- /contracts/com.example.foo/bar/**/*.* +- /mappings/com.example.foo/bar/**/*.* + +TIP: As you can see you have to explicitly provide the group and artifact ids when packaging the +producer stubs. + +The producer would setup the contracts like this: + +[source,bash] +---- +└── src + └── test + └── resources + └── contracts +    └── com.example +       └── beer-api-producer-restdocs +       └── nested +       └── contract3.groovy + +---- + +To achieve proper stub packaging. + +Or using the https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/producer_with_restdocs/pom.xml[Maven `assembly` plugin] or +https://github.com/spring-cloud-samples/spring-cloud-contract-samples/blob/master/producer_with_restdocs/build.gradle[Gradle Jar] task you have to create the following +structure in your stubs jar. + +[source,bash] +---- +└── META-INF + └── com.example + └── beer-api-producer-restdocs + └── 2.0.0 + ├── contracts + │   └── nested +    │ └── contract2.groovy +    └── mappings +    └── mapping.json + +---- + +By maintaining this structure classpath gets scanned and you can profit from the messaging / +HTTP stubs without the need to download artifacts. + ==== Running stubs ===== Limitations diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java index 06f06b4a91..c64c234761 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/BatchStubRunnerFactory.java @@ -41,7 +41,7 @@ public class BatchStubRunnerFactory { private static StubDownloader aetherStubDownloader(StubRunnerOptions stubRunnerOptions) { StubDownloaderBuilderProvider provider = new StubDownloaderBuilderProvider(); - return provider.hasBuilder() ? provider.get().build(stubRunnerOptions) : new AetherStubDownloader(stubRunnerOptions); + return provider.getOrDefaultDownloader(stubRunnerOptions); } public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader) { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ClasspathStubProvider.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ClasspathStubProvider.java new file mode 100644 index 0000000000..44691ea2d7 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/ClasspathStubProvider.java @@ -0,0 +1,203 @@ +package org.springframework.cloud.contract.stubrunner; + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.contract.stubrunner.util.StringUtils; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +/** + * Stub downloader that picks stubs and contracts from the provided resource. + * If no {@link org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties#repositoryRoot} + * is provided then by default classpath is searched according to what has been passed in + * {@link org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties#ids}. The + * pattern to search for stubs looks like this + * + *
    + *
  • {@code META-INF/group.id/artifactid/ ** /*.* }
  • + *
  • {@code contracts/group.id/artifactid/ ** /*.* }
  • + *
  • {@code mappings/group.id/artifactid/ ** /*.* }
  • + *
+ *

+ * + * examples + * + *

+ *

    + *
  • {@code META-INF/com.example/fooservice/1.0.0/ **}
  • + *
  • {@code contracts/com.example/artifactid/ ** /*.* }
  • + *
  • {@code mappings/com.example/artifactid/ ** /*.* }
  • + *
+ * + * @author Marcin Grzejszczak + * @since 1.1.1 + */ +public class ClasspathStubProvider implements StubDownloaderBuilder { + + private static final Log log = LogFactory + .getLog(MethodHandles.lookup().lookupClass()); + + private static final int TEMP_DIR_ATTEMPTS = 10000; + private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( + new DefaultResourceLoader()); + + @Override + public StubDownloader build(final StubRunnerOptions stubRunnerOptions) { + return new StubDownloader() { + @Override + public Map.Entry downloadAndUnpackStubJar( + StubConfiguration config) { + List repoRoots = repoRoot(stubRunnerOptions, config); + List paths = toPaths(repoRoots); + List resources = resolveResources(paths); + if (log.isDebugEnabled()) { + log.debug("For paths " + paths + " found following resources " + resources); + } + if (resources.isEmpty()) { + throw new IllegalStateException("No stubs were found on classpath for [" + config.getGroupId() + ":" + config.getArtifactId() + "]"); + } + final File tmp = createTempDir(); + tmp.deleteOnExit(); + Pattern groupAndArtifactPattern = Pattern.compile( + "^(.*)(" + config.getGroupId() + "." + config.getArtifactId() + ")(.*)$"); + String version = config.getVersion(); + for (Resource resource : resources) { + try { + String relativePath = relativePathPicker(resource, groupAndArtifactPattern); + int lastIndexOf = relativePath.lastIndexOf(File.separator); + String relativePathWithoutFile = lastIndexOf > -1 ? + relativePath.substring(0, lastIndexOf) : + relativePath; + Path directory = Files.createDirectories( + new File(tmp, relativePathWithoutFile).toPath()); + File newFile = new File(directory.toFile(), resource.getFilename()); + if (!newFile.exists() && !isDirectory(resource)) { + Files.copy(resource.getInputStream(), newFile.toPath()); + } + if (log.isDebugEnabled()) { + log.debug("Stored file [" + newFile + "]"); + } + } catch (IOException e) { + log.error("Exception occurred while trying to create dirs", e); + throw new IllegalStateException(e); + } + } + log.info("Unpacked files for [" + config.getGroupId() + ":" + config.getArtifactId() + + ":" + version + "] to folder [" + tmp + "]"); + return new AbstractMap.SimpleEntry<>( + new StubConfiguration(config.getGroupId(), config.getArtifactId(), version, + config.getClassifier()), tmp); + } + + boolean isDirectory(Resource resource) { + try { + return resource.getFile().isDirectory(); + } catch (Exception e) { + if (log.isTraceEnabled()) { + log.trace("Exception occurred while trying to convert path to file for resource [" + resource + "]", e); + } + return false; + } + } + + String relativePathPicker(Resource resource, + Pattern groupAndArtifactPattern) throws IOException { + String uri = resource.getURI().toString(); + Matcher groupAndArtifactMatcher = groupAndArtifactPattern.matcher(uri); + if (groupAndArtifactMatcher.matches()) { + MatchResult groupAndArtifactResult = groupAndArtifactMatcher + .toMatchResult(); + return groupAndArtifactResult.group(2) + File.separator + + groupAndArtifactResult.group(3); + } + else { + throw new IllegalArgumentException("Illegal uri [${uri}]"); + } + } + }; + } + + private List toPaths(List repoRoots) { + List list = new ArrayList<>(); + for (RepoRoot repoRoot : repoRoots) { + list.add(repoRoot.fullPath); + } + return list; + } + + List resolveResources(List paths) { + List resources = new ArrayList<>(); + for (String path : paths) { + try { + List list = Arrays.asList(this.resolver.getResources(path)); + resources.addAll(list); + } + catch (IOException e) { + log.error("Exception occurred while trying to fetch resources from [" + path + "]"); + throw new IllegalStateException(e); + } + } + return resources; + } + + private List repoRoot(StubRunnerOptions stubRunnerOptions, + StubConfiguration configuration) { + if (StringUtils.hasText(stubRunnerOptions.getStubRepositoryRoot())) { + return Collections + .singletonList(new RepoRoot(stubRunnerOptions.getStubRepositoryRoot())); + } + else { + String path = "/**/" + configuration.getGroupId() + "/" + configuration.getArtifactId(); + return Arrays.asList(new RepoRoot("classpath*:/META-INF" + path, "/**/*.*"), + new RepoRoot("classpath*:/contracts" + path, "/**/*.*"), + new RepoRoot("classpath*:/mappings" + path, "/**/*.*")); + } + } + + // Taken from Guava + private File createTempDir() { + File baseDir = new File(System.getProperty("java.io.tmpdir")); + String baseName = System.currentTimeMillis() + "-"; + for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { + File tempDir = new File(baseDir, baseName + counter); + if (tempDir.mkdir()) { + return tempDir; + } + } + throw new IllegalStateException( + "Failed to create directory within " + TEMP_DIR_ATTEMPTS + + " attempts (tried " + baseName + "0 to " + baseName + ( + TEMP_DIR_ATTEMPTS - 1) + ")"); + } + + private static class RepoRoot { + final String repoRoot; + final String fullPath; + + RepoRoot(String repoRoot) { + this.repoRoot = repoRoot; + this.fullPath = repoRoot + ""; + } + + RepoRoot(String repoRoot, String suffix) { + this.repoRoot = repoRoot; + this.fullPath = repoRoot + suffix; + } + } +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubDownloaderBuilderProvider.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubDownloaderBuilderProvider.java index 8660cc76ce..3cc3d201f6 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubDownloaderBuilderProvider.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubDownloaderBuilderProvider.java @@ -1,18 +1,25 @@ package org.springframework.cloud.contract.stubrunner; +import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.StringUtils; /** - * Provider for {@link StubDownloaderBuilder} + * Provider for {@link StubDownloaderBuilder}. It can also pick a default + * downloader if none is provided * * @author Marcin Grzejszczak * @since 1.1.0 */ public class StubDownloaderBuilderProvider { + private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass()); + private final List builders = new ArrayList<>(); public StubDownloaderBuilderProvider() { @@ -24,6 +31,23 @@ public class StubDownloaderBuilderProvider { return this.builders.isEmpty() ? null : this.builders.get(0); } + /** + * If a {@link StubDownloaderBuilder} is present will build a {@link StubDownloader} from it. + * If not will return the defaults basing on the {@link StubRunnerOptions} values + */ + public StubDownloader getOrDefaultDownloader(StubRunnerOptions stubRunnerOptions) { + if (hasBuilder()) { + log.info("A custom Stub Downloader was passed - will pick [" + get() + "]"); + return get().build(stubRunnerOptions); + } + if (!stubRunnerOptions.isWorkOffline() && StringUtils.isEmpty(stubRunnerOptions.getStubRepositoryRoot())) { + log.info("Classpath scanning will be used due to passed propreties"); + return new ClasspathStubProvider().build(stubRunnerOptions); + } + log.info("Will download stubs using Aether"); + return new AetherStubDownloader(stubRunnerOptions); + } + public boolean hasBuilder() { return get() != null; } diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java index 211b04cb96..7ae3830ed7 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/StubRunnerConfiguration.java @@ -23,7 +23,6 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.contract.stubrunner.AetherStubDownloader; import org.springframework.cloud.contract.stubrunner.BatchStubRunner; import org.springframework.cloud.contract.stubrunner.BatchStubRunnerFactory; import org.springframework.cloud.contract.stubrunner.RunningStubs; @@ -72,8 +71,7 @@ public class StubRunnerConfiguration { } StubRunnerOptions stubRunnerOptions = builder.build(); BatchStubRunner batchStubRunner = new BatchStubRunnerFactory(stubRunnerOptions, - this.provider.hasBuilder() ? this.provider.get().build(stubRunnerOptions) - : new AetherStubDownloader(stubRunnerOptions), + this.provider.getOrDefaultDownloader(stubRunnerOptions), this.contractVerifierMessaging != null ? this.contractVerifierMessaging : new NoOpStubMessages()).buildBatchStubRunner(); // TODO: Consider running it in a separate thread diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/MavenContractsDownloader.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/MavenContractsDownloader.java index 815c7b7df2..fd84ae439a 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/MavenContractsDownloader.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/MavenContractsDownloader.java @@ -8,6 +8,7 @@ import org.apache.maven.project.MavenProject; import org.eclipse.aether.RepositorySystemSession; import org.springframework.cloud.contract.maven.verifier.stubrunner.AetherStubDownloaderFactory; import org.springframework.cloud.contract.stubrunner.AetherStubDownloader; +import org.springframework.cloud.contract.stubrunner.ClasspathStubProvider; import org.springframework.cloud.contract.stubrunner.ContractDownloader; import org.springframework.cloud.contract.stubrunner.StubConfiguration; import org.springframework.cloud.contract.stubrunner.StubDownloader; @@ -99,6 +100,9 @@ class MavenContractsDownloader { logStubDownloader(builder); return builder.build(buildOptions()); } + if (StringUtils.isEmpty(this.contractsRepositoryUrl) && !this.contractsWorkOffline) { + return new ClasspathStubProvider().build(buildOptions()); + } return this.aetherStubDownloaderFactory.build(this.repoSession); } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamStubMessages.java index 26da180f71..26f3b655d5 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamStubMessages.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/stream/StreamStubMessages.java @@ -86,7 +86,7 @@ public class StreamStubMessages implements MessageVerifier> { .getBean(ChannelBindingServiceProperties.class); for (Map.Entry entry : channelBindingServiceProperties .getBindings().entrySet()) { - if (entry.getValue().getDestination().equals(destination)) { + if (destination.equals(entry.getValue().getDestination())) { if (log.isDebugEnabled()) { log.debug("Found a channel named [{}] with destination [{}]", entry.getKey(), destination); diff --git a/tests/pom.xml b/tests/pom.xml index 9a0e9bc8d1..5135e12fb5 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -27,6 +27,7 @@ spring-cloud-contract-stub-runner-boot-zookeeper spring-cloud-contract-stub-runner-camel spring-cloud-contract-stub-runner-context-path + spring-cloud-contract-stub-runner-moco-contract-jar spring-cloud-contract-stub-runner-moco spring-cloud-contract-stub-runner-integration spring-cloud-contract-stub-runner-stream 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 974978060d..f41437f6d3 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 @@ -16,8 +16,7 @@ package com.example.loan; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; +import com.google.common.collect.ImmutableMap; import org.junit.Test; import org.springframework.beans.BeanInstantiationException; @@ -25,7 +24,8 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration; -import com.google.common.collect.ImmutableMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; /** * @author Andrew Morgan @@ -64,7 +64,7 @@ public class FailFastLoanApplicationServiceTests { assertThat(throwable.getCause()).isInstanceOf(BeanInstantiationException.class); assertThat(throwable.getCause().getCause()) .isInstanceOf(IllegalStateException.class) - .hasMessage("Remote repositories for stubs are not specified and work offline flag wasn't passed"); + .hasMessageContaining("No stubs were found on classpath "); } } diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/pom.xml b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/pom.xml new file mode 100644 index 0000000000..519bb54c7c --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tests + 1.1.1.BUILD-SNAPSHOT + .. + + spring-cloud-contract-stub-runner-moco-contract-jar + jar + Spring Cloud Contract Stub Runner Moco Contract Jar + Spring Cloud Contract Stub Runner Moco Contract Jar + + 1.8 + + + + org.springframework.cloud + spring-cloud-contract-verifier + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + testCompile + + + + + + + diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/contracts/nested/contract2.groovy b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/contracts/nested/contract2.groovy new file mode 100644 index 0000000000..94fc426530 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/contracts/nested/contract2.groovy @@ -0,0 +1,27 @@ +org.springframework.cloud.contract.spec.Contract.make { + // Human readable description + description 'Sends an order message' + // Label by means of which the output message can be triggered + label 'send_order' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('orderTrigger()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('orders') + // any headers for the output message + headers { + header('contentType': 'application/json') + } + // the body of the output message + body( + orderId: value( + consumer('40058c70-891c-4176-a033-f70bad0c5f77'), + producer(regex('([0-9|a-f]*-*)*'))), + description: "This is the order description" + ) + } +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/nested/shouldSayBye.json b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/nested/shouldSayBye.json new file mode 100644 index 0000000000..e69e45de21 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/nested/shouldSayBye.json @@ -0,0 +1,12 @@ +[ +{ + "request": { + "method" : "get", + "uri": "/bye" + }, + "response": { + "text" : "bye", + "status": 200 + } +} +] \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/unpacked/com/example/fraudDetectionServerMoco/someFolder/shouldSayHello.json b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/shouldSayHello.json similarity index 100% rename from tests/spring-cloud-contract-stub-runner-moco/src/test/resources/unpacked/com/example/fraudDetectionServerMoco/someFolder/shouldSayHello.json rename to tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/META-INF/com.example/fraudDetectionServerMoco/1.0.0/mappings/shouldSayHello.json diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/contracts/com.example/fraudDetectionServerMoco/nested/contract3.groovy b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/contracts/com.example/fraudDetectionServerMoco/nested/contract3.groovy new file mode 100644 index 0000000000..73c6b8a114 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/contracts/com.example/fraudDetectionServerMoco/nested/contract3.groovy @@ -0,0 +1,27 @@ +org.springframework.cloud.contract.spec.Contract.make { + // Human readable description + description 'Sends an order message' + // Label by means of which the output message can be triggered + label 'send_order2' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('orderTrigger()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('orders') + // any headers for the output message + headers { + header('contentType': 'application/json') + } + // the body of the output message + body( + orderId: value( + consumer('40058c70-891c-4176-a033-f70bad0c5f77'), + producer(regex('([0-9|a-f]*-*)*'))), + description: "This is the order description" + ) + } +} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/com.example/fraudDetectionServerMoco/nested/shouldSayBye2.json b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/com.example/fraudDetectionServerMoco/nested/shouldSayBye2.json new file mode 100644 index 0000000000..121619f221 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/com.example/fraudDetectionServerMoco/nested/shouldSayBye2.json @@ -0,0 +1,12 @@ +[ +{ + "request": { + "method" : "get", + "uri": "/bye2" + }, + "response": { + "text" : "bye", + "status": 200 + } +} +] \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/shouldSayHello2.json b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/shouldSayHello2.json new file mode 100644 index 0000000000..388bef458a --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco-contract-jar/src/main/resources/mappings/shouldSayHello2.json @@ -0,0 +1,12 @@ +[ +{ + "request": { + "method" : "get", + "uri": "/name2" + }, + "response": { + "text" : "fraudDetectionServerMoco", + "status": 200 + } +} +] \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco/pom.xml b/tests/spring-cloud-contract-stub-runner-moco/pom.xml index 5e8cddc385..5a951f338c 100644 --- a/tests/spring-cloud-contract-stub-runner-moco/pom.xml +++ b/tests/spring-cloud-contract-stub-runner-moco/pom.xml @@ -29,6 +29,12 @@ org.springframework.cloud spring-cloud-contract-verifier
+ + org.springframework.cloud + spring-cloud-contract-stub-runner-moco-contract-jar + runtime + ${project.version} + junit junit @@ -59,6 +65,16 @@ spock-global-unroll test + + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + + + org.springframework.cloud + spring-cloud-stream-test-support + test + diff --git a/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy b/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy deleted file mode 100644 index dfbd698b19..0000000000 --- a/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/ClasspathStubProvider.groovy +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2013-2016 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 - * - * 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 org.springframework.cloud.contract.stubrunner.provider.moco - -import org.springframework.cloud.contract.stubrunner.StubConfiguration -import org.springframework.cloud.contract.stubrunner.StubDownloader -import org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder -import org.springframework.cloud.contract.stubrunner.StubRunnerOptions -import org.springframework.core.io.DefaultResourceLoader -import org.springframework.core.io.Resource -import org.springframework.core.io.support.PathMatchingResourcePatternResolver - -import java.nio.file.Files - -/** - * Poor man's version of taking stubs from classpath. It needs much more - * love and attention to go to the main sources. - * - * @author Marcin Grzejszczak - */ -class ClasspathStubProvider implements StubDownloaderBuilder { - - private static final int TEMP_DIR_ATTEMPTS = 10000 - - @Override - public StubDownloader build(StubRunnerOptions stubRunnerOptions) { - final StubConfiguration configuration = stubRunnerOptions.getDependencies().first() - PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( - new DefaultResourceLoader()) - try { - String rootFolder = repoRoot(stubRunnerOptions) ?: "**/" + separatedArtifact(configuration) + "/**/*.json" - Resource[] resources = resolver.getResources(rootFolder) - final File tmp = createTempDir() - tmp.deleteOnExit() - // you'd have to write an impl to maintain the folder structure - // this is just for demo - resources.each { Resource resource -> - Files.copy(resource.getInputStream(), new File(tmp, resource.getFile().getName()).toPath()) - } - return new StubDownloader() { - @Override - public Map.Entry downloadAndUnpackStubJar( - StubConfiguration stubConfiguration) { - return new AbstractMap.SimpleEntry(configuration, tmp) - } - } - } catch (IOException e) { - throw new IllegalStateException(e) - } - } - - private String repoRoot(StubRunnerOptions stubRunnerOptions) { - switch (stubRunnerOptions.stubRepositoryRoot) { - case { !it }: - return "" - case { String root -> root.endsWith("**/*.json") }: - return stubRunnerOptions.stubRepositoryRoot - default: - return stubRunnerOptions.stubRepositoryRoot + "/**/*.json" - } - } - - private String separatedArtifact(StubConfiguration configuration) { - return configuration.getGroupId().replace(".", File.separator) + - File.separator + configuration.getArtifactId() - } - - // Taken from Guava - private File createTempDir() { - File baseDir = new File(System.getProperty("java.io.tmpdir")) - String baseName = System.currentTimeMillis() + "-" - for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { - File tempDir = new File(baseDir, baseName + counter) - if (tempDir.mkdir()) { - return tempDir - } - } - throw new IllegalStateException( - "Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried " + baseName + "0 to " + baseName + ( - TEMP_DIR_ATTEMPTS - 1) + ")") - } -} \ No newline at end of file diff --git a/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/MocoHttpServerStubSpec.groovy b/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/MocoHttpServerStubSpec.groovy index d413884df4..2889ab29f7 100644 --- a/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/MocoHttpServerStubSpec.groovy +++ b/tests/spring-cloud-contract-stub-runner-moco/src/test/groovy/org/springframework/cloud/contract/stubrunner/provider/moco/MocoHttpServerStubSpec.groovy @@ -22,8 +22,14 @@ import org.springframework.boot.test.context.SpringBootContextLoader import org.springframework.boot.test.context.SpringBootTest import org.springframework.cloud.contract.stubrunner.StubFinder import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner +import org.springframework.cloud.stream.annotation.EnableBinding +import org.springframework.cloud.stream.annotation.StreamListener +import org.springframework.cloud.stream.messaging.Sink +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.stereotype.Component import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -31,22 +37,66 @@ import spock.lang.Specification * @author Marcin Grzejszczak */ @ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +// tag::[classpath_stub_runner] @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -@AutoConfigureStubRunner( ids = - ["com.example:fraudDetectionServerMoco"], - repositoryRoot = "classpath:unpacked/") +// to use stubs from classpath just provide ids without passing any other properties to +// @AutoConfigureStubRunner +@AutoConfigureStubRunner( ids = ["com.example:fraudDetectionServerMoco"]) +// end::[classpath_stub_runner] @DirtiesContext +@ActiveProfiles("test") class MocoHttpServerStubSpec extends Specification { @Autowired StubFinder stubFinder + @Autowired MyListener myListener def 'should successfully receive a response from a stub'() { + given: + String url = stubFinder.findStubUrl('fraudDetectionServerMoco').toString() expect: - "${stubFinder.findStubUrl('fraudDetectionServerMoco').toString()}/name".toURL().text == 'fraudDetectionServerMoco' + "${url.toString()}/name".toURL().text == 'fraudDetectionServerMoco' + "${url.toString()}/bye".toURL().text == 'bye' + "${url.toString()}/bye2".toURL().text == 'bye' + when: + "${url.toString()}/name2".toURL().text + then: + thrown(IOException) + when: + stubFinder.trigger("send_order") + then: + myListener.model?.description == "This is the order description" + when: + myListener.model = null + stubFinder.trigger("send_order2") + then: + myListener.model?.description == "This is the order description" } @Configuration @EnableAutoConfiguration + @EnableBinding(Sink.class) static class Config { + + @Bean + MyListener myListener() { + return new MyListener() + } + } + + @Component + static class MyListener { + + Model model + + @StreamListener(Sink.INPUT) + void listen(Model data) { + this.model = data + } + } + + static class Model { + String uuid + String description } } + diff --git a/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories b/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories index c0500db23e..6ffd1b43fd 100644 --- a/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories +++ b/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/META-INF/spring.factories @@ -2,7 +2,3 @@ org.springframework.cloud.contract.stubrunner.HttpServerStub=\ org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub -# Example of a custom Stub Downloader Provider -org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ -org.springframework.cloud.contract.stubrunner.provider.moco.ClasspathStubProvider - diff --git a/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/application-test.yaml b/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/application-test.yaml new file mode 100644 index 0000000000..b396496460 --- /dev/null +++ b/tests/spring-cloud-contract-stub-runner-moco/src/test/resources/application-test.yaml @@ -0,0 +1,2 @@ +spring: + cloud.stream.bindings.input.destination: orders \ No newline at end of file