Merge pull request #2665 from hongwei1/develop

test/added tests for V700
This commit is contained in:
Simon Redfern 2026-01-27 16:30:40 +01:00 committed by GitHub
commit a81e208cb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1458 additions and 48 deletions

View File

@ -3078,10 +3078,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
else
getCorrelationId()
val reqHeaders = if (cc.requestHeaders.nonEmpty)
cc.requestHeaders
else
S.request.openOrThrowException(attemptedToOpenAnEmptyBox).request.headers
val reqHeaders = if (cc.requestHeaders.nonEmpty)
cc.requestHeaders
else
S.request.map(_.request.headers).openOr(Nil)
val remoteIpAddress = if (cc.ipAddress.nonEmpty)
cc.ipAddress
@ -5189,4 +5189,4 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
.distinct // List pairs (bank_id, account_id)
}
}
}

View File

@ -2,13 +2,15 @@ package code.api.util.http4s
import cats.data.{EitherT, Kleisli, OptionT}
import cats.effect._
import code.api.v7_0_0.Http4s700
import code.api.APIFailureNewStyle
import code.api.util.APIUtil.ResourceDoc
import code.api.util.ErrorMessages._
import code.api.util.newstyle.ViewNewStyle
import code.api.util.{APIUtil, CallContext, NewStyle}
import code.api.util.{APIUtil, ApiRole, CallContext, NewStyle}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model._
import com.github.dwickern.macros.NameOf.nameOf
import net.liftweb.common.{Box, Empty, Full}
import org.http4s._
import org.http4s.headers.`Content-Type`
@ -70,7 +72,7 @@ object ResourceDocMiddleware extends MdcLoggable {
* - Special case: resource-docs endpoint checks resource_docs_requires_role property
*/
private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = {
if (resourceDoc.partialFunctionName == "getResourceDocsObpV700") {
if (resourceDoc.partialFunctionName == nameOf(Http4s700.Implementations7_0_0.getResourceDocsObpV700)) {
APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
} else {
resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty)
@ -172,7 +174,14 @@ object ResourceDocMiddleware extends MdcLoggable {
private def authorizeRoles(resourceDoc: ResourceDoc, pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
import DSL._
resourceDoc.roles match {
val rolesToCheck: Option[List[ApiRole]] =
if (resourceDoc.partialFunctionName == nameOf(Http4s700.Implementations7_0_0.getResourceDocsObpV700) && APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)) {
Some(List(ApiRole.canReadResourceDoc))
} else {
resourceDoc.roles
}
rolesToCheck match {
case Some(roles) if roles.nonEmpty =>
ctx.user match {
case Full(user) =>

View File

@ -6,12 +6,12 @@ import code.api.Constant._
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
import code.api.util.APIUtil.{EmptyBody, _}
import code.api.util.ApiRole.{canGetCardsForBank, canReadResourceDoc}
import code.api.util.ApiRole.canGetCardsForBank
import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
import code.api.util.http4s.Http4sRequestAttributes.{RequestOps, EndpointHelpers}
import code.api.util.{ApiRole, ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
import code.api.util.{ApiVersionUtils, CallContext, CustomJsonFormats, NewStyle}
import code.api.v1_3_0.JSONFactory1_3_0
import code.api.v1_4_0.JSONFactory1_4_0
import code.api.v4_0_0.JSONFactory400
@ -201,44 +201,26 @@ object Http4s700 {
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
implicit val cc: CallContext = req.callContext
for {
result <- IO.fromFuture(IO {
// Check resource_docs_requires_role property
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
for {
// Authentication based on property
(boxUser, cc1) <- if (resourceDocsRequireRole)
authenticatedAccess(cc)
else
anonymousAccess(cc)
// Role check based on property
_ <- if (resourceDocsRequireRole) {
NewStyle.function.hasAtLeastOneEntitlement(
failMsg = UserHasMissingRoles + canReadResourceDoc.toString
)("", boxUser.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc1)
} else {
Future.successful(())
}
httpParams <- NewStyle.function.extractHttpParamsFromUrl(req.uri.renderString)
tagsParam = httpParams.filter(_.name == "tags").map(_.values).headOption
functionsParam = httpParams.filter(_.name == "functions").map(_.values).headOption
localeParam = httpParams.filter(param => param.name == "locale" || param.name == "language").map(_.values).flatten.headOption
contentParam = httpParams.filter(_.name == "content").map(_.values).flatten.flatMap(ResourceDocsAPIMethodsUtil.stringToContentParam).headOption
apiCollectionIdParam = httpParams.filter(_.name == "api-collection-id").map(_.values).flatten.headOption
tags = tagsParam.map(_.map(ResourceDocTag(_)))
functions = functionsParam.map(_.toList)
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
} yield convertAnyToJsonString(resourceDocsJson)
})
response <- Ok(result)
} yield response
EndpointHelpers.executeAndRespond(req) { _ =>
val queryParams = req.uri.query.multiParams
val tags = queryParams
.get("tags")
.map(_.flatMap(_.split(",").toList).map(_.trim).filter(_.nonEmpty).map(ResourceDocTag(_)).toList)
val functions = queryParams
.get("functions")
.map(_.flatMap(_.split(",").toList).map(_.trim).filter(_.nonEmpty).toList)
val localeParam = queryParams
.get("locale")
.flatMap(_.headOption)
.orElse(queryParams.get("language").flatMap(_.headOption))
.map(_.trim)
.filter(_.nonEmpty)
for {
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
} yield JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
}
}

View File

@ -0,0 +1,454 @@
package code.api.util.http4s
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import net.liftweb.common.{Empty, Full}
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.headers.`Content-Type`
import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag}
/**
* Unit tests for Http4sCallContextBuilder
*
* Tests CallContext building from http4s Request[IO]:
* - URL extraction (including query parameters)
* - Header extraction and conversion to HTTPParam
* - Body extraction for POST requests
* - Correlation ID generation/extraction
* - IP address extraction (X-Forwarded-For and direct)
* - Auth header extraction for all auth types
*
*/
class Http4sCallContextBuilderTest extends FeatureSpec with Matchers with GivenWhenThen {
object Http4sCallContextBuilderTag extends Tag("Http4sCallContextBuilder")
feature("Http4sCallContextBuilder - URL extraction") {
scenario("Extract URL with path only", Http4sCallContextBuilderTag) {
Given("A request with path /obp/v7.0.0/banks")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("URL should match the request URI")
callContext.url should equal("/obp/v7.0.0/banks")
}
scenario("Extract URL with query parameters", Http4sCallContextBuilderTag) {
Given("A request with query parameters")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks?limit=10&offset=0")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("URL should include query parameters")
callContext.url should equal("/obp/v7.0.0/banks?limit=10&offset=0")
}
scenario("Extract URL with path parameters", Http4sCallContextBuilderTag) {
Given("A request with path parameters")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("URL should include path parameters")
callContext.url should equal("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
}
}
feature("Http4sCallContextBuilder - Header extraction") {
scenario("Extract headers and convert to HTTPParam", Http4sCallContextBuilderTag) {
Given("A request with multiple headers")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("Content-Type"), "application/json"),
Header.Raw(org.typelevel.ci.CIString("Accept"), "application/json"),
Header.Raw(org.typelevel.ci.CIString("X-Custom-Header"), "test-value")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Headers should be converted to HTTPParam list")
callContext.requestHeaders should not be empty
callContext.requestHeaders.exists(_.name == "Content-Type") should be(true)
callContext.requestHeaders.exists(_.name == "Accept") should be(true)
callContext.requestHeaders.exists(_.name == "X-Custom-Header") should be(true)
}
scenario("Extract empty headers list", Http4sCallContextBuilderTag) {
Given("A request with no custom headers")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Headers list should be empty or contain only default headers")
// http4s may add default headers, so we just check it's a list
callContext.requestHeaders should be(a[List[_]])
}
}
feature("Http4sCallContextBuilder - Body extraction") {
scenario("Extract body from POST request", Http4sCallContextBuilderTag) {
Given("A POST request with JSON body")
val jsonBody = """{"name": "Test Bank", "id": "test-bank-1"}"""
val request = Request[IO](
method = Method.POST,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withEntity(jsonBody)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Body should be extracted as Some(string)")
callContext.httpBody should be(Some(jsonBody))
}
scenario("Extract empty body from GET request", Http4sCallContextBuilderTag) {
Given("A GET request with no body")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Body should be None")
callContext.httpBody should be(None)
}
scenario("Extract body from PUT request", Http4sCallContextBuilderTag) {
Given("A PUT request with JSON body")
val jsonBody = """{"name": "Updated Bank"}"""
val request = Request[IO](
method = Method.PUT,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks/test-bank-1")
).withEntity(jsonBody)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Body should be extracted")
callContext.httpBody should be(Some(jsonBody))
}
}
feature("Http4sCallContextBuilder - Correlation ID") {
scenario("Extract correlation ID from X-Request-ID header", Http4sCallContextBuilderTag) {
Given("A request with X-Request-ID header")
val requestId = "test-correlation-id-12345"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("X-Request-ID"), requestId)
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Correlation ID should match the header value")
callContext.correlationId should equal(requestId)
}
scenario("Generate correlation ID when header missing", Http4sCallContextBuilderTag) {
Given("A request without X-Request-ID header")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Correlation ID should be generated (UUID format)")
callContext.correlationId should not be empty
// UUID format: 8-4-4-4-12 hex digits
callContext.correlationId should fullyMatch regex "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
}
}
feature("Http4sCallContextBuilder - IP address extraction") {
scenario("Extract IP from X-Forwarded-For header", Http4sCallContextBuilderTag) {
Given("A request with X-Forwarded-For header")
val clientIp = "192.168.1.100"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("X-Forwarded-For"), clientIp)
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("IP address should match the header value")
callContext.ipAddress should equal(clientIp)
}
scenario("Extract first IP from X-Forwarded-For with multiple IPs", Http4sCallContextBuilderTag) {
Given("A request with X-Forwarded-For containing multiple IPs")
val forwardedFor = "192.168.1.100, 10.0.0.1, 172.16.0.1"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("X-Forwarded-For"), forwardedFor)
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("IP address should be the first IP in the list")
callContext.ipAddress should equal("192.168.1.100")
}
scenario("Handle missing IP address", Http4sCallContextBuilderTag) {
Given("A request without X-Forwarded-For or remote address")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("IP address should be empty string")
callContext.ipAddress should equal("")
}
}
feature("Http4sCallContextBuilder - Authentication header extraction") {
scenario("Extract DirectLogin token from DirectLogin header (new format)", Http4sCallContextBuilderTag) {
Given("A request with DirectLogin header")
val token = "eyJhbGciOiJIUzI1NiJ9.eyIiOiIifQ.test"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("DirectLogin"), s"token=$token")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("DirectLogin params should contain token")
callContext.directLoginParams should contain key "token"
callContext.directLoginParams("token") should equal(token)
}
scenario("Extract DirectLogin token from Authorization header (old format)", Http4sCallContextBuilderTag) {
Given("A request with Authorization: DirectLogin header")
val token = "eyJhbGciOiJIUzI1NiJ9.eyIiOiIifQ.test"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("Authorization"), s"DirectLogin token=$token")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("DirectLogin params should contain token")
callContext.directLoginParams should contain key "token"
callContext.directLoginParams("token") should equal(token)
And("Authorization header should be stored")
callContext.authReqHeaderField should equal(Full(s"DirectLogin token=$token"))
}
scenario("Extract DirectLogin with username and password", Http4sCallContextBuilderTag) {
Given("A request with DirectLogin username and password")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("DirectLogin"), """username="testuser", password="testpass", consumer_key="key123"""")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("DirectLogin params should contain all parameters")
callContext.directLoginParams should contain key "username"
callContext.directLoginParams should contain key "password"
callContext.directLoginParams should contain key "consumer_key"
callContext.directLoginParams("username") should equal("testuser")
callContext.directLoginParams("password") should equal("testpass")
callContext.directLoginParams("consumer_key") should equal("key123")
}
scenario("Extract OAuth parameters from Authorization header", Http4sCallContextBuilderTag) {
Given("A request with OAuth Authorization header")
val oauthHeader = """OAuth oauth_consumer_key="consumer123", oauth_token="token456", oauth_signature="sig789""""
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("Authorization"), oauthHeader)
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("OAuth params should be extracted")
callContext.oAuthParams should contain key "oauth_consumer_key"
callContext.oAuthParams should contain key "oauth_token"
callContext.oAuthParams should contain key "oauth_signature"
callContext.oAuthParams("oauth_consumer_key") should equal("consumer123")
callContext.oAuthParams("oauth_token") should equal("token456")
callContext.oAuthParams("oauth_signature") should equal("sig789")
And("Authorization header should be stored")
callContext.authReqHeaderField should equal(Full(oauthHeader))
}
scenario("Extract Bearer token from Authorization header", Http4sCallContextBuilderTag) {
Given("A request with Bearer token")
val bearerToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.signature"
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("Authorization"), s"Bearer $bearerToken")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Authorization header should be stored")
callContext.authReqHeaderField should equal(Full(s"Bearer $bearerToken"))
}
scenario("Handle missing Authorization header", Http4sCallContextBuilderTag) {
Given("A request without Authorization header")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Auth header field should be Empty")
callContext.authReqHeaderField should equal(Empty)
And("DirectLogin params should be empty")
callContext.directLoginParams should be(empty)
And("OAuth params should be empty")
callContext.oAuthParams should be(empty)
}
}
feature("Http4sCallContextBuilder - Request metadata") {
scenario("Extract HTTP verb", Http4sCallContextBuilderTag) {
Given("A POST request")
val request = Request[IO](
method = Method.POST,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("Verb should be POST")
callContext.verb should equal("POST")
}
scenario("Set implementedInVersion from parameter", Http4sCallContextBuilderTag) {
Given("A request with API version v7.0.0")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext with version parameter")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("implementedInVersion should match the parameter")
callContext.implementedInVersion should equal("v7.0.0")
}
scenario("Set startTime to current date", Http4sCallContextBuilderTag) {
Given("A request")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Building CallContext")
val beforeTime = new java.util.Date()
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
val afterTime = new java.util.Date()
Then("startTime should be set and within reasonable range")
callContext.startTime should be(defined)
callContext.startTime.get.getTime should be >= beforeTime.getTime
callContext.startTime.get.getTime should be <= afterTime.getTime
}
}
feature("Http4sCallContextBuilder - Complete integration") {
scenario("Build complete CallContext with all fields", Http4sCallContextBuilderTag) {
Given("A complete POST request with all headers and body")
val jsonBody = """{"name": "Test Bank"}"""
val token = "test-token-123"
val correlationId = "correlation-123"
val clientIp = "192.168.1.100"
val request = Request[IO](
method = Method.POST,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks?limit=10")
).withHeaders(
Header.Raw(org.typelevel.ci.CIString("Content-Type"), "application/json"),
Header.Raw(org.typelevel.ci.CIString("DirectLogin"), s"token=$token"),
Header.Raw(org.typelevel.ci.CIString("X-Request-ID"), correlationId),
Header.Raw(org.typelevel.ci.CIString("X-Forwarded-For"), clientIp)
).withEntity(jsonBody)
When("Building CallContext")
val callContext = Http4sCallContextBuilder.fromRequest(request, "v7.0.0").unsafeRunSync()
Then("All fields should be populated correctly")
callContext.url should equal("/obp/v7.0.0/banks?limit=10")
callContext.verb should equal("POST")
callContext.implementedInVersion should equal("v7.0.0")
callContext.correlationId should equal(correlationId)
callContext.ipAddress should equal(clientIp)
callContext.httpBody should be(Some(jsonBody))
callContext.directLoginParams should contain key "token"
callContext.directLoginParams("token") should equal(token)
callContext.requestHeaders should not be empty
callContext.startTime should be(defined)
}
}
}

View File

@ -0,0 +1,545 @@
package code.api.util.http4s
import code.api.util.APIUtil.ResourceDoc
import code.api.util.ApiTag.ResourceDocTag
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.JsonAST.JObject
import org.http4s._
import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag}
import scala.collection.mutable.ArrayBuffer
/**
* Unit tests for ResourceDocMatcher
*
* Tests ResourceDoc matching and path parameter extraction:
* - Matching by verb and exact path
* - Matching with BANK_ID variable
* - Matching with BANK_ID + ACCOUNT_ID variables
* - Matching with BANK_ID + ACCOUNT_ID + VIEW_ID variables
* - Matching with COUNTERPARTY_ID variable
* - Non-matching requests return None
* - Path parameter extraction for all variable types
*
*/
class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThen {
object ResourceDocMatcherTag extends Tag("ResourceDocMatcher")
// Helper to create minimal ResourceDoc for testing
private def createResourceDoc(
verb: String,
url: String,
operationId: String = "testOperation"
): ResourceDoc = {
ResourceDoc(
partialFunction = null, // Not needed for matching tests
implementedInApiVersion = ApiVersion.v7_0_0,
partialFunctionName = operationId,
requestVerb = verb,
requestUrl = url,
summary = "Test endpoint",
description = "Test description",
exampleRequestBody = JObject(Nil),
successResponseBody = JObject(Nil),
errorResponseBodies = List.empty,
tags = List(ResourceDocTag("test")),
roles = None
)
}
feature("ResourceDocMatcher - Exact path matching") {
scenario("Match GET request with exact path", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching a GET request to /obp/v7.0.0/banks")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBanks")
}
scenario("Match POST request with exact path", ResourceDocMatcherTag) {
Given("A ResourceDoc for POST /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("POST", "/banks", "createBank")
)
When("Matching a POST request to /obp/v7.0.0/banks")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("createBank")
}
scenario("Match request with multi-segment path", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /management/metrics")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/management/metrics", "getMetrics")
)
When("Matching a GET request to /obp/v7.0.0/management/metrics")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/management/metrics")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getMetrics")
}
scenario("Verb mismatch returns None", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching a POST request to /obp/v7.0.0/banks")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs)
Then("Should return None")
result should be(None)
}
scenario("Path mismatch returns None", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching a GET request to /obp/v7.0.0/accounts")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should return None")
result should be(None)
}
}
feature("ResourceDocMatcher - BANK_ID variable matching") {
scenario("Match request with BANK_ID variable", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID", "getBank")
)
When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBank")
}
scenario("Match request with BANK_ID and additional segments", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
)
When("Matching a GET request to /obp/v7.0.0/banks/test-bank-1/accounts")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank-1/accounts")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBankAccounts")
}
scenario("Extract BANK_ID parameter value", ResourceDocMatcherTag) {
Given("A matched ResourceDoc with BANK_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should extract BANK_ID value")
params should contain key "BANK_ID"
params("BANK_ID") should equal("gh.29.de")
}
}
feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID variables") {
scenario("Match request with BANK_ID and ACCOUNT_ID variables", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount")
)
When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de/accounts/test1")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBankAccount")
}
scenario("Extract BANK_ID and ACCOUNT_ID parameter values", ResourceDocMatcherTag) {
Given("A matched ResourceDoc with BANK_ID and ACCOUNT_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount")
When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de/accounts/test1")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should extract both BANK_ID and ACCOUNT_ID values")
params should contain key "BANK_ID"
params should contain key "ACCOUNT_ID"
params("BANK_ID") should equal("gh.29.de")
params("ACCOUNT_ID") should equal("test1")
}
scenario("Match request with BANK_ID, ACCOUNT_ID and additional segments", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/transactions")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/transactions", "getTransactions")
)
When("Matching a GET request to /obp/v7.0.0/banks/test-bank/accounts/acc-123/transactions")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank/accounts/acc-123/transactions")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getTransactions")
}
}
feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID + VIEW_ID variables") {
scenario("Match request with BANK_ID, ACCOUNT_ID and VIEW_ID variables", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView")
)
When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getTransactionsForView")
}
scenario("Extract BANK_ID, ACCOUNT_ID and VIEW_ID parameter values", ResourceDocMatcherTag) {
Given("A matched ResourceDoc with BANK_ID, ACCOUNT_ID and VIEW_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView")
When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should extract all three parameter values")
params should contain key "BANK_ID"
params should contain key "ACCOUNT_ID"
params should contain key "VIEW_ID"
params("BANK_ID") should equal("gh.29.de")
params("ACCOUNT_ID") should equal("test1")
params("VIEW_ID") should equal("owner")
}
scenario("Match request with VIEW_ID in different position", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account", "getAccountForView")
)
When("Matching a GET request to /obp/v7.0.0/banks/test-bank/accounts/acc-1/public/account")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank/accounts/acc-1/public/account")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getAccountForView")
}
}
feature("ResourceDocMatcher - COUNTERPARTY_ID variable") {
scenario("Match request with COUNTERPARTY_ID variable", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty")
)
When("Matching a GET request with counterparty ID")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getCounterparty")
}
scenario("Extract COUNTERPARTY_ID parameter value", ResourceDocMatcherTag) {
Given("A matched ResourceDoc with COUNTERPARTY_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty")
When("Extracting path parameters")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should extract all parameter values including COUNTERPARTY_ID")
params should contain key "BANK_ID"
params should contain key "ACCOUNT_ID"
params should contain key "VIEW_ID"
params should contain key "COUNTERPARTY_ID"
params("BANK_ID") should equal("gh.29.de")
params("ACCOUNT_ID") should equal("test1")
params("VIEW_ID") should equal("owner")
params("COUNTERPARTY_ID") should equal("ff010868-ac7d-4f96-9fc5-70dd5757e891")
}
scenario("Match request with COUNTERPARTY_ID in different URL structure", ResourceDocMatcherTag) {
Given("A ResourceDoc for DELETE /management/counterparties/COUNTERPARTY_ID")
val resourceDocs = ArrayBuffer(
createResourceDoc("DELETE", "/management/counterparties/COUNTERPARTY_ID", "deleteCounterparty")
)
When("Matching a DELETE request")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/management/counterparties/counterparty-123")
val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("deleteCounterparty")
}
}
feature("ResourceDocMatcher - Non-matching requests") {
scenario("Return None when no ResourceDoc matches", ResourceDocMatcherTag) {
Given("ResourceDocs for specific endpoints")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks"),
createResourceDoc("GET", "/banks/BANK_ID", "getBank"),
createResourceDoc("POST", "/banks", "createBank")
)
When("Matching a request that doesn't match any ResourceDoc")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should return None")
result should be(None)
}
scenario("Return None when verb doesn't match", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching a DELETE request to /obp/v7.0.0/banks")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs)
Then("Should return None")
result should be(None)
}
scenario("Return None when path segment count doesn't match", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
)
When("Matching a request with different segment count")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should return None")
result should be(None)
}
scenario("Return None when literal segments don't match", ResourceDocMatcherTag) {
Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
)
When("Matching a request with different literal segment")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/transactions")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should return None")
result should be(None)
}
}
feature("ResourceDocMatcher - Path parameter extraction edge cases") {
scenario("Extract parameters from path with no variables", ResourceDocMatcherTag) {
Given("A ResourceDoc with no path variables")
val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
When("Extracting path parameters")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should return empty map")
params should be(empty)
}
scenario("Extract parameters with special characters in values", ResourceDocMatcherTag) {
Given("A ResourceDoc with BANK_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
When("Extracting path parameters with special characters")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de-test_bank")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should extract the full value including special characters")
params should contain key "BANK_ID"
params("BANK_ID") should equal("gh.29.de-test_bank")
}
scenario("Return empty map when path doesn't match template", ResourceDocMatcherTag) {
Given("A ResourceDoc for /banks/BANK_ID")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
When("Extracting parameters from path with different segment count")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
Then("Should return empty map due to segment count mismatch")
params should be(empty)
}
}
feature("ResourceDocMatcher - attachToCallContext") {
scenario("Attach ResourceDoc to CallContext", ResourceDocMatcherTag) {
Given("A CallContext and a matched ResourceDoc")
val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
val callContext = code.api.util.CallContext(
correlationId = "test-correlation-id"
)
When("Attaching ResourceDoc to CallContext")
val updatedContext = ResourceDocMatcher.attachToCallContext(callContext, resourceDoc)
Then("CallContext should have resourceDocument set")
updatedContext.resourceDocument should be(defined)
updatedContext.resourceDocument.get should equal(resourceDoc)
}
scenario("Attach ResourceDoc sets operationId", ResourceDocMatcherTag) {
Given("A CallContext and a matched ResourceDoc")
val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
val callContext = code.api.util.CallContext(
correlationId = "test-correlation-id"
)
When("Attaching ResourceDoc to CallContext")
val updatedContext = ResourceDocMatcher.attachToCallContext(callContext, resourceDoc)
Then("CallContext should have operationId set")
updatedContext.operationId should be(defined)
updatedContext.operationId.get should equal(resourceDoc.operationId)
}
scenario("Preserve other CallContext fields when attaching ResourceDoc", ResourceDocMatcherTag) {
Given("A CallContext with existing fields")
val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
val originalContext = code.api.util.CallContext(
correlationId = "test-correlation-id",
url = "/obp/v7.0.0/banks",
verb = "GET",
implementedInVersion = "v7.0.0"
)
When("Attaching ResourceDoc to CallContext")
val updatedContext = ResourceDocMatcher.attachToCallContext(originalContext, resourceDoc)
Then("Other fields should be preserved")
updatedContext.correlationId should equal(originalContext.correlationId)
updatedContext.url should equal(originalContext.url)
updatedContext.verb should equal(originalContext.verb)
updatedContext.implementedInVersion should equal(originalContext.implementedInVersion)
}
}
feature("ResourceDocMatcher - Multiple ResourceDocs selection") {
scenario("Select correct ResourceDoc from multiple candidates", ResourceDocMatcherTag) {
Given("Multiple ResourceDocs with different paths")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks"),
createResourceDoc("GET", "/banks/BANK_ID", "getBank"),
createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts"),
createResourceDoc("POST", "/banks", "createBank")
)
When("Matching a specific request")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should select the most specific matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBankAccounts")
}
scenario("Match first ResourceDoc when multiple exact matches exist", ResourceDocMatcherTag) {
Given("Multiple ResourceDocs with same path and verb")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks1"),
createResourceDoc("GET", "/banks", "getBanks2")
)
When("Matching a request")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should return the first matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBanks1")
}
}
feature("ResourceDocMatcher - Case sensitivity") {
scenario("HTTP verb matching is case-insensitive", ResourceDocMatcherTag) {
Given("A ResourceDoc with uppercase GET")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching with lowercase get")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
val result = ResourceDocMatcher.findResourceDoc("get", path, resourceDocs)
Then("Should find the matching ResourceDoc")
result should be(defined)
result.get.partialFunctionName should equal("getBanks")
}
scenario("Path matching is case-sensitive for literal segments", ResourceDocMatcherTag) {
Given("A ResourceDoc for /banks")
val resourceDocs = ArrayBuffer(
createResourceDoc("GET", "/banks", "getBanks")
)
When("Matching with different case /Banks")
val path = Uri.Path.unsafeFromString("/obp/v7.0.0/Banks")
val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
Then("Should not match (case-sensitive)")
result should be(None)
}
}
}

View File

@ -0,0 +1,420 @@
package code.api.v7_0_0
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import code.api.util.ApiRole.{canGetCardsForBank, canReadResourceDoc}
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, BankNotFound, UserHasMissingRoles}
import code.setup.ServerSetupWithTestData
import net.liftweb.json.JValue
import net.liftweb.json.JsonAST.{JArray, JField, JObject, JString}
import net.liftweb.json.JsonParser.parse
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.scalatest.Tag
class Http4s700RoutesTest extends ServerSetupWithTestData {
object Http4s700RoutesTag extends Tag("Http4s700Routes")
private def runAndParseJson(request: Request[IO]): (Status, JValue) = {
val response = Http4s700.wrappedRoutesV700Services.orNotFound.run(request).unsafeRunSync()
val body = response.as[String].unsafeRunSync()
val json = if (body.trim.isEmpty) JObject(Nil) else parse(body)
(response.status, json)
}
private def withDirectLoginToken(request: Request[IO], token: String): Request[IO] = {
request.withHeaders(
Header.Raw(org.typelevel.ci.CIString("DirectLogin"), s"token=$token")
)
}
private def toFieldMap(fields: List[JField]): Map[String, JValue] = {
fields.map(field => field.name -> field.value).toMap
}
feature("Http4s700 root endpoint") {
scenario("Return API info JSON", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/root request")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/root")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with API info fields")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
val keys = fields.map(_.name)
keys should contain("version")
keys should contain("version_status")
keys should contain("git_commit")
keys should contain("connector")
case _ =>
fail("Expected JSON object for root endpoint")
}
}
}
feature("Http4s700 banks endpoint") {
scenario("Return banks list JSON", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/banks request")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/banks")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with banks array")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
val valueOpt = toFieldMap(fields).get("banks")
valueOpt should not be empty
valueOpt.get match {
case JArray(_) =>
succeed
case _ =>
fail("Expected banks field to be an array")
}
case _ =>
fail("Expected JSON object for banks endpoint")
}
}
}
feature("Http4s700 cards endpoint") {
scenario("Reject unauthenticated access to cards", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/cards request without auth headers")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/cards")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 401 Unauthorized with appropriate error message")
status.code shouldBe 401
json match {
case JObject(fields) =>
toFieldMap(fields).get("message") match {
case Some(JString(message)) =>
message should include(AuthenticatedUserIsRequired)
case _ =>
fail("Expected message field as JSON string for cards unauthorized response")
}
case _ =>
fail("Expected JSON object for cards unauthorized response")
}
}
scenario("Return cards list JSON when authenticated", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/cards request with DirectLogin header")
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/cards")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with cards array")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("cards") match {
case Some(JArray(_)) => succeed
case _ => fail("Expected cards field to be an array")
}
case _ => fail("Expected JSON object for cards endpoint")
}
}
}
feature("Http4s700 bank cards endpoint") {
scenario("Return bank cards list JSON when authenticated and entitled", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/banks/BANK_ID/cards request with DirectLogin header and role")
val bankId = testBankId1.value
addEntitlement(bankId, resourceUser1.userId, canGetCardsForBank.toString)
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString(s"/obp/v7.0.0/banks/$bankId/cards?limit=10&offset=0")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with cards array")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("cards") match {
case Some(JArray(_)) => succeed
case _ => fail("Expected cards field to be an array")
}
case _ => fail("Expected JSON object for bank cards endpoint")
}
}
scenario("Reject bank cards access when missing required role", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/banks/BANK_ID/cards request with DirectLogin header but no role")
val bankId = testBankId1.value
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString(s"/obp/v7.0.0/banks/$bankId/cards")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 403 Forbidden")
status.code shouldBe 403
json match {
case JObject(fields) =>
toFieldMap(fields).get("message") match {
case Some(JString(message)) =>
message should include(UserHasMissingRoles)
message should include(canGetCardsForBank.toString)
case _ =>
fail("Expected message field as JSON string for missing-role response")
}
case _ =>
fail("Expected JSON object for missing-role response")
}
}
scenario("Return BankNotFound when bank does not exist and user is entitled", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/banks/BANK_ID/cards request for non-existing bank")
val bankId = "non-existing-bank-id"
addEntitlement(bankId, resourceUser1.userId, canGetCardsForBank.toString)
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString(s"/obp/v7.0.0/banks/$bankId/cards")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 404 Not Found with BankNotFound message")
status.code shouldBe 404
json match {
case JObject(fields) =>
toFieldMap(fields).get("message") match {
case Some(JString(message)) =>
message should include(BankNotFound)
case _ =>
fail("Expected message field as JSON string for BankNotFound response")
}
case _ =>
fail("Expected JSON object for BankNotFound response")
}
}
}
feature("Http4s700 resource-docs endpoint") {
scenario("Allow public access when resource docs role is not required", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp request without auth headers")
setPropsValues("resource_docs_requires_role" -> "false")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with resource_docs array")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("resource_docs") match {
case Some(JArray(_)) =>
succeed
case _ =>
fail("Expected resource_docs field to be an array")
}
case _ =>
fail("Expected JSON object for resource-docs endpoint")
}
}
scenario("Reject unauthenticated access when resource docs role is required", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp request without auth headers and role required")
setPropsValues("resource_docs_requires_role" -> "true")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 401 Unauthorized")
status.code shouldBe 401
json match {
case JObject(fields) =>
toFieldMap(fields).get("message") match {
case Some(JString(message)) =>
message should include(AuthenticatedUserIsRequired)
case _ =>
fail("Expected message field as JSON string for resource-docs unauthorized response")
}
case _ =>
fail("Expected JSON object for resource-docs unauthorized response")
}
}
scenario("Reject access when authenticated but missing canReadResourceDoc role", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp request with auth but no canReadResourceDoc role")
setPropsValues("resource_docs_requires_role" -> "true")
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 403 Forbidden")
status.code shouldBe 403
json match {
case JObject(fields) =>
toFieldMap(fields).get("message") match {
case Some(JString(message)) =>
message should include(UserHasMissingRoles)
message should include(canReadResourceDoc.toString)
case _ =>
fail("Expected message field as JSON string for missing-role response")
}
case _ =>
fail("Expected JSON object for missing-role response")
}
}
scenario("Return docs when authenticated and entitled with canReadResourceDoc", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp request with auth and canReadResourceDoc role")
setPropsValues("resource_docs_requires_role" -> "true")
addEntitlement("", resourceUser1.userId, canReadResourceDoc.toString)
val baseRequest = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp")
)
val request = withDirectLoginToken(baseRequest, token1.value)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK with resource_docs array")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("resource_docs") match {
case Some(JArray(_)) =>
succeed
case _ =>
fail("Expected resource_docs field to be an array")
}
case _ =>
fail("Expected JSON object for resource-docs endpoint")
}
}
scenario("Filter docs by tags parameter", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp?tags=Card request")
setPropsValues("resource_docs_requires_role" -> "false")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp?tags=Card")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK and all returned docs contain Card tag")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("resource_docs") match {
case Some(JArray(resourceDocs)) =>
resourceDocs.foreach {
case JObject(rdFields) =>
toFieldMap(rdFields).get("tags") match {
case Some(JArray(tags)) =>
tags.exists {
case JString(tag) => tag == "Card"
case _ => false
} shouldBe true
case _ =>
fail("Expected tags field to be an array")
}
case _ =>
fail("Expected resource doc to be a JSON object")
}
case _ =>
fail("Expected resource_docs field to be an array")
}
case _ =>
fail("Expected JSON object for resource-docs endpoint")
}
}
scenario("Filter docs by functions parameter", Http4s700RoutesTag) {
Given("GET /obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getBanks request")
setPropsValues("resource_docs_requires_role" -> "false")
val request = Request[IO](
method = Method.GET,
uri = Uri.unsafeFromString("/obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getBanks")
)
When("Running through wrapped routes")
val (status, json) = runAndParseJson(request)
Then("Response is 200 OK and includes GET /banks")
status shouldBe Status.Ok
json match {
case JObject(fields) =>
toFieldMap(fields).get("resource_docs") match {
case Some(JArray(resourceDocs)) =>
resourceDocs.foreach {
case JObject(rdFields) =>
val fieldMap = toFieldMap(rdFields)
(fieldMap.get("request_verb"), fieldMap.get("request_url")) match {
case (Some(JString(verb)), Some(JString(url))) =>
verb shouldBe "GET"
url should endWith("/banks")
case _ =>
fail("Expected request_verb and request_url fields as JSON strings")
}
case _ =>
fail("Expected resource doc to be a JSON object")
}
case _ =>
fail("Expected resource_docs field to be an array")
}
case _ =>
fail("Expected JSON object for resource-docs endpoint")
}
}
}
}