mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
test/(Http4sLiftBridgeParityTest): migrate to network-based Http4sTestServer
- Replace in-process bridge initialization with Http4sTestServer singleton reference - Switch from http4s Request/Response types to dispatch library for HTTP calls - Implement makeHttp4sGetRequest and makeHttp4sPostRequest using dispatch for network requests - Update request/response handling to work with HTTP status codes and header maps - Refactor assertCorrelationId to work with Map[String, String] instead of http4s Headers - Add comprehensive documentation explaining the test's purpose and architecture - Remove LiftRules inconsistency workarounds by testing against real HTTP4S server - Update imports to use dispatch, scala concurrent utilities, and remove http4s internals - This change ensures parity testing matches production behavior by testing over the network
This commit is contained in:
parent
e885d3f504
commit
ed61c86eac
@ -1,31 +1,37 @@
|
||||
package code.api.http4sbridge
|
||||
|
||||
import cats.effect.IO
|
||||
import cats.effect.unsafe.implicits.global
|
||||
import code.Http4sTestServer
|
||||
import code.api.ResponseHeader
|
||||
import code.api.v5_0_0.V500ServerSetup
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.createSystemViewJsonV500
|
||||
import code.api.v5_0_0.ViewJsonV500
|
||||
import code.api.berlin.group.ConstantsBG
|
||||
import code.api.util.APIUtil
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole.{CanCreateSystemView, CanDeleteSystemView, CanGetSystemView, CanUpdateSystemView}
|
||||
import code.api.util.http4s.Http4sLiftWebBridge
|
||||
import code.consumer.Consumers
|
||||
import code.entitlement.Entitlement
|
||||
import code.model.dataAccess.AuthUser
|
||||
import code.views.system.AccountAccess
|
||||
import com.openbankproject.commons.model.UpdateViewJSON
|
||||
import dispatch.Defaults._
|
||||
import dispatch._
|
||||
import net.liftweb.json.JValue
|
||||
import net.liftweb.json.JsonAST.JObject
|
||||
import net.liftweb.json.JsonParser.parse
|
||||
import net.liftweb.json.Serialization.write
|
||||
import net.liftweb.mapper.By
|
||||
import net.liftweb.util.Helpers._
|
||||
import org.http4s.{Header, Headers, Method, Request, Status, Uri}
|
||||
import org.scalatest.Tag
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration.DurationInt
|
||||
|
||||
/**
|
||||
* Http4s Lift Bridge Parity Test
|
||||
*
|
||||
* This test verifies that the HTTP4S server (via Http4sTestServer) produces
|
||||
* responses that match the Lift/Jetty server responses across different API versions
|
||||
* and authentication methods.
|
||||
*
|
||||
* Unlike the previous implementation that ran the bridge in-process (which had
|
||||
* LiftRules inconsistency issues), this test uses Http4sTestServer to test the
|
||||
* real HTTP4S server over the network, matching production behavior.
|
||||
*/
|
||||
class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
|
||||
// Create a test user with known password for DirectLogin testing
|
||||
@ -34,21 +40,13 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
private val testConsumerKey = randomString(40).toLowerCase
|
||||
private val testConsumerSecret = randomString(40).toLowerCase
|
||||
|
||||
// Initialize http4sRoutes after Lift is fully initialized
|
||||
// NOTE: This test has a known limitation - it runs the bridge in the test process,
|
||||
// which has a separate LiftRules instance from the Jetty server process.
|
||||
// The Jetty server (accessed via makePostRequest) has all routes registered,
|
||||
// but the bridge in the test process may not have access to the same routes.
|
||||
// In production (Http4sServer), the bridge runs in the same process as Lift initialization,
|
||||
// so this issue does not occur.
|
||||
private var http4sRoutes: org.http4s.HttpApp[IO] = _
|
||||
// Reference the singleton HTTP4S test server (auto-starts on first access)
|
||||
private val http4sServer = Http4sTestServer
|
||||
private val http4sBaseUrl = s"http://${http4sServer.host}:${http4sServer.port}"
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
super.beforeAll()
|
||||
|
||||
// Initialize http4sRoutes AFTER Lift has been fully initialized by super.beforeAll()
|
||||
http4sRoutes = Http4sLiftWebBridge.withStandardHeaders(Http4sLiftWebBridge.routes).orNotFound
|
||||
|
||||
// Create AuthUser if not exists
|
||||
if (AuthUser.find(By(AuthUser.username, testUsername)).isEmpty) {
|
||||
AuthUser.create
|
||||
@ -85,23 +83,53 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
|
||||
object Http4sLiftBridgeParityTag extends Tag("Http4sLiftBridgeParity")
|
||||
|
||||
private def toHttp4sRequest(reqData: ReqData): Request[IO] = {
|
||||
val method = Method.fromString(reqData.method).getOrElse(Method.GET)
|
||||
val base = Request[IO](method = method, uri = Uri.unsafeFromString(reqData.url))
|
||||
// Set body first
|
||||
val withBody = if (reqData.body.trim.nonEmpty) base.withEntity(reqData.body) else base
|
||||
// Then set headers (including Content-Type) to override defaults
|
||||
val withHeaders = reqData.headers.foldLeft(withBody) { case (req, (key, value)) =>
|
||||
req.putHeaders(Header.Raw(CIString(key), value))
|
||||
private def makeHttp4sGetRequest(path: String, headers: Map[String, String] = Map.empty): (Int, JValue, Map[String, String]) = {
|
||||
val request = url(s"$http4sBaseUrl$path")
|
||||
val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) =>
|
||||
req.addHeader(key, value)
|
||||
}
|
||||
|
||||
try {
|
||||
val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => {
|
||||
val statusCode = p.getStatusCode
|
||||
val body = if (p.getResponseBody != null && p.getResponseBody.trim.nonEmpty) p.getResponseBody else "{}"
|
||||
val json = parse(body)
|
||||
val responseHeaders = p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap
|
||||
(statusCode, json, responseHeaders)
|
||||
}))
|
||||
Await.result(response, DurationInt(10).seconds)
|
||||
} catch {
|
||||
case e: java.util.concurrent.ExecutionException =>
|
||||
// Extract status code from exception message if possible
|
||||
val statusPattern = """(\d{3})""".r
|
||||
statusPattern.findFirstIn(e.getCause.getMessage) match {
|
||||
case Some(code) => (code.toInt, JObject(Nil), Map.empty)
|
||||
case None => throw e
|
||||
}
|
||||
case e: Exception =>
|
||||
throw e
|
||||
}
|
||||
withHeaders
|
||||
}
|
||||
|
||||
private def runHttp4s(reqData: ReqData): (Status, JValue, Headers) = {
|
||||
val response = http4sRoutes.run(toHttp4sRequest(reqData)).unsafeRunSync()
|
||||
val body = response.as[String].unsafeRunSync()
|
||||
val json = if (body.trim.isEmpty) JObject(Nil) else parse(body)
|
||||
(response.status, json, response.headers)
|
||||
private def makeHttp4sPostRequest(path: String, body: String, headers: Map[String, String] = Map.empty): (Int, JValue, Map[String, String]) = {
|
||||
val request = url(s"$http4sBaseUrl$path").POST.setBody(body)
|
||||
val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) =>
|
||||
req.addHeader(key, value)
|
||||
}
|
||||
|
||||
try {
|
||||
val response = Http.default(requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => {
|
||||
val statusCode = p.getStatusCode
|
||||
val responseBody = if (p.getResponseBody != null && p.getResponseBody.trim.nonEmpty) p.getResponseBody else "{}"
|
||||
val json = parse(responseBody)
|
||||
val responseHeaders = p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap
|
||||
(statusCode, json, responseHeaders)
|
||||
}))
|
||||
Await.result(response, DurationInt(10).seconds)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private def hasField(json: JValue, key: String): Boolean = {
|
||||
@ -122,10 +150,10 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
jsonKeys(json).map(_.toLowerCase)
|
||||
}
|
||||
|
||||
private def assertCorrelationId(headers: Headers): Unit = {
|
||||
val header = headers.headers.find(_.name.toString.equalsIgnoreCase(ResponseHeader.`Correlation-Id`))
|
||||
private def assertCorrelationId(headers: Map[String, String]): Unit = {
|
||||
val header = headers.find { case (key, _) => key.equalsIgnoreCase(ResponseHeader.`Correlation-Id`) }
|
||||
header.isDefined shouldBe true
|
||||
header.map(_.value.trim.nonEmpty).getOrElse(false) shouldBe true
|
||||
header.map(_._2.trim.nonEmpty).getOrElse(false) shouldBe true
|
||||
}
|
||||
|
||||
private val standardVersions = List(
|
||||
@ -148,10 +176,9 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
private def runBanksParity(version: String): Unit = {
|
||||
val liftReq = (baseRequest / "obp" / version / "banks").GET
|
||||
val liftResponse = makeGetRequest(liftReq)
|
||||
val reqData = extractParamsAndHeaders(liftReq, "", "")
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = runHttp4s(reqData)
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = makeHttp4sGetRequest(s"/obp/$version/banks")
|
||||
|
||||
http4sStatus.code should equal(liftResponse.code)
|
||||
http4sStatus should equal(liftResponse.code)
|
||||
jsonKeysLower(http4sJson) should equal(jsonKeysLower(liftResponse.body))
|
||||
assertCorrelationId(http4sHeaders)
|
||||
}
|
||||
@ -160,9 +187,12 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
val liftReq = (baseRequest / "open-banking" / version / "accounts").GET <@(user1)
|
||||
val liftResponse = makeGetRequest(liftReq)
|
||||
val reqData = extractParamsAndHeaders(liftReq, "", "")
|
||||
val (http4sStatus, _, http4sHeaders) = runHttp4s(reqData)
|
||||
val (http4sStatus, _, http4sHeaders) = makeHttp4sGetRequest(
|
||||
s"/open-banking/$version/accounts",
|
||||
reqData.headers
|
||||
)
|
||||
|
||||
http4sStatus.code should equal(liftResponse.code)
|
||||
http4sStatus should equal(liftResponse.code)
|
||||
assertCorrelationId(http4sHeaders)
|
||||
}
|
||||
|
||||
@ -185,9 +215,13 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
val liftReq = (base / "accounts").GET <@(user1)
|
||||
val liftResponse = makeGetRequest(liftReq)
|
||||
val reqData = extractParamsAndHeaders(liftReq, "", "")
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = runHttp4s(reqData)
|
||||
val berlinPathStr = berlinPath.mkString("/", "/", "")
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = makeHttp4sGetRequest(
|
||||
s"$berlinPathStr/accounts",
|
||||
reqData.headers
|
||||
)
|
||||
|
||||
http4sStatus.code should equal(liftResponse.code)
|
||||
http4sStatus should equal(liftResponse.code)
|
||||
// Berlin Group responses can differ in top-level keys while still being valid.
|
||||
assertCorrelationId(http4sHeaders)
|
||||
}
|
||||
@ -195,10 +229,9 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
scenario("DirectLogin parity - missing auth header", Http4sLiftBridgeParityTag) {
|
||||
val liftReq = (baseRequest / "my" / "logins" / "direct").POST
|
||||
val liftResponse = makePostRequest(liftReq, "")
|
||||
val reqData = extractParamsAndHeaders(liftReq, "", "")
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = runHttp4s(reqData)
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = makeHttp4sPostRequest("/my/logins/direct", "")
|
||||
|
||||
http4sStatus.code should equal(liftResponse.code)
|
||||
http4sStatus should equal(liftResponse.code)
|
||||
(hasField(http4sJson, "error") || hasField(http4sJson, "message")) shouldBe true
|
||||
assertCorrelationId(http4sHeaders)
|
||||
}
|
||||
@ -213,24 +246,19 @@ class Http4sLiftBridgeParityTest extends V500ServerSetup {
|
||||
|
||||
val liftResponse = makePostRequest(liftReq, "")
|
||||
|
||||
val reqData = ReqData(
|
||||
url = s"http://${server.host}:${server.port}/my/logins/direct",
|
||||
method = "POST",
|
||||
body = "",
|
||||
body_encoding = "UTF-8",
|
||||
headers = Map(
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = makeHttp4sPostRequest(
|
||||
"/my/logins/direct",
|
||||
"",
|
||||
Map(
|
||||
"Authorization" -> directLoginHeader,
|
||||
"Content-Type" -> "application/json"
|
||||
),
|
||||
query_params = Map.empty,
|
||||
form_params = Map.empty
|
||||
)
|
||||
)
|
||||
val (http4sStatus, http4sJson, http4sHeaders) = runHttp4s(reqData)
|
||||
|
||||
// Both should return 201 Created
|
||||
liftResponse.code should equal(201)
|
||||
http4sStatus.code should equal(201)
|
||||
http4sStatus.code should equal(liftResponse.code)
|
||||
http4sStatus should equal(201)
|
||||
http4sStatus should equal(liftResponse.code)
|
||||
|
||||
// Both should have a token field
|
||||
hasField(http4sJson, "token") shouldBe true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user