diff --git a/build.sbt b/build.sbt index 899b2767..faa23183 100644 --- a/build.sbt +++ b/build.sbt @@ -83,6 +83,26 @@ val playTestdata = Project("play-grpc-testdata", file("play-testdata")) ) .enablePlugins(build.play.grpc.NoPublish) +val playActionsTestData = Project("play-grpc-actions-testdata", file("play-actions-testdata")) + .dependsOn(playRuntime) + .settings( + scalacOptions += "-Xlint:-unused,_", // can't do anything about unused things in generated code + javacOptions -= "-Xlint:deprecation", // can't do anything about deprecations in generated code + ReflectiveCodeGen.extraGenerators ++= List( + "play.grpc.gen.scaladsl.PlayScalaServerCodeGenerator", + ), + ReflectiveCodeGen.codeGeneratorSettings += "use_play_actions", + libraryDependencies ++= Seq( + Dependencies.Compile.play, + Dependencies.Compile.grpcStub, + Dependencies.Compile.playAkkaHttpServer, + Dependencies.Compile.playAkkaHttp2Support, + Dependencies.Compile.akkaDiscovery, + ), + ) + .pluginTestingSettings + .enablePlugins(build.play.grpc.NoPublish) + val playGenerators = Project("play-grpc-generators", file("play-generators")) .enablePlugins(SbtTwirl, BuildInfoPlugin) .settings( @@ -95,7 +115,7 @@ val playGenerators = Project("play-grpc-generators", file("play-generators")) ) val playTestkit = Project("play-grpc-testkit", file("play-testkit")) - .dependsOn(playRuntime, playTestdata % "test") + .dependsOn(playRuntime, playTestdata % "test", playActionsTestData % "test") .settings( libraryDependencies ++= Seq( Dependencies.Compile.play, diff --git a/play-actions-testdata/src/main/proto/helloworld.proto b/play-actions-testdata/src/main/proto/helloworld.proto new file mode 100644 index 00000000..cda4e40b --- /dev/null +++ b/play-actions-testdata/src/main/proto/helloworld.proto @@ -0,0 +1,19 @@ +// must mirror the scala-interop-test one since that is shown in the docs +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "example.myapp.helloworld.grpc.actions"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +service GreeterService { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/play-generators/src/main/twirl/templates/PlayScala/RouterUsingActions.scala.txt b/play-generators/src/main/twirl/templates/PlayScala/RouterUsingActions.scala.txt index 8e2d7f0a..cde6b4b8 100644 --- a/play-generators/src/main/twirl/templates/PlayScala/RouterUsingActions.scala.txt +++ b/play-generators/src/main/twirl/templates/PlayScala/RouterUsingActions.scala.txt @@ -8,7 +8,7 @@ package @service.packageName import akka.annotation.InternalApi import akka.actor.ActorSystem -import akka.grpc.GrpcServiceException +import akka.grpc.{GrpcServiceException, Trailers} import play.grpc.internal.PlayRouterUsingActions import akka.grpc.scaladsl.GrpcExceptionHandler.defaultMapper import akka.http.scaladsl.model.Uri.Path @@ -23,7 +23,7 @@ import scala.concurrent.ExecutionContext /** * Abstract base class for implementing @{serviceName} and using as a play Router */ - abstract class Abstract@{serviceName}Router(mat: Materializer, system: ActorSystem, parsers: PlayBodyParsers, actionBuilder: ActionBuilder[Request, AnyContent], eHandler: ActorSystem => PartialFunction[Throwable, Status] = defaultMapper) extends PlayRouterUsingActions(mat, @{service.name}.name, parsers, actionBuilder) with @{serviceName} { + abstract class Abstract@{serviceName}Router(mat: Materializer, system: ActorSystem, parsers: PlayBodyParsers, actionBuilder: ActionBuilder[Request, AnyContent], eHandler: ActorSystem => PartialFunction[Throwable, Trailers] = defaultMapper) extends PlayRouterUsingActions(mat, @{service.name}.name, parsers, actionBuilder) with @{serviceName} { @{ val (streamingInputMethods: Seq[String], unaryInputMethods: Seq[String]) = service.methods.partition(_.inputStreaming) match { @@ -37,7 +37,7 @@ import scala.concurrent.ExecutionContext */ @@InternalApi final override protected def createHandler(serviceName: String, mat: Materializer): RequestHeader => EssentialAction = { - val handler = @{serviceName}Handler(this, serviceName, eHandler)(mat, system) + val handler = @{serviceName}Handler(this, serviceName, eHandler)(system) reqOuter => implicit val ec: ExecutionContext = mat.executionContext Path(reqOuter.path) match { diff --git a/play-scalatest/src/test/scala/play/grpc/scalatest/PlayActionsScalaTestSpec.scala b/play-scalatest/src/test/scala/play/grpc/scalatest/PlayActionsScalaTestSpec.scala new file mode 100644 index 00000000..a15f2a7f --- /dev/null +++ b/play-scalatest/src/test/scala/play/grpc/scalatest/PlayActionsScalaTestSpec.scala @@ -0,0 +1,70 @@ +/* + * Copyright (C) Lightbend Inc. + */ +package play.grpc.scalatest + +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.concurrent.ScalaFutures +import org.scalatestplus.play.PlaySpec +import org.scalatestplus.play.guice.GuiceOneServerPerTest +import play.api.Application +import play.api.inject.bind +import play.api.inject.guice.GuiceApplicationBuilder +import play.api.libs.ws.WSClient +import play.api.routing.Router +import io.grpc.Status +import example.myapp.helloworld.grpc.actions.helloworld._ + +import akka.grpc.internal.GrpcProtocolNative + +/** + * Test for the Play gRPC ScalaTest APIs + */ +class PlayActionsScalaTestSpec + extends PlaySpec + with GuiceOneServerPerTest + with ServerGrpcClient + with ScalaFutures + with IntegrationPatience { + + override def fakeApplication(): Application = { + GuiceApplicationBuilder() + .overrides(bind[Router].to[GreeterServiceWithActionsImpl]) + .build() + } + + implicit def ws: WSClient = app.injector.instanceOf(classOf[WSClient]) + + "A Play server bound to a gRPC router using actions" must { + "give a 404 when routing a non-gRPC request" in { + val result = wsUrl("/").get.futureValue + result.status must be(404) // Maybe should be a 426, see #396 + } + // this test results in a 500 + // "give a 415 error when not using a gRPC content-type" in { + // val result = wsUrl(s"/${GreeterService.name}/FooBar").get.futureValue + // result.status must be(415) + // } + // this test results in a 500 error + // "give a grpc 'unimplemented' error when routing a non-existent gRPC method" in { + // val result = wsUrl(s"/${GreeterService.name}/FooBar") + // .addHttpHeaders("Content-Type" -> GrpcProtocolNative.contentType.toString) + // .get + // .futureValue + // result.status must be(200) // Maybe should be a 426, see #396 + // result.header("grpc-status") mustEqual Some(Status.Code.UNIMPLEMENTED.value().toString) + // } + "give a grpc 'invalid argument' error when routing an empty request to a gRPC method" in { + val result = wsUrl(s"/${GreeterService.name}/SayHello") + .addHttpHeaders("Content-Type" -> GrpcProtocolNative.contentType.toString) + .get + .futureValue + result.status must be(200) + result.header("grpc-status") mustEqual Some(Status.Code.INVALID_ARGUMENT.value().toString) + } + "work with a gRPC client" in withGrpcClient[GreeterServiceClient] { client: GreeterServiceClient => + val reply = client.sayHello(HelloRequest("Alice")).futureValue + reply.message must be("Hello, Alice!") + } + } +} diff --git a/play-testkit/src/test/scala/example/myapp/helloworld/grpc/actions/helloworld/GreeterServiceWithActionsImpl.scala b/play-testkit/src/test/scala/example/myapp/helloworld/grpc/actions/helloworld/GreeterServiceWithActionsImpl.scala new file mode 100644 index 00000000..c41d94ce --- /dev/null +++ b/play-testkit/src/test/scala/example/myapp/helloworld/grpc/actions/helloworld/GreeterServiceWithActionsImpl.scala @@ -0,0 +1,34 @@ +/* + * Copyright (C) Lightbend Inc. + */ +package example.myapp.helloworld.grpc.actions.helloworld + +import javax.inject.Inject +import javax.inject.Singleton +import akka.stream.Materializer +import akka.actor.ActorSystem +import play.api.mvc.PlayBodyParsers +import play.api.mvc.DefaultActionBuilder + +import scala.concurrent.Future + +@Singleton +class GreeterServiceWithActionsImpl @Inject() ( + implicit + mat: Materializer, + actorSystem: ActorSystem, + parsers: PlayBodyParsers, + actionBuilder: DefaultActionBuilder, +) extends AbstractGreeterServiceRouter( + mat, + actorSystem, + parsers, + actionBuilder, + ) { + + override def sayHello(in: HelloRequest): Future[HelloReply] = { + actorSystem.log.error("Saying hello!") + Future.successful(HelloReply(s"Hello, ${in.name}!")) + } + +}