Skip to content

Commit

Permalink
PR feedbacks: move MultiPart utilities into dedicated util file
Browse files Browse the repository at this point in the history
  • Loading branch information
macisamuele committed Feb 6, 2020
1 parent 876bec3 commit 1f2d831
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,111 +2,17 @@ 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 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(
var boundary: String? = null,
var errors: List<String>? = null,
var parts: List<MultiPartInfo>? = null
)
private data class MultiPartInfo(
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
it.subList(1, it.size - 1)
}

val partLinesIterator = partLines.iterator()
val headers = mutableMapOf<String, String>()
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")
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()

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

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
Expand All @@ -120,17 +26,16 @@ class PostFileEndpointTest {
clientFile = RequestBody.create(MediaType.parse("application/json"), "{}")
).blockingGet()

val fileApiStats = rule.server.takeRequest().fileApiStats
val fileApiStats = rule.server.takeRequest().decodeMultiPartBody()
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 = "{}"
fileContent = "{}"
)
)
)
Expand All @@ -145,23 +50,22 @@ class PostFileEndpointTest {
certificateFile = RequestBody.create(MediaType.parse("text/plain"), "Some Text")
).blockingGet()

val fileApiStats = rule.server.takeRequest().fileApiStats
val fileApiStats = rule.server.takeRequest().decodeMultiPartBody()
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 = "{}"
fileContent = "{}"
),
MultiPartInfo(
contentDisposition = "form-data; name=\"certificate_file\"; filename=\"certificate_file\"",
contentType = "text/plain; charset=utf-8",
contentLength = "9",
content = "Some Text"
fileContent = "Some Text"
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.yelp.codegen.generatecodesamples.tools

import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.fail

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 partLinesIterator = part.split("\r\n").drop(1).dropLast(1).iterator()

val headers = mutableMapOf<String, String>()
for (headerLine in partLinesIterator) {
if (headerLine.isEmpty()) {
break
} else {
val (name, value) = headerLine.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 ;)
headers[name] = value
}
}

val content = partLinesIterator.asSequence().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()

val bodyParts = mutableListOf<String>()

if (!boundaryLine.startsWith("--")) {
fail("Boundary should start with '--'")
}

val boundary = boundaryLine.substring(startIndex = 2)
bodyParts.addAll(requestBody.split(boundaryLine))
if (!bodyParts.last().startsWith("--")) {
fail("Start and End Boundaries are different")
} else if (bodyParts.last().substring(startIndex = 2) != "\r\n") {
fail("The last line of the body has to be empty")
}

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

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

0 comments on commit 1f2d831

Please sign in to comment.