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 3 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,179 @@
package com.yelp.codegen.generatecodesamples

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

private data class FileApiStats(
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
var boundary: String? = null,
var errors: List<String>? = null,
var parts: List<MultiPartInfo>? = null
)
private data class MultiPartInfo(
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
val content: String? = null,
val contentDisposition: String? = null,
val contentLength: String? = null,
val contentType: String? = null
)

private fun processPart(part: String): MultiPartInfo {

val partLines = part.split("\r\n").let {
// 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 and
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
it.subList(1, it.size - 1)
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
}

val partLinesIterator = partLines.iterator()
val headers = mutableMapOf<String, String>()
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
for (headerLine in partLinesIterator) {
if (headerLine.isEmpty()) {
break
} else {
val (name, value) = headerLine.split(": ", limit = 2)
headers[name] = value
}
}

val content = partLinesIterator.asSequence().joinToString("\r\n")
macisamuele marked this conversation as resolved.
Show resolved Hide resolved
return MultiPartInfo(
content = content,
contentDisposition = headers["Content-Disposition"],
contentType = headers["Content-Type"],
contentLength = headers["Content-Length"]
)
}

private val RecordedRequest.fileApiStats: FileApiStats
get() {
/**
* 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.splitToSequence("\r\n").first()
macisamuele marked this conversation as resolved.
Show resolved Hide resolved

val errors = mutableListOf<String>()
val bodyParts = mutableListOf<String>()

val response = FileApiStats()

if (!boundaryLine.startsWith("--")) {
errors.add("Boundary should start with '--'")
} else {
response.boundary = boundaryLine.substring(startIndex = 2)
bodyParts.addAll(requestBody.split(boundaryLine))
if (!bodyParts.last().startsWith("--")) {
errors.add("Start and End Boundaries are different")
} else if (bodyParts.last().substring(startIndex = 2) != "\r\n") {
errors.add("The last line of the body has to be empty")
}
}
macisamuele marked this conversation as resolved.
Show resolved Hide resolved

response.errors = errors

if (errors.isEmpty()) {
response.parts = bodyParts
.mapIndexedNotNull { index, part ->
if (index != 0 && index != bodyParts.lastIndex) {
processPart(part)
} else {
null
}
}
}
return response
}

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().fileApiStats
assertNotNull(fileApiStats.boundary)
assertEquals(fileApiStats.errors?.size, 0)
assertEquals(
fileApiStats.parts,
listOf(
MultiPartInfo(
contentDisposition = "form-data; name=\"client_file\"; filename=\"client_file\"",
contentType = "application/json; charset=utf-8",
contentLength = "2",
content = "{}"
)
)
)
}

@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().fileApiStats
assertNotNull(fileApiStats.boundary)
assertEquals(fileApiStats.errors?.size, 0)
assertEquals(
fileApiStats.parts,
listOf(
MultiPartInfo(
contentDisposition = "form-data; name=\"client_file\"; filename=\"client_file\"",
contentType = "application/json; charset=utf-8",
contentLength = "2",
content = "{}"
),
MultiPartInfo(
contentDisposition = "form-data; name=\"certificate_file\"; filename=\"certificate_file\"",
contentType = "text/plain; charset=utf-8",
contentLength = "9",
content = "Some Text"
)
)
)
}

@Test
fun ensureThatEndpointsWithFileAndNotMultipartFormDataAreMarkedAsDeprecated() {
assertTrue(
FileApi::class.java.methods
.first { it.name == "postFileWithoutMultipartFormData" }
.getAnnotationsByType(Deprecated::class.java)
.isNotEmpty()
)
}
}