Enhancement / Bug
I would like to send byte array in multipart file content.
Example contract:
Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test([100, 117, 112, 97] as byte[]))
)
)
}
response {
status 200
}
}
Expected generated stub:
Stub:
{
"id" : "ecf4d0c0-55fa-4dd6-a415-370b7db85f4f",
"request" : {
"url" : "/multipart",
"method" : "PUT",
"headers" : {
"Content-Type" : {
"matches" : "multipart/form-data;boundary=AaB03x.*"
}
},
"bodyPatterns" : [ {
"matches" : ".*--(.*)\r\nContent-Disposition: form-data; name=\"file\"; filename=\".+\"\r\n(Content-Type: .*\r\n)?(Content-Length: \\d+\r\n)?\r\n.+\r\n--\\1.*"
} ]
},
"response" : {
"status" : 200,
"transformers" : [ "response-template" ]
},
"uuid" : "ecf4d0c0-55fa-4dd6-a415-370b7db85f4f"
}
Expected generated test:
def validate_something() throws Exception {
given:
def request = given()
.header("Content-Type", "multipart/form-data;boundary=AaB03x")
.multiPart('file', 'file', [100, 117, 112, 97] as byte[])
when:
def response = given().spec(request)
.put("/multipart")
then:
response.statusCode == 200
}
As I understand currently content of file in multipart section is implicitly changed to bytes in tests but there is no way to configure it in contract. Moreover, current exception, when trying to pass byte [], is in ':generateClientStubs' task.
In my use case I'm using method that returns generated byte [] that I would like to pass in contract as file.
Stack trace:
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':generateClientStubs'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:103)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:73)
at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)
at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:59)
at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:101)
at org.gradle.api.internal.tasks.execution.FinalizeInputFilePropertiesTaskExecuter.execute(FinalizeInputFilePropertiesTaskExecuter.java:44)
at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:88)
at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:62)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:623)
at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:578)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: org.springframework.cloud.contract.verifier.converter.ConversionContractVerifierException: Unable to make conversion of checkInNewProfile.groovy
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1$_closure2.doCall(RecursiveFilesConverter.groovy:105)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1.doCall(RecursiveFilesConverter.groovy:67)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1.call(RecursiveFilesConverter.groovy)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter.processFiles(RecursiveFilesConverter.groovy:66)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$processFiles.call(Unknown Source)
at org.springframework.cloud.contract.verifier.plugin.GenerateWireMockClientStubsFromDslTask.generate(GenerateWireMockClientStubsFromDslTask.groovy:58)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:46)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:788)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:755)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:124)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:113)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:95)
... 30 more
Caused by: java.lang.ClassCastException
I think as a workaround you can write instead:
Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test([100, 117, 112, 97] as byte[]))
)
)
}
response {
status 200
}
}
use new String(byte[])
Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test(new String([100, 117, 112, 97] as byte[])))
)
)
}
response {
status 200
}
}
Workaround looks good for simple scenario where byte array represents text file content (and even those probably would need to be properly escaped). In my case I would like to pass binary file and wrapping it in String isn't enough.
Once they're built, please check out the latest 2.0.0 snapshots and ping me back if things work now.
Repository http://repo.spring.io/snapshot has some archaic snapshot for version 2.0.0.
Where can I find current build for 2.0.0.M8?
2.0.0.BUILD-SNAPSHOT/ 22-Jun-2017 06:12 -
If you go into the folder you'll see that snapshots are newer ;)
I see.
Then I have issue that I don't really understand after migrating to 2.0.0 snapshot from 1.2.4 snappshot.
Could not set unknown property 'contractsWorkOffline' for org.springframework.cloud.contract.verifier.plugin.ContractVerifierExtension
This class on branch 2.0.x in GitHub looks fine :
/**
* If {@code true} then JAR with contracts will be taken from local maven repository
*/
boolean contractsWorkOffline
but
spring-cloud-contract-gradle-plugin-2.0.0.BUILD-20180320.123626-1628.jar 20-Mar-2018 12:36 83.87 KB
doesn't include this field.
Did I miss something? Was that property removed? I thought that 2.0.0 snapshots were build from 2.0.x branch, was I wrong?
:edit
Property contractsWorkOffline is used in gradle i.e.
contracts {
contractsWorkOffline = true
contractDependency {
stringNotation = "com.example:beer-contracts"
}
}
No, they are built from master. We've migrated away from 2.0.x at some point. The issue you have is related to https://github.com/spring-cloud/spring-cloud-contract/issues/287. You can check the new samples to see that (https://github.com/spring-cloud-samples/spring-cloud-contract-samples/commit/b4f0d332b1ece07b90b58ee54eaddfd0d33fc8e1)
Thank you for the clarification.
I've tried to build simple contract from my first comment but unfortunately it crashed. Bellow you can find contract I've tried to build + exception + log from DEBUG.
When passing simple String instead of byte [] generated test looks fine.
Exception:
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':generateClientStubs'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:103)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:73)
at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)
at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:59)
at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:101)
at org.gradle.api.internal.tasks.execution.FinalizeInputFilePropertiesTaskExecuter.execute(FinalizeInputFilePropertiesTaskExecuter.java:44)
at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:88)
at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:62)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:248)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:241)
at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:230)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:623)
at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:578)
at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: org.springframework.cloud.contract.verifier.converter.ConversionContractVerifierException: Unable to make conversion of ByteArrayTest.groovy
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1$_closure2.doCall(RecursiveFilesConverter.groovy:107)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1.doCall(RecursiveFilesConverter.groovy:67)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$_processFiles_closure1.call(RecursiveFilesConverter.groovy)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter.processFiles(RecursiveFilesConverter.groovy:66)
at org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter$processFiles.call(Unknown Source)
at org.springframework.cloud.contract.verifier.plugin.GenerateWireMockClientStubsFromDslTask.generate(GenerateWireMockClientStubsFromDslTask.groovy:58)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:46)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:788)
at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:755)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:124)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:113)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:95)
... 30 more
Caused by: java.lang.ClassCastException
Contract:
package xxx
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
request {
method "PUT"
url "/multipart"
headers {
contentType('multipart/form-data;boundary=AaB03x')
}
multipart(
file: named(
name: value(stub(regex('.+')), test('file')),
content: value(stub(regex('.+')), test([100, 117, 112, 97] as byte[]))
)
)
}
response {
status 200
}
}
]
Debug log:
16:53:58.324 [DEBUG] [org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter] Will create a stub for contract [org.springframework.cloud.contract.verifier.file.ContractMetadata(something/ByteArrayTest.groovy, false, 6, null, [Contract(priority:null, request:Request(property:Request$ClientPatternValueDslProperty(), httpMethods:HttpMethods(), method:DslProperty(clientValue:PUT, serverValue:PUT), url:Url(queryParameters:null, super:DslProperty(clientValue:/multipart, serverValue:/multipart)), urlPath:null, headers:Request$RequestHeaders(), body:null, multipart:Multipart(super:DslProperty(clientValue:[file:NamedProperty(name:DslProperty(clientValue:.+, serverValue:file), value:DslProperty(clientValue:.+, serverValue:[100, 117, 112, 97]))], serverValue:[file:NamedProperty(name:DslProperty(clientValue:.+, serverValue:file), value:DslProperty(clientValue:.+, serverValue:[100, 117, 112, 97]))])), matchers:null), response:Response(Response$ServerPatternValueDslProperty(), org.springframework.cloud.contract.spec.internal.HttpStatus@7f, DslProperty(clientValue:200, serverValue:200), null, null, null, false, null), label:null, description:null, name:null, input:null, outputMessage:null, ignored:false)])]
Hahahahaha
Shame on you! I've just written
println new String([100, 117, 112, 97] as byte[])
As I've mentioned earlier:
In my case I would like to pass binary file and wrapping it in String isn't enough.
It will be hard for me to create String from my byte array. :)
edit:
🤦♂️ [100, 117, 112, 97] 🤣
Ok you misunderstood me. I've written
println new String([100, 117, 112, 97] as byte[])
and I've seen the output... That wasn't nice what I saw ;) As for the problem I'm on the verge of fixing it (I'm just writing the last test to ensure that I don't do a regression in the future)
Ok the snapshots are uploaded, you can double check if things work fine
And I had to slightly change the byte array ;)
It looks good now. Good job 👍
How do I add multiple request parts to my contract:
@PostMapping(value = "/import", headers = {"content-type=multipart/form-data"})
public ResponseEntity<SomeResponse> import(@RequestPart("file1") MultipartFile file1,
@RequestPart("file2") MultipartFile file2,
@RequestPart("file3") Optional<MultipartFile> file3)
throws IOException {
}
Most helpful comment
Ok you misunderstood me. I've written
and I've seen the output... That wasn't nice what I saw ;) As for the problem I'm on the verge of fixing it (I'm just writing the last test to ensure that I don't do a regression in the future)