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
This commit is contained in:
committed by
GitHub
parent
0ea4ecc9e7
commit
c9538548fd
@@ -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<StubConfiguration, File> 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.
|
||||
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.
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@
|
||||
<artifactId>spring-cloud-contract-wiremock</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>http-server-restdocs</artifactId>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <<custom_stub_runner, Custom Stub Runner>> 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
|
||||
----
|
||||
<dependency>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>beer-api-producer-restdocs</artifactId>
|
||||
<classifier>stubs</classifier>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.example.foo</groupId>
|
||||
<artifactId>bar</artifactId>
|
||||
<classifier>superstubs</classifier>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
[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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code META-INF/group.id/artifactid/ ** /*.* }</li>
|
||||
* <li>{@code contracts/group.id/artifactid/ ** /*.* }</li>
|
||||
* <li>{@code mappings/group.id/artifactid/ ** /*.* }</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*
|
||||
* examples
|
||||
*
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>{@code META-INF/com.example/fooservice/1.0.0/ **}</li>
|
||||
* <li>{@code contracts/com.example/artifactid/ ** /*.* }</li>
|
||||
* <li>{@code mappings/com.example/artifactid/ ** /*.* }</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<StubConfiguration, File> downloadAndUnpackStubJar(
|
||||
StubConfiguration config) {
|
||||
List<RepoRoot> repoRoots = repoRoot(stubRunnerOptions, config);
|
||||
List<String> paths = toPaths(repoRoots);
|
||||
List<Resource> 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<String> toPaths(List<RepoRoot> repoRoots) {
|
||||
List<String> list = new ArrayList<>();
|
||||
for (RepoRoot repoRoot : repoRoots) {
|
||||
list.add(repoRoot.fullPath);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
List<Resource> resolveResources(List<String> paths) {
|
||||
List<Resource> resources = new ArrayList<>();
|
||||
for (String path : paths) {
|
||||
try {
|
||||
List<Resource> 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> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<StubDownloaderBuilder> 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public class StreamStubMessages implements MessageVerifier<Message<?>> {
|
||||
.getBean(ChannelBindingServiceProperties.class);
|
||||
for (Map.Entry<String, BindingProperties> 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);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
|
||||
<module>spring-cloud-contract-stub-runner-camel</module>
|
||||
<module>spring-cloud-contract-stub-runner-context-path</module>
|
||||
<module>spring-cloud-contract-stub-runner-moco-contract-jar</module>
|
||||
<module>spring-cloud-contract-stub-runner-moco</module>
|
||||
<module>spring-cloud-contract-stub-runner-integration</module>
|
||||
<module>spring-cloud-contract-stub-runner-stream</module>
|
||||
|
||||
@@ -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 ");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-tests</artifactId>
|
||||
<version>1.1.1.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-contract-stub-runner-moco-contract-jar</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Contract Stub Runner Moco Contract Jar</name>
|
||||
<description>Spring Cloud Contract Stub Runner Moco Contract Jar</description>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-verifier</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"method" : "get",
|
||||
"uri": "/bye"
|
||||
},
|
||||
"response": {
|
||||
"text" : "bye",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"method" : "get",
|
||||
"uri": "/bye2"
|
||||
},
|
||||
"response": {
|
||||
"text" : "bye",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"method" : "get",
|
||||
"uri": "/name2"
|
||||
},
|
||||
"response": {
|
||||
"text" : "fraudDetectionServerMoco",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -29,6 +29,12 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-verifier</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-stub-runner-moco-contract-jar</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
@@ -59,6 +65,16 @@
|
||||
<artifactId>spock-global-unroll</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Stream dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-test-support</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -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<StubConfiguration, File> 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) + ")")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
spring:
|
||||
cloud.stream.bindings.input.destination: orders
|
||||
Reference in New Issue
Block a user