diff --git a/app/definition/BsasApiDefinitionFactory.scala b/app/definition/BsasApiDefinitionFactory.scala index c2b257cb4..1cec831bb 100644 --- a/app/definition/BsasApiDefinitionFactory.scala +++ b/app/definition/BsasApiDefinitionFactory.scala @@ -18,7 +18,7 @@ package definition import shared.config.AppConfig import shared.definition._ -import shared.routing.{Version3, Version4, Version5, Version6} +import routing.Versions._ import javax.inject.{Inject, Singleton} diff --git a/app/routing/BsasVersionRoutingMap.scala b/app/routing/BsasVersionRoutingMap.scala index a384eae37..ea39d8a89 100644 --- a/app/routing/BsasVersionRoutingMap.scala +++ b/app/routing/BsasVersionRoutingMap.scala @@ -18,7 +18,8 @@ package routing import play.api.routing.Router import shared.config.AppConfig -import shared.routing.{Version, Version3, Version4, Version5, Version6, VersionRoutingMap} +import shared.routing.{Version, VersionRoutingMap} +import Versions._ import javax.inject.{Inject, Singleton} diff --git a/app/routing/Versions.scala b/app/routing/Versions.scala new file mode 100644 index 000000000..e5aa56fc2 --- /dev/null +++ b/app/routing/Versions.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed 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 routing + +import shared.routing.Version + +object Versions { + + val Version1: Version = Version("1.0") + val Version2: Version = Version("2.0") + val Version3: Version = Version("3.0") + val Version4: Version = Version("4.0") + val Version5: Version = Version("5.0") + val Version6: Version = Version("6.0") +} diff --git a/app/shared/routing/Version.scala b/app/shared/routing/Version.scala new file mode 100644 index 000000000..c7fdf4d1c --- /dev/null +++ b/app/shared/routing/Version.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * Licensed 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 shared.routing + +import play.api.http.HeaderNames.ACCEPT +import play.api.libs.json._ +import play.api.mvc.{Headers, RequestHeader} + +final case class Version(name: String) { + override def toString: String = name +} + +object Version { + + implicit val versionWrites: Writes[Version] = implicitly[Writes[String]].contramap[Version](_.name) + + implicit val versionReads: Reads[Version] = implicitly[Reads[String]].map(Version(_)) + + private val versionRegex = """application/vnd.hmrc.(\d.\d)\+json""".r + + def apply(request: RequestHeader): Version = + getFromRequest(request).getOrElse(throw new Exception("Missing or unsupported version found in request accept header")) + + def getFromRequest(request: RequestHeader): Either[GetFromRequestError, Version] = + getFrom(request.headers).map(Version(_)) + + private def getFrom(headers: Headers): Either[GetFromRequestError, String] = + headers.get(ACCEPT).collect { case versionRegex(value) => value }.toRight(left = InvalidHeader) + +} + +sealed trait GetFromRequestError +case object InvalidHeader extends GetFromRequestError diff --git a/app/shared/routing/VersionRoutingRequestHandler.scala b/app/shared/routing/VersionRoutingRequestHandler.scala index 6309d9303..31e42b642 100644 --- a/app/shared/routing/VersionRoutingRequestHandler.scala +++ b/app/shared/routing/VersionRoutingRequestHandler.scala @@ -17,11 +17,12 @@ package shared.routing import play.api.http.{DefaultHttpRequestHandler, HttpConfiguration, HttpErrorHandler, HttpFilters} -import play.api.mvc.{DefaultActionBuilder, Handler, RequestHeader, Results} +import play.api.mvc.Results.Status +import play.api.mvc.{DefaultActionBuilder, Handler, RequestHeader} import play.api.routing.Router import play.core.DefaultWebCommands import shared.config.AppConfig -import shared.models.errors.{InvalidAcceptHeaderError, UnsupportedVersionError} +import shared.models.errors.{InvalidAcceptHeaderError, MtdError, UnsupportedVersionError} import javax.inject.{Inject, Singleton} @@ -41,29 +42,25 @@ class VersionRoutingRequestHandler @Inject() (versionRoutingMap: VersionRoutingM filters = filters.filters ) { - private val unsupportedVersionAction = action(Results.NotFound(UnsupportedVersionError.asJson)) - - private val invalidAcceptHeaderError = action(Results.NotAcceptable(InvalidAcceptHeaderError.asJson)) + private def errorAction(error: MtdError) = action(Status(error.httpStatus)(error.asJson)) override def routeRequest(request: RequestHeader): Option[Handler] = { def documentHandler: Option[Handler] = routeWith(versionRoutingMap.defaultRouter)(request) def apiHandler: Option[Handler] = - Versions.getFromRequest(request) match { - case Left(InvalidHeader) => Some(invalidAcceptHeaderError) - case Left(VersionNotFound) => Some(unsupportedVersionAction) + Version.getFromRequest(request) match { + case Left(InvalidHeader) => Some(errorAction(InvalidAcceptHeaderError)) case Right(version) => versionRoutingMap.versionRouter(version) match { - case Some(versionRouter) if config.endpointsEnabled(version) => - routeWith(versionRouter)(request) - case _ => - Some(unsupportedVersionAction) + case Some(versionRouter) if config.endpointsEnabled(version) => routeWith(versionRouter)(request) + case _ => Some(errorAction(UnsupportedVersionError)) } } documentHandler orElse apiHandler + } private def routeWith(router: Router)(request: RequestHeader): Option[Handler] = diff --git a/app/shared/routing/Versions.scala b/app/shared/routing/Versions.scala deleted file mode 100644 index e74c040b9..000000000 --- a/app/shared/routing/Versions.scala +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2023 HM Revenue & Customs - * - * Licensed 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 shared.routing - -import play.api.http.HeaderNames.ACCEPT -import play.api.libs.json.Writes._ -import play.api.libs.json._ -import play.api.mvc.RequestHeader - -object Version { - - def apply(request: RequestHeader): Version = - Versions.getFromRequest(request).getOrElse(throw new Exception("Missing or unsupported version found in request accept header")) - - object VersionWrites extends Writes[Version] { - def writes(version: Version): JsValue = version.asJson - } - - object VersionReads extends Reads[Version] { - - /** @param version - * expecting a JsString e.g. "1.0" - */ - override def reads(version: JsValue): JsResult[Version] = - version - .validate[String] - .flatMap(name => - Versions.getFrom(name) match { - case Left(_) => JsError("Version not recognised") - case Right(version) => JsSuccess(version) - }) - - } - - implicit val versionFormat: Format[Version] = Format(VersionReads, VersionWrites) - -} - -sealed trait Version { - val name: String - lazy val asJson: JsValue = Json.toJson(name) - - override def toString: String = name -} - -case object Version1 extends Version { - val name = "1.0" -} - -case object Version2 extends Version { - val name = "2.0" -} - -case object Version3 extends Version { - val name = "3.0" -} - -case object Version4 extends Version { - val name = "4.0" -} - -case object Version5 extends Version { - val name = "5.0" -} - -case object Version6 extends Version { - val name = "6.0" -} - -case object Version7 extends Version { - val name = "7.0" -} - -case object Version8 extends Version { - val name = "8.0" -} - -case object Version9 extends Version { - val name = "9.0" -} - -object Versions { - - private val versionsByName: Map[String, Version] = Map( - Version1.name -> Version1, - Version2.name -> Version2, - Version3.name -> Version3, - Version4.name -> Version4, - Version5.name -> Version5, - Version6.name -> Version6, - Version7.name -> Version7, - Version8.name -> Version8, - Version9.name -> Version9 - ) - - private val versionRegex = """application/vnd.hmrc.(\d.\d)\+json""".r - - def getFromRequest(request: RequestHeader): Either[GetFromRequestError, Version] = - for { - str <- getFrom(request.headers.headers) - ver <- getFrom(str) - } yield ver - - private def getFrom(headers: Seq[(String, String)]): Either[GetFromRequestError, String] = - headers.collectFirst { case (ACCEPT, versionRegex(ver)) => ver }.toRight(left = InvalidHeader) - - def getFrom(name: String): Either[GetFromRequestError, Version] = - versionsByName.get(name).toRight(left = VersionNotFound) - -} - -sealed trait GetFromRequestError - -case object InvalidHeader extends GetFromRequestError - -case object VersionNotFound extends GetFromRequestError diff --git a/app/shared/utils/ErrorHandler.scala b/app/shared/utils/ErrorHandler.scala index a99f16b12..3283c29e5 100644 --- a/app/shared/utils/ErrorHandler.scala +++ b/app/shared/utils/ErrorHandler.scala @@ -21,7 +21,7 @@ import play.api.http.Status._ import play.api.mvc.Results._ import play.api.mvc._ import shared.models.errors._ -import shared.routing.Versions +import shared.routing.Version import uk.gov.hmrc.auth.core.AuthorisationException import uk.gov.hmrc.http._ import uk.gov.hmrc.play.audit.http.connector.AuditConnector @@ -85,7 +85,7 @@ class ErrorHandler @Inject() ( } } - private def versionIfSpecified(request: RequestHeader): String = Versions.getFromRequest(request).map(_.name).getOrElse("") + private def versionIfSpecified(request: RequestHeader): String = Version.getFromRequest(request).map(_.name).getOrElse("") override def onServerError(request: RequestHeader, ex: Throwable): Future[Result] = { implicit val headerCarrier: HeaderCarrier = HeaderCarrierConverter.fromRequestAndSession(request, request.session) diff --git a/it/shared/endpoints/DocumentationControllerISpec.scala b/it/shared/endpoints/DocumentationControllerISpec.scala index 997c0b93d..fbbcc3d25 100644 --- a/it/shared/endpoints/DocumentationControllerISpec.scala +++ b/it/shared/endpoints/DocumentationControllerISpec.scala @@ -22,7 +22,7 @@ import play.api.http.Status.OK import play.api.libs.json.Json import play.api.libs.ws.WSResponse import shared.config.AppConfig -import shared.routing.{Version, Versions} +import shared.routing.Version import support.IntegrationBaseSpec import scala.util.Try @@ -34,9 +34,8 @@ class DocumentationControllerISpec extends IntegrationBaseSpec { private lazy val enabledVersions: Seq[Version] = (1 to 99).collect { - case num if config.safeEndpointsEnabled(s"$num.0") => - Versions.getFrom(s"$num.0").toOption - }.flatten + case num if config.safeEndpointsEnabled(s"$num.0") => Version(s"$num.0") + } "GET /api/definition" should { "return a 200 with the correct response body" in { diff --git a/test/definition/BsasApiDefinitionFactorySpec.scala b/test/definition/BsasApiDefinitionFactorySpec.scala index 7e8efe47d..ff197553a 100644 --- a/test/definition/BsasApiDefinitionFactorySpec.scala +++ b/test/definition/BsasApiDefinitionFactorySpec.scala @@ -22,7 +22,7 @@ import shared.config.{ConfidenceLevelConfig, MockAppConfig} import shared.definition.APIStatus.BETA import shared.definition._ import shared.mocks.MockHttpClient -import shared.routing.{Version3, Version4, Version5, Version6} +import routing.Versions._ import shared.utils.UnitSpec import uk.gov.hmrc.auth.core.ConfidenceLevel diff --git a/test/shared/controllers/ControllerBaseSpec.scala b/test/shared/controllers/ControllerBaseSpec.scala index 3c8d5c28a..27f280a8e 100644 --- a/test/shared/controllers/ControllerBaseSpec.scala +++ b/test/shared/controllers/ControllerBaseSpec.scala @@ -27,7 +27,7 @@ import shared.config.MockAppConfig import shared.models.audit.{AuditError, AuditEvent, AuditResponse, GenericAuditDetail} import shared.models.domain.Nino import shared.models.errors.{BadRequestError, ErrorWrapper, MtdError} -import shared.routing.{Version, Version9} +import shared.routing.Version import shared.services.{MockAuditService, MockEnrolmentsAuthService, MockMtdIdLookupService} import shared.utils.{MockIdGenerator, UnitSpec} import uk.gov.hmrc.http.HeaderCarrier @@ -44,16 +44,16 @@ abstract class ControllerBaseSpec with ControllerSpecHateoasSupport with MockAppConfig { - protected val apiVersion: Version = Version9 + protected val apiVersion: Version = Version("9.9") - lazy val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = - FakeRequest().withHeaders(HeaderNames.ACCEPT -> s"application/vnd.hmrc.${apiVersion.name}+json") + lazy val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest().withHeaders( + HeaderNames.AUTHORIZATION -> "Bearer Token", + HeaderNames.ACCEPT -> s"application/vnd.hmrc.${apiVersion.name}+json" + ) lazy val cc: ControllerComponents = stubControllerComponents() - lazy val fakeGetRequest: FakeRequest[AnyContentAsEmpty.type] = fakeRequest.withHeaders( - HeaderNames.AUTHORIZATION -> "Bearer Token" - ) + lazy val fakeGetRequest: FakeRequest[AnyContentAsEmpty.type] = fakeRequest def fakePostRequest[T](body: T): FakeRequest[T] = fakeRequest.withBody(body) } diff --git a/test/shared/controllers/DocumentationControllerSpec.scala b/test/shared/controllers/DocumentationControllerSpec.scala index 515760d07..7cac8a932 100644 --- a/test/shared/controllers/DocumentationControllerSpec.scala +++ b/test/shared/controllers/DocumentationControllerSpec.scala @@ -24,7 +24,7 @@ import play.api.{Configuration, Environment} import shared.config.rewriters._ import shared.config.{AppConfig, MockAppConfig} import shared.definition._ -import shared.routing.{Version, Versions} +import shared.routing.Version import uk.gov.hmrc.http.HeaderCarrier import uk.gov.hmrc.play.bootstrap.config.ServicesConfig @@ -35,10 +35,7 @@ class DocumentationControllerSpec extends ControllerBaseSpec with MockAppConfig private val apiVersionName = s"$latestEnabledApiVersion.0" - override protected val apiVersion: Version = - Versions - .getFrom(apiVersionName) - .getOrElse(fail(s"Matching Version object not found for $apiVersionName")) + override protected val apiVersion: Version = Version(apiVersionName) private val apiTitle = "Business Source Adjustable Summary (MTD)" diff --git a/test/shared/controllers/RequestHandlerSpec.scala b/test/shared/controllers/RequestHandlerSpec.scala index 1256dffe2..45a3de55f 100644 --- a/test/shared/controllers/RequestHandlerSpec.scala +++ b/test/shared/controllers/RequestHandlerSpec.scala @@ -32,7 +32,7 @@ import shared.models.audit.{AuditError, AuditEvent, AuditResponse, GenericAuditD import shared.models.auth.UserDetails import shared.models.errors.{ErrorWrapper, MtdError, NinoFormatError} import shared.models.outcomes.ResponseWrapper -import shared.routing.{Version, Version3} +import shared.routing.Version import shared.services.{MockAuditService, ServiceOutcome} import shared.utils.{MockIdGenerator, UnitSpec} import uk.gov.hmrc.http.HeaderCarrier @@ -65,7 +65,8 @@ class RequestHandlerSpec implicit val endpointLogContext: EndpointLogContext = EndpointLogContext(controllerName = "SomeController", endpointName = "someEndpoint") - private val versionHeader = HeaderNames.ACCEPT -> "application/vnd.hmrc.3.0+json" + private val version = Version("9.9") + private val versionHeader = HeaderNames.ACCEPT -> s"application/vnd.hmrc.${version.name}+json" implicit val hc: HeaderCarrier = HeaderCarrier() implicit val ctx: RequestContext = RequestContext.from(mockIdGenerator, endpointLogContext) @@ -77,7 +78,6 @@ class RequestHandlerSpec UserRequest[AnyContent](userDetails, fakeRequest) } - implicit val appConfig: AppConfig = mockAppConfig private val mockService = mock[DummyService] private def service = @@ -182,7 +182,7 @@ class RequestHandlerSpec .withService(mockService.service) .withNoContentResult() - MockAppConfig.allowRequestCannotBeFulfilledHeader(Version3).returns(true).anyNumberOfTimes() + MockAppConfig.allowRequestCannotBeFulfilledHeader(version).returns(true).anyNumberOfTimes() mockDeprecation(NotDeprecated) val expectedContent = Json.parse( @@ -215,7 +215,7 @@ class RequestHandlerSpec service returns Future.successful(Right(ResponseWrapper(serviceCorrelationId, Output))) - MockAppConfig.allowRequestCannotBeFulfilledHeader(Version3).returns(false).anyNumberOfTimes() + MockAppConfig.allowRequestCannotBeFulfilledHeader(version).returns(false).anyNumberOfTimes() mockDeprecation(NotDeprecated) val ctx2: RequestContext = ctx.copy(hc = hc.copy(otherHeaders = List("gov-test-scenario" -> "REQUEST_CANNOT_BE_FULFILLED"))) @@ -336,7 +336,7 @@ class RequestHandlerSpec mockAuditService, auditType = auditType, transactionName = txName, - apiVersion = Version3, + apiVersion = version, params = params, requestBody = requestBody, includeResponse = includeResponse @@ -360,7 +360,7 @@ class RequestHandlerSpec GenericAuditDetail( userDetails, params = params, - apiVersion = Version3.name, + apiVersion = version.name, requestBody = requestBody, `X-CorrelationId` = correlationId, auditResponse = auditResponse) diff --git a/test/shared/definition/ApiDefinitionFactorySpec.scala b/test/shared/definition/ApiDefinitionFactorySpec.scala index bdb271b04..2a8d8bd75 100644 --- a/test/shared/definition/ApiDefinitionFactorySpec.scala +++ b/test/shared/definition/ApiDefinitionFactorySpec.scala @@ -21,7 +21,8 @@ import shared.config.Deprecation.NotDeprecated import shared.config.{AppConfig, ConfidenceLevelConfig, MockAppConfig} import shared.definition.APIStatus.{ALPHA, BETA} import shared.mocks.MockHttpClient -import shared.routing.{Version, Version1, Version3, Version4} +import shared.routing.Version +import routing.Versions._ import shared.utils.UnitSpec import uk.gov.hmrc.auth.core.ConfidenceLevel diff --git a/test/shared/definition/ApiDefinitionSpec.scala b/test/shared/definition/ApiDefinitionSpec.scala index 3920d457b..7cd9b29f5 100644 --- a/test/shared/definition/ApiDefinitionSpec.scala +++ b/test/shared/definition/ApiDefinitionSpec.scala @@ -16,12 +16,12 @@ package shared.definition -import shared.routing.Version3 +import shared.routing.Version import shared.utils.UnitSpec class ApiDefinitionSpec extends UnitSpec { - private val apiVersion: APIVersion = APIVersion(Version3, APIStatus.ALPHA, endpointsEnabled = true) + private val apiVersion: APIVersion = APIVersion(Version("12.3"), APIStatus.ALPHA, endpointsEnabled = true) private val apiDefinition: APIDefinition = APIDefinition("b", "c", "d", List("category"), List(apiVersion), Some(false)) "APIDefinition" when { diff --git a/test/shared/routing/VersionRoutingRequestHandlerSpec.scala b/test/shared/routing/VersionRoutingRequestHandlerSpec.scala index 664195c37..c2c5cdb1e 100644 --- a/test/shared/routing/VersionRoutingRequestHandlerSpec.scala +++ b/test/shared/routing/VersionRoutingRequestHandlerSpec.scala @@ -21,163 +21,143 @@ import org.scalatest.Inside import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.http.HeaderNames.ACCEPT import play.api.http.{HttpConfiguration, HttpErrorHandler, HttpFilters} +import play.api.libs.json.Json import play.api.mvc._ import play.api.routing.Router import play.api.test.FakeRequest import play.api.test.Helpers._ import shared.config.MockAppConfig -import shared.models.errors.{InvalidAcceptHeaderError, UnsupportedVersionError} +import shared.models.errors.{InvalidAcceptHeaderError, MtdError, UnsupportedVersionError} import shared.utils.UnitSpec class VersionRoutingRequestHandlerSpec extends UnitSpec with Inside with MockAppConfig with GuiceOneAppPerSuite { - test => implicit private val actorSystem: ActorSystem = ActorSystem("test") - - val action: DefaultActionBuilder = app.injector.instanceOf[DefaultActionBuilder] + val action: DefaultActionBuilder = app.injector.instanceOf[DefaultActionBuilder] import play.api.mvc.Handler import play.api.routing.sird._ - object DefaultHandler extends Handler - object V3Handler extends Handler - object V4Handler extends Handler + case object DefaultHandler extends Handler - private val defaultRouter = Router.from { case GET(p"") => - DefaultHandler - } + case object V1Handler extends Handler - private val v3Router = Router.from { case GET(p"/v3") => - V3Handler - } + case object V2Handler extends Handler - private val v4Router = Router.from { case GET(p"/v4") => - V4Handler - } + private val version1_enabled = Version("1.0") + private val version2_disabled = Version("2.0") private val routingMap = new VersionRoutingMap { - override val defaultRouter: Router = test.defaultRouter - override val map: Map[Version, Router] = Map(Version3 -> v3Router, Version4 -> v4Router) - } + override val defaultRouter: Router = Router.from { case GET(p"/docs") => DefaultHandler } - "Given a request that end with a trailing slash, and no version header" when { + override val map: Map[Version, Router] = Map( + version1_enabled -> Router.from { case GET(p"/v1") => V1Handler }, + version2_disabled -> Router.from { case GET(p"/v2") => V2Handler } + ) - "the handler is found" should { - "use it" in new Test { - val maybeAcceptHeader: Option[String] = None - MockAppConfig.endpointsEnabled(Version3).returns(true).anyNumberOfTimes() + } - val result: Option[Handler] = requestHandler.routeRequest(buildRequest("/")) - result shouldBe Some(DefaultHandler) - } - } + class Test { + MockAppConfig.endpointsEnabled(version1_enabled).returns(true).anyNumberOfTimes() + MockAppConfig.endpointsEnabled(version2_disabled).returns(false).anyNumberOfTimes() - "the handler isn't found" should { - "try without the trailing slash" in new Test { - val maybeAcceptHeader: Option[String] = None - MockAppConfig.endpointsEnabled(Version3).returns(true).anyNumberOfTimes() + val httpConfiguration: HttpConfiguration = HttpConfiguration("context") + private val errorHandler = mock[HttpErrorHandler] + private val filters = mock[HttpFilters] + (() => filters.filters).stubs().returns(Seq.empty) - val result: Option[Handler] = requestHandler.routeRequest(buildRequest("")) - result shouldBe Some(DefaultHandler) - } - } - } + val requestHandler: VersionRoutingRequestHandler = + new VersionRoutingRequestHandler(routingMap, errorHandler, httpConfiguration, mockAppConfig, filters, action) - "Routing request with a valid version header" should { - handleWithVersionRoutes("/v3", V3Handler, Version3) - } + def buildRequest(path: String)(implicit acceptHeader: Option[String]): RequestHeader = + acceptHeader.foldLeft(FakeRequest("GET", path)) { (req, accept) => req.withHeaders((ACCEPT, accept)) } - "Routing request with another valid version header" should { - handleWithVersionRoutes("/v4", V4Handler, Version4) } - private def handleWithVersionRoutes(path: String, handler: Handler, version: Version): Unit = { - - withClue("request ends with a trailing slash...") { - new Test { - val maybeAcceptHeader: Option[String] = Some(s"application/vnd.hmrc.$version+json") - MockAppConfig.endpointsEnabled(version).returns(true).anyNumberOfTimes() + private def returnHandler(path: String, maybeExpectedHandler: Option[Handler])(implicit acceptHeader: Option[String]): Unit = + s"return handler $maybeExpectedHandler" in new Test { + requestHandler.routeRequest(buildRequest(path)) shouldBe maybeExpectedHandler + } - val result: Option[Handler] = requestHandler.routeRequest(buildRequest(s"$path/")) - result shouldBe Some(handler) - } + private def returnHandlerIgnoringTrailingSlash(path: String, expectedHandler: Handler)(implicit acceptHeader: Option[String]): Unit = { + "matches exactly" must { + returnHandler(path, Some(expectedHandler)) } - withClue("request doesn't end with a trailing slash...") { - new Test { - val maybeAcceptHeader: Option[String] = Some(s"application/vnd.hmrc.$version+json") - MockAppConfig.endpointsEnabled(version).returns(true).anyNumberOfTimes() - val result: Option[Handler] = requestHandler.routeRequest(buildRequest(s"$path")) - result shouldBe Some(handler) - } + "matches except for a trailing slash" must { + returnHandler(s"$path/", Some(expectedHandler)) } } - "Routing requests to non-default router with no version" should { - - "return 406" in new Test { - val maybeAcceptHeader: Option[String] = None + private def returnHandlerThatRespondsWithError(path: String = "/ignored", mtdError: MtdError)(implicit acceptHeader: Option[String]): Unit = + s"return a handler that responds with $mtdError" in new Test { + val request: RequestHeader = buildRequest(path) - private val request = buildRequest("/v1") + inside(requestHandler.routeRequest(request)) { case Some(action: EssentialAction) => + val result = action.apply(request) - inside(requestHandler.routeRequest(request)) { case Some(a: EssentialAction) => - val result = a.apply(request) - - status(result) shouldBe NOT_ACCEPTABLE - contentAsJson(result) shouldBe InvalidAcceptHeaderError.asJson + status(result) shouldBe mtdError.httpStatus + contentAsJson(result) shouldBe Json.toJson(mtdError) } - } - } - - "Routing requests with unsupported version" should { - "return 404" in new Test { - val maybeAcceptHeader: Option[String] = Some("application/vnd.hmrc.5.0+json") + } - private val request = buildRequest("/v1") + "Routing requests" when { + "no version is in the accept header" when { + implicit val acceptHeader: Option[String] = None - inside(requestHandler.routeRequest(request)) { case Some(a: EssentialAction) => - val result = a.apply(request) + "the path matches a handler in the documentation (default) router" must { + returnHandlerIgnoringTrailingSlash("/docs", DefaultHandler) + } - status(result) shouldBe NOT_FOUND - contentAsJson(result) shouldBe UnsupportedVersionError.asJson + "the path does not match a handler in the documentation (default) router" must { + "expect a versioned accept header and return a handler that responds with 406 (InvalidAcceptHeaderError)" must { + returnHandlerThatRespondsWithError(path = "/unmatched", mtdError = InvalidAcceptHeaderError) + } } } - } - "Routing requests with retired v2 version" when { + "a well-formed and enabled version string is in the accept header" when { + implicit val acceptHeader: Option[String] = Some("application/vnd.hmrc.1.0+json") - "return 404 Not Found" in new Test { - val maybeAcceptHeader: Option[String] = Some("application/vnd.hmrc.5.0+json") + "the path matches a handler in the documentation (default) router" must { + returnHandlerIgnoringTrailingSlash("/docs", DefaultHandler) + } - private val request = buildRequest("/v2") - inside(requestHandler.routeRequest(request)) { case Some(a: EssentialAction) => - val result = a.apply(request) - status(result) shouldBe NOT_FOUND - contentAsJson(result) shouldBe UnsupportedVersionError.asJson + "the path does not match a handler in the documentation (default) router" when { + "the path matches a handler for the versioned API router" must { + returnHandlerIgnoringTrailingSlash("/v1", V1Handler) + } + + "the path does not match a handler for the versioned API router" must { + returnHandler("/other", None) + } } } - } - - private abstract class Test { - protected def maybeAcceptHeader: Option[String] + "a well-formed but unknown version string is in the accept header" must { + implicit val acceptHeader: Option[String] = Some("application/vnd.hmrc.5.0+json") - private val httpConfiguration = HttpConfiguration("context") - private val errorHandler = mock[HttpErrorHandler] - private val filters = mock[HttpFilters] + "return a handler that responds with 404 (UnsupportedVersionError)" must { + returnHandlerThatRespondsWithError(mtdError = UnsupportedVersionError) + } + } - (() => filters.filters).stubs().returns(Nil) + "a well-formed but disabled version string is in the accept header" must { + implicit val acceptHeader: Option[String] = Some("application/vnd.hmrc.2.0+json") - protected val requestHandler: VersionRoutingRequestHandler = - new VersionRoutingRequestHandler(routingMap, errorHandler, httpConfiguration, mockAppConfig, filters, action) + "return a handler that responds with 404 (UnsupportedVersionError)" must { + returnHandlerThatRespondsWithError(mtdError = UnsupportedVersionError) + } + } - protected def buildRequest(path: String): RequestHeader = - maybeAcceptHeader - .foldLeft(FakeRequest("GET", path)) { (req, accept) => - req.withHeaders((ACCEPT, accept)) - } + "a malformed version string is in the accept header" must { + implicit val acceptHeader: Option[String] = Some("application/vnd.hmrc.XXXXX+json") + "return a handler that responds with 406 (InvalidAcceptHeaderError)" must { + returnHandlerThatRespondsWithError(mtdError = InvalidAcceptHeaderError) + } + } } } diff --git a/test/shared/routing/VersionSpec.scala b/test/shared/routing/VersionSpec.scala index 060603de2..f9138ccf1 100644 --- a/test/shared/routing/VersionSpec.scala +++ b/test/shared/routing/VersionSpec.scala @@ -19,68 +19,43 @@ package shared.routing import play.api.http.HeaderNames.ACCEPT import play.api.libs.json._ import play.api.test.FakeRequest -import shared.routing.Version.VersionReads import shared.utils.UnitSpec class VersionSpec extends UnitSpec { "serialized to Json" must { + "return the expected Json output" in { - val version: Version = Version3 - val expected = Json.parse(""" "3.0" """) - val result = Json.toJson(version) - result shouldBe expected + Json.toJson(Version("1.2")) shouldBe JsString("1.2") } } - "Versions" when { - "retrieved from a request header" should { - "return Version for valid header" in { - Versions.getFromRequest(FakeRequest().withHeaders((ACCEPT, "application/vnd.hmrc.9.0+json"))) shouldBe Right(Version9) - } - - "return InvalidHeader when the version header is missing" in { - Versions.getFromRequest(FakeRequest().withHeaders()) shouldBe Left(InvalidHeader) - } - - "return VersionNotFound for unrecognised version" in { - Versions.getFromRequest(FakeRequest().withHeaders((ACCEPT, "application/vnd.hmrc.0.0+json"))) shouldBe Left(VersionNotFound) - } - - "return InvalidHeader for a header format that doesn't match regex" in { - Versions.getFromRequest(FakeRequest().withHeaders((ACCEPT, "invalidHeaderFormat"))) shouldBe Left(InvalidHeader) + "Version" when { + "retrieved from a request header" must { + "return the specified version" in { + Version.getFromRequest(FakeRequest().withHeaders((ACCEPT, "application/vnd.hmrc.1.2+json"))) shouldBe Right(Version("1.2")) } } - } - - "VersionReads" should { - "successfully read Version3" in { - val versionJson: JsValue = JsString(Version3.name) - val result: JsResult[Version] = VersionReads.reads(versionJson) - result shouldEqual JsSuccess(Version3) + "return InvalidHeader when the version header is missing" in { + Version.getFromRequest(FakeRequest().withHeaders()) shouldBe Left(InvalidHeader) } - "successfully read Version4" in { - val versionJson: JsValue = JsString(Version4.name) - val result: JsResult[Version] = VersionReads.reads(versionJson) - - result shouldEqual JsSuccess(Version4) + "return an error if the Accept header value is invalid" in { + Version.getFromRequest(FakeRequest().withHeaders((ACCEPT, "application/XYZ.2.0+json"))) shouldBe Left(InvalidHeader) } + } - "return error for unrecognised version" in { - val versionJson: JsValue = JsString("UnknownVersion") - val result: JsResult[Version] = VersionReads.reads(versionJson) - - result shouldBe a[JsError] + "VersionReads" should { + "successfully read Version" in { + JsString("1.2").as[Version] shouldBe Version("1.2") } } "toString" should { "return the version name" in { - val result = Version3.toString - result shouldBe Version3.name + Version("1.2").toString shouldBe "1.2" } } -} +} \ No newline at end of file