mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:27:05 +00:00
feature/(v5.0.0): add products endpoints to http4s routes
Implement GET /banks/{bankId}/products and GET /banks/{bankId}/products/{productCode} endpoints in the http4s v5.0.0 routes. Enhance error response converter to parse APIFailureNewStyle from exception messages. Add corresponding unit and contract parity tests.
This commit is contained in:
parent
cd34ffde55
commit
7c2e788d84
@ -5,6 +5,7 @@ import code.api.APIFailureNewStyle
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.CallContext
|
||||
import net.liftweb.common.{Failure => LiftFailure}
|
||||
import net.liftweb.json.JsonParser.parse
|
||||
import net.liftweb.json.compactRender
|
||||
import net.liftweb.json.JsonDSL._
|
||||
import org.http4s._
|
||||
@ -30,6 +31,19 @@ object ErrorResponseConverter {
|
||||
|
||||
implicit val formats: Formats = CustomJsonFormats.formats
|
||||
private val jsonContentType: `Content-Type` = `Content-Type`(MediaType.application.json)
|
||||
|
||||
private def tryExtractApiFailureFromExceptionMessage(error: Throwable): Option[APIFailureNewStyle] = {
|
||||
val msg = Option(error.getMessage).getOrElse("").trim
|
||||
if (msg.startsWith("{") && msg.contains("\"failCode\"") && msg.contains("\"failMsg\"")) {
|
||||
try {
|
||||
Some(parse(msg).extract[APIFailureNewStyle])
|
||||
} catch {
|
||||
case _: Throwable => None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OBP standard error response format.
|
||||
@ -53,7 +67,11 @@ object ErrorResponseConverter {
|
||||
def toHttp4sResponse(error: Throwable, callContext: CallContext): IO[Response[IO]] = {
|
||||
error match {
|
||||
case e: APIFailureNewStyle => apiFailureToResponse(e, callContext)
|
||||
case _ => unknownErrorToResponse(error, callContext)
|
||||
case _ =>
|
||||
tryExtractApiFailureFromExceptionMessage(error) match {
|
||||
case Some(apiFailure) => apiFailureToResponse(apiFailure, callContext)
|
||||
case None => unknownErrorToResponse(error, callContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,13 +8,16 @@ import code.api.util.APIUtil.{EmptyBody, ResourceDoc}
|
||||
import code.api.util.ApiTag._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.util.http4s.ResourceDocMiddleware
|
||||
import code.api.util.http4s.Http4sRequestAttributes.EndpointHelpers
|
||||
import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
|
||||
import code.api.util.http4s.ErrorResponseConverter
|
||||
import code.api.util.{CustomJsonFormats, NewStyle}
|
||||
import code.api.util.APIUtil.getProductsIsPublic
|
||||
import code.api.v4_0_0.JSONFactory400
|
||||
import code.api.v5_0_0.JSONFactory500
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
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 net.liftweb.json.JsonAST.prettyRender
|
||||
import net.liftweb.json.{Extraction, Formats}
|
||||
@ -31,6 +34,19 @@ object Http4s500 {
|
||||
implicit val formats: Formats = CustomJsonFormats.formats
|
||||
implicit def convertAnyToJsonString(any: Any): String = prettyRender(Extraction.decompose(any))
|
||||
|
||||
private def okJson[A](a: A): IO[Response[IO]] = {
|
||||
val jsonString = prettyRender(Extraction.decompose(a))
|
||||
Ok(jsonString)
|
||||
}
|
||||
|
||||
private def executeFuture[A](req: Request[IO])(f: => scala.concurrent.Future[A]): IO[Response[IO]] = {
|
||||
implicit val cc: code.api.util.CallContext = req.callContext
|
||||
IO.fromFuture(IO(f)).attempt.flatMap {
|
||||
case Right(result) => okJson(result)
|
||||
case Left(err) => ErrorResponseConverter.toHttp4sResponse(err, cc)
|
||||
}
|
||||
}
|
||||
|
||||
val implementedInApiVersion: ScannedApiVersion = ApiVersion.v5_0_0
|
||||
val versionStatus: String = ApiVersionStatus.STABLE.toString
|
||||
val resourceDocs: ArrayBuffer[ResourceDoc] = ArrayBuffer[ResourceDoc]()
|
||||
@ -135,11 +151,81 @@ object Http4s500 {
|
||||
}
|
||||
}
|
||||
|
||||
private val productsAuthErrorBodies =
|
||||
if (getProductsIsPublic) List(BankNotFound, UnknownError)
|
||||
else List(AuthenticatedUserIsRequired, BankNotFound, UnknownError)
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
nameOf(getProducts),
|
||||
"GET",
|
||||
"/banks/BANK_ID/products",
|
||||
"Get Products",
|
||||
s"""Get products offered by the bank specified by BANK_ID.
|
||||
|
|
||||
|Can filter with attributes name and values.
|
||||
|URL params example: /banks/some-bank-id/products?&limit=50&offset=1
|
||||
|
|
||||
|${code.api.util.APIUtil.userAuthenticationMessage(!getProductsIsPublic)}""".stripMargin,
|
||||
EmptyBody,
|
||||
productsJsonV400,
|
||||
productsAuthErrorBodies,
|
||||
List(apiTagProduct),
|
||||
http4sPartialFunction = Some(getProducts)
|
||||
)
|
||||
|
||||
val getProducts: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "banks" / bankId / "products" =>
|
||||
executeFuture(req) {
|
||||
val cc = req.callContext
|
||||
val params = req.uri.query.multiParams.toList.map { case (k, vs) =>
|
||||
GetProductsParam(k, vs.toList)
|
||||
}
|
||||
for {
|
||||
(products, callContext) <- NewStyle.function.getProducts(BankId(bankId), params, Some(cc))
|
||||
} yield JSONFactory400.createProductsJson(products)
|
||||
}
|
||||
}
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
null,
|
||||
implementedInApiVersion,
|
||||
nameOf(getProduct),
|
||||
"GET",
|
||||
"/banks/BANK_ID/products/PRODUCT_CODE",
|
||||
"Get Bank Product",
|
||||
s"""Returns information about a financial Product offered by the bank specified by BANK_ID and PRODUCT_CODE.
|
||||
|
|
||||
|${code.api.util.APIUtil.userAuthenticationMessage(!getProductsIsPublic)}""".stripMargin,
|
||||
EmptyBody,
|
||||
productJsonV400,
|
||||
productsAuthErrorBodies ::: List(ProductNotFoundByProductCode),
|
||||
List(apiTagProduct),
|
||||
http4sPartialFunction = Some(getProduct)
|
||||
)
|
||||
|
||||
val getProduct: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||
case req @ GET -> `prefixPath` / "banks" / bankId / "products" / productCode =>
|
||||
executeFuture(req) {
|
||||
val cc = req.callContext
|
||||
val bankIdObj = BankId(bankId)
|
||||
val productCodeObj = ProductCode(productCode)
|
||||
for {
|
||||
(product, callContext) <- NewStyle.function.getProduct(bankIdObj, productCodeObj, Some(cc))
|
||||
(productAttributes, callContext) <- NewStyle.function.getProductAttributesByBankAndCode(bankIdObj, productCodeObj, callContext)
|
||||
(productFees, callContext) <- NewStyle.function.getProductFeesFromProvider(bankIdObj, productCodeObj, callContext)
|
||||
} yield JSONFactory400.createProductJson(product, productAttributes, productFees)
|
||||
}
|
||||
}
|
||||
|
||||
val allRoutes: HttpRoutes[IO] =
|
||||
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
|
||||
root(req)
|
||||
.orElse(getBanks(req))
|
||||
.orElse(getBank(req))
|
||||
.orElse(getProducts(req))
|
||||
.orElse(getProduct(req))
|
||||
}
|
||||
|
||||
val allRoutesWithMiddleware: HttpRoutes[IO] =
|
||||
|
||||
@ -93,4 +93,44 @@ class Http4s500RoutesTest extends ServerSetupWithTestData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
feature("Http4s500 products endpoints") {
|
||||
|
||||
scenario("Return products list JSON", Http4s500RoutesTag) {
|
||||
val request = Request[IO](
|
||||
method = Method.GET,
|
||||
uri = Uri.unsafeFromString(s"/obp/v5.0.0/banks/${APIUtil.defaultBankId}/products")
|
||||
)
|
||||
|
||||
val (status, json) = runAndParseJson(request)
|
||||
|
||||
status shouldBe Status.Ok
|
||||
json match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("products") match {
|
||||
case Some(JArray(_)) => succeed
|
||||
case _ => fail("Expected products field to be an array")
|
||||
}
|
||||
case _ =>
|
||||
fail("Expected JSON object for products endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
scenario("Return 404 for missing product", Http4s500RoutesTag) {
|
||||
val request = Request[IO](
|
||||
method = Method.GET,
|
||||
uri = Uri.unsafeFromString(s"/obp/v5.0.0/banks/${APIUtil.defaultBankId}/products/DOES_NOT_EXIST")
|
||||
)
|
||||
|
||||
val (status, json) = runAndParseJson(request)
|
||||
|
||||
status shouldBe Status.NotFound
|
||||
json match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("message") should not be empty
|
||||
case _ =>
|
||||
fail("Expected JSON object for error response")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,5 +107,56 @@ class V500ContractParityTest extends V500ServerSetup {
|
||||
getStringField(liftResponse.body, "id") shouldBe Some(bankId)
|
||||
getStringField(http4sJson, "id") shouldBe Some(bankId)
|
||||
}
|
||||
|
||||
scenario("products list returns consistent status and products array shape", V500ContractParityTag) {
|
||||
val bankId = APIUtil.defaultBankId
|
||||
val liftResponse = makeGetRequest((v5_0_0_Request / "banks" / bankId / "products").GET)
|
||||
val (http4sStatus, http4sJson) = http4sRunAndParseJson(s"/obp/v5.0.0/banks/$bankId/products")
|
||||
|
||||
liftResponse.code should equal(http4sStatus.code)
|
||||
|
||||
liftResponse.body match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("products") match {
|
||||
case Some(JArray(_)) => succeed
|
||||
case _ => fail("Expected Lift products field to be an array")
|
||||
}
|
||||
case _ =>
|
||||
fail("Expected Lift JSON object for products endpoint")
|
||||
}
|
||||
|
||||
http4sJson match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("products") match {
|
||||
case Some(JArray(_)) => succeed
|
||||
case _ => fail("Expected http4s products field to be an array")
|
||||
}
|
||||
case _ =>
|
||||
fail("Expected http4s JSON object for products endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
scenario("product returns consistent 404 for missing product", V500ContractParityTag) {
|
||||
val bankId = APIUtil.defaultBankId
|
||||
val productCode = "DOES_NOT_EXIST"
|
||||
val liftResponse = makeGetRequest((v5_0_0_Request / "banks" / bankId / "products" / productCode).GET)
|
||||
val (http4sStatus, http4sJson) = http4sRunAndParseJson(s"/obp/v5.0.0/banks/$bankId/products/$productCode")
|
||||
|
||||
liftResponse.code should equal(http4sStatus.code)
|
||||
|
||||
liftResponse.body match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("message") should not be empty
|
||||
case _ =>
|
||||
fail("Expected Lift JSON object for missing product error")
|
||||
}
|
||||
|
||||
http4sJson match {
|
||||
case JObject(fields) =>
|
||||
toFieldMap(fields).get("message") should not be empty
|
||||
case _ =>
|
||||
fail("Expected http4s JSON object for missing product error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user