From 33e491adc58b3ee3a37194339f09afa70d42a0e2 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Sun, 25 Mar 2018 15:29:38 -0700 Subject: Use patch unmarshaller --- .../xyz/driver/core/rest/PatchDirectivesTest.scala | 92 ++++++++++++++++++++++ .../xyz/driver/core/rest/PatchSupportTest.scala | 90 --------------------- 2 files changed, 92 insertions(+), 90 deletions(-) create mode 100644 src/test/scala/xyz/driver/core/rest/PatchDirectivesTest.scala delete mode 100644 src/test/scala/xyz/driver/core/rest/PatchSupportTest.scala (limited to 'src/test/scala/xyz/driver/core') diff --git a/src/test/scala/xyz/driver/core/rest/PatchDirectivesTest.scala b/src/test/scala/xyz/driver/core/rest/PatchDirectivesTest.scala new file mode 100644 index 0000000..6a6b035 --- /dev/null +++ b/src/test/scala/xyz/driver/core/rest/PatchDirectivesTest.scala @@ -0,0 +1,92 @@ +package xyz.driver.core.rest + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.`Content-Type` +import akka.http.scaladsl.server.{Directives, Route} +import akka.http.scaladsl.testkit.ScalatestRouteTest +import org.scalatest.{FlatSpec, Matchers} +import spray.json._ +import xyz.driver.core.{Id, Name} +import xyz.driver.core.json._ + +import scala.concurrent.Future + +class PatchDirectivesTest + extends FlatSpec with Matchers with ScalatestRouteTest with SprayJsonSupport with DefaultJsonProtocol + with Directives with PatchDirectives { + case class Bar(name: Name[Bar], size: Int) + case class Foo(id: Id[Foo], name: Name[Foo], rank: Int, bar: Option[Bar]) + implicit val barFormat: RootJsonFormat[Bar] = jsonFormat2(Bar) + implicit val fooFormat: RootJsonFormat[Foo] = jsonFormat4(Foo) + + val testFoo: Foo = Foo(Id("1"), Name(s"Foo"), 1, Some(Bar(Name("Bar"), 10))) + + def route(retrieve: => Future[Option[Foo]]): Route = + Route.seal(path("api" / "v1" / "foos" / IdInPath[Foo]) { fooId => + entity(as[Patchable[Foo]]) { fooPatchable => + mergePatch(fooPatchable, retrieve) { updatedFoo => + complete(updatedFoo) + } + } + }) + + val MergePatchContentType = ContentType(`application/merge-patch+json`) + val ContentTypeHeader = `Content-Type`(MergePatchContentType) + def jsonEntity(json: String, contentType: ContentType.NonBinary = MergePatchContentType): RequestEntity = + HttpEntity(contentType, json) + + "PatchSupport" should "allow partial updates to an existing object" in { + val fooRetrieve = Future.successful(Some(testFoo)) + + Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""")) ~> route(fooRetrieve) ~> check { + handled shouldBe true + responseAs[Foo] shouldBe testFoo.copy(rank = 4) + } + } + + it should "merge deeply nested objects" in { + val fooRetrieve = Future.successful(Some(testFoo)) + + Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4, "bar": {"name": "My Bar"}}""")) ~> route(fooRetrieve) ~> check { + handled shouldBe true + responseAs[Foo] shouldBe testFoo.copy(rank = 4, bar = Some(Bar(Name("My Bar"), 10))) + } + } + + it should "return a 404 if the object is not found" in { + val fooRetrieve = Future.successful(None) + + Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""")) ~> route(fooRetrieve) ~> check { + handled shouldBe true + status shouldBe StatusCodes.NotFound + } + } + + it should "handle nulls on optional values correctly" in { + val fooRetrieve = Future.successful(Some(testFoo)) + + Patch("/api/v1/foos/1", jsonEntity("""{"bar": null}""")) ~> route(fooRetrieve) ~> check { + handled shouldBe true + responseAs[Foo] shouldBe testFoo.copy(bar = None) + } + } + + it should "return a 400 for nulls on non-optional values" in { + val fooRetrieve = Future.successful(Some(testFoo)) + + Patch("/api/v1/foos/1", jsonEntity("""{"rank": null}""")) ~> route(fooRetrieve) ~> check { + handled shouldBe true + status shouldBe StatusCodes.BadRequest + } + } + + it should "return a 415 for incorrect Content-Type" in { + val fooRetrieve = Future.successful(Some(testFoo)) + + Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""", ContentTypes.`application/json`)) ~> route(fooRetrieve) ~> check { + status shouldBe StatusCodes.UnsupportedMediaType + responseAs[String] should include("application/merge-patch+json") + } + } +} diff --git a/src/test/scala/xyz/driver/core/rest/PatchSupportTest.scala b/src/test/scala/xyz/driver/core/rest/PatchSupportTest.scala deleted file mode 100644 index 5c7faf8..0000000 --- a/src/test/scala/xyz/driver/core/rest/PatchSupportTest.scala +++ /dev/null @@ -1,90 +0,0 @@ -package xyz.driver.core.rest - -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.`Content-Type` -import akka.http.scaladsl.server.{Directives, Route} -import akka.http.scaladsl.testkit.ScalatestRouteTest -import org.scalatest.{FlatSpec, Matchers} -import spray.json._ -import xyz.driver.core.{Id, Name} -import xyz.driver.core.json._ - -import scala.concurrent.Future - -class PatchSupportTest - extends FlatSpec with Matchers with ScalatestRouteTest with SprayJsonSupport with DefaultJsonProtocol - with Directives with PatchSupport { - case class Bar(name: Name[Bar], size: Int) - case class Foo(id: Id[Foo], name: Name[Foo], rank: Int, bar: Option[Bar]) - implicit val barFormat: RootJsonFormat[Bar] = jsonFormat2(Bar) - implicit val fooFormat: RootJsonFormat[Foo] = jsonFormat4(Foo) - - val testFoo: Foo = Foo(Id("1"), Name(s"Foo"), 1, Some(Bar(Name("Bar"), 10))) - - def route(implicit patchRetrievable: PatchRetrievable[Foo]): Route = - Route.seal(path("api" / "v1" / "foos" / IdInPath[Foo]) { fooId => - patch(as[Foo], fooId) { patchedFoo => - complete(patchedFoo) - } - }) - - def jsonEntity(json: String): RequestEntity = HttpEntity(ContentTypes.`application/json`, json) - - val ContentTypeHeader = `Content-Type`(ContentType.parse("application/merge-patch+json").right.get) - - "PatchSupport" should "allow partial updates to an existing object" in { - implicit val fooPatchable = PatchRetrievable[Foo](id => _ => Future.successful(Some(testFoo.copy(id = id)))) - - Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""")).withHeaders(ContentTypeHeader) ~> route ~> check { - handled shouldBe true - responseAs[Foo] shouldBe testFoo.copy(rank = 4) - } - } - - it should "merge deeply nested objects" in { - implicit val fooPatchable = PatchRetrievable[Foo](id => _ => Future.successful(Some(testFoo.copy(id = id)))) - - Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4, "bar": {"name": "My Bar"}}""")) - .withHeaders(ContentTypeHeader) ~> route ~> check { - handled shouldBe true - responseAs[Foo] shouldBe testFoo.copy(rank = 4, bar = Some(Bar(Name("My Bar"), 10))) - } - } - - it should "return a 404 if the object is not found" in { - implicit val fooPatchable = PatchRetrievable[Foo](_ => _ => Future.successful(None)) - - Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""")).withHeaders(ContentTypeHeader) ~> route ~> check { - handled shouldBe true - status shouldBe StatusCodes.NotFound - } - } - - it should "handle nulls on optional values correctly" in { - implicit val fooPatchable = PatchRetrievable[Foo](id => _ => Future.successful(Some(testFoo.copy(id = id)))) - - Patch("/api/v1/foos/1", jsonEntity("""{"bar": null}""")).withHeaders(ContentTypeHeader) ~> route ~> check { - handled shouldBe true - responseAs[Foo] shouldBe testFoo.copy(bar = None) - } - } - - it should "return a 400 for nulls on non-optional values" in { - implicit val fooPatchable = PatchRetrievable[Foo](id => _ => Future.successful(Some(testFoo.copy(id = id)))) - - Patch("/api/v1/foos/1", jsonEntity("""{"rank": null}""")).withHeaders(ContentTypeHeader) ~> route ~> check { - handled shouldBe true - status shouldBe StatusCodes.BadRequest - } - } - - it should "return a 400 for incorrect Content-Type" in { - implicit val fooPatchable = PatchRetrievable[Foo](id => _ => Future.successful(Some(testFoo.copy(id = id)))) - - Patch("/api/v1/foos/1", jsonEntity("""{"rank": 4}""")) ~> route ~> check { - status shouldBe StatusCodes.BadRequest - responseAs[String] should include("application/merge-patch+json") - } - } -} -- cgit v1.2.3