Написание тестового примера для загрузки файлов в Play 2.1 и Scala
Я нашел следующий вопрос/ответ:
Тест MultipartFormData в Play 2.0 FakeRequest
Но в Play 2.1 все изменилось. Я пробовал адаптировать пример следующим образом:
"Application" should {
"Upload Photo" in {
running(FakeApplication()) {
val data = new MultipartFormData(Map(), List(
FilePart("qqfile", "message", Some("Content-Type: multipart/form-data"),
TemporaryFile(getClass().getResource("/test/photos/DSC03024.JPG").getFile()))
), List())
val Some(result) = routeAndCall(FakeRequest(POST, "/admin/photo/upload", FakeHeaders(), data))
status(result) must equalTo(CREATED)
headers(result) must contain(LOCATION)
contentType(result) must beSome("application/json")
Однако всякий раз, когда я пытаюсь запустить запрос, я получаю исключение с нулевым указателем:
[error] ! Upload Photo
[error] NullPointerException: null (PhotoManagementSpec.scala:25)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3$$anonfun$apply$4.apply(PhotoManagementSpec.scala:28)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3$$anonfun$apply$4.apply(PhotoManagementSpec.scala:25)
[error] play.api.test.Helpers$.running(Helpers.scala:40)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3.apply(PhotoManagementSpec.scala:25)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3.apply(PhotoManagementSpec.scala:25)
Если я попытаюсь заменить устаревший routeAndCall только маршрутом (и удалить параметр вокруг результата), я получаю ошибку компиляции, заявляя, что не может написать экземпляр MultipartFormData [TemporaryFile] для ответа HTTP.
Какой правильный способ создать этот тест в Play 2.1 с помощью Scala?
Изменить: пробовал модифицировать код для проверки только контроллера:
"Application" should {
"Upload Photo" in {
val data = new MultipartFormData(Map(), List(
FilePart("qqfile", "message", Some("Content-Type: multipart/form-data"),
TemporaryFile(getClass().getResource("/test/photos/DSC03024.JPG").getFile()))
), List())
val result = controllers.Photo.upload()(FakeRequest(POST, "/admin/photo/upload",FakeHeaders(),data))
status(result) must equalTo(OK)
contentType(result) must beSome("text/html")
charset(result) must beSome("utf-8")
contentAsString(result) must contain("Hello Bob")
}
Но теперь я получаю ошибку типа во всех тестовых условиях вокруг таких результатов:
[error] found : play.api.libs.iteratee.Iteratee[Array[Byte],play.api.mvc.Result]
[error] required: play.api.mvc.Result
Я не понимаю, почему я получаю Interator для байт-массивов, сопоставленных с результатами. Может ли это иметь какое-то отношение к тому, как я использую собственный парсер для тела? Мое определение контроллера выглядит следующим образом:
def upload = Action(CustomParsers.multipartFormDataAsBytes) { request =>
request.body.file("qqfile").map { upload =>
Используя синтаксический анализатор формы из этого сообщения: Вытягивание файлов из MultipartFormData в память в Play2/ Scala
Ответы
Ответ 1
Play 2.3 включает более новую версию httpmime.jar, требующую незначительных исправлений. Основываясь на решении Marcus, используя механизм Play Writeable, сохраняя при этом некоторые из синтаксического сахара из моего решения Play 2.1, это то, что я придумал:
import scala.language.implicitConversions
import java.io.{ByteArrayOutputStream, File}
import org.apache.http.entity.ContentType
import org.apache.http.entity.mime.MultipartEntityBuilder
import org.apache.http.entity.mime.content._
import org.specs2.mutable.Specification
import play.api.http._
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc.{Codec, MultipartFormData}
import play.api.test.Helpers._
import play.api.test.{FakeApplication, FakeRequest}
trait FakeMultipartUpload {
implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[MultipartFormData[TemporaryFile]] = {
val builder = MultipartEntityBuilder.create().setBoundary("12345678")
def transform(multipart: MultipartFormData[TemporaryFile]): Array[Byte] = {
multipart.dataParts.foreach { part =>
part._2.foreach { p2 =>
builder.addPart(part._1, new StringBody(p2, ContentType.create("text/plain", "UTF-8")))
}
}
multipart.files.foreach { file =>
val part = new FileBody(file.ref.file, ContentType.create(file.contentType.getOrElse("application/octet-stream")), file.filename)
builder.addPart(file.key, part)
}
val outputStream = new ByteArrayOutputStream
builder.build.writeTo(outputStream)
outputStream.toByteArray
}
new Writeable[MultipartFormData[TemporaryFile]](transform, Some(builder.build.getContentType.getValue))
}
/** shortcut for generating a MultipartFormData with one file part which more fields can be added to */
def fileUpload(key: String, file: File, contentType: String): MultipartFormData[TemporaryFile] = {
MultipartFormData(
dataParts = Map(),
files = Seq(FilePart[TemporaryFile](key, file.getName, Some(contentType), TemporaryFile(file))),
badParts = Seq(),
missingFileParts = Seq())
}
/** shortcut for a request body containing a single file attachment */
case class WrappedFakeRequest[A](fr: FakeRequest[A]) {
def withFileUpload(key: String, file: File, contentType: String) = {
fr.withBody(fileUpload(key, file, contentType))
}
}
implicit def toWrappedFakeRequest[A](fr: FakeRequest[A]) = WrappedFakeRequest(fr)
}
class MyTest extends Specification with FakeMultipartUpload {
"uploading" should {
"be easier than this" in {
running(FakeApplication()) {
val uploadFile = new File("/tmp/file.txt")
val req = FakeRequest(POST, "/upload/path").
withFileUpload("image", uploadFile, "image/gif")
val response = route(req).get
status(response) must equalTo(OK)
}
}
}
}
Ответ 2
Мне удалось заставить это работать с Play 2.1 на основе различных предложений списка рассылки. Вот как я это делаю:
import scala.language.implicitConversions
import java.io.{ ByteArrayOutputStream, File }
import org.apache.http.entity.mime.MultipartEntity
import org.apache.http.entity.mime.content.{ ContentBody, FileBody }
import org.specs2.mutable.Specification
import play.api.http.Writeable
import play.api.test.{ FakeApplication, FakeRequest }
import play.api.test.Helpers._
trait FakeMultipartUpload {
case class WrappedFakeRequest[A](fr: FakeRequest[A]) {
def withMultipart(parts: (String, ContentBody)*) = {
// create a multipart form
val entity = new MultipartEntity()
parts.foreach { part =>
entity.addPart(part._1, part._2)
}
// serialize the form
val outputStream = new ByteArrayOutputStream
entity.writeTo(outputStream)
val bytes = outputStream.toByteArray
// inject the form into our request
val headerContentType = entity.getContentType.getValue
fr.withBody(bytes).withHeaders(CONTENT_TYPE -> headerContentType)
}
def withFileUpload(fileParam: String, file: File, contentType: String) = {
withMultipart(fileParam -> new FileBody(file, contentType))
}
}
implicit def toWrappedFakeRequest[A](fr: FakeRequest[A]) = WrappedFakeRequest(fr)
// override Play equivalent Writeable so that the content-type header from the FakeRequest is used instead of application/octet-stream
implicit val wBytes: Writeable[Array[Byte]] = Writeable(identity, None)
}
class MyTest extends Specification with FakeMultipartUpload {
"uploading" should {
"be easier than this" in {
running(FakeApplication()) {
val uploadFile = new File("/tmp/file.txt")
val req = FakeRequest(POST, "/upload/path").
withFileUpload("image", uploadFile, "image/gif")
val response = route(req).get
status(response) must equalTo(OK)
}
}
}
}
Ответ 3
Я изменил код Alex, чтобы действовать как Writable, который лучше интегрируется в Play 2.2.2
package test
import play.api.http._
import play.api.mvc.MultipartFormData.FilePart
import play.api.libs.iteratee._
import play.api.libs.Files.TemporaryFile
import play.api.mvc.{Codec, MultipartFormData }
import java.io.{FileInputStream, ByteArrayOutputStream}
import org.apache.commons.io.IOUtils
import org.apache.http.entity.mime.MultipartEntity
import org.apache.http.entity.mime.content._
object MultipartWriteable {
/**
* `Writeable` for multipart/form-data.
*
*/
implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[MultipartFormData[TemporaryFile]] = {
val entity = new MultipartEntity()
def transform(multipart: MultipartFormData[TemporaryFile]):Array[Byte] = {
multipart.dataParts.foreach { part =>
part._2.foreach { p2 =>
entity.addPart(part._1, new StringBody(p2))
}
}
multipart.files.foreach { file =>
val part = new FileBody(file.ref.file, file.filename, file.contentType.getOrElse("application/octet-stream"), null)
entity.addPart(file.key, part)
}
val outputStream = new ByteArrayOutputStream
entity.writeTo(outputStream)
val bytes = outputStream.toByteArray
outputStream.close
bytes
}
new Writeable[MultipartFormData[TemporaryFile]](transform, Some(entity.getContentType.getValue))
}
}
Таким образом можно написать что-то вроде этого:
val filePart:MultipartFormData.FilePart[TemporaryFile] = MultipartFormData.FilePart(...)
val fileParts:Seq[MultipartFormData.FilePart[TemporaryFile]] = Seq(filePart)
val dataParts:Map[String, Seq[String]] = ...
val multipart = new MultipartFormData[TemporaryFile](dataParts, fileParts, List(), List())
val request = FakeRequest(POST, "/url", FakeHeaders(), multipart)
var result = route(request).get
Ответ 4
Следуя предложению EEColor, я получил следующее:
"Upload Photo" in {
val file = scala.io.Source.fromFile(getClass().getResource("/photos/DSC03024.JPG").getFile())(scala.io.Codec.ISO8859).map(_.toByte).toArray
val data = new MultipartFormData(Map(), List(
FilePart("qqfile", "DSC03024.JPG", Some("image/jpeg"),
file)
), List())
val result = controllers.Photo.upload()(FakeRequest(POST, "/admin/photos/upload",FakeHeaders(),data))
status(result) must equalTo(CREATED)
headers(result) must haveKeys(LOCATION)
contentType(result) must beSome("application/json")
}
Ответ 5
Здесь моя версия Writeable [AnyContentAsMultipartFormData]:
import java.io.File
import play.api.http.{HeaderNames, Writeable}
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc.{AnyContentAsMultipartFormData, Codec, MultipartFormData}
object MultipartFormDataWritable {
val boundary = "--------ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
def formatDataParts(data: Map[String, Seq[String]]) = {
val dataParts = data.flatMap { case (key, values) =>
values.map { value =>
val name = s""""$key""""
s"--$boundary\r\n${HeaderNames.CONTENT_DISPOSITION}: form-data; name=$name\r\n\r\n$value\r\n"
}
}.mkString("")
Codec.utf_8.encode(dataParts)
}
def filePartHeader(file: FilePart[TemporaryFile]) = {
val name = s""""${file.key}""""
val filename = s""""${file.filename}""""
val contentType = file.contentType.map { ct =>
s"${HeaderNames.CONTENT_TYPE}: $ct\r\n"
}.getOrElse("")
Codec.utf_8.encode(s"--$boundary\r\n${HeaderNames.CONTENT_DISPOSITION}: form-data; name=$name; filename=$filename\r\n$contentType\r\n")
}
val singleton = Writeable[MultipartFormData[TemporaryFile]](
transform = { form: MultipartFormData[TemporaryFile] =>
formatDataParts(form.dataParts) ++
form.files.flatMap { file =>
val fileBytes = Files.readAllBytes(Paths.get(file.ref.file.getAbsolutePath))
filePartHeader(file) ++ fileBytes ++ Codec.utf_8.encode("\r\n")
} ++
Codec.utf_8.encode(s"--$boundary--")
},
contentType = Some(s"multipart/form-data; boundary=$boundary")
)
}
implicit val anyContentAsMultipartFormWritable: Writeable[AnyContentAsMultipartFormData] = {
MultipartFormDataWritable.singleton.map(_.mdf)
}
Он адаптирован (и исправлены некоторые ошибки): https://github.com/jroper/playframework/blob/multpart-form-data-writeable/framework/src/play/src/main/scala/play/api/http/Writeable.scala#L108
Смотрите весь пост здесь, если вам интересно: http://tech.fongmun.com/post/125479939452/test-multipartformdata-in-play
Ответ 6
Для меня лучшим решением для этой проблемы является Alex Varju one
Вот версия, обновленная для Play 2.5:
object FakeMultipartUpload {
implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[AnyContentAsMultipartFormData] = {
val builder = MultipartEntityBuilder.create().setBoundary("12345678")
def transform(multipart: AnyContentAsMultipartFormData): ByteString = {
multipart.mdf.dataParts.foreach { part =>
part._2.foreach { p2 =>
builder.addPart(part._1, new StringBody(p2, ContentType.create("text/plain", "UTF-8")))
}
}
multipart.mdf.files.foreach { file =>
val part = new FileBody(file.ref.file, ContentType.create(file.contentType.getOrElse("application/octet-stream")), file.filename)
builder.addPart(file.key, part)
}
val outputStream = new ByteArrayOutputStream
builder.build.writeTo(outputStream)
ByteString(outputStream.toByteArray)
}
new Writeable(transform, Some(builder.build.getContentType.getValue))
}
}
Ответ 7
В Play 2.6.x вы можете написать тестовые примеры для тестирования API загрузки файлов следующим образом:
class HDFSControllerTest extends Specification {
"HDFSController" should {
"return 200 Status for file Upload" in new WithApplication {
val tempFile = SingletonTemporaryFileCreator.create("txt","csv")
tempFile.deleteOnExit()
val data = new MultipartFormData[TemporaryFile](Map(),
List(FilePart("metadata", "text1.csv", Some("text/plain"), tempFile)), List())
val res: Option[Future[Result]] = route(app, FakeRequest(POST, "/api/hdfs").withMultipartFormDataBody(data))
print(contentAsString(res.get))
res must beSome.which(status(_) == OK)
}
}
}