Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File uploads tests #101

Merged
merged 5 commits into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,13 @@ abstract class SharedCodegen : DefaultCodegen(), CodegenConfig {
codegenOperation.vendorExtensions[X_UNSAFE_OPERATION] = true
}
codegenOperation.isMultipart = isMultipartOperation(operation)

if (!codegenOperation.isMultipart && codegenOperation.allParams.any { it.isFile }) {
// According to the swagger specifications in order to send files the operation must
// consume multipart/form-data (https://swagger.io/docs/specification/2-0/file-upload/)
codegenOperation.vendorExtensions[X_UNSAFE_OPERATION] = true
}

return codegenOperation
}

Expand Down
6 changes: 2 additions & 4 deletions plugin/src/main/resources/kotlin/retrofit2/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ interface {{classname}} {
{{/vendorExtensions.hasOperationHeaders}}
@{{httpMethod}}("{{{path}}}"){{#vendorExtensions.x-unsafe-operation}}{{#isDeprecated}}
@Deprecated(message = "Deprecated and unsafe to use"){{/isDeprecated}}{{^isDeprecated}}
@Deprecated(message = "Unsafe to use"){{/isDeprecated}}
{{/vendorExtensions.x-unsafe-operation}}{{^vendorExtensions.x-unsafe-operation}}{{#isDeprecated}}
@Deprecated(message = "Deprecated"){{/isDeprecated}}
{{/vendorExtensions.x-unsafe-operation}}
@Deprecated(message = "Unsafe to use"){{/isDeprecated}}{{/vendorExtensions.x-unsafe-operation}}{{^vendorExtensions.x-unsafe-operation}}{{#isDeprecated}}
@Deprecated(message = "Deprecated"){{/isDeprecated}}{{/vendorExtensions.x-unsafe-operation}}
{{#vendorExtensions.x-function-qualifiers}}{{vendorExtensions.x-function-qualifiers}} {{/vendorExtensions.x-function-qualifiers}}fun {{operationId}}({{#hasParams}}
{{#allParams}} {{>retrofit2/queryParams}}{{>retrofit2/pathParams}}{{>retrofit2/headerParams}}{{>retrofit2/bodyParams}}{{>retrofit2/formParams}}{{#hasMore}},{{{newline}}}{{/hasMore}}{{/allParams}}
{{/hasParams}}): {{{returnType}}}
Expand Down
56 changes: 55 additions & 1 deletion samples/junit-tests/junit_tests_specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,65 @@
},
"/post/file": {
"post": {
"consumes": [
"multipart/form-data"
],
"operationId": "post_file",
"parameters": [
{
"in": "formData",
"name": "file",
"name": "client_file",
"required": true,
"type": "file"
}
],
"responses": {
"default": {
"description": "Something"
}
},
"tags": [
"file"
]
}
},
"/post/file_without_consumes": {
"post": {
"operationId": "post_file_without_multipart_form_data",
"parameters": [
{
"in": "formData",
"name": "client_file",
"required": true,
"type": "file"
}
],
"responses": {
"default": {
"description": "Something"
}
},
"tags": [
"file"
]
}
},
"/post/multiple_files": {
"post": {
"consumes": [
"multipart/form-data"
],
"operationId": "post_multiple_files",
"parameters": [
{
"in": "formData",
"name": "client_file",
"required": true,
"type": "file"
},
{
"in": "formData",
"name": "certificate_file",
"required": true,
"type": "file"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,41 @@ import retrofit2.http.POST
interface FileApi {
/**
* The endpoint is owned by junittests service owner
* @param file (required)
* @param clientFile (required)
*/
@retrofit2.http.FormUrlEncoded
@retrofit2.http.Multipart
@Headers(
"X-Operation-ID: post_file"
)
@POST("/post/file")
fun postFile(
@retrofit2.http.Field("file\"; filename=\"file") file: RequestBody
@retrofit2.http.Part("client_file\"; filename=\"client_file") clientFile: RequestBody
): Completable
/**
* The endpoint is owned by junittests service owner
* @param clientFile (required)
*/
@retrofit2.http.FormUrlEncoded
@Headers(
"X-Operation-ID: post_file_without_multipart_form_data"
)
@POST("/post/file_without_consumes")
@Deprecated(message = "Unsafe to use")
fun postFileWithoutMultipartFormData(
@retrofit2.http.Field("client_file\"; filename=\"client_file") clientFile: RequestBody
): Completable
/**
* The endpoint is owned by junittests service owner
* @param clientFile (required)
* @param certificateFile (required)
*/
@retrofit2.http.Multipart
@Headers(
"X-Operation-ID: post_multiple_files"
)
@POST("/post/multiple_files")
fun postMultipleFiles(
@retrofit2.http.Part("client_file\"; filename=\"client_file") clientFile: RequestBody,
@retrofit2.http.Part("certificate_file\"; filename=\"certificate_file") certificateFile: RequestBody
): Completable
}
2 changes: 1 addition & 1 deletion samples/junit-tests/src/main/java/swagger.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.yelp.codegen.generatecodesamples

import com.yelp.codegen.generatecodesamples.apis.FileApi
import com.yelp.codegen.generatecodesamples.tools.MockServerApiRule
import com.yelp.codegen.generatecodesamples.tools.MultiPartInfo
import com.yelp.codegen.generatecodesamples.tools.decodeMultiPartBody
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.mockwebserver.MockResponse
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test

class PostFileEndpointTest {

@get:Rule
val rule = MockServerApiRule()

@Test
fun sendFileTest() {
rule.server.enqueue(MockResponse())

rule.getApi<FileApi>().postFile(
clientFile = RequestBody.create(MediaType.parse("application/json"), "{}")
).blockingGet()

val fileApiStats = rule.server.takeRequest().decodeMultiPartBody()
assertNotNull(fileApiStats.boundary)
assertEquals(
listOf(
MultiPartInfo(
contentDisposition = "form-data; name=\"client_file\"; filename=\"client_file\"",
contentType = "application/json; charset=utf-8",
contentLength = "2",
fileContent = "{}"
)
),
fileApiStats.parts
)
}

@Test
fun sendMultipleFilesTest() {
rule.server.enqueue(MockResponse())

rule.getApi<FileApi>().postMultipleFiles(
clientFile = RequestBody.create(MediaType.parse("application/json"), "{}"),
certificateFile = RequestBody.create(MediaType.parse("text/plain"), "Some Text")
).blockingGet()

val fileApiStats = rule.server.takeRequest().decodeMultiPartBody()
assertNotNull(fileApiStats.boundary)
assertEquals(
listOf(
MultiPartInfo(
contentDisposition = "form-data; name=\"client_file\"; filename=\"client_file\"",
contentType = "application/json; charset=utf-8",
contentLength = "2",
fileContent = "{}"
),
MultiPartInfo(
contentDisposition = "form-data; name=\"certificate_file\"; filename=\"certificate_file\"",
contentType = "text/plain; charset=utf-8",
contentLength = "9",
fileContent = "Some Text"
)
),
fileApiStats.parts
)
}

@Test
fun ensureThatEndpointsWithFileAndNotMultipartFormDataAreMarkedAsDeprecated() {
assertTrue(
FileApi::class.java.methods
.first { it.name == "postFileWithoutMultipartFormData" }
.getAnnotationsByType(Deprecated::class.java)
.isNotEmpty()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.yelp.codegen.generatecodesamples.tools

import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue

data class FileApiStats(
val boundary: String,
val parts: List<MultiPartInfo>
)

data class MultiPartInfo(
val fileContent: String? = null,
val contentDisposition: String? = null,
val contentLength: String? = null,
val contentType: String? = null
)

private fun processPart(part: String): MultiPartInfo {
// part would look like "\r\nHeader-Name: Header-Value\r\n...\r\n\r\nbody\r\n"
// Here we're removing the first and last part as we know that are empty
val partLines = part.split("\r\n").drop(1).dropLast(1)

val indexBoundaryHeadersContent = partLines.indexOf("")

val headers = partLines.subList(0, indexBoundaryHeadersContent).map {
val (name, value) = it.split(": ", limit = 2)
// Note: in theory we can have the same header name multiples times
// We're not dealing with it as this is a test utility ;)
name to value
}.toMap()

val content = partLines.subList(indexBoundaryHeadersContent, partLines.size).drop(1).joinToString("\r\n")
return MultiPartInfo(
fileContent = content,
contentDisposition = headers["Content-Disposition"],
contentType = headers["Content-Type"],
contentLength = headers["Content-Length"]
)
}

/**
* Extract statistics from the body of the recorded request (assuming that it is multipart body)
*/
fun RecordedRequest.decodeMultiPartBody(): FileApiStats {
/**
* requestBody has to be compliant to the HTTP Specifications for MultiPart (RFC 2387)
*
* This means that the body will look like
* --<BOUNDARY>\r\n
* <Content1 Header Name>: <Content1 Header Value>\r\n
* [<Content1 Header Name>: <Content1 Header Value>\r\n]
* \r\n
* <Content1>\r\n
* --<BOUNDARY>\r\n
* <Content# Header Name>: <Content# Header Value>\r\n
* [<Content# Header Name>: <Content# Header Value>\r\n]
* \r\n
* <Content#>\r\n
* --<BOUNDARY>--\r\n
* \r\n
*/
val requestBody = this.body.readUtf8()

val boundaryLine = requestBody.split("\r\n").first()

assertTrue(
"Boundary should start with '--'",
boundaryLine.startsWith("--")
)

val boundary = boundaryLine.substring(startIndex = 2)
val bodyParts = requestBody.split(boundaryLine)

assertTrue(
"Start and End Boundaries are different",
bodyParts.last().startsWith("--")
)
assertEquals(
"The last line of the body has to be empty",
"\r\n",
bodyParts.last().substring(startIndex = 2)
)

val parts = bodyParts
.drop(1)
.dropLast(1)
.map { processPart(it) }

return FileApiStats(
boundary = boundary,
parts = parts
)
}