Adding a mechanism to disable the local vs remote stub location
When fetching stubs / contracts in a CI, shared environment, what might happen is that both the producer and the consumer reuse the same local Maven repository. Due to this, the framework, responsible for downloading a stub JAR from remote location, can't decide which JAR should be picked, local or remote one. That caused the `"The artifact was found in the local repository but you have explicitly stated that it should be downloaded from a remote one"` exception and failed the build. For such cases we're introducing the property mechanism: - via `stubrunner.snapshot-check.skip` system property - via `STUBRUNNER_SNAPSHOT_CHECK_SKIP` environment variable if either of these values is set to `true`, then the stub downloader will not verify the origin of the downloaded JAR. fixes gh-545
This commit is contained in:
16
README.adoc
16
README.adoc
@@ -166,7 +166,7 @@ From the Consumer perspective, when shooting a request in the integration test:
|
||||
(2) - With the "PUT" method
|
||||
(3) - to the URL "/fraudcheck"
|
||||
(4) - with the JSON body that
|
||||
* has a field `clientId` that matches a regular expression `[0-9]{10}`
|
||||
* has a field `client.id` that matches a regular expression `[0-9]{10}`
|
||||
* has a field `loanAmount` that is equal to `99999`
|
||||
(5) - with header `Content-Type` equal to `application/json`
|
||||
(6) - then the response will be sent with
|
||||
@@ -181,7 +181,7 @@ From the Producer perspective, in the autogenerated producer-side test:
|
||||
(2) - With the "PUT" method
|
||||
(3) - to the URL "/fraudcheck"
|
||||
(4) - with the JSON body that
|
||||
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
|
||||
* has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
|
||||
* has a field `loanAmount` that is equal to `99999`
|
||||
(5) - with header `Content-Type` equal to `application/json`
|
||||
(6) - then the test will assert if the response has been sent with
|
||||
@@ -223,7 +223,7 @@ response: # (7)
|
||||
#(2) - With the "PUT" method
|
||||
#(3) - to the URL "/fraudcheck"
|
||||
#(4) - with the JSON body that
|
||||
# * has a field `clientId`
|
||||
# * has a field `client.id`
|
||||
# * has a field `loanAmount` that is equal to `99999`
|
||||
#(5) - with header `Content-Type` equal to `application/json`
|
||||
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
|
||||
@@ -239,7 +239,7 @@ response: # (7)
|
||||
#(2) - With the "PUT" method
|
||||
#(3) - to the URL "/fraudcheck"
|
||||
#(4) - with the JSON body that
|
||||
# * has a field `clientId` `1234567890`
|
||||
# * has a field `client.id` `1234567890`
|
||||
# * has a field `loanAmount` that is equal to `99999`
|
||||
#(5) - with header `Content-Type` equal to `application/json`
|
||||
#(7) - then the test will assert if the response has been sent with
|
||||
@@ -523,7 +523,7 @@ From the Consumer perspective, when shooting a request in the integration test:
|
||||
(2) - With the "PUT" method
|
||||
(3) - to the URL "/fraudcheck"
|
||||
(4) - with the JSON body that
|
||||
* has a field `clientId` that matches a regular expression `[0-9]{10}`
|
||||
* has a field `client.id` that matches a regular expression `[0-9]{10}`
|
||||
* has a field `loanAmount` that is equal to `99999`
|
||||
(5) - with header `Content-Type` equal to `application/json`
|
||||
(6) - then the response will be sent with
|
||||
@@ -538,7 +538,7 @@ From the Producer perspective, in the autogenerated producer-side test:
|
||||
(2) - With the "PUT" method
|
||||
(3) - to the URL "/fraudcheck"
|
||||
(4) - with the JSON body that
|
||||
* has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
|
||||
* has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
|
||||
* has a field `loanAmount` that is equal to `99999`
|
||||
(5) - with header `Content-Type` equal to `application/json`
|
||||
(6) - then the test will assert if the response has been sent with
|
||||
@@ -580,7 +580,7 @@ response: # (7)
|
||||
#(2) - With the "PUT" method
|
||||
#(3) - to the URL "/fraudcheck"
|
||||
#(4) - with the JSON body that
|
||||
# * has a field `clientId`
|
||||
# * has a field `client.id`
|
||||
# * has a field `loanAmount` that is equal to `99999`
|
||||
#(5) - with header `Content-Type` equal to `application/json`
|
||||
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
|
||||
@@ -596,7 +596,7 @@ response: # (7)
|
||||
#(2) - With the "PUT" method
|
||||
#(3) - to the URL "/fraudcheck"
|
||||
#(4) - with the JSON body that
|
||||
# * has a field `clientId` `1234567890`
|
||||
# * has a field `client.id` `1234567890`
|
||||
# * has a field `loanAmount` that is equal to `99999`
|
||||
#(5) - with header `Content-Type` equal to `application/json`
|
||||
#(7) - then the test will assert if the response has been sent with
|
||||
|
||||
@@ -393,7 +393,6 @@ following sections:
|
||||
* <<maven-different-base>>
|
||||
* <<maven-invoking-generated-tests>>
|
||||
* <<maven-sts>>
|
||||
* <<maven-consumer>>
|
||||
|
||||
[[maven-add-plugin]]
|
||||
==== Add maven plugin
|
||||
@@ -767,67 +766,6 @@ In order to fix this issue, provide the following section in your `pom.xml`:
|
||||
</build>
|
||||
----
|
||||
|
||||
[[maven-consumer]]
|
||||
==== Spring Cloud Contract Verifier on the Consumer Side
|
||||
|
||||
You can also use the Spring Cloud Contract Verifier for the consumer side. To do so, use
|
||||
the plugin so that it only converts the contracts and generates the stubs. To achieve
|
||||
that, you need to configure Spring Cloud Contract Verifier plugin in exactly the same way
|
||||
as you would for a provider. You need to copy contracts stored in
|
||||
`src/test/resources/contracts` and generate WireMock JSON stubs using the
|
||||
`mvn generateStubs` command. By default, the generated WireMock mapping is stored in a
|
||||
directory named `target/mappings`. From these generated mappings, your project should
|
||||
create additional artifacts with a classifier of `stubs` for easy deployment to the maven
|
||||
repository.
|
||||
|
||||
Here is a sample configuration:
|
||||
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
<plugin>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
||||
<version>${verifier-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>convert</goal>
|
||||
<goal>generateStubs</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
----
|
||||
|
||||
When present, JSON stubs can be used in consumer automated tests, as shown here:
|
||||
|
||||
[source,groovy,indent=0]
|
||||
----
|
||||
@RunWith(SpringTestRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureStubRunner
|
||||
public class LoanApplicationServiceTests {
|
||||
|
||||
@Autowired
|
||||
LoanApplicationService service;
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyApplyForLoan() {
|
||||
//given:
|
||||
LoanApplication application =
|
||||
new LoanApplication(new Client("12345678901"), 123.123);
|
||||
//when:
|
||||
LoanApplicationResult loanApplication = service.loanApplication(application);
|
||||
// then:
|
||||
assertThat(loanApplication.loanApplicationStatus).isEqualTo(LoanApplicationStatus.LOAN_APPLIED);
|
||||
assertThat(loanApplication.rejectionReason).isNull();
|
||||
}
|
||||
}
|
||||
----
|
||||
`LoanApplication` makes a call to the `FraudDetection` service. This request is handled
|
||||
by a WireMock server configured with stubs generated by the Spring Cloud Contract
|
||||
Verifier.
|
||||
|
||||
=== Stubs and Transitive Dependencies
|
||||
|
||||
The Maven and Gradle plugin that add the tasks that create the stubs jar for you. One
|
||||
@@ -872,6 +810,24 @@ For example, you might decide to have no dependencies at all.
|
||||
As a consumer, if you add the stub dependency to your classpath, you can explicitly
|
||||
exclude the unwanted dependencies.
|
||||
|
||||
=== CI Server setup
|
||||
|
||||
When fetching stubs / contracts in a CI, shared environment, what might happen is that
|
||||
both the producer and the consumer reuse the same local Maven repository. Due to this,
|
||||
the framework, responsible for downloading a stub JAR from remote location,
|
||||
can't decide which JAR should be picked, local or remote one. That caused
|
||||
the `"The artifact was found in the local repository but you have explicitly
|
||||
stated that it should be downloaded from a remote one"` exception
|
||||
and failed the build.
|
||||
|
||||
For such cases we're introducing the property mechanism:
|
||||
|
||||
- via `stubrunner.snapshot-check.skip` system property
|
||||
- via `STUBRUNNER_SNAPSHOT_CHECK_SKIP` environment variable
|
||||
|
||||
if either of these values is set to `true`, then the stub downloader will not
|
||||
verify the origin of the downloaded JAR.
|
||||
|
||||
=== Scenarios
|
||||
|
||||
You can handle scenarios with Spring Cloud Contract Verifier. All you need to do is to
|
||||
|
||||
@@ -72,6 +72,8 @@ public class AetherStubDownloader implements StubDownloader {
|
||||
private static final String ARTIFACT_EXTENSION = "jar";
|
||||
private static final String LATEST_ARTIFACT_VERSION = "(,]";
|
||||
private static final String LATEST_VERSION_IN_IVY = "+";
|
||||
private static final String STUBRUNNER_SNAPSHOT_CHECK_SKIP_SYSTEM_PROP = "stubrunner.snapshot-check.skip";
|
||||
private static final String STUBRUNNER_SNAPSHOT_CHECK_SKIP_ENV_VAR = "STUBRUNNER_SNAPSHOT_CHECK_SKIP";
|
||||
|
||||
private final List<RemoteRepository> remoteRepos;
|
||||
private final RepositorySystem repositorySystem;
|
||||
@@ -170,7 +172,7 @@ public class AetherStubDownloader implements StubDownloader {
|
||||
ArtifactResult result = this.repositorySystem.resolveArtifact(this.session, request);
|
||||
log.info("Resolved artifact [" + artifact + "] to "
|
||||
+ result.getArtifact().getFile());
|
||||
if (resolvedFromLocalRepo(result) && shouldDownloadFromRemote()) {
|
||||
if (!skipSnapshotCheck() && resolvedFromLocalRepo(result) && shouldDownloadFromRemote()) {
|
||||
throw new IllegalStateException("The artifact was found in the local repository "
|
||||
+ "but you have explicitly stated that it should be downloaded from a remote one");
|
||||
}
|
||||
@@ -189,7 +191,17 @@ public class AetherStubDownloader implements StubDownloader {
|
||||
+ "] and classifier [" + classifier + "] in " + this.remoteRepos,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean skipSnapshotCheck() {
|
||||
String skipSnapCheckProp = System.getProperty(STUBRUNNER_SNAPSHOT_CHECK_SKIP_SYSTEM_PROP);
|
||||
String skipSnapCheckEnv = getSkipSnapEnvProp();
|
||||
return Boolean.parseBoolean(skipSnapCheckProp) || Boolean.parseBoolean(skipSnapCheckEnv);
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
String getSkipSnapEnvProp() {
|
||||
return System.getenv(STUBRUNNER_SNAPSHOT_CHECK_SKIP_ENV_VAR);
|
||||
}
|
||||
|
||||
private boolean resolvedFromLocalRepo(ArtifactResult result) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.springframework.cloud.contract.stubrunner.util.StringUtils;
|
||||
/**
|
||||
* Technical options related to running StubRunner
|
||||
*
|
||||
* Use {@class StubRunnerOptionsBuilder} to build this object.
|
||||
* Use {@see StubRunnerOptionsBuilder} to build this object.
|
||||
*
|
||||
* @see StubRunnerOptionsBuilder
|
||||
*/
|
||||
|
||||
@@ -3,93 +3,133 @@ package org.springframework.cloud.contract.stubrunner
|
||||
import io.specto.hoverfly.junit.HoverflyRule
|
||||
import org.eclipse.aether.RepositorySystemSession
|
||||
import org.junit.Rule
|
||||
import org.springframework.util.ResourceUtils
|
||||
import spock.lang.Ignore
|
||||
import spock.lang.Specification
|
||||
import spock.util.environment.RestoreSystemProperties
|
||||
|
||||
import org.springframework.util.ResourceUtils
|
||||
|
||||
class AetherStubDownloaderSpec extends Specification {
|
||||
|
||||
@Rule
|
||||
HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode("simulation.json")
|
||||
@Rule
|
||||
HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode("simulation.json")
|
||||
|
||||
// CI tools sometimes can't reach the `test.jfrog.io` address
|
||||
// @IgnoreIf({ Boolean.valueOf(env['CI']) })
|
||||
@Ignore("There's sth wrong with the test jfrog API")
|
||||
def 'Should be able to download from a repository using username and password authentication'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withUsername("andrew.morgan")
|
||||
.withPassword("k+hbZp8rpolRucXB09dGE/CxPXxidQryQUYSGbeo6JE=")
|
||||
.withProxy("localhost", hoverflyRule.proxyPort)
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
// CI tools sometimes can't reach the `test.jfrog.io` address
|
||||
// @IgnoreIf({ Boolean.valueOf(env['CI']) })
|
||||
@Ignore("There's sth wrong with the test jfrog API")
|
||||
def 'Should be able to download from a repository using username and password authentication'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withUsername("andrew.morgan")
|
||||
.withPassword("k+hbZp8rpolRucXB09dGE/CxPXxidQryQUYSGbeo6JE=")
|
||||
.withProxy("localhost", hoverflyRule.proxyPort)
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("io.test", "test-simulations-svc", "1.0-SNAPSHOT"))
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("io.test", "test-simulations-svc", "1.0-SNAPSHOT"))
|
||||
|
||||
then:
|
||||
jar != null
|
||||
}
|
||||
then:
|
||||
jar != null
|
||||
}
|
||||
|
||||
def 'Should throw an exception when artifact not found'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withWorkOffline(true)
|
||||
.build()
|
||||
def 'Should throw an exception when artifact not found'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withWorkOffline(true)
|
||||
.build()
|
||||
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("non.existing.group", "missing-artifact-id", "1.0-SNAPSHOT"))
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("non.existing.group", "missing-artifact-id", "1.0-SNAPSHOT"))
|
||||
|
||||
then:
|
||||
IllegalStateException e = thrown(IllegalStateException)
|
||||
e.message.contains("Exception occurred while trying to download a stub for group")
|
||||
}
|
||||
then:
|
||||
IllegalStateException e = thrown(IllegalStateException)
|
||||
e.message.contains("Exception occurred while trying to download a stub for group")
|
||||
}
|
||||
|
||||
def 'Should throw an exception when a jar is in local m2 and not in remote repo'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
def 'Should throw an exception when a jar is in local m2 and not in remote repo'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("org.springframework.cloud", "spring-cloud-contract-spec", "+", ""))
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("org.springframework.cloud", "spring-cloud-contract-spec", "+", ""))
|
||||
|
||||
then:
|
||||
IllegalStateException e = thrown(IllegalStateException)
|
||||
e.message.contains("The artifact was found in the local repository but you have explicitly stated that it should be downloaded from a remote one")
|
||||
}
|
||||
then:
|
||||
IllegalStateException e = thrown(IllegalStateException)
|
||||
e.message.contains("The artifact was found in the local repository but you have explicitly stated that it should be downloaded from a remote one")
|
||||
}
|
||||
|
||||
@RestoreSystemProperties
|
||||
def 'Should use local repository from settings.xml'() {
|
||||
given:
|
||||
File tempSettings = File.createTempFile("settings", ".xml")
|
||||
def m2repoFolder = 'm2repo' + File.separator + 'repository'
|
||||
tempSettings.text = '<settings><localRepository>' +
|
||||
ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + m2repoFolder).getAbsolutePath() + '</localRepository></settings>'
|
||||
System.setProperty("org.apache.maven.user-settings", tempSettings.getAbsolutePath())
|
||||
RepositorySystemSession repositorySystemSession =
|
||||
AetherFactories.newSession(AetherFactories.newRepositorySystem(), true);
|
||||
@RestoreSystemProperties
|
||||
def 'Should not throw an exception when a jar is in local m2 and not in remote repo and system property disabled snapshot check'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
|
||||
and:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withWorkOffline(true)
|
||||
.build()
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(
|
||||
new StubConfiguration("org.springframework.cloud.contract.verifier.stubs",
|
||||
"bootService", "0.0.1-SNAPSHOT"))
|
||||
and:
|
||||
System.properties.setProperty("stubrunner.snapshot-check.skip", "true")
|
||||
|
||||
then:
|
||||
jar != null
|
||||
repositorySystemSession.getLocalRepository().getBasedir().getAbsolutePath().endsWith(m2repoFolder)
|
||||
}
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("org.springframework.cloud", "spring-cloud-contract-spec", "+", ""))
|
||||
|
||||
then:
|
||||
jar != null
|
||||
}
|
||||
|
||||
def 'Should not throw an exception when a jar is in local m2 and not in remote repo and env property disabled snapshot check'() {
|
||||
given:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withStubRepositoryRoot("https://test.jfrog.io/test/libs-snapshot-local")
|
||||
.build()
|
||||
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions) {
|
||||
@Override
|
||||
String getSkipSnapEnvProp() {
|
||||
return "true"
|
||||
}
|
||||
}
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(new StubConfiguration("org.springframework.cloud", "spring-cloud-contract-spec", "+", ""))
|
||||
|
||||
then:
|
||||
jar != null
|
||||
}
|
||||
|
||||
@RestoreSystemProperties
|
||||
def 'Should use local repository from settings.xml'() {
|
||||
given:
|
||||
File tempSettings = File.createTempFile("settings", ".xml")
|
||||
def m2repoFolder = 'm2repo' + File.separator + 'repository'
|
||||
tempSettings.text = '<settings><localRepository>' +
|
||||
ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + m2repoFolder).getAbsolutePath() + '</localRepository></settings>'
|
||||
System.setProperty("org.apache.maven.user-settings", tempSettings.getAbsolutePath())
|
||||
RepositorySystemSession repositorySystemSession =
|
||||
AetherFactories.newSession(AetherFactories.newRepositorySystem(), true);
|
||||
|
||||
and:
|
||||
StubRunnerOptions stubRunnerOptions = new StubRunnerOptionsBuilder()
|
||||
.withWorkOffline(true)
|
||||
.build()
|
||||
AetherStubDownloader aetherStubDownloader = new AetherStubDownloader(stubRunnerOptions)
|
||||
|
||||
when:
|
||||
def jar = aetherStubDownloader.downloadAndUnpackStubJar(
|
||||
new StubConfiguration("org.springframework.cloud.contract.verifier.stubs",
|
||||
"bootService", "0.0.1-SNAPSHOT"))
|
||||
|
||||
then:
|
||||
jar != null
|
||||
repositorySystemSession.getLocalRepository().getBasedir().getAbsolutePath().endsWith(m2repoFolder)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user