feature/(v5.0.0): add single bank endpoint to http4s routes

Implement GET /banks/{BANK_ID} endpoint in the http4s v5.0.0 routes, returning bank details in JSON format. This provides parity with the existing Lift-based API and enables direct http4s client access to individual bank resources.

Add corresponding test scenarios to verify the endpoint functionality and ensure response consistency between http4s and Lift implementations.
This commit is contained in:
hongwei 2026-01-28 14:55:57 +01:00
parent d463b71c00
commit cd34ffde55
3 changed files with 82 additions and 4 deletions

View File

@ -11,8 +11,10 @@ import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.http4s.Http4sRequestAttributes.EndpointHelpers
import code.api.util.{CustomJsonFormats, NewStyle}
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.util.{ApiVersion, ApiVersionStatus, ScannedApiVersion}
import net.liftweb.json.JsonAST.prettyRender
import net.liftweb.json.{Extraction, Formats}
@ -101,10 +103,43 @@ object Http4s500 {
}
}
resourceDocs += ResourceDoc(
null,
implementedInApiVersion,
nameOf(getBank),
"GET",
"/banks/BANK_ID",
"Get Bank",
"""Get the bank specified by BANK_ID
|Returns information about a single bank specified by BANK_ID including:
|
|* Bank code and full name of bank
|* Logo URL
|* Website""",
EmptyBody,
bankJson500,
List(
UnknownError,
BankNotFound
),
apiTagBank :: apiTagPSD2AIS :: apiTagPsd2 :: Nil,
http4sPartialFunction = Some(getBank)
)
val getBank: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "banks" / bankId =>
EndpointHelpers.withBank(req) { (bank, cc) =>
for {
(attributes, callContext) <- NewStyle.function.getBankAttributesByBank(BankId(bankId), Some(cc))
} yield JSONFactory500.createBankJSON500(bank, attributes)
}
}
val allRoutes: HttpRoutes[IO] =
Kleisli[HttpF, Request[IO], Response[IO]] { req: Request[IO] =>
root(req)
.orElse(getBanks(req))
.orElse(getBank(req))
}
val allRoutesWithMiddleware: HttpRoutes[IO] =
@ -113,4 +148,3 @@ object Http4s500 {
val wrappedRoutesV500Services: HttpRoutes[IO] = Implementations5_0_0.allRoutesWithMiddleware
}

View File

@ -2,6 +2,7 @@ package code.api.v5_0_0
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import code.api.util.APIUtil
import code.setup.ServerSetupWithTestData
import net.liftweb.json.JValue
import net.liftweb.json.JsonAST.{JArray, JField, JObject}
@ -70,5 +71,26 @@ class Http4s500RoutesTest extends ServerSetupWithTestData {
}
}
}
}
feature("Http4s500 bank endpoint") {
scenario("Return single bank JSON", Http4s500RoutesTag) {
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString(s"/obp/v5.0.0/banks/${APIUtil.defaultBankId}")
)
val (status, json) = runAndParseJson(request)
status shouldBe Status.Ok
json match {
case JObject(fields) =>
val keys = fields.map(_.name)
keys should contain("id")
keys should contain("bank_code")
case _ =>
fail("Expected JSON object for get bank endpoint")
}
}
}
}

View File

@ -2,8 +2,9 @@ package code.api.v5_0_0
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import code.api.util.APIUtil
import net.liftweb.json.JValue
import net.liftweb.json.JsonAST.{JArray, JField, JObject}
import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString}
import net.liftweb.json.JsonParser.parse
import org.http4s.{Method, Request, Status, Uri}
import org.scalatest.Tag
@ -27,6 +28,17 @@ class V500ContractParityTest extends V500ServerSetup {
fields.map(field => field.name -> field.value).toMap
}
private def getStringField(json: JValue, key: String): Option[String] = {
json match {
case JObject(fields) =>
toFieldMap(fields).get(key) match {
case Some(JString(v)) => Some(v)
case _ => None
}
case _ => None
}
}
feature("V500 Lift vs http4s parity") {
scenario("root returns consistent status and key fields", V500ContractParityTag) {
@ -84,6 +96,16 @@ class V500ContractParityTest extends V500ServerSetup {
fail("Expected http4s JSON object for banks endpoint")
}
}
scenario("bank returns consistent status and bank id", V500ContractParityTag) {
val bankId = APIUtil.defaultBankId
val liftResponse = makeGetRequest((v5_0_0_Request / "banks" / bankId).GET)
val (http4sStatus, http4sJson) = http4sRunAndParseJson(s"/obp/v5.0.0/banks/$bankId")
liftResponse.code should equal(http4sStatus.code)
getStringField(liftResponse.body, "id") shouldBe Some(bankId)
getStringField(http4sJson, "id") shouldBe Some(bankId)
}
}
}