diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 557ae450d..4839c6b02 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -371,6 +371,13 @@ 2.9.8 + + + org.atteo + evo-inflector + 1.2.2 + + diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 5328d33f0..c2035f20b 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -39,6 +39,8 @@ connector=mapped #methodRouting.cache.ttl.seconds=30 ## webui props cache time-to-live in seconds #webui.props.cache.ttl.seconds=20 +## DynamicEntity cache time-to-live in seconds +#dynamicEntity.cache.ttl.seconds=20 ## enable logging all the database queries in log file #logging.database.queries.enable=true diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index d96e4b32f..041f578a9 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -54,6 +54,7 @@ import code.customer.internalMapping.MappedCustomerIDMapping import code.customer.{MappedCustomer, MappedCustomerMessage} import code.customeraddress.MappedCustomerAddress import code.database.authorisation.Authorisation +import code.dynamicEntity.DynamicEntity import code.entitlement.MappedEntitlement import code.entitlementrequest.MappedEntitlementRequest import code.fx.{MappedCurrency, MappedFXRate} @@ -655,5 +656,6 @@ object ToSchemify { MethodRouting, WebUiProps, Authorisation, + DynamicEntity, )++ APIBuilder_Connector.allAPIBuilderModels } diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 89ab23f05..ce82e2061 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -351,6 +351,18 @@ object ApiRole { case class CanDeleteWebUiProps(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteWebUiProps = CanDeleteWebUiProps() + case class CanGetDynamicEntities(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDynamicEntities = CanGetDynamicEntities() + + case class CanCreateDynamicEntity(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateDynamicEntity = CanCreateDynamicEntity() + + case class CanUpdateDynamicEntity(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateDynamicEntity = CanUpdateDynamicEntity() + + case class CanDeleteDynamicEntity(requiresBankId: Boolean = false) extends ApiRole + lazy val canDeleteDynamicEntity = CanDeleteDynamicEntity() + private val roles = canSearchAllTransactions :: canSearchAllAccounts :: @@ -390,7 +402,7 @@ object ApiRole { canUpdateCardsForBank :: canGetCardsForBank :: canCreateBranch :: - canCreateBranchAtAnyBank :: + canCreateBranchAtAnyBank :: canUpdateBranch :: canCreateAtm :: canCreateAtmAtAnyBank :: @@ -454,12 +466,16 @@ object ApiRole { canGetMethodRoutings :: canCreateMethodRouting :: canUpdateMethodRouting :: - canDeleteMethodRouting :: + canDeleteMethodRouting :: canUpdateCustomerNumber :: canCreateHistoricalTransaction :: canGetWebUiProps :: canCreateWebUiProps :: canDeleteWebUiProps :: + canGetDynamicEntities :: + canCreateDynamicEntity :: + canUpdateDynamicEntity :: + canDeleteDynamicEntity :: Nil lazy val rolesMappedToClasses = roles.map(_.getClass) diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 92398634c..138d2689e 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -59,6 +59,7 @@ object ApiTag { val apiTagConsent = ResourceDocTag("Consent") val apiTagMethodRouting = ResourceDocTag("Method-Routing") val apiTagWebUiProps = ResourceDocTag("WebUi-Props") + val apiTagDynamicEntity= ResourceDocTag("Dynamic-Entity") // To mark the Berlin Group APIs suggested order of implementation val apiTagBerlinGroupM = ResourceDocTag("Berlin-Group-M") diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 253cfda8f..c69c5f2f2 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -402,6 +402,9 @@ object ErrorMessages { // WebUiProps Exceptions (OBP-8XXXX) val InvalidWebUiProps = "OBP-80001: Incorrect format of name." + // DynamicEntity Exceptions (OBP-9XXXX) + val DynamicEntityNotFoundByDynamicEntityId = "OBP-90001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id." + /////////// diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 12db6fa22..4a9df1d15 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -16,6 +16,7 @@ import code.bankconnectors.Connector import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.consumer.Consumers import code.context.UserAuthContextUpdate +import code.dynamicEntity.{DynamicEntityProvider, DynamicEntityT} import code.entitlement.Entitlement import code.entitlementrequest.EntitlementRequest import code.fx.{FXRate, MappedFXRate, fx} @@ -84,6 +85,7 @@ object NewStyle { object function { + import scala.concurrent.ExecutionContext.Implicits.global def getBranch(bankId : BankId, branchId : BranchId, callContext: Option[CallContext]): OBPReturnType[BranchT] = { @@ -1395,6 +1397,35 @@ object NewStyle { } } } + + def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT): Future[Box[DynamicEntityT]] = Future { + DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) + } + + def deleteDynamicEntity(dynamicEntityId: String): Future[Box[Boolean]] = Future { + DynamicEntityProvider.connectorMethodProvider.vend.delete(dynamicEntityId) + } + + def getDynamicEntityById(dynamicEntityId : String, callContext: Option[CallContext]): OBPReturnType[DynamicEntityT] = { + val dynamicEntityBox: Box[DynamicEntityT] = DynamicEntityProvider.connectorMethodProvider.vend.getById(dynamicEntityId) + val dynamicEntity = unboxFullOrFail(dynamicEntityBox, callContext, DynamicEntityNotFoundByDynamicEntityId) + Future{ + (dynamicEntity, callContext) + } + } + + private[this] val dynamicEntityTTL = APIUtil.getPropsValue(s"dynamicEntity.cache.ttl.seconds", "0").toInt + + def getDynamicEntities(): List[DynamicEntityT] = { + import scala.concurrent.duration._ + + var cacheKey = (randomUUID().toString, randomUUID().toString, randomUUID().toString) + CacheKeyFromArguments.buildCacheKey { + Caching.memoizeSyncWithProvider(Some(cacheKey.toString()))(dynamicEntityTTL second) { + DynamicEntityProvider.connectorMethodProvider.vend.getDynamicEntities() + } + } + } def makeHistoricalPayment( fromAccount: BankAccount, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 461ec1a27..9a933ef62 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -3,13 +3,15 @@ package code.api.v4_0_0 import code.api.ChargePolicy import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._ import code.api.util.APIUtil._ -import code.api.util.ApiRole.canCreateAnyTransactionRequest +import code.api.util.ApiRole._ import code.api.util.ApiTag._ -import code.api.util.ErrorMessages.{AccountNotFound, AllowedAttemptsUsedUp, BankNotFound, CounterpartyBeneficiaryPermit, InsufficientAuthorisationToCreateTransactionRequest, InvalidAccountIdFormat, InvalidBankIdFormat, InvalidChallengeAnswer, InvalidChallengeType, InvalidChargePolicy, InvalidISOCurrencyCode, InvalidJsonFormat, InvalidNumber, InvalidTransactionRequesChallengeId, InvalidTransactionRequestCurrency, InvalidTransactionRequestType, NotPositiveAmount, TransactionDisabled, TransactionRequestStatusNotInitiated, TransactionRequestTypeHasChanged, UnknownError, UserNoPermissionAccessView, UserNotLoggedIn, ViewNotFound} +import code.api.util.ErrorMessages.{AccountNotFound, AllowedAttemptsUsedUp, BankNotFound, CounterpartyBeneficiaryPermit, InsufficientAuthorisationToCreateTransactionRequest, InvalidAccountIdFormat, InvalidBankIdFormat, InvalidChallengeAnswer, InvalidChallengeType, InvalidChargePolicy, InvalidISOCurrencyCode, InvalidJsonFormat, InvalidNumber, InvalidTransactionRequesChallengeId, InvalidTransactionRequestCurrency, InvalidTransactionRequestType, NotPositiveAmount, TransactionDisabled, TransactionRequestStatusNotInitiated, TransactionRequestTypeHasChanged, UnknownError, UserHasMissingRoles, UserNoPermissionAccessView, UserNotLoggedIn, ViewNotFound} import code.api.util.NewStyle.HttpCode import code.api.util._ import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJsonV140} import code.api.v2_1_0._ +import code.api.v3_1_0.ListResult +import code.dynamicEntity.DynamicEntityCommons import code.model.toUserExtended import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._ import code.transactionrequests.TransactionRequests.TransactionRequestTypes @@ -17,15 +19,17 @@ import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{app import code.util.Helper import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model._ -import net.liftweb.common.Full +import net.liftweb.common.{Box, Full} import net.liftweb.http.rest.RestHelper import net.liftweb.json.Serialization.write import net.liftweb.json._ -import net.liftweb.util.Props +import net.liftweb.util.StringHelpers +import org.atteo.evo.inflector.English import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future trait APIMethods400 { self: RestHelper => @@ -60,76 +64,74 @@ trait APIMethods400 { List(UnknownError), Catalogs(Core, PSD2, OBWG), apiTagBank :: apiTagPSD2AIS :: apiTagNewStyle :: Nil) - - lazy val getBanks : OBPEndpoint = { + + lazy val getBanks: OBPEndpoint = { case "banks" :: Nil JsonGet _ => { cc => for { (_, callContext) <- anonymousAccess(cc) (banks, callContext) <- NewStyle.function.getBanks(callContext) - } yield{ + } yield { (JSONFactory400.createBanksJson(banks), HttpCode.`200`(callContext)) } - + } } - - val exchangeRates = + + val exchangeRates = APIUtil.getPropsValue("webui_api_explorer_url", "") + - "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate" + "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate" // This text is used in the various Create Transaction Request resource docs val transactionRequestGeneralText = s"""Initiate a Payment via creating a Transaction Request. | - |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed. + |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed. | - |A `Transaction Request` can have one of several states. + |A `Transaction Request` can have one of several states. | - |`Transactions` are modeled on items in a bank statement that represent the movement of money. + |`Transactions` are modeled on items in a bank statement that represent the movement of money. | - |`Transaction Requests` are requests to move money which may or may not succeeed and thus result in a `Transaction`. + |`Transaction Requests` are requests to move money which may or may not succeeed and thus result in a `Transaction`. | - |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds. + |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds. | - |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate). + |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate). | - |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response. + |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response. |This provides some commonality and one URL for many different payment or transfer types with enough flexibility to validate them differently. | - |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL. + |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL. | - |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types. + |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types. | - |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered. + |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered. | - |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created. + |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created. | - |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account. + |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account. | - |The following static FX rates are available in sandbox mode: + |The following static FX rates are available in sandbox mode: | - |${exchangeRates} + |${exchangeRates} | - | - |Transaction Requests satisfy PSD2 requirements thus: | - |1) A transaction can be initiated by a third party application. + |Transaction Requests satisfy PSD2 requirements thus: | - |2) The customer is informed of the charge that will incurred. + |1) A transaction can be initiated by a third party application. | - |3) The call supports delegated authentication (OAuth) + |2) The customer is informed of the charge that will incurred. | - |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow. + |3) The call supports delegated authentication (OAuth) | - |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests) + |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow. | - |${authenticationRequiredMessage(true)} + |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests) | - |""" - - + |${authenticationRequiredMessage(true)} + | + |""" // ACCOUNT. (we no longer create a resource doc for the general case) @@ -170,7 +172,7 @@ trait APIMethods400 { ), Catalogs(Core, PSD2, OBWG), List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagNewStyle)) - + // ACCOUNT_OTP. (we no longer create a resource doc for the general case) resourceDocs += ResourceDoc( createTransactionRequestAccountOtp, @@ -252,7 +254,7 @@ trait APIMethods400 { List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagNewStyle)) - val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50") + val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50") val sharedChargePolicy = ChargePolicy.withName("SHARED") // Transaction Request (SEPA) @@ -334,8 +336,6 @@ trait APIMethods400 { Some(List(canCreateAnyTransactionRequest))) - - // Different Transaction Request approaches: lazy val createTransactionRequestAccount = createTransactionRequest lazy val createTransactionRequestAccountOtp = createTransactionRequest @@ -351,14 +351,18 @@ trait APIMethods400 { for { (Full(u), callContext) <- authorizedAccess(cc) _ <- NewStyle.function.isEnabledTransactionRequests() - _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {isValidID(accountId.value)} - _ <- Helper.booleanToFuture(InvalidBankIdFormat) {isValidID(bankId.value)} + _ <- Helper.booleanToFuture(InvalidAccountIdFormat) { + isValidID(accountId.value) + } + _ <- Helper.booleanToFuture(InvalidBankIdFormat) { + isValidID(bankId.value) + } (_, callContext) <- NewStyle.function.getBank(bankId, callContext) (fromAccount, callContext) <- NewStyle.function.checkBankAccountExists(bankId, accountId, callContext) _ <- NewStyle.function.view(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), callContext) _ <- Helper.booleanToFuture(InsufficientAuthorisationToCreateTransactionRequest) { - u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId,fromAccount.accountId)) == true || + u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId, fromAccount.accountId)) == true || hasEntitlement(fromAccount.bankId.value, u.userId, ApiRole.canCreateAnyTransactionRequest) == true } @@ -393,7 +397,7 @@ trait APIMethods400 { transDetailsJson.value.currency == fromAccount.currency } - (createdTransactionRequest,callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match { + (createdTransactionRequest, callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match { case ACCOUNT | SANDBOX_TAN => { for { transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { @@ -404,7 +408,9 @@ trait APIMethods400 { toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))} + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) + } (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u, viewId, @@ -429,7 +435,9 @@ trait APIMethods400 { toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints))} + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) + } (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u, viewId, @@ -453,7 +461,7 @@ trait APIMethods400 { toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) toAccount <- NewStyle.function.toBankAccount(toCounterparty, callContext) - // Check we can send money to it. + // Check we can send money to it. _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit") { toCounterparty.isBeneficiary == true } @@ -461,7 +469,9 @@ trait APIMethods400 { _ <- Helper.booleanToFuture(s"$InvalidChargePolicy") { ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) } - transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints))} + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints)) + } (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u, viewId, fromAccount, @@ -492,7 +502,9 @@ trait APIMethods400 { _ <- Helper.booleanToFuture(s"$InvalidChargePolicy") { ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) } - transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints))} + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints)) + } (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u, viewId, fromAccount, @@ -513,7 +525,9 @@ trait APIMethods400 { } // Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge' transactionRequestAccountJSON = TransactionRequestAccountJsonV140(fromAccount.bankId.value, fromAccount.accountId.value) - transDetailsSerialized <- NewStyle.function.tryons (UnknownError, 400, callContext){write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints))} + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints)) + } (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv210(u, viewId, fromAccount, @@ -547,11 +561,11 @@ trait APIMethods400 { | |This endpoint totally depends on createTransactionRequest, it need get the following data from createTransactionRequest response body. | - |1)`TRANSACTION_REQUEST_TYPE` : is the same as createTransactionRequest request URL . + |1)`TRANSACTION_REQUEST_TYPE` : is the same as createTransactionRequest request URL . | |2)`TRANSACTION_REQUEST_ID` : is the `id` field in createTransactionRequest response body. | - |3) `id` : is `challenge.id` field in createTransactionRequest response body. + |3) `id` : is `challenge.id` field in createTransactionRequest response body. | |4) `answer` : must be `123`. if it is in sandbox mode. If it kafka mode, the answer can be got by phone message or other security ways. | @@ -583,8 +597,12 @@ trait APIMethods400 { // Check we have a User (Full(u), callContext) <- authorizedAccess(cc) _ <- NewStyle.function.isEnabledTransactionRequests() - _ <- Helper.booleanToFuture(InvalidAccountIdFormat) {isValidID(accountId.value)} - _ <- Helper.booleanToFuture(InvalidBankIdFormat) {isValidID(bankId.value)} + _ <- Helper.booleanToFuture(InvalidAccountIdFormat) { + isValidID(accountId.value) + } + _ <- Helper.booleanToFuture(InvalidBankIdFormat) { + isValidID(bankId.value) + } challengeAnswerJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $ChallengeAnswerJSON ", 400, callContext) { json.extract[ChallengeAnswerJSON] } @@ -594,7 +612,7 @@ trait APIMethods400 { _ <- NewStyle.function.view(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), callContext) _ <- Helper.booleanToFuture(InsufficientAuthorisationToCreateTransactionRequest) { - u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId,fromAccount.accountId)) == true || + u.hasOwnerViewAccess(BankIdAccountId(fromAccount.bankId, fromAccount.accountId)) == true || hasEntitlement(fromAccount.bankId.value, u.userId, ApiRole.canCreateAnyTransactionRequest) == true } @@ -627,7 +645,7 @@ trait APIMethods400 { List( OTP_VIA_API.toString, OTP_VIA_WEB_FORM.toString - ).exists(_ == existingTransactionRequest.challenge.challenge_type) + ).exists(_ == existingTransactionRequest.challenge.challenge_type) } challengeAnswerOBP <- NewStyle.function.validateChallengeAnswerInOBPSide(challengeAnswerJson.id, challengeAnswerJson.answer, callContext) @@ -644,7 +662,7 @@ trait APIMethods400 { // All Good, proceed with the Transaction creation... (transactionRequest, callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match { - case TRANSFER_TO_PHONE | TRANSFER_TO_ATM | TRANSFER_TO_ACCOUNT=> + case TRANSFER_TO_PHONE | TRANSFER_TO_ATM | TRANSFER_TO_ACCOUNT => NewStyle.function.createTransactionAfterChallengeV300(u, fromAccount, transReqId, transactionRequestType, callContext) case _ => NewStyle.function.createTransactionAfterChallengeV210(fromAccount, existingTransactionRequest, callContext) @@ -655,8 +673,361 @@ trait APIMethods400 { } } } - + + resourceDocs += ResourceDoc( + getDynamicEntities, + implementedInApiVersion, + nameOf(getDynamicEntities), + "GET", + "/management/dynamic_entities", + "Get DynamicEntities", + s"""Get the all DynamicEntities.""", + emptyObjectJson, + ListResult( + "dynamic_entities", + (List(DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin))) + ) + , + List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle), + Some(List(canGetDynamicEntities)) + ) + + + lazy val getDynamicEntities: OBPEndpoint = { + case "management" :: "dynamic_entities" :: Nil JsonGet req => { + cc => + for { + (Full(u), callContext) <- authorizedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetDynamicEntities, callContext) + dynamicEntities <- Future(NewStyle.function.getDynamicEntities()) + } yield { + val listCommons: List[DynamicEntityCommons] = dynamicEntities + (ListResult("dynamic_entities", listCommons), HttpCode.`200`(callContext)) + } + } + } + + private def validateDynamicEntityJson(data: DynamicEntityCommons) = { + val metadataJson = net.liftweb.json.parse(data.metadataJson) + + val rqs = (metadataJson \ "definitions" \ data.entityName \ "required").extract[Array[String]] + + val propertiesFields = (metadataJson \ "definitions" \ data.entityName \ "properties").asInstanceOf[JObject].values + require(rqs.toSet.diff(propertiesFields.keySet).isEmpty) + propertiesFields.values.foreach(pair => { + val map = pair.asInstanceOf[Map[String, _]] + require(map("type").isInstanceOf[String]) + require(map("example") != null) + }) + } + + resourceDocs += ResourceDoc( + createDynamicEntity, + implementedInApiVersion, + nameOf(createDynamicEntity), + "POST", + "/management/dynamic_entities", + "Add DynamicEntity", + s"""Add a DynamicEntity. + | + | + |${authenticationRequiredMessage(true)} + | + |Explaination of Fields: + | + |* method_name is required String value + |* connector_name is required String value + |* is_bank_id_exact_match is required boolean value, if bank_id_pattern is exact bank_id value, this value is true; if bank_id_pattern is null or a regex, this value is false + |* bank_id_pattern is optional String value, it can be null, a exact bank_id or a regex + |* parameters is optional array of key value pairs. You can set some paremeters for this method + | + |note: + | + |* if bank_id_pattern is regex, special characters need to do escape, for example: bank_id_pattern = "some\\-id_pattern_\\d+" + |""", + DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin), + DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin, dynamicEntityId = Some("dynamic-entity-id")), + List( + UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle), + Some(List(canCreateDynamicEntity))) + + lazy val createDynamicEntity: OBPEndpoint = { + case "management" :: "dynamic_entities" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(u), callContext) <- authorizedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canCreateDynamicEntity, callContext) + failMsg = s"$InvalidJsonFormat The Json body should be the ${classOf[DynamicEntityCommons]}, and metadataJson should be the same structure as document example." + postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { + val data = json.extract[DynamicEntityCommons] + validateDynamicEntityJson(data) + data + } + + Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData) + } yield { + val commonsData: DynamicEntityCommons = dynamicEntity + (commonsData, HttpCode.`201`(callContext)) + } + } + } + + + resourceDocs += ResourceDoc( + updateDynamicEntity, + implementedInApiVersion, + nameOf(updateDynamicEntity), + "PUT", + "/management/dynamic_entities/DYNAMIC_ENTITY_ID", + "Update DynamicEntity", + s"""Update a DynamicEntity. + | + | + |${authenticationRequiredMessage(true)} + | + |Explanations of Fields: + | + |* method_name is required String value + |* connector_name is required String value + |* is_bank_id_exact_match is required boolean value, if bank_id_pattern is exact bank_id value, this value is true; if bank_id_pattern is null or a regex, this value is false + |* bank_id_pattern is optional String value, it can be null, a exact bank_id or a regex + |* parameters is optional array of key value pairs. You can set some paremeters for this method + |note: + | + |* if bank_id_pattern is regex, special characters need to do escape, for example: bank_id_pattern = "some\\-id_pattern_\\d+" + |""", + DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin), + DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin, dynamicEntityId = Some("dynamic-entity-id")), + List( + UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle), + Some(List(canUpdateDynamicEntity))) + + lazy val updateDynamicEntity: OBPEndpoint = { + case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonPut json -> _ => { + cc => + for { + (Full(u), callContext) <- authorizedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateDynamicEntity, callContext) + + failMsg = s"$InvalidJsonFormat The Json body should be the ${classOf[DynamicEntityCommons]}, and metadataJson should be the same structure as document example." + putData <- NewStyle.function.tryons(failMsg, 400, callContext) { + val data = json.extract[DynamicEntityCommons].copy(dynamicEntityId = Some(dynamicEntityId)) + validateDynamicEntityJson(data) + data + } + + (_, _) <- NewStyle.function.getDynamicEntityById(dynamicEntityId, callContext) + + Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData) + } yield { + val commonsData: DynamicEntityCommons = dynamicEntity + (commonsData, HttpCode.`200`(callContext)) + } + } + } + + resourceDocs += ResourceDoc( + deleteDynamicEntity, + implementedInApiVersion, + nameOf(deleteDynamicEntity), + "DELETE", + "/management/dynamic_entities/DYNAMIC_ENTITY_ID", + "Delete DynamicEntity", + s"""Delete a DynamicEntity specified by DYNAMIC_ENTITY_ID. + | + | + |${authenticationRequiredMessage(true)} + | + |""", + emptyObjectJson, + emptyObjectJson, + List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + Catalogs(notCore, notPSD2, notOBWG), + List(apiTagDynamicEntity, apiTagApi, apiTagNewStyle), + Some(List(canDeleteDynamicEntity))) + + lazy val deleteDynamicEntity: OBPEndpoint = { + case "management" :: "dynamic_entities" :: dynamicEntityId :: Nil JsonDelete _ => { + cc => + for { + (Full(u), callContext) <- authorizedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canDeleteDynamicEntity, callContext) + deleted: Box[Boolean] <- NewStyle.function.deleteDynamicEntity(dynamicEntityId) + } yield { + (deleted, HttpCode.`200`(callContext)) + } + } + } + + + lazy val genericEndpoint: OBPEndpoint = { + case EntityName(entityName) :: Nil JsonGet req => { + cc => + Future { + import net.liftweb.json.JsonDSL._ + val listName = StringHelpers.snakify(English.plural(entityName)) + val resultList = MockerConnector.getAll(entityName) + + val jValue: JValue = listName -> resultList + + (jValue, HttpCode.`200`(Some(cc))) + } + } + case EntityName(entityName, id) JsonGet req => { + cc => + Future { + (MockerConnector.getSingle(entityName, id), HttpCode.`200`(Some(cc))) + } + } + case EntityName(entityName) :: Nil JsonPost json -> _ => { + cc => + Future { + (MockerConnector.persist(entityName, json.asInstanceOf[JObject]), HttpCode.`201`(Some(cc))) + } + } + case EntityName(entityName, id) JsonPut json -> _ => { + cc => + Future { + (MockerConnector.persist(entityName, json.asInstanceOf[JObject], Some(id)), HttpCode.`200`(Some(cc))) + } + } + case EntityName(entityName, id) JsonDelete req => { + cc => + Future { + (MockerConnector.delete(entityName, id), HttpCode.`200`(Some(cc))) + } + } + } + } + } object APIMethods400 extends RestHelper with APIMethods400 { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala new file mode 100644 index 000000000..5f6387bac --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala @@ -0,0 +1,94 @@ +package code.api.v4_0_0 + +import code.api.util.APIUtil.generateUUID +import code.api.util.ErrorMessages.{InvalidJsonFormat, InvalidUrl} +import code.api.util.NewStyle +import net.liftweb.common.Box +import net.liftweb.json._ + +import scala.collection.immutable.{List, Nil} + + +object EntityName { + + def unapply(entityName: String): Option[String] = MockerConnector.definitionsMap.keySet.find(entityName ==) + + def unapply(url: List[String]): Option[(String, String)] = url match { + case entityName :: id :: Nil => MockerConnector.definitionsMap.keySet.find(entityName ==).map((_, id)) + case _ => None + } + +} + +object MockerConnector { + + lazy val definitionsMap = NewStyle.function.getDynamicEntities().map(it => (it.entityName, DynamicEntityInfo(it.metadataJson, it.entityName))).toMap + + val persistedEntities = scala.collection.mutable.Map[String, (String, JObject)]() + + def persist(entityName: String, requestBody: JObject, id: Option[String] = None) = { + val idValue = id.orElse(Some(generateUUID())) + val entityToPersist = this.definitionsMap(entityName).toReponse(requestBody, id) + val haveIdEntity = (entityToPersist \ "id") match { + case JNothing => JObject(JField("id", JString(idValue.get)) :: entityToPersist.obj) + case _ => entityToPersist + } + persistedEntities.put(idValue.get, (entityName, haveIdEntity)) + haveIdEntity + } + + def getSingle(entityName: String, id: String) = { + persistedEntities.find(pair => pair._1 == id && pair._2._1 == entityName).map(_._2._2).getOrElse(throw new RuntimeException(s"$InvalidUrl not exists entity of id = $id")) + } + + def getAll(entityName: String) = persistedEntities.values.filter(_._1 == entityName).map(_._2) + + def delete(entityName: String, id: String): Box[Boolean] = persistedEntities.exists(it => it._1 == id && it._2._1 == entityName) match { + case true => persistedEntities.remove(id).map(_ => true) + case false => Some(false) + } +} +case class DynamicEntityInfo(defination: String, entityName: String) { + + import net.liftweb.json + + val subEntities: List[DynamicEntityInfo] = Nil + + val jsonTypeMap = Map[String, Class[_]]( + ("boolean", classOf[JBool]), + ("string", classOf[JString]), + ("array", classOf[JArray]), + ("integer", classOf[JInt]), + ("number", classOf[JDouble]), + ) + + val definationJson = json.parse(defination).asInstanceOf[JObject] + val entity = (definationJson \ "definitions" \ entityName).asInstanceOf[JObject] + + def toReponse(result: JObject, id: Option[String]): JObject = { + + val fieldNameToTypeName: Map[String, String] = (entity \ "properties") + .asInstanceOf[JObject] + .obj + .map(field => (field.name, (field.value \ "type").asInstanceOf[JString].s)) + .toMap + + val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName + .mapValues(jsonTypeMap(_)) + + val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet + + val fields = result.obj.filter(it => fieldNameToType.keySet.contains(it.name)) + + def check(v: Boolean, msg: String) = if (!v) throw new RuntimeException(msg) + // if there are field type are not match the definitions, there must be bug. + fields.foreach(it => check(fieldNameToType(it.name).isInstance(it.value), s"""$InvalidJsonFormat "${it.name}" required type is "${fieldNameToTypeName(it.name)}".""")) + // if there are required field not presented, must be some bug. + requiredFieldNames.foreach(it => check(fields.exists(_.name == it), s"""$InvalidJsonFormat required field "$it" not presented.""")) + + (id, fields.exists(_.name == "id")) match { + case (Some(idValue), false) => JObject(JField("id", JString(idValue)) :: fields) + case _ => JObject(fields) + } + } +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala index 7be4e2ddd..654f8f56a 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala @@ -29,7 +29,7 @@ package code.api.v4_0_0 import code.api.OBPRestHelper import code.api.util.APIUtil.{OBPEndpoint, ResourceDoc, getAllowedEndpoints} import code.api.util.{ApiVersion, VersionedOBPApis} -import code.api.v1_3_0.{APIMethods130} +import code.api.v1_3_0.APIMethods130 import code.api.v1_4_0.APIMethods140 import code.api.v2_0_0.APIMethods200 import code.api.v2_1_0.APIMethods210 @@ -385,7 +385,11 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w val endpointsOf4_0_0 = Implementations4_0_0.getBanks :: Implementations4_0_0.createTransactionRequest :: - Implementations4_0_0.answerTransactionRequestChallenge :: + Implementations4_0_0.answerTransactionRequestChallenge :: + Implementations4_0_0.getDynamicEntities :: + Implementations4_0_0.createDynamicEntity :: + Implementations4_0_0.updateDynamicEntity :: + Implementations4_0_0.deleteDynamicEntity :: Nil val allResourceDocs = Implementations4_0_0.resourceDocs ++ @@ -398,7 +402,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w Implementations1_4_0.resourceDocs ++ Implementations1_3_0.resourceDocs ++ Implementations1_2_1.resourceDocs - + def findResourceDoc(pf: OBPEndpoint): Option[ResourceDoc] = { allResourceDocs.find(_.partialFunction==pf) } @@ -422,7 +426,7 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w routes.foreach(route => { oauthServe(apiPrefix{route}, findResourceDoc(route)) }) - + oauthServe(apiPrefix{Implementations4_0_0.genericEndpoint}, None) logger.info(s"version $version has been run! There are ${routes.length} routes.") } diff --git a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala new file mode 100644 index 000000000..3cde2e04a --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala @@ -0,0 +1,42 @@ +package code.dynamicEntity + +import com.openbankproject.commons.model.{Converter, JsonFieldReName} +import net.liftweb.common.Box +import net.liftweb.util.SimpleInjector + +object DynamicEntityProvider extends SimpleInjector { + + val connectorMethodProvider = new Inject(buildOne _) {} + + def buildOne: MappedDynamicEntityProvider.type = MappedDynamicEntityProvider +} + +trait DynamicEntityT { + def dynamicEntityId: Option[String] + def entityName: String + def metadataJson: String +} + +case class DynamicEntityCommons(entityName: String, + metadataJson: String, + dynamicEntityId: Option[String] = None, + ) extends DynamicEntityT with JsonFieldReName + +object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons] + + +trait DynamicEntityProvider { + def getById(dynamicEntityId: String): Box[DynamicEntityT] + + def getDynamicEntities(): List[DynamicEntityT] + + def createOrUpdate(dynamicEntity: DynamicEntityT): Box[DynamicEntityT] + + def delete(dynamicEntityId: String):Box[Boolean] +} + + + + + + diff --git a/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala new file mode 100644 index 000000000..4ac2a8baa --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala @@ -0,0 +1,64 @@ +package code.dynamicEntity + +import code.api.util.CustomJsonFormats +import code.util.MappedUUID +import net.liftweb.common.{Box, Empty, EmptyBox, Full} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo +import org.apache.commons.lang3.StringUtils + +object MappedDynamicEntityProvider extends DynamicEntityProvider with CustomJsonFormats{ + + override def getById(dynamicEntityId: String): Box[DynamicEntityT] = DynamicEntity.find( + By(DynamicEntity.DynamicEntityId, dynamicEntityId) + ) + + override def getDynamicEntities(): List[DynamicEntity] = { + DynamicEntity.findAll() + } + + override def createOrUpdate(dynamicEntity: DynamicEntityT): Box[DynamicEntityT] = { + + //to find exists dynamicEntity, if dynamicEntityId supplied, query by dynamicEntityId, or use entityName and dynamicEntityId to do query + val existsDynamicEntity: Box[DynamicEntity] = dynamicEntity.dynamicEntityId match { + case Some(id) if (StringUtils.isNotBlank(id)) => getByDynamicEntityId(id) + case _ => Empty + } + val entityToPersist = existsDynamicEntity match { + case _: EmptyBox => DynamicEntity.create + case Full(dynamicEntity) => dynamicEntity + } + + tryo{ + entityToPersist + .EntityName(dynamicEntity.entityName) + .MetadataJson(dynamicEntity.metadataJson) + .saveMe() + } + } + + + override def delete(dynamicEntityId: String): Box[Boolean] = getByDynamicEntityId(dynamicEntityId).map(_.delete_!) + + private[this] def getByDynamicEntityId(dynamicEntityId: String): Box[DynamicEntity] = DynamicEntity.find(By(DynamicEntity.DynamicEntityId, dynamicEntityId)) + +} + +class DynamicEntity extends DynamicEntityT with LongKeyedMapper[DynamicEntity] with IdPK with CustomJsonFormats{ + + override def getSingleton = DynamicEntity + + object DynamicEntityId extends MappedUUID(this) + object EntityName extends MappedString(this, 255) + + object MetadataJson extends MappedText(this) + + override def dynamicEntityId: Option[String] = Option(DynamicEntityId.get) + override def entityName: String = EntityName.get + override def metadataJson: String = MetadataJson.get +} + +object DynamicEntity extends DynamicEntity with LongKeyedMetaMapper[DynamicEntity] { + override def dbIndexes = UniqueIndex(DynamicEntityId) :: UniqueIndex(MetadataJson) :: super.dbIndexes +} + diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala new file mode 100644 index 000000000..917ca1d83 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicEntityTest.scala @@ -0,0 +1,224 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2018, TESOBE Ltd + +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 Ltd +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + */ +package code.api.v4_0_0 + +import code.api.ErrorMessage +import code.api.util.ApiRole._ +import code.api.util.ApiVersion +import code.api.util.ErrorMessages._ +import code.api.v4_0_0.APIMethods400.Implementations4_0_0 +import code.dynamicEntity.DynamicEntityCommons +import code.entitlement.Entitlement +import com.github.dwickern.macros.NameOf.nameOf +import net.liftweb.json.Serialization.write +import org.scalatest.Tag +import code.api.util.APIUtil.OAuth._ +import scala.collection.immutable.List + +class DynamicEntityTest extends V400ServerSetup { + + /** + * 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.v4_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createDynamicEntity)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.updateDynamicEntity)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getDynamicEntities)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.deleteDynamicEntity)) + + val rightEntity = DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin) + // wrong metadataJson + val wrongEntity = DynamicEntityCommons(entityName = "FooBar", metadataJson = + """ + |{ + | "definitions": { + | "FooBar": { + | "required": [ + | "name" + | ], + | "properties": { + | "name_wrong": { + | "type": "string", + | "example": "James Brown" + | }, + | "number": { + | "type": "integer", + | "example": "698761728934" + | } + | } + | } + | } + |} + |""".stripMargin) + + + feature("Add a DynamicEntity v4.0.4- Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST + val response400 = makePostRequest(request400, write(rightEntity)) + Then("We should get a 400") + response400.code should equal(400) + And("error should be " + UserNotLoggedIn) + response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + } + feature("Update a DynamicEntity v4.0.4- Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint2, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic_entities"/ "some-method-routing-id").PUT + val response400 = makePutRequest(request400, write(rightEntity)) + Then("We should get a 400") + response400.code should equal(400) + And("error should be " + UserNotLoggedIn) + response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + } + feature("Get DynamicEntities v4.0.4- Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint3, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic_entities").GET + val response400 = makeGetRequest(request400) + Then("We should get a 400") + response400.code should equal(400) + And("error should be " + UserNotLoggedIn) + response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + } + feature("Delete the DynamicEntity specified by METHOD_ROUTING_ID v4.0.4- Unauthorized access") { + scenario("We will call the endpoint without user credentials", ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "METHOD_ROUTING_ID").DELETE + val response400 = makeDeleteRequest(request400) + Then("We should get a 400") + response400.code should equal(400) + And("error should be " + UserNotLoggedIn) + response400.body.extract[ErrorMessage].message should equal (UserNotLoggedIn) + } + } + + + feature("Add a DynamicEntity v4.0.4- Unauthorized access - Authorized access") { + scenario("We will call the endpoint without the proper Role " + canCreateDynamicEntity, ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.4without a Role " + canCreateTaxResidence) + val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1) + val response400 = makePostRequest(request400, write(rightEntity)) + Then("We should get a 403") + response400.code should equal(403) + And("error should be " + UserHasMissingRoles + CanCreateDynamicEntity) + response400.body.extract[ErrorMessage].message should equal (UserHasMissingRoles + CanCreateDynamicEntity) + } + + scenario("We will call the endpoint with the proper Role " + canCreateDynamicEntity , ApiEndpoint1, ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateDynamicEntity.toString) + When("We make a request v4.0.0") + val request400 = (v4_0_0_Request / "management" / "dynamic_entities").POST <@(user1) + val response400 = makePostRequest(request400, write(rightEntity)) + Then("We should get a 201") + response400.code should equal(201) + val customerJson = response400.body.extract[DynamicEntityCommons] + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanUpdateDynamicEntity.toString) + When("We make a request v4.0.4with the Role " + canUpdateDynamicEntity) + + { + // update success + val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / customerJson.dynamicEntityId.get ).PUT <@(user1) + val metadataJson = customerJson.metadataJson + val entityName = customerJson.entityName + val response400 = makePutRequest(request400, write(customerJson.copy(entityName = "Hello", metadataJson.replace(entityName, "Hello")))) + Then("We should get a 200") + response400.code should equal(200) + val dynamicEntitiesJson = response400.body.extract[DynamicEntityCommons] + dynamicEntitiesJson.entityName should be ("Hello") + } + + { + // update a not exists DynamicEntity + val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / "not-exists-id" ).PUT <@(user1) + val response400 = makePutRequest(request400, write(customerJson)) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should startWith (DynamicEntityNotFoundByDynamicEntityId) + } + + { + // update a DynamicEntity with wrong metadataJson + val request400 = (v4_0_0_Request / "management" / "dynamic_entities" / customerJson.dynamicEntityId.get ).PUT <@(user1) + val response400 = makePutRequest(request400, write(wrongEntity)) + Then("We should get a 400") + response400.code should equal(400) + response400.body.extract[ErrorMessage].message should startWith (InvalidJsonFormat) + } + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetDynamicEntities.toString) + When("We make a request v4.0.4with the Role " + canGetDynamicEntities) + val requestGet400 = (v4_0_0_Request / "management" / "dynamic_entities").GET <@(user1) + val responseGet400 = makeGetRequest(requestGet400) + Then("We should get a 200") + responseGet400.code should equal(200) + val json = responseGet400.body \ "dynamic_entities" + val dynamicEntitiesGetJson = json.extract[List[DynamicEntityCommons]] + + dynamicEntitiesGetJson.size should be (1) + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteDynamicEntity.toString) + When("We make a request v4.0.4with the Role " + canDeleteDynamicEntity) + val requestDelete400 = (v4_0_0_Request / "management" / "dynamic_entities" / dynamicEntitiesGetJson.head.dynamicEntityId.get).DELETE <@(user1) + val responseDelete400 = makeDeleteRequest(requestDelete400) + Then("We should get a 200") + responseDelete400.code should equal(200) + + } + } + + +}