mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:47:18 +00:00
Merge pull request #2665 from hongwei1/develop
test/added tests for V700
This commit is contained in:
commit
a81e208cb1
@ -3078,10 +3078,10 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
|||||||
else
|
else
|
||||||
getCorrelationId()
|
getCorrelationId()
|
||||||
|
|
||||||
val reqHeaders = if (cc.requestHeaders.nonEmpty)
|
val reqHeaders = if (cc.requestHeaders.nonEmpty)
|
||||||
cc.requestHeaders
|
cc.requestHeaders
|
||||||
else
|
else
|
||||||
S.request.openOrThrowException(attemptedToOpenAnEmptyBox).request.headers
|
S.request.map(_.request.headers).openOr(Nil)
|
||||||
|
|
||||||
val remoteIpAddress = if (cc.ipAddress.nonEmpty)
|
val remoteIpAddress = if (cc.ipAddress.nonEmpty)
|
||||||
cc.ipAddress
|
cc.ipAddress
|
||||||
@ -5189,4 +5189,4 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
|||||||
.distinct // List pairs (bank_id, account_id)
|
.distinct // List pairs (bank_id, account_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,15 @@ package code.api.util.http4s
|
|||||||
|
|
||||||
import cats.data.{EitherT, Kleisli, OptionT}
|
import cats.data.{EitherT, Kleisli, OptionT}
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
|
import code.api.v7_0_0.Http4s700
|
||||||
import code.api.APIFailureNewStyle
|
import code.api.APIFailureNewStyle
|
||||||
import code.api.util.APIUtil.ResourceDoc
|
import code.api.util.APIUtil.ResourceDoc
|
||||||
import code.api.util.ErrorMessages._
|
import code.api.util.ErrorMessages._
|
||||||
import code.api.util.newstyle.ViewNewStyle
|
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 code.util.Helper.MdcLoggable
|
||||||
import com.openbankproject.commons.model._
|
import com.openbankproject.commons.model._
|
||||||
|
import com.github.dwickern.macros.NameOf.nameOf
|
||||||
import net.liftweb.common.{Box, Empty, Full}
|
import net.liftweb.common.{Box, Empty, Full}
|
||||||
import org.http4s._
|
import org.http4s._
|
||||||
import org.http4s.headers.`Content-Type`
|
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
|
* - Special case: resource-docs endpoint checks resource_docs_requires_role property
|
||||||
*/
|
*/
|
||||||
private def needsAuthentication(resourceDoc: ResourceDoc): Boolean = {
|
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)
|
APIUtil.getPropsAsBoolValue("resource_docs_requires_role", false)
|
||||||
} else {
|
} else {
|
||||||
resourceDoc.errorResponseBodies.contains($AuthenticatedUserIsRequired) || resourceDoc.roles.exists(_.nonEmpty)
|
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] = {
|
private def authorizeRoles(resourceDoc: ResourceDoc, pathParams: Map[String, String], ctx: ValidationContext): Validation[ValidationContext] = {
|
||||||
import DSL._
|
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 =>
|
case Some(roles) if roles.nonEmpty =>
|
||||||
ctx.user match {
|
ctx.user match {
|
||||||
case Full(user) =>
|
case Full(user) =>
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import code.api.Constant._
|
|||||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
|
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
|
||||||
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
|
import code.api.ResourceDocs1_4_0.{ResourceDocs140, ResourceDocsAPIMethodsUtil}
|
||||||
import code.api.util.APIUtil.{EmptyBody, _}
|
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.ApiTag._
|
||||||
import code.api.util.ErrorMessages._
|
import code.api.util.ErrorMessages._
|
||||||
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
|
import code.api.util.http4s.{Http4sRequestAttributes, ResourceDocMiddleware}
|
||||||
import code.api.util.http4s.Http4sRequestAttributes.{RequestOps, EndpointHelpers}
|
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_3_0.JSONFactory1_3_0
|
||||||
import code.api.v1_4_0.JSONFactory1_4_0
|
import code.api.v1_4_0.JSONFactory1_4_0
|
||||||
import code.api.v4_0_0.JSONFactory400
|
import code.api.v4_0_0.JSONFactory400
|
||||||
@ -201,44 +201,26 @@ object Http4s700 {
|
|||||||
|
|
||||||
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
val getResourceDocsObpV700: HttpRoutes[IO] = HttpRoutes.of[IO] {
|
||||||
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
|
case req @ GET -> `prefixPath` / "resource-docs" / requestedApiVersionString / "obp" =>
|
||||||
implicit val cc: CallContext = req.callContext
|
EndpointHelpers.executeAndRespond(req) { _ =>
|
||||||
for {
|
val queryParams = req.uri.query.multiParams
|
||||||
result <- IO.fromFuture(IO {
|
val tags = queryParams
|
||||||
// Check resource_docs_requires_role property
|
.get("tags")
|
||||||
val resourceDocsRequireRole = getPropsAsBoolValue("resource_docs_requires_role", false)
|
.map(_.flatMap(_.split(",").toList).map(_.trim).filter(_.nonEmpty).map(ResourceDocTag(_)).toList)
|
||||||
|
val functions = queryParams
|
||||||
for {
|
.get("functions")
|
||||||
// Authentication based on property
|
.map(_.flatMap(_.split(",").toList).map(_.trim).filter(_.nonEmpty).toList)
|
||||||
(boxUser, cc1) <- if (resourceDocsRequireRole)
|
val localeParam = queryParams
|
||||||
authenticatedAccess(cc)
|
.get("locale")
|
||||||
else
|
.flatMap(_.headOption)
|
||||||
anonymousAccess(cc)
|
.orElse(queryParams.get("language").flatMap(_.headOption))
|
||||||
|
.map(_.trim)
|
||||||
// Role check based on property
|
.filter(_.nonEmpty)
|
||||||
_ <- if (resourceDocsRequireRole) {
|
for {
|
||||||
NewStyle.function.hasAtLeastOneEntitlement(
|
requestedApiVersion <- Future(ApiVersionUtils.valueOf(requestedApiVersionString))
|
||||||
failMsg = UserHasMissingRoles + canReadResourceDoc.toString
|
resourceDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil)
|
||||||
)("", boxUser.map(_.userId).getOrElse(""), ApiRole.canReadResourceDoc :: Nil, cc1)
|
filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(resourceDocs, tags, functions)
|
||||||
} else {
|
} yield JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
420
obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala
Normal file
420
obp-api/src/test/scala/code/api/v7_0_0/Http4s700RoutesTest.scala
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user