diff --git a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala index 0a64fbded..ee290d10d 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/Http4s500.scala @@ -5,6 +5,7 @@ import cats.effect._ import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil.{EmptyBody, ResourceDoc} +import code.api.util.APIUtil import code.api.util.ApiTag._ import code.api.util.ErrorMessages._ import code.api.util.http4s.ResourceDocMiddleware @@ -19,6 +20,8 @@ import com.openbankproject.commons.model.BankId import com.openbankproject.commons.model.ProductCode import com.openbankproject.commons.dto.GetProductsParam import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion} +import dispatch.{Http => DispatchHttp, as => DispatchAs, url => DispatchUrl} +import java.nio.charset.StandardCharsets import net.liftweb.json.JsonAST.prettyRender import net.liftweb.json.{Extraction, Formats} import org.http4s._ @@ -54,6 +57,8 @@ object Http4s500 { object Implementations5_0_0 { val prefixPath = Root / ApiPathZero.toString / implementedInApiVersion.toString + private val prefixPathString = s"/${ApiPathZero.toString}/${implementedInApiVersion.toString}" + private val liftProxyBaseUrl = APIUtil.getPropsValue("http4s.lift_proxy_base_url", "http://localhost:8080") resourceDocs += ResourceDoc( null, @@ -219,6 +224,42 @@ object Http4s500 { } } + private def proxyToLift(req: Request[IO]): IO[Response[IO]] = { + val targetUrl = liftProxyBaseUrl.stripSuffix("/") + req.uri.renderString + val filteredHeaders = req.headers.headers + .filterNot(h => { + val name = h.name.toString.toLowerCase + name == "host" || name == "content-length" || name == "transfer-encoding" + }) + .map(h => h.name.toString -> h.value) + .toMap + + for { + body <- req.bodyText.compile.string + dispatchReq = ( + DispatchUrl(targetUrl) + .setMethod(req.method.name) + .setBodyEncoding(StandardCharsets.UTF_8) + .setBody(body) + <:< filteredHeaders + ) + liftResp <- IO.fromFuture(IO(DispatchHttp.default(dispatchReq > DispatchAs.Response(p => p)))) + status = org.http4s.Status.fromInt(liftResp.getStatusCode).getOrElse(org.http4s.Status.InternalServerError) + responseBody = liftResp.getResponseBody + correlationHeader = Option(liftResp.getHeader("Correlation-Id")).filter(_.nonEmpty) + base = Response[IO](status).withEntity(responseBody) + withCorrelation = correlationHeader match { + case Some(value) => base.putHeaders(Header.Raw(org.typelevel.ci.CIString("Correlation-Id"), value)) + case None => base + } + } yield withCorrelation + } + + val proxy: HttpRoutes[IO] = HttpRoutes.of[IO] { + case req if req.uri.path.renderString.startsWith(prefixPathString) => + proxyToLift(req) + } + val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] => root(req) @@ -226,6 +267,7 @@ object Http4s500 { .orElse(getBank(req)) .orElse(getProducts(req)) .orElse(getProduct(req)) + .orElse(proxy(req)) } val allRoutesWithMiddleware: HttpRoutes[IO] = diff --git a/obp-api/src/test/scala/code/api/v5_0_0/V500ContractParityTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/V500ContractParityTest.scala index 65b39a043..fe663e12c 100644 --- a/obp-api/src/test/scala/code/api/v5_0_0/V500ContractParityTest.scala +++ b/obp-api/src/test/scala/code/api/v5_0_0/V500ContractParityTest.scala @@ -3,10 +3,13 @@ package code.api.v5_0_0 import cats.effect.IO import cats.effect.unsafe.implicits.global import code.api.util.APIUtil +import code.api.util.APIUtil.OAuth._ import net.liftweb.json.JValue import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString} import net.liftweb.json.JsonParser.parse import org.http4s.{Method, Request, Status, Uri} +import org.http4s.Header +import org.typelevel.ci.CIString import org.scalatest.Tag class V500ContractParityTest extends V500ServerSetup { @@ -146,17 +149,49 @@ class V500ContractParityTest extends V500ServerSetup { liftResponse.body match { case JObject(fields) => - toFieldMap(fields).get("message") should not be empty + toFieldMap(fields).get("message").isDefined shouldBe true case _ => fail("Expected Lift JSON object for missing product error") } http4sJson match { case JObject(fields) => - toFieldMap(fields).get("message") should not be empty + toFieldMap(fields).get("message").isDefined shouldBe true case _ => fail("Expected http4s JSON object for missing product error") } } + + scenario("private accounts endpoint is served (proxy parity)", V500ContractParityTag) { + val bankId = APIUtil.defaultBankId + val liftResponse = getPrivateAccounts(bankId, user1) + val liftReq = (v5_0_0_Request / "banks" / bankId / "accounts" / "private").GET <@(user1) + val reqData = extractParamsAndHeaders(liftReq, "", "") + + val baseRequest = Request[IO]( + method = Method.GET, + uri = Uri.unsafeFromString(s"/obp/v5.0.0/banks/$bankId/accounts/private") + ) + val request = reqData.headers.foldLeft(baseRequest) { case (r, (k, v)) => + r.putHeaders(Header.Raw(CIString(k), v)) + } + + val response = Http4s500.wrappedRoutesV500Services.orNotFound.run(request).unsafeRunSync() + val http4sStatus = response.status + val body = response.as[String].unsafeRunSync() + val http4sJson = if (body.trim.isEmpty) JObject(Nil) else parse(body) + + liftResponse.code should equal(http4sStatus.code) + + http4sJson match { + case JObject(fields) => + toFieldMap(fields).get("accounts") match { + case Some(JArray(_)) => succeed + case _ => fail("Expected accounts field to be an array") + } + case _ => + fail("Expected http4s JSON object for private accounts endpoint") + } + } } }