From edbfe3d11eefe10f6d45752d1132e7349e1c6750 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 28 Sep 2017 10:28:55 -0700 Subject: Add DriverRoute trait and clean up DriverApp --- .../scala/xyz/driver/core/rest/DriverRoute.scala | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/main/scala/xyz/driver/core/rest/DriverRoute.scala (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala new file mode 100644 index 0000000..20cc556 --- /dev/null +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -0,0 +1,84 @@ +package xyz.driver.core.rest + +import java.sql.SQLException + +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.StatusCodes.{BadRequest, Conflict, InternalServerError} +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{ExceptionHandler, RequestContext, Route} +import com.typesafe.scalalogging.Logger +import org.slf4j.MDC +import xyz.driver.core.rest +import xyz.driver.core.rest.errors.APIError + +import scala.compat.Platform.ConcurrentModificationException + +trait DriverRoute { + val log: Logger + + def route: Route + + def routeWithDefaults: Route = handleExceptions(ExceptionHandler(exceptionHandler)) { + route + } + + /** + * Override me for custom exception handling + * + * @return Exception handling route for exception type + */ + protected def exceptionHandler: PartialFunction[Throwable, Route] = { + case api: APIError if api.isPatientSensitive => + ctx => + log.info("PHI Sensitive error") + errorResponse(ctx, InternalServerError, "Server error", api)(ctx) + + case api: APIError => + ctx => + log.info("API Error") + errorResponse(ctx, api.statusCode, api.message, api)(ctx) + + case is: IllegalStateException => + ctx => + log.warn(s"Request is not allowed to ${ctx.request.method} ${ctx.request.uri}", is) + errorResponse(ctx, BadRequest, message = is.getMessage, is)(ctx) + + case cm: ConcurrentModificationException => + ctx => + log.warn(s"Concurrent modification of the resource ${ctx.request.method} ${ctx.request.uri}", cm) + errorResponse(ctx, Conflict, "Resource was changed concurrently, try requesting a newer version", cm)(ctx) + + case se: SQLException => + ctx => + log.warn(s"Database exception for the resource ${ctx.request.method} ${ctx.request.uri}", se) + errorResponse(ctx, InternalServerError, "Data access error", se)(ctx) + + case t: Throwable => + ctx => + log.warn(s"Request to ${ctx.request.method} ${ctx.request.uri} could not be handled normally", t) + errorResponse(ctx, InternalServerError, t.getMessage, t)(ctx) + } + + protected def errorResponse[T <: Throwable](ctx: RequestContext, + statusCode: StatusCode, + message: String, + exception: T): Route = { + + val trackingId = rest.extractTrackingId(ctx.request) + val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(ctx.request)) + + MDC.put("trackingId", trackingId) + + optionalHeaderValueByType[Origin](()) { originHeader => + val responseHeaders = List[HttpHeader](tracingHeader, + allowOrigin(originHeader), + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*)) + + respondWithHeaders(responseHeaders) { + complete(HttpResponse(statusCode, entity = message)) + } + } + } +} -- cgit v1.2.3 From e5911d0f0fa382f37a22978a5bae1d6c4b47963f Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 17 Oct 2017 14:57:31 -0700 Subject: Rename APIError -> ServiceException and subclasses --- .../scala/xyz/driver/core/rest/DriverRoute.scala | 6 ++-- .../xyz/driver/core/rest/errors/APIError.scala | 40 --------------------- .../driver/core/rest/errors/serviceException.scala | 41 ++++++++++++++++++++++ 3 files changed, 44 insertions(+), 43 deletions(-) delete mode 100644 src/main/scala/xyz/driver/core/rest/errors/APIError.scala create mode 100644 src/main/scala/xyz/driver/core/rest/errors/serviceException.scala (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 20cc556..f3260d0 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -10,7 +10,7 @@ import akka.http.scaladsl.server.{ExceptionHandler, RequestContext, Route} import com.typesafe.scalalogging.Logger import org.slf4j.MDC import xyz.driver.core.rest -import xyz.driver.core.rest.errors.APIError +import xyz.driver.core.rest.errors.ServiceException import scala.compat.Platform.ConcurrentModificationException @@ -29,12 +29,12 @@ trait DriverRoute { * @return Exception handling route for exception type */ protected def exceptionHandler: PartialFunction[Throwable, Route] = { - case api: APIError if api.isPatientSensitive => + case api: ServiceException if api.isPatientSensitive => ctx => log.info("PHI Sensitive error") errorResponse(ctx, InternalServerError, "Server error", api)(ctx) - case api: APIError => + case api: ServiceException => ctx => log.info("API Error") errorResponse(ctx, api.statusCode, api.message, api)(ctx) diff --git a/src/main/scala/xyz/driver/core/rest/errors/APIError.scala b/src/main/scala/xyz/driver/core/rest/errors/APIError.scala deleted file mode 100644 index e0400fb..0000000 --- a/src/main/scala/xyz/driver/core/rest/errors/APIError.scala +++ /dev/null @@ -1,40 +0,0 @@ -package xyz.driver.core.rest.errors - -import akka.http.scaladsl.model.{StatusCode, StatusCodes} - -abstract class APIError extends Throwable { - def isPatientSensitive: Boolean = false - - def statusCode: StatusCode - def message: String -} - -final case class InvalidInputError(override val message: String = "Invalid input", - override val isPatientSensitive: Boolean = false) - extends APIError { - override def statusCode: StatusCode = StatusCodes.BadRequest -} - -final case class InvalidActionError(override val message: String = "This action is not allowed", - override val isPatientSensitive: Boolean = false) - extends APIError { - override def statusCode: StatusCode = StatusCodes.Forbidden -} - -final case class ResourceNotFoundError(override val message: String = "Resource not found", - override val isPatientSensitive: Boolean = false) - extends APIError { - override def statusCode: StatusCode = StatusCodes.NotFound -} - -final case class ExternalServiceTimeoutError(override val message: String = "Another service took too long to respond", - override val isPatientSensitive: Boolean = false) - extends APIError { - override def statusCode: StatusCode = StatusCodes.GatewayTimeout -} - -final case class DatabaseError(override val message: String = "Database access error", - override val isPatientSensitive: Boolean = false) - extends APIError { - override def statusCode: StatusCode = StatusCodes.InternalServerError -} diff --git a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala new file mode 100644 index 0000000..94f9734 --- /dev/null +++ b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala @@ -0,0 +1,41 @@ +package xyz.driver.core.rest.errors + +import akka.http.scaladsl.model.{StatusCode, StatusCodes} + +abstract class ServiceException extends Exception { + def isPatientSensitive: Boolean = false + + def statusCode: StatusCode + def message: String +} + +final case class InvalidInputException(override val message: String = "Invalid input", + override val isPatientSensitive: Boolean = false) + extends ServiceException { + override def statusCode: StatusCode = StatusCodes.BadRequest +} + +final case class InvalidActionException(override val message: String = "This action is not allowed", + override val isPatientSensitive: Boolean = false) + extends ServiceException { + override def statusCode: StatusCode = StatusCodes.Forbidden +} + +final case class ResourceNotFoundException(override val message: String = "Resource not found", + override val isPatientSensitive: Boolean = false) + extends ServiceException { + override def statusCode: StatusCode = StatusCodes.NotFound +} + +final case class ExternalServiceTimeoutException(override val message: String = + "Another service took too long to respond", + override val isPatientSensitive: Boolean = false) + extends ServiceException { + override def statusCode: StatusCode = StatusCodes.GatewayTimeout +} + +final case class DatabaseException(override val message: String = "Database access error", + override val isPatientSensitive: Boolean = false) + extends ServiceException { + override def statusCode: StatusCode = StatusCodes.InternalServerError +} -- cgit v1.2.3 From e7d849b55f975f1c119cebb22f95d9de903ae3c3 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 19 Oct 2017 13:49:22 -0700 Subject: Stop catching throwable, remove PHI filtering, move status code logic to exception handler --- .../scala/xyz/driver/core/rest/DriverRoute.scala | 54 +++++++++++++++------- .../driver/core/rest/errors/serviceException.scala | 40 ++++------------ 2 files changed, 46 insertions(+), 48 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index f3260d0..be9c783 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -3,14 +3,14 @@ package xyz.driver.core.rest import java.sql.SQLException import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.StatusCodes.{BadRequest, Conflict, InternalServerError} +import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{ExceptionHandler, RequestContext, Route} import com.typesafe.scalalogging.Logger import org.slf4j.MDC import xyz.driver.core.rest -import xyz.driver.core.rest.errors.ServiceException +import xyz.driver.core.rest.errors._ import scala.compat.Platform.ConcurrentModificationException @@ -29,38 +29,58 @@ trait DriverRoute { * @return Exception handling route for exception type */ protected def exceptionHandler: PartialFunction[Throwable, Route] = { - case api: ServiceException if api.isPatientSensitive => - ctx => - log.info("PHI Sensitive error") - errorResponse(ctx, InternalServerError, "Server error", api)(ctx) - - case api: ServiceException => - ctx => - log.info("API Error") - errorResponse(ctx, api.statusCode, api.message, api)(ctx) + case serviceException: ServiceException => + serviceExceptionHandler(serviceException) case is: IllegalStateException => ctx => log.warn(s"Request is not allowed to ${ctx.request.method} ${ctx.request.uri}", is) - errorResponse(ctx, BadRequest, message = is.getMessage, is)(ctx) + errorResponse(ctx, StatusCodes.BadRequest, message = is.getMessage, is)(ctx) case cm: ConcurrentModificationException => ctx => log.warn(s"Concurrent modification of the resource ${ctx.request.method} ${ctx.request.uri}", cm) - errorResponse(ctx, Conflict, "Resource was changed concurrently, try requesting a newer version", cm)(ctx) + errorResponse(ctx, + StatusCodes.Conflict, + "Resource was changed concurrently, try requesting a newer version", + cm)(ctx) case se: SQLException => ctx => log.warn(s"Database exception for the resource ${ctx.request.method} ${ctx.request.uri}", se) - errorResponse(ctx, InternalServerError, "Data access error", se)(ctx) + errorResponse(ctx, StatusCodes.InternalServerError, "Data access error", se)(ctx) - case t: Throwable => + case t: Exception => ctx => log.warn(s"Request to ${ctx.request.method} ${ctx.request.uri} could not be handled normally", t) - errorResponse(ctx, InternalServerError, t.getMessage, t)(ctx) + errorResponse(ctx, StatusCodes.InternalServerError, t.getMessage, t)(ctx) + } + + protected def serviceExceptionHandler(serviceException: ServiceException): Route = { + val statusCode = serviceException match { + case e: InvalidInputException => + log.info("Invalid client input error", e) + StatusCodes.BadRequest + case e: InvalidActionException => + log.info("Invalid client action error", e) + StatusCodes.Forbidden + case e: ResourceNotFoundException => + log.info("Resource not found error", e) + StatusCodes.NotFound + case e: ExternalServiceTimeoutException => + log.error("Service timeout error", e) + StatusCodes.GatewayTimeout + case e: DatabaseException => + log.error("Database error", e) + StatusCodes.InternalServerError + } + + { (ctx: RequestContext) => + errorResponse(ctx, statusCode, serviceException.message, serviceException)(ctx) + } } - protected def errorResponse[T <: Throwable](ctx: RequestContext, + protected def errorResponse[T <: Exception](ctx: RequestContext, statusCode: StatusCode, message: String, exception: T): Route = { diff --git a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala index 94f9734..7aa70bf 100644 --- a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala +++ b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala @@ -1,41 +1,19 @@ package xyz.driver.core.rest.errors -import akka.http.scaladsl.model.{StatusCode, StatusCodes} - abstract class ServiceException extends Exception { - def isPatientSensitive: Boolean = false - - def statusCode: StatusCode def message: String } -final case class InvalidInputException(override val message: String = "Invalid input", - override val isPatientSensitive: Boolean = false) - extends ServiceException { - override def statusCode: StatusCode = StatusCodes.BadRequest -} +final case class InvalidInputException(override val message: String = "Invalid input") extends ServiceException -final case class InvalidActionException(override val message: String = "This action is not allowed", - override val isPatientSensitive: Boolean = false) - extends ServiceException { - override def statusCode: StatusCode = StatusCodes.Forbidden -} +final case class InvalidActionException(override val message: String = "This action is not allowed") + extends ServiceException -final case class ResourceNotFoundException(override val message: String = "Resource not found", - override val isPatientSensitive: Boolean = false) - extends ServiceException { - override def statusCode: StatusCode = StatusCodes.NotFound -} +final case class ResourceNotFoundException(override val message: String = "Resource not found") + extends ServiceException -final case class ExternalServiceTimeoutException(override val message: String = - "Another service took too long to respond", - override val isPatientSensitive: Boolean = false) - extends ServiceException { - override def statusCode: StatusCode = StatusCodes.GatewayTimeout -} +final case class ExternalServiceTimeoutException( + override val message: String = "Another service took too long to respond") + extends ServiceException -final case class DatabaseException(override val message: String = "Database access error", - override val isPatientSensitive: Boolean = false) - extends ServiceException { - override def statusCode: StatusCode = StatusCodes.InternalServerError -} +final case class DatabaseException(override val message: String = "Database access error") extends ServiceException -- cgit v1.2.3 From 473d2586c20d35fbb56e11da816140ae40bcb463 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 24 Oct 2017 14:35:23 -0700 Subject: Change DriverRoute log val to def --- src/main/scala/xyz/driver/core/rest/DriverRoute.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index be9c783..79dff8b 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -15,7 +15,7 @@ import xyz.driver.core.rest.errors._ import scala.compat.Platform.ConcurrentModificationException trait DriverRoute { - val log: Logger + def log: Logger def route: Route -- cgit v1.2.3 From 5db157103a1982f00ca72e8f6b925344debce36e Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 25 Oct 2017 13:34:46 -0700 Subject: Move all CORS headers to DriverRoute from DriverApp --- src/main/scala/xyz/driver/core/app/DriverApp.scala | 52 ++++++++-------------- .../scala/xyz/driver/core/rest/DriverRoute.scala | 21 +++++++-- 2 files changed, 36 insertions(+), 37 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/app/DriverApp.scala b/src/main/scala/xyz/driver/core/app/DriverApp.scala index 4110c37..2bb61c0 100644 --- a/src/main/scala/xyz/driver/core/app/DriverApp.scala +++ b/src/main/scala/xyz/driver/core/app/DriverApp.scala @@ -73,47 +73,31 @@ class DriverApp(appName: String, protected def appRoute: Route = { val serviceTypes = modules.flatMap(_.routeTypes) val swaggerService = swaggerOverride(serviceTypes) - val swaggerRoutes = swaggerService.routes ~ swaggerService.swaggerUI + val swaggerRoute = swaggerService.routes ~ swaggerService.swaggerUI val versionRt = versionRoute(version, gitHash, time.currentTime()) + val combinedRoute = modules.map(_.route).foldLeft(versionRt ~ healthRoute ~ swaggerRoute)(_ ~ _) - extractHost { origin => - extractClientIP { ip => - optionalHeaderValueByType[Origin](()) { originHeader => - trace(tracer) { ctx => - val trackingId = rest.extractTrackingId(ctx.request) - MDC.put("trackingId", trackingId) + (extractHost & extractClientIP & trace(tracer)) { + case (origin, ip) => + ctx => + val trackingId = rest.extractTrackingId(ctx.request) + MDC.put("trackingId", trackingId) - val updatedStacktrace = - (rest.extractStacktrace(ctx.request) ++ Array(appName)).mkString("->") - MDC.put("stack", updatedStacktrace) + val updatedStacktrace = + (rest.extractStacktrace(ctx.request) ++ Array(appName)).mkString("->") + MDC.put("stack", updatedStacktrace) - storeRequestContextToMdc(ctx.request, origin, ip) + storeRequestContextToMdc(ctx.request, origin, ip) - val trackingHeader = RawHeader(ContextHeaders.TrackingIdHeader, trackingId) + log.info(s"""Received request {"method":"${ctx.request.method.value}","url": "${ctx.request.uri}"}""") - val responseHeaders = List[HttpHeader]( - trackingHeader, - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(rest.AllowedHeaders: _*), - `Access-Control-Expose-Headers`(rest.AllowedHeaders: _*) - ) - - log.info(s"""Received request {"method":"${ctx.request.method.value}","url": "${ctx.request.uri}"}""") + val contextWithTrackingId = + ctx.withRequest( + ctx.request + .addHeader(RawHeader(ContextHeaders.TrackingIdHeader, trackingId)) + .addHeader(RawHeader(ContextHeaders.StacktraceHeader, updatedStacktrace))) - val contextWithTrackingId = - ctx.withRequest( - ctx.request - .addHeader(RawHeader(ContextHeaders.TrackingIdHeader, trackingId)) - .addHeader(RawHeader(ContextHeaders.StacktraceHeader, updatedStacktrace))) - - respondWithHeaders(responseHeaders) { - modules - .map(_.route) - .foldLeft(versionRt ~ healthRoute ~ swaggerRoutes)(_ ~ _) - }(contextWithTrackingId) - } - } - } + combinedRoute(contextWithTrackingId) } } diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 79dff8b..d42bd75 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -6,7 +6,7 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{ExceptionHandler, RequestContext, Route} +import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RequestContext, Route} import com.typesafe.scalalogging.Logger import org.slf4j.MDC import xyz.driver.core.rest @@ -19,8 +19,23 @@ trait DriverRoute { def route: Route - def routeWithDefaults: Route = handleExceptions(ExceptionHandler(exceptionHandler)) { - route + def routeWithDefaults: Route = { + (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler)))(route) + } + + protected def defaultResponseHeaders: Directive0 = { + (extractRequest & optionalHeaderValueByType[Origin](())) tflatMap { + case (request, originHeader) => + val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(request)) + val responseHeaders = List[HttpHeader]( + tracingHeader, + allowOrigin(originHeader), + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*) + ) + + respondWithHeaders(responseHeaders) + } } /** -- cgit v1.2.3 From d28544ead7aefc6c2ed1fe0b9a0f75fac25c81d6 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 25 Oct 2017 16:41:04 -0700 Subject: Remove duplicate CORS header code from errorResponse method --- src/main/scala/xyz/driver/core/rest/DriverRoute.scala | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index d42bd75..9af6657 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -99,21 +99,8 @@ trait DriverRoute { statusCode: StatusCode, message: String, exception: T): Route = { - - val trackingId = rest.extractTrackingId(ctx.request) - val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(ctx.request)) - + val trackingId = rest.extractTrackingId(ctx.request) MDC.put("trackingId", trackingId) - - optionalHeaderValueByType[Origin](()) { originHeader => - val responseHeaders = List[HttpHeader](tracingHeader, - allowOrigin(originHeader), - `Access-Control-Allow-Headers`(AllowedHeaders: _*), - `Access-Control-Expose-Headers`(AllowedHeaders: _*)) - - respondWithHeaders(responseHeaders) { - complete(HttpResponse(statusCode, entity = message)) - } - } + complete(HttpResponse(statusCode, entity = message)) } } -- cgit v1.2.3 From 595d199f5e41c8e48131cec98b23452bc7ed6ef1 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 1 Nov 2017 09:24:16 -0700 Subject: Add DriverRouteTest --- .../scala/xyz/driver/core/rest/DriverRoute.scala | 3 + .../driver/core/rest/errors/serviceException.scala | 4 +- src/test/scala/xyz/driver/core/RestTest.scala | 16 ---- .../xyz/driver/core/rest/DriverRouteTest.scala | 89 ++++++++++++++++++++++ src/test/scala/xyz/driver/core/rest/RestTest.scala | 15 ++++ 5 files changed, 109 insertions(+), 18 deletions(-) delete mode 100644 src/test/scala/xyz/driver/core/RestTest.scala create mode 100644 src/test/scala/xyz/driver/core/rest/DriverRouteTest.scala create mode 100644 src/test/scala/xyz/driver/core/rest/RestTest.scala (limited to 'src/main/scala/xyz/driver/core/rest/DriverRoute.scala') diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala index 9af6657..eb9a31a 100644 --- a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -82,6 +82,9 @@ trait DriverRoute { case e: ResourceNotFoundException => log.info("Resource not found error", e) StatusCodes.NotFound + case e: ExternalServiceException => + log.error("Error while calling another service", e) + StatusCodes.InternalServerError case e: ExternalServiceTimeoutException => log.error("Service timeout error", e) StatusCodes.GatewayTimeout diff --git a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala index ca1f759..e91a3c2 100644 --- a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala +++ b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala @@ -1,6 +1,6 @@ package xyz.driver.core.rest.errors -abstract class ServiceException extends Exception { +sealed abstract class ServiceException extends Exception { def message: String } @@ -13,7 +13,7 @@ final case class ResourceNotFoundException(override val message: String = "Resou extends ServiceException final case class ExternalServiceException(serviceName: String, serviceMessage: String) extends ServiceException { - override def message = s"Error while calling another service: $serviceMessage" + override def message = s"Error while calling '$serviceName': $serviceMessage" } final case class ExternalServiceTimeoutException(serviceName: String) extends ServiceException { diff --git a/src/test/scala/xyz/driver/core/RestTest.scala b/src/test/scala/xyz/driver/core/RestTest.scala deleted file mode 100644 index efb9d07..0000000 --- a/src/test/scala/xyz/driver/core/RestTest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.driver.core.rest - -import org.scalatest.{FlatSpec, Matchers} - -import akka.util.ByteString - -class RestTest extends FlatSpec with Matchers { - "`escapeScriptTags` function" should "escap script tags properly" in { - val dirtyString = " akkaComplete} +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.testkit.ScalatestRouteTest +import com.typesafe.scalalogging.Logger +import org.scalatest.{AsyncFlatSpec, Matchers} +import xyz.driver.core.logging.NoLogger +import xyz.driver.core.rest.errors._ + +import scala.concurrent.Future + +class DriverRouteTest extends AsyncFlatSpec with ScalatestRouteTest with Matchers { + class TestRoute(override val route: Route) extends DriverRoute { + override def log: Logger = NoLogger + } + + "DriverRoute" should "respond with 200 OK for a basic route" in { + val route = new TestRoute(akkaComplete(StatusCodes.OK)) + + Get("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.OK + } + } + + it should "respond with a 400 for an InvalidInputException" in { + val route = new TestRoute(akkaComplete(Future.failed[String](InvalidInputException()))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.BadRequest + responseAs[String] shouldBe "Invalid input" + } + } + + it should "respond with a 400 for InvalidActionException" in { + val route = new TestRoute(akkaComplete(Future.failed[String](InvalidActionException()))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.Forbidden + responseAs[String] shouldBe "This action is not allowed" + } + } + + it should "respond with a 404 for ResourceNotFoundException" in { + val route = new TestRoute(akkaComplete(Future.failed[String](ResourceNotFoundException()))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.NotFound + responseAs[String] shouldBe "Resource not found" + } + } + + it should "respond with a 500 for ExternalServiceException" in { + val error = ExternalServiceException("GET /api/v1/users/", "Permission denied") + val route = new TestRoute(akkaComplete(Future.failed[String](error))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.InternalServerError + responseAs[String] shouldBe "Error while calling 'GET /api/v1/users/': Permission denied" + } + } + + it should "respond with a 503 for ExternalServiceTimeoutException" in { + val error = ExternalServiceTimeoutException("GET /api/v1/users/") + val route = new TestRoute(akkaComplete(Future.failed[String](error))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.GatewayTimeout + responseAs[String] shouldBe "GET /api/v1/users/ took too long to respond" + } + } + + it should "respond with a 500 for DatabaseException" in { + val route = new TestRoute(akkaComplete(Future.failed[String](DatabaseException()))) + + Post("/api/v1/foo/bar") ~> route.routeWithDefaults ~> check { + handled shouldBe true + status shouldBe StatusCodes.InternalServerError + responseAs[String] shouldBe "Database access error" + } + } +} diff --git a/src/test/scala/xyz/driver/core/rest/RestTest.scala b/src/test/scala/xyz/driver/core/rest/RestTest.scala new file mode 100644 index 0000000..2c3fb7f --- /dev/null +++ b/src/test/scala/xyz/driver/core/rest/RestTest.scala @@ -0,0 +1,15 @@ +package xyz.driver.core.rest + +import akka.util.ByteString +import org.scalatest.{FlatSpec, Matchers} + +class RestTest extends FlatSpec with Matchers { + "`escapeScriptTags` function" should "escap script tags properly" in { + val dirtyString = "