diff --git a/build.sbt b/build.sbt index 3b667b20f..8cdf7fbd6 100644 --- a/build.sbt +++ b/build.sbt @@ -59,6 +59,7 @@ lazy val userProjects: Seq[ProjectReference] = List[ProjectReference]( httpCaching, httpCors, httpTestkit, + httpTestkitMunit, httpMarshallersScala, httpMarshallersJava, httpSprayJson, @@ -224,6 +225,15 @@ lazy val httpTestkit = project("http-testkit") .enablePlugins(ReproducibleBuildsPlugin) .disablePlugins(MimaPlugin) // testkit, no bin compat guaranteed +lazy val httpTestkitMunit = project("http-testkit-munit") + .settings(commonSettings) + .settings(AutomaticModuleName.settings("pekko.http.testkit.munit")) + .dependsOn(http, httpTestkit) + .addPekkoModuleDependency("pekko-stream-testkit", "provided", PekkoCoreDependency.default) + .addPekkoModuleDependency("pekko-testkit", "provided", PekkoCoreDependency.default) + .settings(Dependencies.httpTestkitMunit) + .disablePlugins(MimaPlugin) // testkit, no bin compat guaranteed + lazy val httpTests = project("http-tests") .settings(commonSettings) .settings(Dependencies.httpTests) diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/Http.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/Http.scala index cbaf8ac86..ee8e6ab8c 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/Http.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/Http.scala @@ -79,6 +79,7 @@ class HttpExt @InternalStableApi /* constructor signature is hardcoded in Teleme "pekko-http", "pekko-http-caching", "pekko-http-testkit", + "pekko-http-testkit-munit", "pekko-http-marshallers-scala", "pekko-http-marshallers-java", "pekko-http-spray-json", diff --git a/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTest.scala b/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTest.scala new file mode 100644 index 000000000..399f862b1 --- /dev/null +++ b/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTest.scala @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pekko.http.scaladsl.testkit.munit + +import org.apache.pekko.http.scaladsl.testkit.RouteTest + +trait MunitRouteTest extends MunitTestFramework with RouteTest diff --git a/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitTestFramework.scala b/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitTestFramework.scala new file mode 100644 index 000000000..3e4ed54ee --- /dev/null +++ b/http-testkit-munit/src/main/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitTestFramework.scala @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pekko.http.scaladsl.testkit.munit + +import munit.FunSuite +import org.apache.pekko.http.scaladsl.server.ExceptionHandler +import org.apache.pekko.http.scaladsl.testkit.TestFrameworkInterface + +trait MunitTestFramework extends FunSuite with TestFrameworkInterface { + override def failTest(msg: String): Nothing = throw new AssertionError(msg) + + override def afterAll(): Unit = { + cleanUp() + super.afterAll() + } + + override val testExceptionHandler: ExceptionHandler = ExceptionHandler { + case e: munit.ComparisonFailException => throw e + case e: munit.FailSuiteException => throw e + case e: munit.FailException => throw e + case e: java.lang.AssertionError => throw e + } +} diff --git a/http-testkit-munit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTestSpec.scala b/http-testkit-munit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTestSpec.scala new file mode 100644 index 000000000..37c51e5bb --- /dev/null +++ b/http-testkit-munit/src/test/scala/org/apache/pekko/http/scaladsl/testkit/munit/MunitRouteTestSpec.scala @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.pekko.http.scaladsl.testkit.munit + +import scala.concurrent.Await +import scala.concurrent.duration._ + +import org.apache.pekko +import pekko.http.scaladsl.model._ +import HttpMethods._ +import StatusCodes._ +import pekko.http.scaladsl.server._ +import Directives._ +import munit.FailException +import pekko.http.scaladsl.model.headers.{ `X-Forwarded-Proto`, RawHeader } +import pekko.actor.ActorRef +import pekko.pattern.ask +import pekko.stream.scaladsl.Source +import pekko.testkit._ +import pekko.util.{ ByteString, Timeout } + +class MunitRouteTestSpec extends MunitRouteTest { + override def testConfigSource: String = "pekko.http.server.transparent-head-requests = on" // see test below + + test("The MunitRouteTest should support the most simple and direct route test") { + Get() ~> complete(HttpResponse()) ~> { rr => + rr.awaitResult + assertEquals(rr.response, HttpResponse()) + } + } + + test("The MunitRouteTest should support a test using a directive and some checks") { + val pinkHeader = RawHeader("Fancy", "pink") + Get() ~> addHeader(pinkHeader) ~> { + respondWithHeader(pinkHeader) { + complete("abc") + } + } ~> check { + assertEquals(status, OK) + assertEquals(responseEntity, HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")) + assertEquals(header("Fancy"), Some(pinkHeader)) + } + } + + test("The MunitRouteTest should support a test using ~!> and some checks") { + // raw here, should have been parsed into modelled header when going through an actual server when using `~!>` + val extraHeader = RawHeader("X-Forwarded-Proto", "abc") + Get() ~!> { + respondWithHeader(extraHeader) { + complete("abc") + } + } ~> check { + assertEquals(status, OK) + assertEquals(responseEntity, HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")) + assertEquals(header[`X-Forwarded-Proto`].get, `X-Forwarded-Proto`("abc")) + } + } + + test("The MunitRouteTest should support test checking a route that returns infinite chunks") { + Get() ~> { + val infiniteSource = + Source.unfold(0L)(acc => Some((acc + 1, acc))) + .throttle(1, 20.millis) + .map(i => ByteString(i.toString)) + complete(HttpEntity(ContentTypes.`application/octet-stream`, infiniteSource)) + } ~> check { + assertEquals(status, OK) + assertEquals(contentType, ContentTypes.`application/octet-stream`) + val future = chunksStream.take(5).runFold(Vector.empty[Int])(_ :+ _.data.utf8String.toInt) + assertEquals(Await.result(future, 5.seconds), (0 until 5).toVector) + } + } + + test("The MunitRouteTest should support proper rejection collection") { + Post("/abc", "content") ~> { + (get | put) { + complete("naah") + } + } ~> check { + assertEquals(rejections, List(MethodRejection(GET), MethodRejection(PUT))) + } + } + + test("The MunitRouteTest should support separation of route execution from checking") { + val pinkHeader = RawHeader("Fancy", "pink") + + case object Command + val service = TestProbe() + val handler = TestProbe() + implicit def serviceRef: ActorRef = service.ref + implicit val askTimeout: Timeout = 1.second.dilated + + val result = + Get() ~> pinkHeader ~> { + respondWithHeader(pinkHeader) { + complete(handler.ref.ask(Command).mapTo[String]) + } + } ~> runRoute + + handler.expectMsg(Command) + handler.reply("abc") + + check { + assertEquals(status, OK) + assertEquals(responseEntity, HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")) + assertEquals(header("Fancy"), Some(pinkHeader)) + }(result) + } + + test("The MunitRouteTest should support failing the test inside the route") { + val route = get { + fail("BOOM") + } + + intercept[FailException] { + Get() ~> route + } + } + + test("The MunitRouteTest should support throwing an AssertionError inside the route") { + val route = get { + throw new AssertionError("test") + } + + intercept[AssertionError] { + Get() ~> route + } + } + + test("The MunitRouteTest should support internal server error") { + val route = get { + throw new RuntimeException("BOOM") + } + + Get() ~> route ~> check { + assertEquals(status, InternalServerError) + } + } + + test("The MunitRouteTest should fail if testing a HEAD request with ~> and `transparent-head-request = on`") { + def runTest(): Unit = Head() ~> complete("Ok") ~> check {} + + interceptMessage[AssertionError]( + "`pekko.http.server.transparent-head-requests = on` not supported in RouteTest using `~>`. " + + "Use `~!>` instead for a full-stack test, e.g. `req ~!> route ~> check {...}`") { + runTest() + } + } +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 93ac37170..955ceaedb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -79,6 +79,7 @@ object Dependencies { val sprayJson = Compile.sprayJson % "test" val junit = Compile.junit % "test" val specs2 = "org.specs2" %% "specs2-core" % "4.20.3" + val munit = "org.scalameta" %% "munit" % "0.7.29" val scalacheck = "org.scalacheck" %% "scalacheck" % scalaCheckVersion % "test" val junitIntf = "com.github.sbt" % "junit-interface" % "0.13.3" % "test" @@ -124,6 +125,9 @@ object Dependencies { Test.junit, Test.junitIntf, Compile.junit % "provided", Test.scalatest.withConfigurations(Some("provided; test")))) + lazy val httpTestkitMunit = + l ++= Seq(Test.munit % "provided; test") + lazy val httpTests = l ++= Seq(Test.junit, Test.scalatest, Test.junitIntf) lazy val httpXml = Seq(