From 6a4449203266b1fe3771a4f4dfa96b11f318aded Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 3 Nov 2026 13:42:01 +0100 Subject: [PATCH 1/3] feat(deletion): cascade account deletion to the challenges of consents bound to it DELETE /obp/v4.0.0/management/cascading/banks/{BANK_ID}/accounts/{ACCOUNT_ID} now also removes the SCA challenges of every consent whose JWT views name the account. The consents are read from the users who hold or can see the account, before their account access rows are deleted. The bank cascade deletes each account through the same code. --- .../scala/deletion/DeleteAccountCascade.scala | 36 ++++++++++++- .../api/v4_0_0/DeleteAccountCascadeTest.scala | 53 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala b/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala index 0a1f1eb81f..653841567a 100644 --- a/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala +++ b/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala @@ -28,11 +28,14 @@ TESOBE (http://www.tesobe.com/) package deletion import code.accountattribute.MappedAccountAttribute +import code.accountholders.AccountHolders import code.api.APIFailureNewStyle import code.api.util.APIUtil.fullBoxOrException +import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} import code.api.util.ErrorMessages.CouldNotDeleteCascade import code.bankconnectors.Connector import code.cards.MappedPhysicalCard +import code.consent.Consents import code.counterpartyattribute.CounterpartyAttribute import code.counterpartylimit.CounterpartyLimit import code.entitlement.MappedEntitlement @@ -43,11 +46,14 @@ import code.transactionrequests.MappedTransactionRequest import code.views.system.{AccountAccess, ViewDefinition} import code.webhook.MappedAccountWebhook import com.openbankproject.commons.model.{AccountId, BankId} +import com.openbankproject.commons.util.JsonAliases import deletion.DeletionUtil.databaseAtomicTask import net.liftweb.common.{Box, Empty, Full} import net.liftweb.db.DB import net.liftweb.mapper.{By, ByList} import net.liftweb.util.DefaultConnectionIdentifier +import net.liftweb.util.Helpers.tryo +import org.json4s.Formats import scala.collection.immutable.List @@ -55,9 +61,12 @@ object DeleteAccountCascade { def delete(bankId: BankId, accountId: AccountId): Boolean = { // The tasks run in list order: the challenges are found through the account's transaction - // requests, so they must go before deleteTransactionRequests and deleteTransactions remove them. + // requests, so they must go before deleteTransactionRequests and deleteTransactions remove them; + // the consent challenges are found through the users who hold or can see the account, so they + // must go before deleteAccountAccess removes the rows that name those users. val doneTasks = - deleteTransactionRequestChallenges(bankId, accountId) :: + deleteConsentChallenges(bankId, accountId) :: + deleteTransactionRequestChallenges(bankId, accountId) :: deleteTransactionRequests(bankId, accountId) :: deleteDoubleEntryBookTransactions(bankId, accountId) :: deleteTransactions(bankId, accountId) :: @@ -210,6 +219,29 @@ object DeleteAccountCascade { } } + // A consent names its accounts only in the views of its JWT, so the consents of the users who hold + // or can see this account are read to find the ones bound to it. A consent bound to other accounts + // as well loses its challenges too: they record the authorisation of the consent as a whole. + private def deleteConsentChallenges(bankId: BankId, accountId: AccountId): Boolean = { + implicit val formats: Formats = CustomJsonFormats.formats + val holderIds = AccountHolders.accountHolders.vend.getAccountHolders(bankId, accountId).map(_.userId) + val viewerIds = AccountAccess.findAll( + By(AccountAccess.bank_id, bankId.value), + By(AccountAccess.account_id, accountId.value) + ).flatMap(_.user_fk.foreign.map(_.userId)) + val consentIds = (holderIds.toList ++ viewerIds).distinct + .flatMap(Consents.consentProvider.vend.getConsentsByUser) + .filter { consent => + JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) + .flatMap(payload => tryo(JsonAliases.parse(payload).extract[ConsentJWT])) + .exists(_.views.exists(view => view.bank_id == bankId.value && view.account_id == accountId.value)) + } + .map(_.consentId) + .distinct + consentIds.isEmpty || MappedExpectedChallengeAnswer.bulkDelete_!!( + ByList(MappedExpectedChallengeAnswer.ConsentId, consentIds) + ) + } private def deleteTransactionRequestChallenges(bankId: BankId, accountId: AccountId): Boolean = { val transactionRequestIds = MappedTransactionRequest.findAll( By(MappedTransactionRequest.mFrom_BankId, bankId.value), diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala index f9b5b2f8bf..046e07b598 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala @@ -38,8 +38,10 @@ import code.api.util.ErrorMessages.{UserHasMissingRoles, AuthenticatedUserIsRequ import code.api.v3_1_0.CreateAccountResponseJsonV310 import code.api.v4_0_0.Http4s400.Implementations4_0_0 import code.cards.MappedPhysicalCard +import code.consent.Consents import code.entitlement.{Entitlement, MappedEntitlement} import code.model.dataAccess.MappedBankAccount +import code.transactionChallenge.MappedExpectedChallengeAnswer import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.{AccountId, AmountOfMoneyJsonV121, BankId, ErrorMessage} import com.openbankproject.commons.util.ApiVersion @@ -243,4 +245,55 @@ class DeleteAccountCascadeTest extends V400ServerSetup { } } + feature(s"test $ApiEndpoint1 version $VersionOfApi - consent challenges") { + scenario("Deleting an account deletes the SCA challenges of the consents bound to it", ApiEndpoint1, VersionOfApi) { + setPropsValues("suggested_default_sca_method" -> "DUMMY") + Given("Two banks that each have an account with the same ACCOUNT_ID, both held by the same user") + val bankId = BankId(randomString(10)) + val otherBankId = BankId(randomString(10)) + createBank(bankId.value) + createBank(otherBankId.value) + val accountId = AccountId(randomString(10)) + createAccountRelevantResource(Some(resourceUser1), bankId, accountId, "EUR") + createAccountRelevantResource(Some(resourceUser1), otherBankId, accountId, "EUR") + + // A UK consent is lodged without a PSU and bound to the accounts the PSU selects when it + // answers the challenge, the way Portal authorises one. + def authorisedUKConsent(bank: BankId): String = { + val consentId = Consents.consentProvider.vend.saveUKConsent( + user = None, bankId = None, accountIds = None, consumerId = None, + permissions = List("ReadAccountsBasic"), + expirationDateTime = Some(new java.util.Date(System.currentTimeMillis() + 3600000L)), + transactionFromDateTime = None, transactionToDateTime = None, + apiStandard = Some("UKOpenBanking"), apiVersion = Some("4.0.1") + ).openOrThrowException("consent creation failed").consentId + val consentPath = v5_1_0_Request / "banks" / bank.value / "consents" / consentId / "authorise" + val challenge = makePostRequest((consentPath / "challenge").POST <@ (user1), "") + challenge.code should equal(200) + val challengeId = (challenge.body \ "challenge_id").extract[String] + val authorised = makePostRequest(consentPath.POST <@ (user1), + s"""{"account_ids":["${accountId.value}"],"challenge_id":"$challengeId","answer":"123"}""") + authorised.code should equal(200) + consentId + } + def challengesOf(consentId: String): Int = + MappedExpectedChallengeAnswer.findAll(By(MappedExpectedChallengeAnswer.ConsentId, consentId)).size + val consent = authorisedUKConsent(bankId) + val otherBankConsent = authorisedUKConsent(otherBankId) + challengesOf(consent) should equal(1) + challengesOf(otherBankConsent) should equal(1) + + When("We delete the account at the first bank with the cascading endpoint") + Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) + val request = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId.value / + "accounts" / accountId.value).DELETE <@ (user1) + makeDeleteRequest(request).code should equal(200) + + Then("The challenge of the consent bound to the deleted account is gone") + challengesOf(consent) should equal(0) + And("The challenge of the consent bound to the other bank's account is kept") + challengesOf(otherBankConsent) should equal(1) + } + } + } From 7263721867f370f6e01fa1d3d9280517d0a316b7 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 3 Nov 2026 14:34:01 +0100 Subject: [PATCH 2/3] test: name the repeated cascade step once in DeleteAccountCascadeTest --- .../scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala index 046e07b598..1a4b90abb0 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala @@ -64,6 +64,8 @@ class DeleteAccountCascadeTest extends V400ServerSetup { object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.deleteAccountCascade)) + private val deleteAtFirstBank = "We delete the account at the first bank with the cascading endpoint" + lazy val bankId = randomBankId lazy val bankAccount = randomPrivateAccountViaEndpoint(bankId) lazy val addAccountJson = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(user_id = resourceUser1.userId, balance = AmountOfMoneyJsonV121("EUR","0")) @@ -197,7 +199,7 @@ class DeleteAccountCascadeTest extends V400ServerSetup { val card = addCard(bankId) val otherBankCard = addCard(otherBankId) - When("We delete the account at the first bank with the cascading endpoint") + When(deleteAtFirstBank) Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) val request = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId.value / "accounts" / accountId.value).DELETE <@ (user1) @@ -231,7 +233,7 @@ class DeleteAccountCascadeTest extends V400ServerSetup { ) otherOwnerEntitlements should equal(1L) - When("We delete the account at the first bank with the cascading endpoint") + When(deleteAtFirstBank) Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) val request = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId.value / "accounts" / accountId.value).DELETE <@ (user1) @@ -283,7 +285,7 @@ class DeleteAccountCascadeTest extends V400ServerSetup { challengesOf(consent) should equal(1) challengesOf(otherBankConsent) should equal(1) - When("We delete the account at the first bank with the cascading endpoint") + When(deleteAtFirstBank) Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) val request = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId.value / "accounts" / accountId.value).DELETE <@ (user1) From 36308526732b0eacb8d10246af499fe9ac6eadf0 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 3 Nov 2026 15:30:38 +0100 Subject: [PATCH 3/3] fix(deletion): find the consent challenges to cascade through the challenge table The account cascade looked for consents through the users who hold or can see the account, which misses a Berlin Group consent whose first challenge is still unanswered: no PSU is bound to it yet. The consents are now read from the challenges that carry a consent id and kept when their JWT names the account. The test now uses such a Berlin Group consent instead of an authorised UK consent, whose challenge is deleted once answered. --- .../scala/deletion/DeleteAccountCascade.scala | 26 ++--- .../v1_3/BerlinGroupConsentFixtures.scala | 13 +-- ...teAccountCascadeConsentChallengeTest.scala | 97 +++++++++++++++++++ .../api/v4_0_0/DeleteAccountCascadeTest.scala | 53 ---------- 4 files changed, 114 insertions(+), 75 deletions(-) create mode 100644 obp-api/src/test/scala/code/api/berlin/group/v1_3/DeleteAccountCascadeConsentChallengeTest.scala diff --git a/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala b/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala index 653841567a..aeacade05f 100644 --- a/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala +++ b/obp-api/src/main/scala/deletion/DeleteAccountCascade.scala @@ -28,7 +28,6 @@ TESOBE (http://www.tesobe.com/) package deletion import code.accountattribute.MappedAccountAttribute -import code.accountholders.AccountHolders import code.api.APIFailureNewStyle import code.api.util.APIUtil.fullBoxOrException import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} @@ -50,7 +49,7 @@ import com.openbankproject.commons.util.JsonAliases import deletion.DeletionUtil.databaseAtomicTask import net.liftweb.common.{Box, Empty, Full} import net.liftweb.db.DB -import net.liftweb.mapper.{By, ByList} +import net.liftweb.mapper.{By, ByList, NotBy} import net.liftweb.util.DefaultConnectionIdentifier import net.liftweb.util.Helpers.tryo import org.json4s.Formats @@ -61,9 +60,7 @@ object DeleteAccountCascade { def delete(bankId: BankId, accountId: AccountId): Boolean = { // The tasks run in list order: the challenges are found through the account's transaction - // requests, so they must go before deleteTransactionRequests and deleteTransactions remove them; - // the consent challenges are found through the users who hold or can see the account, so they - // must go before deleteAccountAccess removes the rows that name those users. + // requests, so they must go before deleteTransactionRequests and deleteTransactions remove them. val doneTasks = deleteConsentChallenges(bankId, accountId) :: deleteTransactionRequestChallenges(bankId, accountId) :: @@ -219,25 +216,22 @@ object DeleteAccountCascade { } } - // A consent names its accounts only in the views of its JWT, so the consents of the users who hold - // or can see this account are read to find the ones bound to it. A consent bound to other accounts - // as well loses its challenges too: they record the authorisation of the consent as a whole. + // A consent names its accounts only in the views of its JWT, so the consents that still have + // challenges are read and those whose JWT names this account lose them. The consents are found + // through their challenges rather than through users: a Berlin Group consent awaiting its first + // challenge answer has no PSU bound to it yet. A consent bound to other accounts as well loses its + // challenges too: they record the authorisation of the consent as a whole. private def deleteConsentChallenges(bankId: BankId, accountId: AccountId): Boolean = { implicit val formats: Formats = CustomJsonFormats.formats - val holderIds = AccountHolders.accountHolders.vend.getAccountHolders(bankId, accountId).map(_.userId) - val viewerIds = AccountAccess.findAll( - By(AccountAccess.bank_id, bankId.value), - By(AccountAccess.account_id, accountId.value) - ).flatMap(_.user_fk.foreign.map(_.userId)) - val consentIds = (holderIds.toList ++ viewerIds).distinct - .flatMap(Consents.consentProvider.vend.getConsentsByUser) + val consentIds = MappedExpectedChallengeAnswer.findAll(NotBy(MappedExpectedChallengeAnswer.ConsentId, "")) + .map(_.ConsentId.get).distinct + .flatMap(Consents.consentProvider.vend.getConsentByConsentId(_).toList) .filter { consent => JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) .flatMap(payload => tryo(JsonAliases.parse(payload).extract[ConsentJWT])) .exists(_.views.exists(view => view.bank_id == bankId.value && view.account_id == accountId.value)) } .map(_.consentId) - .distinct consentIds.isEmpty || MappedExpectedChallengeAnswer.bulkDelete_!!( ByList(MappedExpectedChallengeAnswer.ConsentId, consentIds) ) diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupConsentFixtures.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupConsentFixtures.scala index 12973f60ca..5aa89419ab 100644 --- a/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupConsentFixtures.scala +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/BerlinGroupConsentFixtures.scala @@ -66,14 +66,15 @@ trait BerlinGroupConsentFixtures extends BerlinGroupServerSetupV1_3 with Default def getNextMonthDate(): String = LocalDate.now().plusMonths(1).format(DateTimeFormatter.ISO_LOCAL_DATE) - /** One account, addressed by the first IBAN routing in the test data. */ - def bgConsentPostBody(): PostConsentJson = { - val acountRoutingIban = BankAccountRouting + /** One account, addressed by `iban`, or by the first IBAN routing in the test data. */ + def bgConsentPostBody(iban: Option[String] = None): PostConsentJson = { + val address = iban.getOrElse(BankAccountRouting .findAll(By(BankAccountRouting.AccountRoutingScheme, AccountRoutingScheme.IBAN.toString)).head + .accountRouting.address) PostConsentJson( access = ConsentAccessJson( accounts = Option(List(ConsentAccessAccountsJson( - iban = Some(acountRoutingIban.accountRouting.address), + iban = Some(address), bban = None, pan = None, maskedPan = None, @@ -131,8 +132,8 @@ trait BerlinGroupConsentFixtures extends BerlinGroupServerSetupV1_3 with Default * An unclaimed (PSU-less) consent lodged under testConsumer, built straight through the provider * -- the shape POST /consents leaves behind for a client_credentials caller. */ - def createUnclaimedBerlinGroupConsent(): ConsentTrait = { - val postJsonBody = bgConsentPostBody() + def createUnclaimedBerlinGroupConsent(iban: Option[String] = None): ConsentTrait = { + val postJsonBody = bgConsentPostBody(iban) val validUntilDate = BgSpecValidation.getDate(postJsonBody.validUntil) val createdConsent = Consents.consentProvider.vend.createBerlinGroupConsent( diff --git a/obp-api/src/test/scala/code/api/berlin/group/v1_3/DeleteAccountCascadeConsentChallengeTest.scala b/obp-api/src/test/scala/code/api/berlin/group/v1_3/DeleteAccountCascadeConsentChallengeTest.scala new file mode 100644 index 0000000000..3a5252bd41 --- /dev/null +++ b/obp-api/src/test/scala/code/api/berlin/group/v1_3/DeleteAccountCascadeConsentChallengeTest.scala @@ -0,0 +1,97 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2026, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.api.berlin.group.v1_3 + +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.StartConsentAuthorisationJson +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole +import code.entitlement.Entitlement +import code.model.dataAccess.BankAccountRouting +import code.transactionChallenge.MappedExpectedChallengeAnswer +import com.openbankproject.commons.model.enums.AccountRoutingScheme +import net.liftweb.mapper.By +import org.scalatest.Tag + +/** + * A Berlin Group consent names its accounts when it is lodged, so once its PSU starts the + * authorisation the consent is bound to an account and has a challenge nobody has answered yet. + * Deleting that account with the cascading endpoint must take the challenge along; a challenge of a + * consent bound to another account stays. + */ +class DeleteAccountCascadeConsentChallengeTest extends BerlinGroupConsentFixtures { + + object DeleteAccountCascadeConsentChallenge extends Tag("DeleteAccountCascadeConsentChallenge") + + private def ibanOf(bankId: String, accountId: String): String = + BankAccountRouting.find( + By(BankAccountRouting.BankId, bankId), + By(BankAccountRouting.AccountId, accountId), + By(BankAccountRouting.AccountRoutingScheme, AccountRoutingScheme.IBAN.toString) + ).openOrThrowException(s"no IBAN for $bankId/$accountId").accountRouting.address + + private def challengesOf(consentId: String): Int = + MappedExpectedChallengeAnswer.findAll(By(MappedExpectedChallengeAnswer.ConsentId, consentId)).size + + // The consent is lodged for one account and resourceUser1 starts its authorisation, which binds + // the PSU and mints the challenge; the challenge is left unanswered. + private def consentAwaitingItsChallenge(bankId: String, accountId: String): String = { + val consentId = createUnclaimedBerlinGroupConsent(Some(ibanOf(bankId, accountId))).consentId + val started = makePostRequest( + (V1_3_BG / "consents" / consentId / "authorisations").POST <@ (user1), + """{"scaAuthenticationData":""}""") + started.code should equal(201) + started.body.extract[StartConsentAuthorisationJson].authorisationId should not be empty + consentId + } + + feature("Deleting an account by cascade deletes the challenges of the consents bound to it") { + scenario("A Berlin Group consent whose challenge is still unanswered", DeleteAccountCascadeConsentChallenge) { + setPropsValues("suggested_default_sca_method" -> "DUMMY") + Given("Two accounts resourceUser1 holds, each named by a consent awaiting its challenge") + val held = ibanAddressableAccountsHeldBy(resourceUser1).toList.sorted + held.size should be >= 2 + val (bankId, accountId) = held.head + val (otherBankId, otherAccountId) = held(1) + val consent = consentAwaitingItsChallenge(bankId, accountId) + val otherConsent = consentAwaitingItsChallenge(otherBankId, otherAccountId) + challengesOf(consent) should equal(1) + challengesOf(otherConsent) should equal(1) + + When("The first account is deleted with the cascading endpoint") + Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) + val request = (baseRequest / "obp" / "v4.0.0" / "management" / "cascading" / "banks" / bankId / + "accounts" / accountId).DELETE <@ (user1) + makeDeleteRequest(request).code should equal(200) + + Then("The challenge of the consent bound to the deleted account is gone") + challengesOf(consent) should equal(0) + And("The challenge of the consent bound to the other account is kept") + challengesOf(otherConsent) should equal(1) + } + } +} diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala index 1a4b90abb0..1f85427746 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/DeleteAccountCascadeTest.scala @@ -38,10 +38,8 @@ import code.api.util.ErrorMessages.{UserHasMissingRoles, AuthenticatedUserIsRequ import code.api.v3_1_0.CreateAccountResponseJsonV310 import code.api.v4_0_0.Http4s400.Implementations4_0_0 import code.cards.MappedPhysicalCard -import code.consent.Consents import code.entitlement.{Entitlement, MappedEntitlement} import code.model.dataAccess.MappedBankAccount -import code.transactionChallenge.MappedExpectedChallengeAnswer import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.{AccountId, AmountOfMoneyJsonV121, BankId, ErrorMessage} import com.openbankproject.commons.util.ApiVersion @@ -247,55 +245,4 @@ class DeleteAccountCascadeTest extends V400ServerSetup { } } - feature(s"test $ApiEndpoint1 version $VersionOfApi - consent challenges") { - scenario("Deleting an account deletes the SCA challenges of the consents bound to it", ApiEndpoint1, VersionOfApi) { - setPropsValues("suggested_default_sca_method" -> "DUMMY") - Given("Two banks that each have an account with the same ACCOUNT_ID, both held by the same user") - val bankId = BankId(randomString(10)) - val otherBankId = BankId(randomString(10)) - createBank(bankId.value) - createBank(otherBankId.value) - val accountId = AccountId(randomString(10)) - createAccountRelevantResource(Some(resourceUser1), bankId, accountId, "EUR") - createAccountRelevantResource(Some(resourceUser1), otherBankId, accountId, "EUR") - - // A UK consent is lodged without a PSU and bound to the accounts the PSU selects when it - // answers the challenge, the way Portal authorises one. - def authorisedUKConsent(bank: BankId): String = { - val consentId = Consents.consentProvider.vend.saveUKConsent( - user = None, bankId = None, accountIds = None, consumerId = None, - permissions = List("ReadAccountsBasic"), - expirationDateTime = Some(new java.util.Date(System.currentTimeMillis() + 3600000L)), - transactionFromDateTime = None, transactionToDateTime = None, - apiStandard = Some("UKOpenBanking"), apiVersion = Some("4.0.1") - ).openOrThrowException("consent creation failed").consentId - val consentPath = v5_1_0_Request / "banks" / bank.value / "consents" / consentId / "authorise" - val challenge = makePostRequest((consentPath / "challenge").POST <@ (user1), "") - challenge.code should equal(200) - val challengeId = (challenge.body \ "challenge_id").extract[String] - val authorised = makePostRequest(consentPath.POST <@ (user1), - s"""{"account_ids":["${accountId.value}"],"challenge_id":"$challengeId","answer":"123"}""") - authorised.code should equal(200) - consentId - } - def challengesOf(consentId: String): Int = - MappedExpectedChallengeAnswer.findAll(By(MappedExpectedChallengeAnswer.ConsentId, consentId)).size - val consent = authorisedUKConsent(bankId) - val otherBankConsent = authorisedUKConsent(otherBankId) - challengesOf(consent) should equal(1) - challengesOf(otherBankConsent) should equal(1) - - When(deleteAtFirstBank) - Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, ApiRole.canDeleteAccountCascade.toString) - val request = (v4_0_0_Request / "management" / "cascading" / "banks" / bankId.value / - "accounts" / accountId.value).DELETE <@ (user1) - makeDeleteRequest(request).code should equal(200) - - Then("The challenge of the consent bound to the deleted account is gone") - challengesOf(consent) should equal(0) - And("The challenge of the consent bound to the other bank's account is kept") - challengesOf(otherBankConsent) should equal(1) - } - } - }