mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 17:37:00 +00:00
Merge remote-tracking branch 'Simon/develop' into develop
# Conflicts: # obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala # obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala
This commit is contained in:
commit
cb9a3fd1c3
@ -158,6 +158,12 @@ paginator.displayingrecords = Mostrando %s-%s de %s
|
||||
open_bank_project_is = Open Bank Project es
|
||||
and_commercial_licenses = TESOBE y licencias comerciales
|
||||
|
||||
#FAQS
|
||||
what.is.the.correct.base.URL.for.this.instance = \u00bfCu\u00e1l es la URL base correcta para esta caja de arena (Sandbox)?
|
||||
The.base.URL.is = La URL base es
|
||||
Please.make.sure.you.are.using.this.in.all.your.API.calls = Por favor, aseg\u00farese de que utiliza esto en todas sus llamadas a la API
|
||||
|
||||
|
||||
# Country names
|
||||
country_1 = United States
|
||||
country_2 = Afghanistan
|
||||
|
||||
@ -4102,6 +4102,14 @@ object SwaggerDefinitionsJSON {
|
||||
branch_id = branchIdExample.value,
|
||||
account_routings = List(accountRoutingJsonV121)
|
||||
)
|
||||
val createAccountRequestJsonV500 = CreateAccountRequestJsonV500(
|
||||
user_id = Some(userIdExample.value),
|
||||
label = labelExample.value,
|
||||
product_code = productCodeExample.value,
|
||||
balance = Some(amountOfMoneyJsonV121),
|
||||
branch_id = Some(branchIdExample.value),
|
||||
account_routings = Some(List(accountRoutingJsonV121))
|
||||
)
|
||||
|
||||
val settlementAccountRequestJson = SettlementAccountRequestJson(
|
||||
user_id = userIdExample.value,
|
||||
@ -4639,6 +4647,14 @@ object SwaggerDefinitionsJSON {
|
||||
description = descriptionExample.value,
|
||||
meta = metaJson,
|
||||
)
|
||||
val putProductJsonV500 = PutProductJsonV500(
|
||||
parent_product_code = parentProductCodeExample.value,
|
||||
name = productNameExample.value,
|
||||
more_info_url = Some(moreInfoUrlExample.value),
|
||||
terms_and_conditions_url = Some(termsAndConditionsUrlExample.value),
|
||||
description = Some(descriptionExample.value),
|
||||
meta = Some(metaJson)
|
||||
)
|
||||
|
||||
val createMessageJsonV400 = CreateMessageJsonV400(
|
||||
message = messageExample.value,
|
||||
|
||||
@ -12,23 +12,25 @@ import code.api.v2_1_0.JSONFactory210
|
||||
import code.api.v3_0_0.JSONFactory300
|
||||
import code.api.v3_1_0._
|
||||
import code.api.v4_0_0.JSONFactory400.createCustomersMinimalJson
|
||||
import code.api.v4_0_0.{JSONFactory400, PutProductJsonV400}
|
||||
import code.api.v5_0_0.JSONFactory500.createPhysicalCardJson
|
||||
import code.bankconnectors.Connector
|
||||
import code.consent.{ConsentRequests, Consents}
|
||||
import code.entitlement.Entitlement
|
||||
import code.model.dataAccess.BankAccountCreation
|
||||
import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _}
|
||||
import code.util.Helper
|
||||
import code.views.Views
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model.enums.StrongCustomerAuthentication
|
||||
import com.openbankproject.commons.model.{AccountId, BankId, BankIdAccountId, CardAction, CardAttributeCommons, CardCollectionInfo, CardPostedInfo, CardReplacementInfo, CardReplacementReason, CreditLimit, CreditRating, CustomerFaceImage, PinResetInfo, PinResetReason, TransactionRequestType, UserAuthContextUpdateStatus, View, ViewId}
|
||||
import com.openbankproject.commons.model.{AccountId, AccountRouting, BankId, BankIdAccountId, CardAction, CardAttributeCommons, CardCollectionInfo, CardPostedInfo, CardReplacementInfo, CardReplacementReason, CreditLimit, CreditRating, CustomerFaceImage, PinResetInfo, PinResetReason, ProductCode, TransactionRequestType, UserAuthContextUpdateStatus, View, ViewId}
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.common.Full
|
||||
import net.liftweb.common.{Empty, Full}
|
||||
import net.liftweb.http.Req
|
||||
import net.liftweb.http.rest.RestHelper
|
||||
import net.liftweb.json
|
||||
import net.liftweb.json.compactRender
|
||||
import net.liftweb.json.{Extraction, compactRender, prettyRender}
|
||||
import net.liftweb.util.Helpers.tryo
|
||||
import net.liftweb.util.Props
|
||||
|
||||
@ -244,6 +246,144 @@ trait APIMethods500 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resourceDocs += ResourceDoc(
|
||||
createAccount,
|
||||
implementedInApiVersion,
|
||||
"createAccount",
|
||||
"PUT",
|
||||
"/banks/BANK_ID/accounts/ACCOUNT_ID",
|
||||
"Create Account",
|
||||
"""Create Account at bank specified by BANK_ID with Id specified by ACCOUNT_ID.
|
||||
|
|
||||
|The User can create an Account for themself - or - the User that has the USER_ID specified in the POST body.
|
||||
|
|
||||
|If the PUT body USER_ID *is* specified, the logged in user must have the Role canCreateAccount. Once created, the Account will be owned by the User specified by USER_ID.
|
||||
|
|
||||
|If the PUT body USER_ID is *not* specified, the account will be owned by the logged in User.
|
||||
|
|
||||
|The 'product_code' field SHOULD be a product_code from Product.
|
||||
|If the 'product_code' matches a product_code from Product, account attributes will be created that match the Product Attributes.
|
||||
|
|
||||
|Note: The Amount MUST be zero.""".stripMargin,
|
||||
createAccountRequestJsonV500,
|
||||
createAccountResponseJsonV310,
|
||||
List(
|
||||
InvalidJsonFormat,
|
||||
BankNotFound,
|
||||
UserNotLoggedIn,
|
||||
InvalidUserId,
|
||||
InvalidAccountIdFormat,
|
||||
InvalidBankIdFormat,
|
||||
UserNotFoundById,
|
||||
UserHasMissingRoles,
|
||||
InvalidAccountBalanceAmount,
|
||||
InvalidAccountInitialBalance,
|
||||
InitialBalanceMustBeZero,
|
||||
InvalidAccountBalanceCurrency,
|
||||
AccountIdAlreadyExists,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagAccount,apiTagOnboarding, apiTagNewStyle),
|
||||
Some(List(canCreateAccount))
|
||||
)
|
||||
|
||||
|
||||
lazy val createAccount : OBPEndpoint = {
|
||||
// Create a new account
|
||||
case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: Nil JsonPut json -> _ => {
|
||||
cc =>{
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
(account, callContext) <- Connector.connector.vend.checkBankAccountExists(bankId, accountId, callContext)
|
||||
_ <- Helper.booleanToFuture(AccountIdAlreadyExists, cc=callContext){
|
||||
account.isEmpty
|
||||
}
|
||||
failMsg = s"$InvalidJsonFormat The Json body should be the ${prettyRender(Extraction.decompose(createAccountRequestJsonV310))} "
|
||||
createAccountJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
|
||||
json.extract[CreateAccountRequestJsonV500]
|
||||
}
|
||||
loggedInUserId = u.userId
|
||||
userIdAccountOwner = createAccountJson.user_id match {
|
||||
case Some(userId) => userId
|
||||
case _ => loggedInUserId
|
||||
}
|
||||
_ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){
|
||||
isValidID(accountId.value)
|
||||
}
|
||||
_ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext){
|
||||
isValidID(accountId.value)
|
||||
}
|
||||
(postedOrLoggedInUser,callContext) <- NewStyle.function.findByUserId(userIdAccountOwner, callContext)
|
||||
// User can create account for self or an account for another user if they have CanCreateAccount role
|
||||
_ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext){
|
||||
isValidID(accountId.value)
|
||||
}
|
||||
_ <- {userIdAccountOwner == loggedInUserId} match {
|
||||
case true => Future.successful(Full(Unit))
|
||||
case false =>
|
||||
NewStyle.function.hasEntitlement(
|
||||
bankId.value,
|
||||
loggedInUserId,
|
||||
canCreateAccount,
|
||||
callContext,
|
||||
s"${UserHasMissingRoles} $canCreateAccount or create account for self"
|
||||
)
|
||||
}
|
||||
initialBalanceAsString = createAccountJson.balance.map(_.amount).getOrElse("0")
|
||||
accountType = createAccountJson.product_code
|
||||
accountLabel = createAccountJson.label
|
||||
initialBalanceAsNumber <- NewStyle.function.tryons(InvalidAccountInitialBalance, 400, callContext) {
|
||||
BigDecimal(initialBalanceAsString)
|
||||
}
|
||||
_ <- Helper.booleanToFuture(InitialBalanceMustBeZero, cc=callContext){0 == initialBalanceAsNumber}
|
||||
_ <- Helper.booleanToFuture(InvalidISOCurrencyCode, cc=callContext){isValidCurrencyISOCode(createAccountJson.balance.map(_.currency).getOrElse("EUR"))}
|
||||
currency = createAccountJson.balance.map(_.currency).getOrElse("EUR")
|
||||
(_, callContext ) <- NewStyle.function.getBank(bankId, callContext)
|
||||
_ <- Helper.booleanToFuture(s"$InvalidAccountRoutings Duplication detected in account routings, please specify only one value per routing scheme", 400, cc=callContext){
|
||||
createAccountJson.account_routings.getOrElse(Nil).map(_.scheme).distinct.size == createAccountJson.account_routings.getOrElse(Nil).size
|
||||
}
|
||||
alreadyExistAccountRoutings <- Future.sequence(createAccountJson.account_routings.getOrElse(Nil).map(accountRouting =>
|
||||
NewStyle.function.getAccountRouting(Some(bankId), accountRouting.scheme, accountRouting.address, callContext).map(_ => Some(accountRouting)).fallbackTo(Future.successful(None))
|
||||
))
|
||||
alreadyExistingAccountRouting = alreadyExistAccountRoutings.collect {
|
||||
case Some(accountRouting) => s"bankId: $bankId, scheme: ${accountRouting.scheme}, address: ${accountRouting.address}"
|
||||
}
|
||||
_ <- Helper.booleanToFuture(s"$AccountRoutingAlreadyExist (${alreadyExistingAccountRouting.mkString("; ")})", cc=callContext) {
|
||||
alreadyExistingAccountRouting.isEmpty
|
||||
}
|
||||
(bankAccount,callContext) <- NewStyle.function.createBankAccount(
|
||||
bankId,
|
||||
accountId,
|
||||
accountType,
|
||||
accountLabel,
|
||||
currency,
|
||||
initialBalanceAsNumber,
|
||||
postedOrLoggedInUser.name,
|
||||
createAccountJson.branch_id.getOrElse(""),
|
||||
createAccountJson.account_routings.getOrElse(Nil).map(r => AccountRouting(r.scheme, r.address)),
|
||||
callContext
|
||||
)
|
||||
(productAttributes, callContext) <- NewStyle.function.getProductAttributesByBankAndCode(bankId, ProductCode(accountType), callContext)
|
||||
(accountAttributes, callContext) <- NewStyle.function.createAccountAttributes(
|
||||
bankId,
|
||||
accountId,
|
||||
ProductCode(accountType),
|
||||
productAttributes,
|
||||
None,
|
||||
callContext: Option[CallContext]
|
||||
)
|
||||
} yield {
|
||||
//1 Create or Update the `Owner` for the new account
|
||||
//2 Add permission to the user
|
||||
//3 Set the user as the account holder
|
||||
BankAccountCreation.setAccountHolderAndRefreshUserAccountAccess(bankId, accountId, postedOrLoggedInUser, callContext)
|
||||
(JSONFactory310.createAccountJSON(userIdAccountOwner, bankAccount, accountAttributes), HttpCode.`201`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
@ -1021,6 +1161,86 @@ trait APIMethods500 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
createProduct,
|
||||
implementedInApiVersion,
|
||||
nameOf(createProduct),
|
||||
"PUT",
|
||||
"/banks/BANK_ID/products/PRODUCT_CODE",
|
||||
"Create Product",
|
||||
s"""Create or Update Product for the Bank.
|
||||
|
|
||||
|
|
||||
|Typical Super Family values / Asset classes are:
|
||||
|
|
||||
|Debt
|
||||
|Equity
|
||||
|FX
|
||||
|Commodity
|
||||
|Derivative
|
||||
|
|
||||
|$productHiearchyAndCollectionNote
|
||||
|
|
||||
|
|
||||
|${authenticationRequiredMessage(true) }
|
||||
|
|
||||
|
|
||||
|""",
|
||||
putProductJsonV500,
|
||||
productJsonV400.copy(attributes = None, fees = None),
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
$BankNotFound,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagProduct, apiTagNewStyle),
|
||||
Some(List(canCreateProduct, canCreateProductAtAnyBank))
|
||||
)
|
||||
lazy val createProduct: OBPEndpoint = {
|
||||
case "banks" :: BankId(bankId) :: "products" :: ProductCode(productCode) :: Nil JsonPut json -> _ => {
|
||||
cc =>
|
||||
for {
|
||||
(Full(u), callContext) <- SS.user
|
||||
_ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext)
|
||||
failMsg = s"$InvalidJsonFormat The Json body should be the $PutProductJsonV400 "
|
||||
product <- NewStyle.function.tryons(failMsg, 400, callContext) {
|
||||
json.extract[PutProductJsonV500]
|
||||
}
|
||||
parentProductCode <- product.parent_product_code.trim.nonEmpty match {
|
||||
case false =>
|
||||
Future(Empty)
|
||||
case true =>
|
||||
Future(Connector.connector.vend.getProduct(bankId, ProductCode(product.parent_product_code))) map {
|
||||
getFullBoxOrFail(_, callContext, ParentProductNotFoundByProductCode + " {" + product.parent_product_code + "}", 400)
|
||||
}
|
||||
}
|
||||
success <- Future(Connector.connector.vend.createOrUpdateProduct(
|
||||
bankId = bankId.value,
|
||||
code = productCode.value,
|
||||
parentProductCode = parentProductCode.map(_.code.value).toOption,
|
||||
name = product.name,
|
||||
category = null,
|
||||
family = null,
|
||||
superFamily = null,
|
||||
moreInfoUrl = product.more_info_url.getOrElse(""),
|
||||
termsAndConditionsUrl = product.terms_and_conditions_url.getOrElse(""),
|
||||
details = null,
|
||||
description = product.description.getOrElse(""),
|
||||
metaLicenceId = product.meta.map(_.license.id).getOrElse(""),
|
||||
metaLicenceName = product.meta.map(_.license.name).getOrElse("")
|
||||
)) map {
|
||||
connectorEmptyResponse(_, callContext)
|
||||
}
|
||||
} yield {
|
||||
(JSONFactory400.createProductJson(success), HttpCode.`201`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
addCardForBank,
|
||||
implementedInApiVersion,
|
||||
|
||||
@ -29,6 +29,7 @@ package code.api.v5_0_0
|
||||
import java.util.Date
|
||||
import code.api.util.APIUtil.{stringOptionOrNull, stringOrNull}
|
||||
import code.api.v1_2_1.BankRoutingJsonV121
|
||||
import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, MetaJsonV140}
|
||||
import code.api.v1_3_0.JSONFactory1_3_0.{cardActionsToString, createAccountJson, createPinResetJson, createReplacementJson}
|
||||
import code.api.v1_3_0.{PinResetJSON, ReplacementJSON}
|
||||
import code.api.v1_4_0.JSONFactory1_4_0.CustomerFaceImageJson
|
||||
@ -61,6 +62,15 @@ case class BankJson500(
|
||||
attributes: Option[List[BankAttributeBankResponseJsonV400]]
|
||||
)
|
||||
|
||||
case class CreateAccountRequestJsonV500(
|
||||
user_id : Option[String],
|
||||
label : String,
|
||||
product_code : String,
|
||||
balance : Option[AmountOfMoneyJsonV121],
|
||||
branch_id : Option[String],
|
||||
account_routings: Option[List[AccountRoutingJsonV121]]
|
||||
)
|
||||
|
||||
case class PostCustomerJsonV500(
|
||||
legal_name: String,
|
||||
mobile_phone_number: String,
|
||||
@ -81,6 +91,15 @@ case class PostCustomerJsonV500(
|
||||
name_suffix: Option[String] = None
|
||||
)
|
||||
|
||||
case class PutProductJsonV500(
|
||||
parent_product_code: String,
|
||||
name: String,
|
||||
more_info_url: Option[String] = None,
|
||||
terms_and_conditions_url: Option[String] = None,
|
||||
description: Option[String] = None,
|
||||
meta: Option[MetaJsonV140] = None,
|
||||
)
|
||||
|
||||
case class UserAuthContextJsonV500(
|
||||
user_auth_context_id: String,
|
||||
user_id: String,
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
<hr/>
|
||||
<h3>
|
||||
<button aria-expanded="false" onclick="mouseClick(this)" onKeyDown="EnterKeyPressed(this)">
|
||||
What is the correct base URL for this sandbox?
|
||||
<lift:loc locid="what.is.the.correct.base.URL.for.this.instance">What is the correct base URL for this sandbox5?</lift:loc>
|
||||
</button>
|
||||
</h3>
|
||||
<p id="main-faq-item0" class="collapse">
|
||||
The base URL is<br/>
|
||||
<lift:loc locid="The.base.URL.is">The base URL is</lift:loc><br/>
|
||||
<a class="api-link" data-lift="WebUI.apiLink"
|
||||
href="">http://apisandbox.openbankproject.com</a><br/>
|
||||
Please make sure you are using this in all your API calls
|
||||
<lift:loc locid="Please.make.sure.you.are.using.this.in.all.your.API.calls">Please make sure you are using this in all your API calls</lift:loc><br/>
|
||||
</p>
|
||||
<img src="/media/images/icons/chevron_down_thick.svg" alt="">
|
||||
</div>
|
||||
|
||||
244
obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala
Normal file
244
obp-api/src/test/scala/code/api/v5_0_0/AccountTest.scala
Normal file
@ -0,0 +1,244 @@
|
||||
package code.api.v5_0_0
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import code.api.Constant
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.APIUtil.extractErrorMessageCode
|
||||
import code.api.util.ApiRole
|
||||
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
|
||||
import code.api.v2_0_0.BasicAccountJSON
|
||||
import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0
|
||||
import code.api.v3_0_0.CoreAccountsJsonV300
|
||||
import code.api.v3_0_0.OBPAPI3_0_0.Implementations3_0_0
|
||||
import code.api.v3_1_0.CreateAccountResponseJsonV310
|
||||
import code.api.v4_0_0.{AccountsBalancesJsonV400, ModeratedCoreAccountJsonV400}
|
||||
import code.api.v5_0_0.OBPAPI5_0_0.Implementations5_0_0
|
||||
import code.entitlement.Entitlement
|
||||
import code.setup.DefaultUsers
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.enums.AccountRoutingScheme
|
||||
import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, ErrorMessage}
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
class AccountTest extends V500ServerSetup with DefaultUsers {
|
||||
|
||||
object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString)
|
||||
object ApiEndpoint2 extends Tag(nameOf(Implementations5_0_0.createAccount))
|
||||
//We need this endpoint to test the result
|
||||
object ApiEndpoint4 extends Tag(nameOf(Implementations3_0_0.corePrivateAccountsAllBanks))
|
||||
object ApiEndpoint5 extends Tag(nameOf(Implementations2_0_0.getPrivateAccountsAtOneBank))
|
||||
object ApiEndpoint6 extends Tag(nameOf(Implementations3_0_0.getPrivateAccountById))
|
||||
|
||||
lazy val testBankId = testBankId1
|
||||
lazy val putCreateAccountJSONV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310.copy(user_id = resourceUser1.userId, balance = AmountOfMoneyJsonV121("EUR","0"))
|
||||
lazy val putCreateAccountOtherUserJsonV310 = SwaggerDefinitionsJSON.createAccountRequestJsonV310
|
||||
.copy(user_id = resourceUser2.userId, balance = AmountOfMoneyJsonV121("EUR","0"),
|
||||
account_routings = List(AccountRoutingJsonV121(Random.nextString(10), Random.nextString(10))))
|
||||
|
||||
val userAccountId = UUID.randomUUID.toString
|
||||
val user2AccountId = UUID.randomUUID.toString
|
||||
|
||||
|
||||
feature(s"Create Account $VersionOfApi - Unauthorized access") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) {
|
||||
When(s"We make a request $VersionOfApi")
|
||||
val request310 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "ACCOUNT_ID" ).PUT
|
||||
val response310 = makePutRequest(request310, write(putCreateAccountJSONV310))
|
||||
Then("We should get a 401")
|
||||
response310.code should equal(401)
|
||||
And("error should be " + UserNotLoggedIn)
|
||||
response310.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
feature(s"Create Account $VersionOfApi - Authorized access") {
|
||||
scenario("We will call the endpoint with user credentials", ApiEndpoint2, VersionOfApi) {
|
||||
When(s"We make a request $VersionOfApi")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString())
|
||||
val request = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID" ).PUT <@(user1)
|
||||
val response = makePutRequest(request, write(putCreateAccountJSONV310))
|
||||
Then("We should get a 201")
|
||||
response.code should equal(201)
|
||||
//for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method,
|
||||
//it is an asynchronous process, need some time to be done.
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
|
||||
|
||||
val account = response.body.extract[CreateAccountResponseJsonV310]
|
||||
account.product_code should be (putCreateAccountJSONV310.product_code)
|
||||
account.`label` should be (putCreateAccountJSONV310.`label`)
|
||||
account.balance.amount.toDouble should be (putCreateAccountJSONV310.balance.amount.toDouble)
|
||||
account.balance.currency should be (putCreateAccountJSONV310.balance.currency)
|
||||
account.branch_id should be (putCreateAccountJSONV310.branch_id)
|
||||
account.user_id should be (putCreateAccountJSONV310.user_id)
|
||||
account.label should be (putCreateAccountJSONV310.label)
|
||||
account.account_routings should be (putCreateAccountJSONV310.account_routings)
|
||||
|
||||
|
||||
//We need to waite some time for the account creation, because we introduce `AuthUser.refreshUser(user, callContext)`
|
||||
//It may not finished when we call the get accounts directly.
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
|
||||
Then(s"we call $ApiEndpoint4 to get the account back")
|
||||
val requestApiEndpoint4 = (v5_0_0_Request / "my" / "accounts" ).PUT <@(user1)
|
||||
val responseApiEndpoint4 = makeGetRequest(requestApiEndpoint4)
|
||||
|
||||
|
||||
|
||||
responseApiEndpoint4.code should equal(200)
|
||||
val accounts = responseApiEndpoint4.body.extract[CoreAccountsJsonV300].accounts
|
||||
accounts.map(_.id).toList.toString() contains(account.account_id) should be (true)
|
||||
|
||||
Then(s"we call $ApiEndpoint5 to get the account back")
|
||||
val requestApiEndpoint5 = (v5_0_0_Request /"banks" / testBankId.value / "accounts").GET <@ (user1)
|
||||
val responseApiEndpoint5 = makeGetRequest(requestApiEndpoint5)
|
||||
|
||||
Then("We should get a 200")
|
||||
responseApiEndpoint5.code should equal(200)
|
||||
responseApiEndpoint5.body.extract[List[BasicAccountJSON]].toList.toString() contains(account.account_id) should be (true)
|
||||
|
||||
|
||||
val requestGetApiEndpoint3 = (v5_0_0_Request / "banks" / testBankId.value / "balances").GET <@ (user1)
|
||||
val responseGetApiEndpoint3 = makeGetRequest(requestGetApiEndpoint3)
|
||||
responseGetApiEndpoint3.code should equal(200)
|
||||
responseGetApiEndpoint3.body.extract[AccountsBalancesJsonV400].accounts.map(_.account_id) contains(account.account_id) should be (true)
|
||||
|
||||
|
||||
Then(s"We make a request $VersionOfApi but with other user")
|
||||
val request500WithNewAccountId = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID2" ).PUT <@(user2)
|
||||
val responseWithNoRole = makePutRequest(request500WithNewAccountId, write(putCreateAccountOtherUserJsonV310))
|
||||
Then("We should get a 403 and some error message")
|
||||
responseWithNoRole.code should equal(403)
|
||||
responseWithNoRole.body.toString contains(extractErrorMessageCode(UserHasMissingRoles)) should be (true)
|
||||
|
||||
|
||||
Then("We grant the roles and test it again")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser2.userId, ApiRole.canCreateAccount.toString)
|
||||
val responseWithOtherUserV500 = makePutRequest(request500WithNewAccountId, write(putCreateAccountOtherUserJsonV310))
|
||||
|
||||
val account2 = responseWithOtherUserV500.body.extract[CreateAccountResponseJsonV310]
|
||||
account2.product_code should be (putCreateAccountOtherUserJsonV310.product_code)
|
||||
account2.`label` should be (putCreateAccountOtherUserJsonV310.`label`)
|
||||
account2.balance.amount.toDouble should be (putCreateAccountOtherUserJsonV310.balance.amount.toDouble)
|
||||
account2.balance.currency should be (putCreateAccountOtherUserJsonV310.balance.currency)
|
||||
account2.branch_id should be (putCreateAccountOtherUserJsonV310.branch_id)
|
||||
account2.user_id should be (putCreateAccountOtherUserJsonV310.user_id)
|
||||
account2.label should be (putCreateAccountOtherUserJsonV310.label)
|
||||
account2.account_routings should be (putCreateAccountOtherUserJsonV310.account_routings)
|
||||
|
||||
}
|
||||
|
||||
scenario("Create new account will have system owner view, and other use also have the system owner view should not get the account back", ApiEndpoint2, VersionOfApi) {
|
||||
When(s"We make a request $VersionOfApi")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString)
|
||||
val request500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId ).PUT <@(user1)
|
||||
val putCreateAccountJson = putCreateAccountJSONV310.copy(account_routings = List(AccountRoutingJsonV121("AccountNumber", "15649885656")))
|
||||
val response500 = makePutRequest(request500, write(putCreateAccountJson))
|
||||
Then("We should get a 201")
|
||||
response500.code should equal(201)
|
||||
//for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method,
|
||||
//it is an asynchronous process, need some time to be done.
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
|
||||
val account = response500.body.extract[CreateAccountResponseJsonV310]
|
||||
account.product_code should be (putCreateAccountJson.product_code)
|
||||
account.`label` should be (putCreateAccountJson.`label`)
|
||||
account.balance.amount.toDouble should be (putCreateAccountJson.balance.amount.toDouble)
|
||||
account.balance.currency should be (putCreateAccountJson.balance.currency)
|
||||
account.branch_id should be (putCreateAccountJson.branch_id)
|
||||
account.user_id should be (putCreateAccountJson.user_id)
|
||||
account.label should be (putCreateAccountJson.label)
|
||||
account.account_routings should be (putCreateAccountJson.account_routings)
|
||||
|
||||
|
||||
Then(s"we call $ApiEndpoint6 to get the account back")
|
||||
val requestApiEndpoint6 = (v5_0_0_Request /"banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID/ "account" ).GET <@(user1)
|
||||
val responseApiEndpoint6 = makeGetRequest(requestApiEndpoint6)
|
||||
|
||||
responseApiEndpoint6.code should equal(200)
|
||||
val accountEndpoint6 = responseApiEndpoint6.body.extract[ModeratedCoreAccountJsonV400]
|
||||
accountEndpoint6.id should be (userAccountId)
|
||||
accountEndpoint6.label should be (account.label)
|
||||
|
||||
Then(s"we prepare the user2 will create a new account ($ApiEndpoint2)and he will have system view, and to call get account ($ApiEndpoint6) and compare the result.")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser2.userId, ApiRole.canCreateAccount.toString)
|
||||
val requestUser2_500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId ).PUT <@(user2)
|
||||
val responseUser2_500 = makePutRequest(requestUser2_500, write(putCreateAccountJSONV310.copy(user_id = resourceUser2.userId, balance = AmountOfMoneyJsonV121("EUR","0"))))
|
||||
Then("We should get a 201")
|
||||
responseUser2_500.code should equal(201)
|
||||
|
||||
//for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method,
|
||||
//it is an asynchronous process, need some time to be done.
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
|
||||
|
||||
Then(s"we call $ApiEndpoint6 to get the account back by user2")
|
||||
val requestApiUser2Endpoint6 = (v5_0_0_Request /"banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID/ "account" ).GET <@(user2)
|
||||
val responseApiUser2Endpoint6 = makeGetRequest(requestApiUser2Endpoint6)
|
||||
//This mean, the user2 can not get access to user1's account!
|
||||
responseApiUser2Endpoint6.code should not equal(200)
|
||||
|
||||
}
|
||||
|
||||
scenario("Create new account with an already existing routing scheme/address should not create the account", ApiEndpoint2, VersionOfApi) {
|
||||
When(s"We make a request $VersionOfApi to create the first account")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString)
|
||||
val request310_1 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / "TEST_ACCOUNT_ID_1" ).PUT <@(user1)
|
||||
val response310_1 = makePutRequest(request310_1, write(putCreateAccountJSONV310))
|
||||
Then("We should get a 201")
|
||||
response310_1.code should equal(201)
|
||||
|
||||
//for create account endpoint, we need to wait for `setAccountHolderAndRefreshUserAccountAccess` method,
|
||||
//it is an asynchronous process, need some time to be done.
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
val account = response310_1.body.extract[CreateAccountResponseJsonV310]
|
||||
account.product_code should be (putCreateAccountJSONV310.product_code)
|
||||
account.`label` should be (putCreateAccountJSONV310.`label`)
|
||||
account.balance.amount.toDouble should be (putCreateAccountJSONV310.balance.amount.toDouble)
|
||||
account.balance.currency should be (putCreateAccountJSONV310.balance.currency)
|
||||
account.branch_id should be (putCreateAccountJSONV310.branch_id)
|
||||
account.user_id should be (putCreateAccountJSONV310.user_id)
|
||||
account.label should be (putCreateAccountJSONV310.label)
|
||||
account.account_routings should be (putCreateAccountJSONV310.account_routings)
|
||||
|
||||
When(s"We make a request $VersionOfApi to create the second account with an already existing scheme/address")
|
||||
val request310_2 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId ).PUT <@(user1)
|
||||
val response310_2 = makePutRequest(request310_2, write(putCreateAccountJSONV310))
|
||||
Then("We should get a 400 in the createAccount response")
|
||||
response310_2.code should equal(400)
|
||||
response310_2.body.toString should include("OBP-30115: Account Routing already exist.")
|
||||
|
||||
Then(s"The second account should not be created")
|
||||
val requestApiGetAccount = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / user2AccountId / Constant.SYSTEM_OWNER_VIEW_ID / "account" ).GET <@(user1)
|
||||
val responseApiGetAccount = makeGetRequest(requestApiGetAccount)
|
||||
And("We should get a 404 in the getAccount response")
|
||||
responseApiGetAccount.code should equal(404)
|
||||
}
|
||||
|
||||
scenario("Create new account with a duplication in routing scheme should not create the account", ApiEndpoint2, VersionOfApi) {
|
||||
When(s"We make a request $VersionOfApi to create the account")
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId.value, resourceUser1.userId, ApiRole.canCreateAccount.toString)
|
||||
val request500 = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId ).PUT <@(user1)
|
||||
val putCreateAccountJsonWithRoutingSchemeDuplication = putCreateAccountJSONV310.copy(account_routings =
|
||||
List(AccountRoutingJsonV121(AccountRoutingScheme.IBAN.toString, Random.nextString(10)),
|
||||
AccountRoutingJsonV121(AccountRoutingScheme.IBAN.toString, Random.nextString(10))))
|
||||
val response500 = makePutRequest(request500, write(putCreateAccountJsonWithRoutingSchemeDuplication))
|
||||
Then("We should get a 400 in the createAccount response")
|
||||
response500.code should equal(400)
|
||||
response500.body.toString should include ("Duplication detected in account routings, please specify only one value per routing scheme")
|
||||
|
||||
Then(s"The account should not be created")
|
||||
val requestApiGetAccount = (v5_0_0_Request / "banks" / testBankId.value / "accounts" / userAccountId / Constant.SYSTEM_OWNER_VIEW_ID / "account" ).GET <@(user1)
|
||||
val responseApiGetAccount = makeGetRequest(requestApiGetAccount)
|
||||
And("We should get a 404 in the getAccount response")
|
||||
responseApiGetAccount.code should equal(404)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
169
obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala
Normal file
169
obp-api/src/test/scala/code/api/v5_0_0/ProductTest.scala
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v5_0_0
|
||||
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.v4_0_0.OBPAPI4_0_0.Implementations4_0_0
|
||||
import code.api.v5_0_0.OBPAPI5_0_0.Implementations5_0_0
|
||||
import code.api.v4_0_0.{ProductJsonV400, ProductsJsonV400}
|
||||
import code.entitlement.Entitlement
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
|
||||
import scala.collection.immutable.Nil
|
||||
|
||||
class ProductTest extends V500ServerSetup {
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
super.beforeAll()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
super.afterAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString)
|
||||
object ApiEndpoint1 extends Tag(nameOf(Implementations5_0_0.createProduct))
|
||||
object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.getProduct))
|
||||
object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getProducts))
|
||||
|
||||
lazy val testBankId = randomBankId
|
||||
lazy val parentPutProductJsonV500: PutProductJsonV500 = SwaggerDefinitionsJSON.putProductJsonV500.copy(parent_product_code ="")
|
||||
def createProduct(code: String, json: PutProductJsonV500): ProductJsonV400 = {
|
||||
When("We try to create a product v4.0.0")
|
||||
val request500 = (v5_0_0_Request / "banks" / testBankId / "products" / code).PUT <@ (user1)
|
||||
val response500 = makePutRequest(request500, write(json))
|
||||
Then("We should get a 201")
|
||||
response500.code should equal(201)
|
||||
val product = response500.body.extract[ProductJsonV400]
|
||||
product.product_code shouldBe code
|
||||
product.parent_product_code shouldBe json.parent_product_code
|
||||
product.bank_id shouldBe testBankId
|
||||
product.name shouldBe json.name
|
||||
product.more_info_url shouldBe json.more_info_url.getOrElse("")
|
||||
product.terms_and_conditions_url shouldBe json.terms_and_conditions_url.getOrElse("")
|
||||
product.description shouldBe json.description.getOrElse("")
|
||||
product
|
||||
}
|
||||
|
||||
feature("Create Product v4.0.0") {
|
||||
scenario("We will call the Add endpoint without a user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v4.0.0")
|
||||
val request400 = (v5_0_0_Request / "banks" / testBankId / "products" / "CODE").PUT
|
||||
val response400 = makePutRequest(request400, write(parentPutProductJsonV500))
|
||||
Then("We should get a 401")
|
||||
response400.code should equal(401)
|
||||
And("error should be " + UserNotLoggedIn)
|
||||
response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn)
|
||||
}
|
||||
scenario("We will call the Add endpoint without a proper role", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v4.0.0")
|
||||
val request500 = (v5_0_0_Request / "banks" / testBankId / "products" / "CODE").PUT <@(user1)
|
||||
val response500 = makePutRequest(request500, write(parentPutProductJsonV500))
|
||||
Then("We should get a 403")
|
||||
response500.code should equal(403)
|
||||
val createProductEntitlements = canCreateProduct :: canCreateProductAtAnyBank :: Nil
|
||||
val createProductEntitlementsRequiredText = UserHasMissingRoles + createProductEntitlements.mkString(" or ")
|
||||
And("error should be " + createProductEntitlementsRequiredText)
|
||||
response500.body.extract[ErrorMessage].message contains (createProductEntitlementsRequiredText) should be (true)
|
||||
}
|
||||
scenario("We will call the Add endpoint with user credentials and role", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, CanCreateProduct.toString)
|
||||
|
||||
// Create an grandparent
|
||||
val grandparent: ProductJsonV400 = createProduct(code = "GRANDPARENT_CODE", json = parentPutProductJsonV500)
|
||||
|
||||
// Create an parent
|
||||
val product: ProductJsonV400 = createProduct(code = "PARENT_CODE", json = parentPutProductJsonV500.copy(parent_product_code = grandparent.product_code))
|
||||
|
||||
// Get
|
||||
val requestGet400 = (v5_0_0_Request / "banks" / product.bank_id / "products" / product.product_code ).GET <@(user1)
|
||||
val responseGet400 = makeGetRequest(requestGet400)
|
||||
Then("We should get a 200")
|
||||
responseGet400.code should equal(200)
|
||||
val product1 = responseGet400.body.extract[ProductJsonV400]
|
||||
|
||||
// Create an child
|
||||
val childPutProductJsonV400 = parentPutProductJsonV500.copy(parent_product_code = product.product_code)
|
||||
createProduct(code = "PRODUCT_CODE", json = childPutProductJsonV400)
|
||||
|
||||
// Get
|
||||
val requestGetAll400 = (v5_0_0_Request / "banks" / product.bank_id / "products").GET <@(user1)
|
||||
val responseGetAll400 = makeGetRequest(requestGetAll400)
|
||||
Then("We should get a 200")
|
||||
responseGetAll400.code should equal(200)
|
||||
val products: ProductsJsonV400 = responseGetAll400.body.extract[ProductsJsonV400]
|
||||
products.products.size shouldBe 3
|
||||
}
|
||||
scenario("We will call the Add endpoint with user credentials and role and minimal PUT JSON", ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, VersionOfApi) {
|
||||
Entitlement.entitlement.vend.addEntitlement(testBankId, resourceUser1.userId, CanCreateProduct.toString)
|
||||
// Create an grandparent
|
||||
val grandparent: ProductJsonV400 = createProduct(
|
||||
code = "GRANDPARENT_CODE",
|
||||
json = PutProductJsonV500(
|
||||
name = parentPutProductJsonV500.name,
|
||||
parent_product_code = parentPutProductJsonV500.parent_product_code
|
||||
)
|
||||
)
|
||||
// Create an parent
|
||||
val product: ProductJsonV400 = createProduct(code = "PARENT_CODE", json = parentPutProductJsonV500.copy(parent_product_code = grandparent.product_code))
|
||||
|
||||
// Get
|
||||
val requestGet400 = (v5_0_0_Request / "banks" / product.bank_id / "products" / product.product_code ).GET <@(user1)
|
||||
val responseGet400 = makeGetRequest(requestGet400)
|
||||
Then("We should get a 200")
|
||||
responseGet400.code should equal(200)
|
||||
val product1 = responseGet400.body.extract[ProductJsonV400]
|
||||
|
||||
// Create an child
|
||||
val childPutProductJsonV400 = parentPutProductJsonV500.copy(parent_product_code = product.product_code)
|
||||
createProduct(code = "PRODUCT_CODE", json = childPutProductJsonV400)
|
||||
|
||||
// Get
|
||||
val requestGetAll400 = (v5_0_0_Request / "banks" / product.bank_id / "products").GET <@(user1)
|
||||
val responseGetAll400 = makeGetRequest(requestGetAll400)
|
||||
Then("We should get a 200")
|
||||
responseGetAll400.code should equal(200)
|
||||
val products: ProductsJsonV400 = responseGetAll400.body.extract[ProductsJsonV400]
|
||||
products.products.size shouldBe 3
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
27
obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala
Normal file
27
obp-api/src/test/scala/code/api/v5_0_0/V500ServerSetup.scala
Normal file
@ -0,0 +1,27 @@
|
||||
package code.api.v5_0_0
|
||||
|
||||
import code.api.v4_0_0.BanksJson400
|
||||
import code.setup.{APIResponse, DefaultUsers, ServerSetupWithTestData}
|
||||
import com.openbankproject.commons.util.ApiShortVersions
|
||||
import dispatch.Req
|
||||
|
||||
import scala.util.Random.nextInt
|
||||
|
||||
trait V500ServerSetup extends ServerSetupWithTestData with DefaultUsers {
|
||||
|
||||
def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0"
|
||||
def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString
|
||||
def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString
|
||||
|
||||
def randomBankId : String = {
|
||||
def getBanksInfo : APIResponse = {
|
||||
val request = v5_0_0_Request / "banks"
|
||||
makeGetRequest(request)
|
||||
}
|
||||
val banksJson = getBanksInfo.body.extract[BanksJson400]
|
||||
val randomPosition = nextInt(banksJson.banks.size)
|
||||
val bank = banksJson.banks(randomPosition)
|
||||
bank.id
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user