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:
Marcin Grzejszczak
2018-02-16 13:35:56 +01:00
parent 5de9959561
commit d87e669a23
5 changed files with 147 additions and 139 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
*/

View File

@@ -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)
}
}