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

Unable to test simple Controller with JSON Body #7877

Closed
aaabramov opened this issue Oct 3, 2017 · 5 comments
Closed

Unable to test simple Controller with JSON Body #7877

aaabramov opened this issue Oct 3, 2017 · 5 comments
Assignees

Comments

@aaabramov
Copy link
Contributor

Are you looking for help?

No

Play Version

2.6.5

API

Scala

Operating System

MacOS:

Darwin ***.local 17.0.0 Darwin Kernel Version 17.0.0: Thu Aug 24 21:48:19 PDT 2017; root:xnu-4570.1.46~2/RELEASE_X86_64 x86_64

JDK (Oracle 1.8.0_72, OpenJDK 1.8.x, Azul Zing)

java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

Library Dependencies

scalaVersion := "2.12.3"
"org.scalatestplus.play" %% "scalatestplus-play" % "3.1.1"

Expected Behavior

  1. Status of tested controller method is returned

Actual Behavior

  1. 400 status is returned
  2. The following error message is returned in response body

Bad Request
For request 'GET /' [Invalid Json: No content to map due to end-of-input at [Source: akka.util.ByteIterator$ByteArrayIterator$$anon$1@161d95c6; line: 1, column: 0]]

Reproducible Test Case

Please, find the following repository to reproduce the issue:
https://github.com/Abrasha/playframework-issue-example

@richdougherty
Copy link
Member

Thanks for the report. I'll take a look at this now.

@richdougherty
Copy link
Member

Hi @Abrasha, if you add type annotations into your test you can sort of see what's happening:

val request: Request[AnyContentAsJson] = FakeRequest()
  .withJsonBody(Json.toJson(person))
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Accumulator[ByteString, Result] = acceptAction.apply(request)
assert(status(response) === CREATED)

The problem is that acceptAction.apply is overloaded - you can either pass a RequestHeader or a Request[JsValue]. Since you're trying to pass a Request[AnyContentAsJson] this matches the RequestHeader version. This returns an Accumulator.

When you call homeController.accept.apply you don't get a result back, you get an Accumulator[ByteString, Result]. When you call status(response) this feeds an empty ByteString to the accumulator, resulting in a parsing error.

The fix is to make a Request[JsValue] which matches the type of acceptAction. If you use a Request[JsValue] it calls the correct accept method, returning a Future[Result]

val request: Request[AnyContentAsJson] = FakeRequest()
  .withBody(Json.toJson(person)) // <--- change here
  .withHeaders(CONTENT_TYPE -> JSON)
val acceptAction: Action[JsValue] = homeController.accept
val response: Future[Result] = acceptAction.apply(request) // <-- different type here
assert(status(response) === CREATED)

The alternative fix is to use an AnyContent BodyParser—which is the default anyway. See https://www.playframework.com/documentation/2.6.x/ScalaTestingWithScalaTest#Unit-Testing-EssentialAction for an example how this works. You'll need to use request.body.asJson.get inside your action to get the result.

By the way, I realise this is very confusing but unfortunately it's too hard to change Play to make it less confusing. There are several design issues coming together here:

  • Request extends RequestHeader - this causes issues all over the place, but we can't change it now
  • Action overloads the apply method for everything instead of having methods with separate names, e.g. accumulator(rh: RequestHeader) and invoke(r: Request[T])
  • AnyContent should only be used rarely but it's the default in our code and docs
  • Action is also an ActionBuilder—again using apply methods—and making it confusing to understand how Actions are built

I'm going to close this ticket now because it works as designed and we unfortunately can't change the design!

@richdougherty richdougherty self-assigned this Oct 4, 2017
@indriesergiu
Copy link

@richdougherty Thank you for your explanation, you save me a lot of time :)

@mikail-khan
Copy link

This is one of my pet hates about Play, the overloading semantics are insane and poorly documented.

@armtuk
Copy link

armtuk commented Feb 25, 2021

I'm having a similar problem, but I'm intentionally using a custom parser. I need the body to be the exact string literal that came in because I have to sign the body, and so the content must be string identical to what the source sent so it can be signed and match the original signature.

I would like to test, but I'm also getting the empty message. How do I send the write ByteString to the Accumulator?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants