From 36def31cecf07df31791bfe7442d1d6884c743ca Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 27 Mar 2026 00:46:55 +0100 Subject: [PATCH 1/5] bugfix/re-apply berlin group props in beforeEach to survive PropsReset rollback --- obp-api/src/test/scala/code/setup/ServerSetup.scala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 7993c0db1c..26d3b1aaa7 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -60,9 +60,18 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("starConnector_supported_types" -> "mapped,internal,cardano_vJun2025") setPropsValues("connector" -> "star") - // Berlin Group + // Berlin Group - set in trait body for initial setup setPropsValues("berlin_group_mandatory_headers" -> "") setPropsValues("berlin_group_mandatory_header_consent" -> "") + + override def beforeEach(): Unit = { + super.beforeEach() + // Re-apply Berlin Group props after each PropsReset.afterEach() restores lockedProviders + setPropsValues( + "berlin_group_mandatory_headers" -> "", + "berlin_group_mandatory_header_consent" -> "" + ) + } // Set system properties to force Pekko to use random available ports // This prevents conflicts when both RunWebApp and tests are running From 38fdd73160e96fd2ca1b219c7bf10ca46c6bf3a4 Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 27 Mar 2026 01:06:58 +0100 Subject: [PATCH 2/5] test/add signing basket test with real payment id and status verification --- .../v1_3/SigningBasketServiceSBSApiTest.scala | 82 +++++++++++++++++-- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala index 1fd8f1d215..1c7184fdc4 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala @@ -1,13 +1,19 @@ package code.api.berlin.group.v1_3 +import code.api.Constant.SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID import code.api.berlin.group.ConstantsBG -import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{AuthorisationJsonV13, ErrorMessagesBG, PostSigningBasketJsonV13, ScaStatusJsonV13, SigningBasketGetResponseJson, SigningBasketResponseJson, StartPaymentAuthorisationJson} +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{AuthorisationJsonV13, ErrorMessagesBG, InitiatePaymentResponseJson, PostSigningBasketJsonV13, ScaStatusJsonV13, SigningBasketGetResponseJson, SigningBasketResponseJson, StartPaymentAuthorisationJson} +import code.api.berlin.group.v1_3.model.TransactionStatus import code.api.builder.SigningBasketsApi.APIMethods_SigningBasketsApi import code.api.util.APIUtil.OAuth._ import code.api.util.ErrorMessages._ +import code.model.dataAccess.BankAccountRouting import code.setup.{APIResponse, DefaultUsers} +import code.views.Views import com.github.dwickern.macros.NameOf.nameOf -import com.openbankproject.commons.model.enums.StrongCustomerAuthenticationStatus +import com.openbankproject.commons.model.ViewId +import com.openbankproject.commons.model.enums.{AccountRoutingScheme, PaymentServiceTypes, StrongCustomerAuthenticationStatus, TransactionRequestTypes} +import net.liftweb.mapper.By import org.scalatest.Tag class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with DefaultUsers { @@ -21,6 +27,29 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def object getSigningBasketAuthorisation extends Tag(nameOf(APIMethods_SigningBasketsApi.getSigningBasketAuthorisation)) object updateSigningBasketPsuData extends Tag(nameOf(APIMethods_SigningBasketsApi.updateSigningBasketPsuData)) + // Helper: create a real SEPA payment via BG PIS API and return its paymentId + private def createRealPaymentId(): String = { + val accountsRoutingIban = BankAccountRouting.findAll(By(BankAccountRouting.AccountRoutingScheme, AccountRoutingScheme.IBAN.toString)) + val ibanFrom = accountsRoutingIban.head + val ibanTo = accountsRoutingIban.last + Views.views.vend.systemView(ViewId(SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID)).foreach(view => + Views.views.vend.grantAccessToSystemView(ibanFrom.bankId, ibanFrom.accountId, view, resourceUser1) + ) + val initiatePaymentJson = + s"""{ + | "debtorAccount": { "iban": "${ibanFrom.accountRouting.address}" }, + | "instructedAmount": { "currency": "EUR", "amount": "10" }, + | "creditorAccount": { "iban": "${ibanTo.accountRouting.address}" }, + | "creditorName": "TestCreditor" + |}""".stripMargin + val requestPost = (V1_3_BG / PaymentServiceTypes.payments.toString / TransactionRequestTypes.SEPA_CREDIT_TRANSFERS.toString).POST <@ (user1) + val response: APIResponse = makePostRequest(requestPost, initiatePaymentJson) + response.code should equal(201) + val payment = response.body.extract[InitiatePaymentResponseJson] + payment.transactionStatus should be(TransactionStatus.ACCP.code) + payment.paymentId + } + feature(s"test the BG v1.3 - ${createSigningBasket.name}") { scenario("Failed Case - Unauthenticated Access", BerlinGroupV1_3, SBS, createSigningBasket) { val postJson = @@ -61,21 +90,26 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def } } - // TODO Add check that paymentId is an existing transaction request feature(s"test the BG v1.3 -${createSigningBasket.name}") { - scenario("Failed Case - Successful", BerlinGroupV1_3, SBS, createSigningBasket) { + scenario("Success Case - 201 with real paymentId and response body validation", BerlinGroupV1_3, SBS, createSigningBasket) { + val realPaymentId = createRealPaymentId() val postJson = s"""{ | "paymentIds": [ - | "123qwert456789", - | "12345qwert7899" + | "${realPaymentId}" | ] |}""".stripMargin val requestPost = (V1_3_BG / "signing-baskets").POST <@ (user1) val response: APIResponse = makePostRequest(requestPost, postJson) - Then("We should get a 201 ") + Then("We should get a 201") response.code should equal(201) + val createdBasket = response.body.extract[SigningBasketResponseJson] + createdBasket.basketId should not be empty + createdBasket.transactionStatus should be(ConstantsBG.SigningBasketsStatus.RCVD.toString.toLowerCase()) + createdBasket._links.self.href should not be empty + createdBasket._links.status.href should not be empty + createdBasket._links.startAuthorisation.href should not be empty } } @@ -90,6 +124,40 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def And("error should be " + error) responseGet.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(error) } + scenario("Success Case - 200 with real paymentId and payment status validation", BerlinGroupV1_3, SBS, getSigningBasket) { + // Create a real payment first, then create a basket referencing it + val realPaymentId = createRealPaymentId() + val postJson = + s"""{ + | "paymentIds": [ + | "${realPaymentId}" + | ] + |}""".stripMargin + val requestPost = (V1_3_BG / "signing-baskets").POST <@ (user1) + val responsePost: APIResponse = makePostRequest(requestPost, postJson) + responsePost.code should equal(201) + val basketId = responsePost.body.extract[SigningBasketResponseJson].basketId + + // Verify basket GET returns correct data including the real paymentId + val requestGet = (V1_3_BG / "signing-baskets" / basketId).GET <@ (user1) + val responseGet = makeGetRequest(requestGet) + Then("We should get a 200") + responseGet.code should be(200) + val basket = responseGet.body.extract[SigningBasketGetResponseJson] + basket.transactionStatus should be(ConstantsBG.SigningBasketsStatus.RCVD.toString.toLowerCase()) + basket.payments.isDefined should be(true) + basket.payments.get should contain(realPaymentId) + + // Verify each paymentId in the basket has a valid payment status + Then("Each payment in the basket should return a valid status") + basket.payments.get.foreach { pid => + val requestPaymentStatus = (V1_3_BG / PaymentServiceTypes.payments.toString / TransactionRequestTypes.SEPA_CREDIT_TRANSFERS.toString / pid / "status").GET <@ (user1) + val responsePaymentStatus = makeGetRequest(requestPaymentStatus) + responsePaymentStatus.code should be(200) + val txStatus = (responsePaymentStatus.body \ "transactionStatus").extract[String] + txStatus should be(TransactionStatus.RCVD.code) + } + } } feature(s"test the BG v1.3 - ${getSigningBasketStatus.name}") { From 7d922e74ca53ed2d6184272c79277bc8a3a1b89e Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 27 Mar 2026 01:14:30 +0100 Subject: [PATCH 3/5] test/add real paymentId and per-payment ACCP status check in getSigningBasket scenario --- .../api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala index 1c7184fdc4..be42e546a3 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala @@ -155,7 +155,7 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def val responsePaymentStatus = makeGetRequest(requestPaymentStatus) responsePaymentStatus.code should be(200) val txStatus = (responsePaymentStatus.body \ "transactionStatus").extract[String] - txStatus should be(TransactionStatus.RCVD.code) + txStatus should be(TransactionStatus.ACCP.code) } } } From da3ec422435f14c279765e127740be7cc2259e8e Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 27 Mar 2026 01:16:06 +0100 Subject: [PATCH 4/5] test/use two real paymentIds in getSigningBasket scenario and check ACCP status for each --- .../v1_3/SigningBasketServiceSBSApiTest.scala | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala index be42e546a3..31eacb283d 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/SigningBasketServiceSBSApiTest.scala @@ -124,13 +124,15 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def And("error should be " + error) responseGet.body.extract[ErrorMessagesBG].tppMessages.head.text should startWith(error) } - scenario("Success Case - 200 with real paymentId and payment status validation", BerlinGroupV1_3, SBS, getSigningBasket) { - // Create a real payment first, then create a basket referencing it - val realPaymentId = createRealPaymentId() + scenario("Success Case - 200 with multiple real paymentIds and payment status validation", BerlinGroupV1_3, SBS, getSigningBasket) { + // Create two real payments, then create a basket referencing both + val paymentId1 = createRealPaymentId() + val paymentId2 = createRealPaymentId() val postJson = s"""{ | "paymentIds": [ - | "${realPaymentId}" + | "${paymentId1}", + | "${paymentId2}" | ] |}""".stripMargin val requestPost = (V1_3_BG / "signing-baskets").POST <@ (user1) @@ -138,7 +140,7 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def responsePost.code should equal(201) val basketId = responsePost.body.extract[SigningBasketResponseJson].basketId - // Verify basket GET returns correct data including the real paymentId + // Verify basket GET returns correct data including both real paymentIds val requestGet = (V1_3_BG / "signing-baskets" / basketId).GET <@ (user1) val responseGet = makeGetRequest(requestGet) Then("We should get a 200") @@ -146,10 +148,11 @@ class SigningBasketServiceSBSApiTest extends BerlinGroupServerSetupV1_3 with Def val basket = responseGet.body.extract[SigningBasketGetResponseJson] basket.transactionStatus should be(ConstantsBG.SigningBasketsStatus.RCVD.toString.toLowerCase()) basket.payments.isDefined should be(true) - basket.payments.get should contain(realPaymentId) + basket.payments.get should contain(paymentId1) + basket.payments.get should contain(paymentId2) - // Verify each paymentId in the basket has a valid payment status - Then("Each payment in the basket should return a valid status") + // Verify each paymentId in the basket has ACCP status + Then("Each payment in the basket should return ACCP status") basket.payments.get.foreach { pid => val requestPaymentStatus = (V1_3_BG / PaymentServiceTypes.payments.toString / TransactionRequestTypes.SEPA_CREDIT_TRANSFERS.toString / pid / "status").GET <@ (user1) val responsePaymentStatus = makeGetRequest(requestPaymentStatus) From 55ed03b38c9a9be71d3f773e2bf6edfd89bdf09c Mon Sep 17 00:00:00 2001 From: hongwei Date: Fri, 27 Mar 2026 09:43:54 +0100 Subject: [PATCH 5/5] bugfix/reject duplicate or already-completed paymentIds in updateSigningBasketPsuData finalised branch --- .../code/api/berlin/group/v1_3/SigningBasketsApi.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala index 2b02ec4ce5..ba30f9e4bf 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/SigningBasketsApi.scala @@ -495,7 +495,14 @@ There are the following request types on this access path: val basket = SigningBasketX.signingBasketProvider.vend.getSigningBasketByBasketId(basketId) val existAll: Box[Boolean] = basket.flatMap(_.payments.map(_.forall(i => Connector.connector.vend.getTransactionRequestImpl(TransactionRequestId(i), callContext).isDefined))) - if (existAll.getOrElse(false)) { + val alreadyCompleted: List[String] = + basket.flatMap(_.payments).getOrElse(Nil).filter { i => + Connector.connector.vend.getTransactionRequestImpl(TransactionRequestId(i), callContext) + .exists(_._1.status == COMPLETED.toString) + } + if (alreadyCompleted.nonEmpty) { + unboxFullOrFail(Empty, callContext, s"$InvalidConnectorResponse Some of paymentIds [${alreadyCompleted.mkString(",")}] are already completed") + } else if (existAll.getOrElse(false)) { basket.map { i => i.payments.map(_.map { i => NewStyle.function.saveTransactionRequestStatusImpl(TransactionRequestId(i), COMPLETED.toString, callContext)