mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 15:27:01 +00:00
Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
2720f10165
@ -1,19 +1,32 @@
|
||||
# Contributing
|
||||
|
||||
|
||||
## Hello!
|
||||
|
||||
Thank you for your interest in contributing to the Open Bank Project!
|
||||
## Coding Standards
|
||||
|
||||
### Character Encoding
|
||||
|
||||
- **Use UTF-8 encoding** for all source files
|
||||
- **DO NOT use emojis** in source code (scripts, Scala, Java, config files, etc.)
|
||||
- **Emojis are only allowed in Markdown (.md) files** - use them if you must.
|
||||
- **Avoid non-ASCII characters** in code unless absolutely necessary (e.g., comments in non-English languages)
|
||||
- Use plain ASCII alternatives in source code:
|
||||
- Instead of checkmark use [OK] or PASS
|
||||
- Instead of X mark use [FAIL] or ERROR
|
||||
- Instead of multiply use x
|
||||
- Instead of arrow use -> or <-
|
||||
Thank you for your interest in contributing to the Open Bank Project!
|
||||
|
||||
## Pull requests
|
||||
|
||||
If submitting a pull request please read and sign our [CLA](http://github.com/OpenBankProject/OBP-API/blob/develop/Harmony_Individual_Contributor_Assignment_Agreement.txt) first.
|
||||
If submitting a pull request please read and sign our [CLA](http://github.com/OpenBankProject/OBP-API/blob/develop/Harmony_Individual_Contributor_Assignment_Agreement.txt) first.
|
||||
In the first instance it is sufficient if you create a text file of the CLA with your name and include that in a git commit description.
|
||||
If you end up making large changes to the source code, we might ask for a paper signed copy of your CLA sent by email to contact@tesobe.com
|
||||
If you end up making large changes to the source code, we might ask for a paper signed copy of your CLA sent by email to contact@tesobe.com
|
||||
|
||||
## Git commit messages
|
||||
|
||||
Please structure git commit messages in a way as shown below:
|
||||
|
||||
1. bugfix/Something
|
||||
2. feature/Something
|
||||
3. docfix/Something
|
||||
@ -89,6 +102,7 @@ When naming variables use strict camel case e.g. use myUrl not myURL. This is so
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended order of checks at an endpoint
|
||||
|
||||
```scala
|
||||
@ -98,30 +112,34 @@ When naming variables use strict camel case e.g. use myUrl not myURL. This is so
|
||||
for {
|
||||
// 1. makes sure the user which attempts to use the endpoint is authorized
|
||||
(Full(u), callContext) <- authorizedAccess(cc)
|
||||
// 2. makes sure the user which attempts to use the endpoint is allowed to consume it
|
||||
// 2. makes sure the user which attempts to use the endpoint is allowed to consume it
|
||||
_ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext)
|
||||
// 3. checks the endpoint constraints
|
||||
(_, callContext) <- NewStyle.function.getBank(bankId, callContext)
|
||||
failMsg = s"$InvalidJsonFormat The Json body should be the $PostPutProductJsonV310 "
|
||||
...
|
||||
```
|
||||
|
||||
Please note that that checks at an endpoint should be applied only in case an user is authorized and has privilege to consume the endpoint. Otherwise we can reveal sensitive data to the user. For instace if we reorder the checks in next way:
|
||||
|
||||
```scala
|
||||
// 1. makes sure the user which attempts to use the endpoint is authorized
|
||||
(Full(u), callContext) <- authorizedAccess(cc)
|
||||
// 3. checks the endpoint constraints
|
||||
(_, callContext) <- NewStyle.function.getBank(bankId, callContext)
|
||||
failMsg = s"$InvalidJsonFormat The Json body should be the $PostPutProductJsonV310 "
|
||||
failMsg = s"$InvalidJsonFormat The Json body should be the $PostPutProductJsonV310 "
|
||||
(Full(u), callContext) <- authorizedAccess(cc)
|
||||
// 2. makes sure the user which attempts to use the endpoint is allowed to consume it
|
||||
_ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext)
|
||||
// 2. makes sure the user which attempts to use the endpoint is allowed to consume it
|
||||
_ <- NewStyle.function.hasAtLeastOneEntitlement(failMsg = createProductEntitlementsRequiredText)(bankId.value, u.userId, createProductEntitlements, callContext)
|
||||
```
|
||||
|
||||
the user which cannot consume the endpoint still can check does some bank exist or not at that instance. It's not the issue if banks are public data at the instance but it wouldn't be the only business case all the time.
|
||||
|
||||
## Writing tests
|
||||
|
||||
When you write a test for an endpoint please tag it with a version and the endpoint.
|
||||
An example of how to tag tests:
|
||||
|
||||
```scala
|
||||
class FundsAvailableTest extends V310ServerSetup {
|
||||
|
||||
@ -152,10 +170,11 @@ class FundsAvailableTest extends V310ServerSetup {
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Code Generation
|
||||
We support to generate the OBP-API code from the following three types of json. You can choose one of them as your own requirements.
|
||||
|
||||
We support to generate the OBP-API code from the following three types of json. You can choose one of them as your own requirements.
|
||||
|
||||
1 Choose one of the following types: type1 or type2 or type3
|
||||
2 Modify the json file your selected, for now, we only support these three types: String, Double, Int. other types may throw the exceptions
|
||||
@ -163,19 +182,24 @@ We support to generate the OBP-API code from the following three types of json.
|
||||
4 Run/Restart OBP-API project.
|
||||
5 Run API_Exploer project to test your new APIs. (click the Tag `APIBuilder B1)
|
||||
|
||||
Here are the three types:
|
||||
Here are the three types:
|
||||
|
||||
Type1: If you use `modelSource.json`, please run `APIBuilderModel.scala` main method
|
||||
|
||||
```
|
||||
/OBP-API/obp-api/src/main/resources/apiBuilder/APIModelSource.json
|
||||
/OBP-API/obp-api/src/main/scala/code/api/APIBuilder/APIBuilderModel.scala
|
||||
```
|
||||
|
||||
Type2: If you use `apisResource.json`, please run `APIBuilder.scala` main method
|
||||
|
||||
```
|
||||
/OBP-API/obp-api/src/main/resources/apiBuilder/apisResource.json
|
||||
OBP-API/src/main/scala/code/api/APIBuilder/APIBuilder.scala
|
||||
```
|
||||
|
||||
Type3: If you use `swaggerResource.json`, please run `APIBuilderSwagger.scala` main method
|
||||
|
||||
```
|
||||
/OBP-API/obp-api/src/main/resources/apiBuilder/swaggerResource.json
|
||||
OBP-API/src/main/scala/code/api/APIBuilder/APIBuilderSwagger.scala
|
||||
|
||||
@ -586,7 +586,7 @@
|
||||
<forkMode>once</forkMode>
|
||||
<junitxml>.</junitxml>
|
||||
<filereports>WDF TestSuite.txt</filereports>
|
||||
<argLine>-Drun.mode=test -XX:MaxMetaspaceSize=512m -Xms512m -Xmx512m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED</argLine>
|
||||
<argLine>-Drun.mode=test -XX:MaxMetaspaceSize=512m -Xms512m -Xmx512m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.jar=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED</argLine>
|
||||
<tagsToExclude>code.external</tagsToExclude>
|
||||
</configuration>
|
||||
<executions>
|
||||
|
||||
@ -27,6 +27,11 @@ starConnector_supported_types = mapped,internal
|
||||
# Connector cache time-to-live in seconds, caching disabled if not set
|
||||
#connector.cache.ttl.seconds=3
|
||||
|
||||
# Disable metrics writing during tests to prevent database bloat
|
||||
# Metrics accumulate with every API call - with 2000+ tests this can create 100,000+ records
|
||||
# causing MetricsTest to hang on bulkDelete operations
|
||||
# Note: Specific tests (like code.api.v5_1_0.MetricTest) explicitly enable this when needed
|
||||
write_metrics = false
|
||||
|
||||
#this is needed for oauth to work. it's important to access the api over this url, e.g.
|
||||
# if this is 127.0.0.1 don't use localhost to access it.
|
||||
@ -56,8 +61,9 @@ End of minimum settings
|
||||
# if connector is mapped, set a database backend. If not set, this will be set to an in-memory h2 database by default
|
||||
# you can use a no config needed h2 database by setting db.driver=org.h2.Driver and not including db.url
|
||||
# Please note that since update o version 2.1.214 we use NON_KEYWORDS=VALUE to bypass reserved word issue in SQL statements
|
||||
# IMPORTANT: For tests, use test_only_lift_proto.db so the cleanup script can safely delete it
|
||||
#db.driver=org.h2.Driver
|
||||
#db.url=jdbc:h2:./lift_proto.db;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE
|
||||
#db.url=jdbc:h2:./test_only_lift_proto.db;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE
|
||||
|
||||
#set this to false if you don't want the api payments call to work
|
||||
payments_enabled=false
|
||||
@ -117,4 +123,4 @@ allow_public_views =true
|
||||
#external.port=8080
|
||||
|
||||
# Enable /Disable Create password reset url endpoint
|
||||
#ResetPasswordUrlEnabled=true
|
||||
#ResetPasswordUrlEnabled=true
|
||||
|
||||
@ -2,7 +2,7 @@ package code.abacrule
|
||||
|
||||
/**
|
||||
* ABAC Rule Examples
|
||||
*
|
||||
*
|
||||
* This file contains example ABAC rules that can be used as templates.
|
||||
* Copy the rule code (the string in quotes) when creating new ABAC rules via the API.
|
||||
*/
|
||||
@ -15,21 +15,21 @@ object AbacRuleExamples {
|
||||
* Only users with "admin" in their email address can access
|
||||
*/
|
||||
val adminOnlyRule: String =
|
||||
"""user.emailAddress.contains("admin")"""
|
||||
"""user.emailAddress.contains(\"admin\")"""
|
||||
|
||||
/**
|
||||
* Example 2: Specific User Provider
|
||||
* Only allow users from a specific authentication provider
|
||||
*/
|
||||
val providerCheckRule: String =
|
||||
"""user.provider == "obp""""
|
||||
"""user.provider == \"obp\""""
|
||||
|
||||
/**
|
||||
* Example 3: User Email Domain
|
||||
* Only allow users from specific email domain
|
||||
*/
|
||||
val emailDomainRule: String =
|
||||
"""user.emailAddress.endsWith("@example.com")"""
|
||||
"""user.emailAddress.endsWith(\"@example.com\")"""
|
||||
|
||||
/**
|
||||
* Example 4: User Has Username
|
||||
@ -45,14 +45,14 @@ object AbacRuleExamples {
|
||||
* Only allow access to a specific bank
|
||||
*/
|
||||
val specificBankRule: String =
|
||||
"""bankOpt.exists(_.bankId.value == "gh.29.uk")"""
|
||||
"""bankOpt.exists(_.bankId.value == \"gh.29.uk\")"""
|
||||
|
||||
/**
|
||||
* Example 6: Bank Short Name Check
|
||||
* Only allow access to banks with specific short name
|
||||
*/
|
||||
val bankShortNameRule: String =
|
||||
"""bankOpt.exists(_.shortName.contains("Example"))"""
|
||||
"""bankOpt.exists(_.shortName.contains(\"Example\"))"""
|
||||
|
||||
/**
|
||||
* Example 7: Bank Must Be Present
|
||||
@ -86,21 +86,21 @@ object AbacRuleExamples {
|
||||
* Only allow access to accounts with specific currency
|
||||
*/
|
||||
val currencyRule: String =
|
||||
"""accountOpt.exists(_.currency == "EUR")"""
|
||||
"""accountOpt.exists(_.currency == \"EUR\")"""
|
||||
|
||||
/**
|
||||
* Example 11: Account Type Check
|
||||
* Only allow access to savings accounts
|
||||
*/
|
||||
val accountTypeRule: String =
|
||||
"""accountOpt.exists(_.accountType == "SAVINGS")"""
|
||||
"""accountOpt.exists(_.accountType == \"SAVINGS\")"""
|
||||
|
||||
/**
|
||||
* Example 12: Account Label Contains
|
||||
* Only allow access to accounts with specific label
|
||||
*/
|
||||
val accountLabelRule: String =
|
||||
"""accountOpt.exists(_.label.contains("VIP"))"""
|
||||
"""accountOpt.exists(_.label.contains(\"VIP\"))"""
|
||||
|
||||
// ==================== TRANSACTION-BASED RULES ====================
|
||||
|
||||
@ -127,14 +127,14 @@ object AbacRuleExamples {
|
||||
* Only allow access to specific transaction types
|
||||
*/
|
||||
val transactionTypeRule: String =
|
||||
"""transactionOpt.exists(_.transactionType == "PAYMENT")"""
|
||||
"""transactionOpt.exists(_.transactionType == \"PAYMENT\")"""
|
||||
|
||||
/**
|
||||
* Example 16: Transaction Currency Check
|
||||
* Only allow access to transactions in specific currency
|
||||
*/
|
||||
val transactionCurrencyRule: String =
|
||||
"""transactionOpt.exists(_.currency == "USD")"""
|
||||
"""transactionOpt.exists(_.currency == \"USD\")"""
|
||||
|
||||
// ==================== CUSTOMER-BASED RULES ====================
|
||||
|
||||
@ -143,21 +143,21 @@ object AbacRuleExamples {
|
||||
* Only allow access if customer email is from specific domain
|
||||
*/
|
||||
val customerEmailDomainRule: String =
|
||||
"""customerOpt.exists(_.email.endsWith("@corporate.com"))"""
|
||||
"""customerOpt.exists(_.email.endsWith(\"@corporate.com\"))"""
|
||||
|
||||
/**
|
||||
* Example 18: Customer Legal Name Check
|
||||
* Only allow access to customers with specific name pattern
|
||||
*/
|
||||
val customerNameRule: String =
|
||||
"""customerOpt.exists(_.legalName.contains("Corporation"))"""
|
||||
"""customerOpt.exists(_.legalName.contains(\"Corporation\"))"""
|
||||
|
||||
/**
|
||||
* Example 19: Customer Mobile Number Pattern
|
||||
* Only allow access to customers with specific mobile pattern
|
||||
*/
|
||||
val customerMobileRule: String =
|
||||
"""customerOpt.exists(_.mobilePhoneNumber.startsWith("+44"))"""
|
||||
"""customerOpt.exists(_.mobilePhoneNumber.startsWith(\"+44\"))"""
|
||||
|
||||
// ==================== COMBINED RULES ====================
|
||||
|
||||
@ -166,15 +166,15 @@ object AbacRuleExamples {
|
||||
* Managers can only access specific bank
|
||||
*/
|
||||
val managerBankRule: String =
|
||||
"""user.emailAddress.contains("manager") &&
|
||||
|bankOpt.exists(_.bankId.value == "gh.29.uk")""".stripMargin
|
||||
"""user.emailAddress.contains(\"manager\") &&
|
||||
|bankOpt.exists(_.bankId.value == \"gh.29.uk\")""".stripMargin
|
||||
|
||||
/**
|
||||
* Example 21: High Value Account Access
|
||||
* Only managers can access high-value accounts
|
||||
*/
|
||||
val managerHighValueRule: String =
|
||||
"""user.emailAddress.contains("manager") &&
|
||||
"""user.emailAddress.contains(\"manager\") &&
|
||||
|accountOpt.exists(account => {
|
||||
| account.balance.toString.toDoubleOption.exists(_ > 50000.0)
|
||||
|})""".stripMargin
|
||||
@ -184,27 +184,27 @@ object AbacRuleExamples {
|
||||
* Auditors can only view completed transactions
|
||||
*/
|
||||
val auditorTransactionRule: String =
|
||||
"""user.emailAddress.contains("auditor") &&
|
||||
|transactionOpt.exists(_.status == "COMPLETED")""".stripMargin
|
||||
"""user.emailAddress.contains(\"auditor\") &&
|
||||
|transactionOpt.exists(_.status == \"COMPLETED\")""".stripMargin
|
||||
|
||||
/**
|
||||
* Example 23: VIP Customer Manager Access
|
||||
* Only specific managers can access VIP customer accounts
|
||||
*/
|
||||
val vipManagerRule: String =
|
||||
"""(user.emailAddress.contains("vip-manager") || user.emailAddress.contains("director")) &&
|
||||
|accountOpt.exists(_.label.contains("VIP"))""".stripMargin
|
||||
"""(user.emailAddress.contains(\"vip-manager\") || user.emailAddress.contains(\"director\")) &&
|
||||
|accountOpt.exists(_.label.contains(\"VIP\"))""".stripMargin
|
||||
|
||||
/**
|
||||
* Example 24: Multi-Condition Access
|
||||
* Complex rule with multiple conditions
|
||||
*/
|
||||
val complexRule: String =
|
||||
"""user.emailAddress.contains("manager") &&
|
||||
|user.provider == "obp" &&
|
||||
|bankOpt.exists(_.bankId.value == "gh.29.uk") &&
|
||||
"""user.emailAddress.contains(\"manager\") &&
|
||||
|user.provider == \"obp\" &&
|
||||
|bankOpt.exists(_.bankId.value == \"gh.29.uk\") &&
|
||||
|accountOpt.exists(account => {
|
||||
| account.currency == "GBP" &&
|
||||
| account.currency == \"GBP\" &&
|
||||
| account.balance.toString.toDoubleOption.exists(_ > 5000.0) &&
|
||||
| account.balance.toString.toDoubleOption.exists(_ < 100000.0)
|
||||
|})""".stripMargin
|
||||
@ -216,7 +216,7 @@ object AbacRuleExamples {
|
||||
* Deny access to specific user
|
||||
*/
|
||||
val blockUserRule: String =
|
||||
"""!user.emailAddress.contains("blocked@example.com")"""
|
||||
"""!user.emailAddress.contains(\"blocked@example.com\")"""
|
||||
|
||||
/**
|
||||
* Example 26: Block Inactive Accounts
|
||||
@ -241,7 +241,7 @@ object AbacRuleExamples {
|
||||
* Use regex-like pattern matching
|
||||
*/
|
||||
val emailPatternRule: String =
|
||||
"""user.emailAddress.matches(".*@(internal|corporate)\\.com")"""
|
||||
"""user.emailAddress.matches(\".*@(internal|corporate)\\\\.com\")"""
|
||||
|
||||
/**
|
||||
* Example 29: Multiple Bank Access
|
||||
@ -249,7 +249,7 @@ object AbacRuleExamples {
|
||||
*/
|
||||
val multipleBanksRule: String =
|
||||
"""bankOpt.exists(bank => {
|
||||
| val allowedBanks = Set("gh.29.uk", "de.10.de", "us.01.us")
|
||||
| val allowedBanks = Set(\"gh.29.uk\", \"de.10.de\", \"us.01.us\")
|
||||
| allowedBanks.contains(bank.bankId.value)
|
||||
|})""".stripMargin
|
||||
|
||||
@ -269,9 +269,9 @@ object AbacRuleExamples {
|
||||
* Allow access if any condition is true
|
||||
*/
|
||||
val orLogicRule: String =
|
||||
"""user.emailAddress.contains("admin") ||
|
||||
|user.emailAddress.contains("manager") ||
|
||||
|user.emailAddress.contains("director")""".stripMargin
|
||||
"""user.emailAddress.contains(\"admin\") ||
|
||||
|user.emailAddress.contains(\"manager\") ||
|
||||
|user.emailAddress.contains(\"director\")""".stripMargin
|
||||
|
||||
/**
|
||||
* Example 32: Nested Option Handling
|
||||
@ -311,7 +311,7 @@ object AbacRuleExamples {
|
||||
| )
|
||||
|} else {
|
||||
| // Default case
|
||||
| user.emailAddress.contains("admin")
|
||||
| user.emailAddress.contains(\"admin\")
|
||||
|}""".stripMargin
|
||||
|
||||
// ==================== HELPER FUNCTIONS ====================
|
||||
@ -366,4 +366,4 @@ object AbacRuleExamples {
|
||||
* List all available example names
|
||||
*/
|
||||
def listExampleNames: List[String] = getAllExamples.keys.toList.sorted
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,6 +663,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
List(apiTagDocumentation, apiTagApi)
|
||||
)
|
||||
|
||||
// Note: Swagger format requires special character escaping because it builds JSON via string concatenation (unlike OBP/OpenAPI formats which use case class serialization)
|
||||
|
||||
def getResourceDocsSwagger : OBPEndpoint = {
|
||||
case "resource-docs" :: requestedApiVersionString :: "swagger" :: Nil JsonGet _ => {
|
||||
|
||||
@ -40,6 +40,35 @@ import scala.reflect.runtime.universe
|
||||
|
||||
object SwaggerJSONFactory extends MdcLoggable {
|
||||
type Coll[T] = GenTraversableLike[T, _]
|
||||
|
||||
/**
|
||||
* Escapes a string value to be safely included in JSON.
|
||||
* Handles quotes, backslashes, newlines, and other special characters.
|
||||
*/
|
||||
private def escapeJsonString(value: String): String = {
|
||||
if (value == null) return ""
|
||||
value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t")
|
||||
.replace("\b", "\\b")
|
||||
.replace("\f", "\\f")
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts any value to a JSON example string.
|
||||
* Handles JValue, String, and other types with proper escaping.
|
||||
*/
|
||||
private def safeExampleValue(value: Any): String = {
|
||||
value match {
|
||||
case null | None => ""
|
||||
case v: JValue => try { escapeJsonString(JsonUtils.toString(v)) } catch { case e: Exception => logger.warn(s"Failed to convert JValue to string for example: ${e.getMessage}"); "" }
|
||||
case v: String => escapeJsonString(v)
|
||||
case v => escapeJsonString(v.toString)
|
||||
}
|
||||
}
|
||||
//Info Object
|
||||
//link ->https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#infoObject
|
||||
case class InfoJson(
|
||||
@ -107,14 +136,26 @@ object SwaggerJSONFactory extends MdcLoggable {
|
||||
| }
|
||||
|}
|
||||
|""".stripMargin
|
||||
json.parse(definition)
|
||||
try {
|
||||
json.parse(definition)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(s"Failed to parse ListResult schema JSON: ${e.getMessage}\nJSON was: $definition")
|
||||
throw new RuntimeException(s"Invalid JSON in ListResult schema generation: ${e.getMessage}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
case class JObjectSchemaJson(jObject: JObject) extends ResponseObjectSchemaJson with JsonAble {
|
||||
|
||||
override def toJValue(implicit format: Formats): json.JValue = {
|
||||
val schema = buildSwaggerSchema(typeOf[JObject], jObject)
|
||||
json.parse(schema)
|
||||
try {
|
||||
json.parse(schema)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(s"Failed to parse JObject schema JSON: ${e.getMessage}\nSchema was: $schema")
|
||||
throw new RuntimeException(s"Invalid JSON in JObject schema generation: ${e.getMessage}", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -122,7 +163,13 @@ object SwaggerJSONFactory extends MdcLoggable {
|
||||
|
||||
override def toJValue(implicit format: Formats): json.JValue = {
|
||||
val schema = buildSwaggerSchema(typeOf[JArray], jArray)
|
||||
json.parse(schema)
|
||||
try {
|
||||
json.parse(schema)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(s"Failed to parse JArray schema JSON: ${e.getMessage}\nSchema was: $schema")
|
||||
throw new RuntimeException(s"Invalid JSON in JArray schema generation: ${e.getMessage}", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -646,8 +693,7 @@ object SwaggerJSONFactory extends MdcLoggable {
|
||||
}
|
||||
def example = exampleValue match {
|
||||
case null | None => ""
|
||||
case v: JValue => s""", "example": "${JsonUtils.toString(v)}" """
|
||||
case v => s""", "example": "$v" """
|
||||
case v => s""", "example": "${safeExampleValue(v)}" """
|
||||
}
|
||||
|
||||
paramType match {
|
||||
@ -968,11 +1014,12 @@ object SwaggerJSONFactory extends MdcLoggable {
|
||||
.toList
|
||||
.map(it => {
|
||||
val (errorName, errorMessage) = it
|
||||
val escapedMessage = escapeJsonString(errorMessage.toString)
|
||||
s""""Error$errorName": {
|
||||
| "properties": {
|
||||
| "message": {
|
||||
| "type": "string",
|
||||
| "example": "$errorMessage"
|
||||
| "example": "$escapedMessage"
|
||||
| }
|
||||
| }
|
||||
}""".stripMargin
|
||||
@ -989,7 +1036,14 @@ object SwaggerJSONFactory extends MdcLoggable {
|
||||
//Make a final string
|
||||
val definitions = "{\"definitions\":{" + particularDefinitionsPart + "}}"
|
||||
//Make a jsonAST from a string
|
||||
parse(definitions)
|
||||
try {
|
||||
parse(definitions)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
logger.error(s"Failed to parse Swagger definitions JSON: ${e.getMessage}")
|
||||
logger.error(s"JSON was: ${definitions.take(500)}...")
|
||||
throw new RuntimeException(s"Invalid JSON in Swagger definitions generation. This may be due to unescaped special characters in examples or field names. Error: ${e.getMessage}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -83,9 +83,7 @@ trait APIMethodsDynamicEntity {
|
||||
val singleName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "")
|
||||
val isGetAll = StringUtils.isBlank(id)
|
||||
|
||||
// e.g: "someMultiple-part_Name" -> ["Some", "Multiple", "Part", "Name"]
|
||||
val capitalizedNameParts = entityName.split("(?<=[a-z0-9])(?=[A-Z])|-|_").map(_.capitalize).filterNot(_.trim.isEmpty)
|
||||
val splitName = s"""${capitalizedNameParts.mkString(" ")}"""
|
||||
val splitName = entityName
|
||||
val splitNameWithBankId = if (bankId.isDefined)
|
||||
s"""$splitName(${bankId.getOrElse("")})"""
|
||||
else
|
||||
@ -169,9 +167,7 @@ trait APIMethodsDynamicEntity {
|
||||
case EntityName(bankId, entityName, _, isPersonalEntity) JsonPost json -> _ => { cc =>
|
||||
val singleName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "")
|
||||
val operation: DynamicEntityOperation = CREATE
|
||||
// e.g: "someMultiple-part_Name" -> ["Some", "Multiple", "Part", "Name"]
|
||||
val capitalizedNameParts = entityName.split("(?<=[a-z0-9])(?=[A-Z])|-|_").map(_.capitalize).filterNot(_.trim.isEmpty)
|
||||
val splitName = s"""${capitalizedNameParts.mkString(" ")}"""
|
||||
val splitName = entityName
|
||||
val splitNameWithBankId = if (bankId.isDefined)
|
||||
s"""$splitName(${bankId.getOrElse("")})"""
|
||||
else
|
||||
@ -230,9 +226,7 @@ trait APIMethodsDynamicEntity {
|
||||
case EntityName(bankId, entityName, id, isPersonalEntity) JsonPut json -> _ => { cc =>
|
||||
val singleName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "")
|
||||
val operation: DynamicEntityOperation = UPDATE
|
||||
// e.g: "someMultiple-part_Name" -> ["Some", "Multiple", "Part", "Name"]
|
||||
val capitalizedNameParts = entityName.split("(?<=[a-z0-9])(?=[A-Z])|-|_").map(_.capitalize).filterNot(_.trim.isEmpty)
|
||||
val splitName = s"""${capitalizedNameParts.mkString(" ")}"""
|
||||
val splitName = entityName
|
||||
val splitNameWithBankId = if (bankId.isDefined)
|
||||
s"""$splitName(${bankId.getOrElse("")})"""
|
||||
else
|
||||
@ -303,9 +297,7 @@ trait APIMethodsDynamicEntity {
|
||||
}
|
||||
case EntityName(bankId, entityName, id, isPersonalEntity) JsonDelete _ => { cc =>
|
||||
val operation: DynamicEntityOperation = DELETE
|
||||
// e.g: "someMultiple-part_Name" -> ["Some", "Multiple", "Part", "Name"]
|
||||
val capitalizedNameParts = entityName.split("(?<=[a-z0-9])(?=[A-Z])|-|_").map(_.capitalize).filterNot(_.trim.isEmpty)
|
||||
val splitName = s"""${capitalizedNameParts.mkString(" ")}"""
|
||||
val splitName = entityName
|
||||
val splitNameWithBankId = if (bankId.isDefined)
|
||||
s"""$splitName(${bankId.getOrElse("")})"""
|
||||
else
|
||||
|
||||
@ -29,7 +29,7 @@ object EntityName {
|
||||
case "my" :: entityName :: id :: Nil =>
|
||||
DynamicEntityHelper.definitionsMap.find(definitionMap => definitionMap._1._1 == None && definitionMap._1._2 == entityName && definitionMap._2.bankId.isEmpty && definitionMap._2.hasPersonalEntity)
|
||||
.map(_ => (None, entityName, id, true))
|
||||
|
||||
|
||||
//eg: /FooBar21
|
||||
case entityName :: Nil =>
|
||||
DynamicEntityHelper.definitionsMap.find(definitionMap => definitionMap._1._1 == None && definitionMap._1._2 == entityName && definitionMap._2.bankId.isEmpty)
|
||||
@ -39,7 +39,7 @@ object EntityName {
|
||||
DynamicEntityHelper.definitionsMap.find(definitionMap => definitionMap._1._1 == None && definitionMap._1._2 == entityName && definitionMap._2.bankId.isEmpty)
|
||||
.map(_ => (None, entityName, id, false))
|
||||
|
||||
|
||||
|
||||
//eg: /Banks/BANK_ID/my/FooBar21
|
||||
case "banks" :: bankId :: "my" :: entityName :: Nil =>
|
||||
DynamicEntityHelper.definitionsMap.find(definitionMap => definitionMap._1._1 == Some(bankId) && definitionMap._1._2 == entityName && definitionMap._2.bankId == Some(bankId) && definitionMap._2.hasPersonalEntity)
|
||||
@ -58,14 +58,14 @@ object EntityName {
|
||||
case "banks" :: bankId :: entityName :: id :: Nil =>
|
||||
DynamicEntityHelper.definitionsMap.find(definitionMap => definitionMap._1._1 == Some(bankId) && definitionMap._1._2 == entityName && definitionMap._2.bankId == Some(bankId))
|
||||
.map(_ => (Some(bankId),entityName, id, false))//no bank:
|
||||
|
||||
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
object DynamicEntityHelper {
|
||||
private val implementedInApiVersion = ApiVersion.v4_0_0
|
||||
|
||||
|
||||
// (Some(BankId), EntityName, DynamicEntityInfo)
|
||||
def definitionsMap: Map[(Option[String], String), DynamicEntityInfo] = NewStyle.function.getDynamicEntities(None, true).map(it => ((it.bankId, it.entityName), DynamicEntityInfo(it.metadataJson, it.entityName, it.bankId, it.hasPersonalEntity))).toMap
|
||||
|
||||
@ -82,7 +82,7 @@ object DynamicEntityHelper {
|
||||
// eg: entityName = PetEntity => entityIdName = pet_entity_id
|
||||
s"${entityName}_Id".replaceAll(regexPattern, "_").toLowerCase
|
||||
}
|
||||
|
||||
|
||||
def operationToResourceDoc: Map[(DynamicEntityOperation, String), ResourceDoc] = {
|
||||
val addPrefix = APIUtil.getPropsAsBoolValue("dynamic_entities_have_prefix", true)
|
||||
|
||||
@ -98,7 +98,7 @@ object DynamicEntityHelper {
|
||||
// Csem_case -> Csem Case
|
||||
// _Csem_case -> _Csem Case
|
||||
// csem-case -> Csem Case
|
||||
def prettyTagName(s: String) = s.capitalize.split("(?<=[^-_])[-_]+").reduceLeft(_ + " " + _.capitalize)
|
||||
def prettyTagName(s: String) = s
|
||||
|
||||
def apiTag(entityName: String, singularName: String): ResourceDocTag = {
|
||||
|
||||
@ -139,15 +139,13 @@ object DynamicEntityHelper {
|
||||
(dynamicEntityInfo: DynamicEntityInfo): mutable.Map[(DynamicEntityOperation, String), ResourceDoc] = {
|
||||
val entityName = dynamicEntityInfo.entityName
|
||||
val hasPersonalEntity = dynamicEntityInfo.hasPersonalEntity
|
||||
|
||||
val splitName = entityName
|
||||
// e.g: "someMultiple-part_Name" -> ["Some", "Multiple", "Part", "Name"]
|
||||
val capitalizedNameParts = entityName.split("(?<=[a-z0-9])(?=[A-Z])|-|_").map(_.capitalize).filterNot(_.trim.isEmpty)
|
||||
val splitName = s"""${capitalizedNameParts.mkString(" ")}"""
|
||||
val splitNameWithBankId = if (dynamicEntityInfo.bankId.isDefined)
|
||||
s"""$splitName(${dynamicEntityInfo.bankId.getOrElse("")})"""
|
||||
else
|
||||
s"""$splitName(${dynamicEntityInfo.bankId.getOrElse("")})"""
|
||||
else
|
||||
s"""$splitName"""
|
||||
|
||||
|
||||
val mySplitNameWithBankId = s"My$splitNameWithBankId"
|
||||
|
||||
val idNameInUrl = StringHelpers.snakify(dynamicEntityInfo.idName).toUpperCase()
|
||||
@ -193,7 +191,7 @@ object DynamicEntityHelper {
|
||||
Some(List(dynamicEntityInfo.canGetRole)),
|
||||
createdByBankId= dynamicEntityInfo.bankId
|
||||
)
|
||||
|
||||
|
||||
resourceDocs += (DynamicEntityOperation.GET_ONE, splitNameWithBankId) -> ResourceDoc(
|
||||
endPoint,
|
||||
implementedInApiVersion,
|
||||
@ -339,7 +337,7 @@ object DynamicEntityHelper {
|
||||
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
|
||||
createdByBankId= dynamicEntityInfo.bankId
|
||||
)
|
||||
|
||||
|
||||
resourceDocs += (DynamicEntityOperation.GET_ONE, mySplitNameWithBankId) -> ResourceDoc(
|
||||
endPoint,
|
||||
implementedInApiVersion,
|
||||
@ -365,7 +363,7 @@ object DynamicEntityHelper {
|
||||
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
|
||||
createdByBankId= dynamicEntityInfo.bankId
|
||||
)
|
||||
|
||||
|
||||
resourceDocs += (DynamicEntityOperation.CREATE, mySplitNameWithBankId) -> ResourceDoc(
|
||||
endPoint,
|
||||
implementedInApiVersion,
|
||||
@ -393,7 +391,7 @@ object DynamicEntityHelper {
|
||||
List(apiTag, apiTagDynamicEntity, apiTagDynamic),
|
||||
createdByBankId= dynamicEntityInfo.bankId
|
||||
)
|
||||
|
||||
|
||||
resourceDocs += (DynamicEntityOperation.UPDATE, mySplitNameWithBankId) -> ResourceDoc(
|
||||
endPoint,
|
||||
implementedInApiVersion,
|
||||
@ -422,7 +420,7 @@ object DynamicEntityHelper {
|
||||
Some(List(dynamicEntityInfo.canUpdateRole)),
|
||||
createdByBankId= dynamicEntityInfo.bankId
|
||||
)
|
||||
|
||||
|
||||
resourceDocs += (DynamicEntityOperation.DELETE, mySplitNameWithBankId) -> ResourceDoc(
|
||||
endPoint,
|
||||
implementedInApiVersion,
|
||||
@ -505,7 +503,7 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt
|
||||
val idName = StringUtils.uncapitalize(entityName) + "Id"
|
||||
|
||||
val listName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "_list")
|
||||
|
||||
|
||||
val singleName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "")
|
||||
|
||||
val jsonTypeMap: Map[String, Class[_]] = DynamicEntityFieldType.nameToValue.mapValues(_.jValueType)
|
||||
@ -575,7 +573,7 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt
|
||||
JObject(exampleFields)
|
||||
}
|
||||
val bankIdJObject: JObject = ("bank-id" -> ExampleValue.bankIdExample.value)
|
||||
|
||||
|
||||
def getSingleExample: JObject = if (bankId.isDefined){
|
||||
val SingleObject: JObject = (singleName -> (JObject(JField(idName, JString(ExampleValue.idExample.value)) :: getSingleExampleWithoutId.obj)))
|
||||
bankIdJObject merge SingleObject
|
||||
@ -585,7 +583,7 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt
|
||||
|
||||
def getExampleList: JObject = if (bankId.isDefined){
|
||||
val objectList: JObject = (listName -> JArray(List(getSingleExample)))
|
||||
bankIdJObject merge objectList
|
||||
bankIdJObject merge objectList
|
||||
} else{
|
||||
(listName -> JArray(List(getSingleExample)))
|
||||
}
|
||||
@ -597,33 +595,33 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt
|
||||
}
|
||||
|
||||
object DynamicEntityInfo {
|
||||
def canCreateRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanCreateDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanCreateDynamicEntity_System" + entityName, false)
|
||||
def canUpdateRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanUpdateDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanUpdateDynamicEntity_System" + entityName, false)
|
||||
|
||||
def canGetRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
def canCreateRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanGetDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanGetDynamicEntity_System" + entityName, false)
|
||||
|
||||
def canDeleteRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanDeleteDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanDeleteDynamicEntity_System" + entityName, false)
|
||||
getOrCreateDynamicApiRole("CanCreateDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanCreateDynamicEntity_System" + entityName, false)
|
||||
def canUpdateRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanUpdateDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanUpdateDynamicEntity_System" + entityName, false)
|
||||
|
||||
def canGetRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanGetDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanGetDynamicEntity_System" + entityName, false)
|
||||
|
||||
def canDeleteRole(entityName: String, bankId:Option[String]): ApiRole =
|
||||
if(bankId.isDefined)
|
||||
getOrCreateDynamicApiRole("CanDeleteDynamicEntity_" + entityName, true)
|
||||
else
|
||||
getOrCreateDynamicApiRole("CanDeleteDynamicEntity_System" + entityName, false)
|
||||
|
||||
def roleNames(entityName: String, bankId:Option[String]): List[String] = List(
|
||||
canCreateRole(entityName, bankId),
|
||||
canCreateRole(entityName, bankId),
|
||||
canUpdateRole(entityName, bankId),
|
||||
canGetRole(entityName, bankId),
|
||||
canGetRole(entityName, bankId),
|
||||
canDeleteRole(entityName, bankId)
|
||||
).map(_.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,59 +14,87 @@ import code.util.Helper.MdcLoggable
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
// Test case classes for JSON escaping tests
|
||||
case class TestWithQuotes(name: String, description: String)
|
||||
case class TestWithNewlines(text: String)
|
||||
case class AbacRule(rule: String)
|
||||
|
||||
class SwaggerFactoryUnitTest extends V140ServerSetup with MdcLoggable {
|
||||
|
||||
feature("Unit tests for the translateEntity method") {
|
||||
scenario("Test the $colon faild case") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.license)
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.license)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
translateCaseClassToSwaggerFormatString should not include ("$colon")
|
||||
}
|
||||
scenario("Test the the List[Case Class] in translateEntity function") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.postCounterpartyJSON)
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(
|
||||
SwaggerDefinitionsJSON.postCounterpartyJSON
|
||||
)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
translateCaseClassToSwaggerFormatString should not include ("$colon")
|
||||
}
|
||||
|
||||
scenario("Test `null` in translateEntity function") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.counterpartyMetadataJson)
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(
|
||||
SwaggerDefinitionsJSON.counterpartyMetadataJson
|
||||
)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
translateCaseClassToSwaggerFormatString should not include ("$colon")
|
||||
}
|
||||
|
||||
scenario("Test `SecondaryIdentification: Option[String] = None,` in translateEntity function") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.accountInnerJsonUKOpenBanking_v200.copy(SecondaryIdentification = Some("1111")))
|
||||
scenario(
|
||||
"Test `SecondaryIdentification: Option[String] = None,` in translateEntity function"
|
||||
) {
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(
|
||||
SwaggerDefinitionsJSON.accountInnerJsonUKOpenBanking_v200
|
||||
.copy(SecondaryIdentification = Some("1111"))
|
||||
)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
//This optional type should be "1111", should not contain Some(1111)
|
||||
// This optional type should be "1111", should not contain Some(1111)
|
||||
translateCaseClassToSwaggerFormatString should not include ("""Some(1111)""")
|
||||
}
|
||||
|
||||
scenario("Test `product_attributes = Some(List(productAttributeResponseJson))` in translateEntity function") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.productJsonV310)
|
||||
scenario(
|
||||
"Test `product_attributes = Some(List(productAttributeResponseJson))` in translateEntity function"
|
||||
) {
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(
|
||||
SwaggerDefinitionsJSON.productJsonV310
|
||||
)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
translateCaseClassToSwaggerFormatString should not include ("""/definitions/scala.Some""")
|
||||
translateCaseClassToSwaggerFormatString should not include ("""$colon""")
|
||||
}
|
||||
|
||||
|
||||
scenario("Test `enumeration` for translateEntity function") {
|
||||
val translateCaseClassToSwaggerFormatString: String = SwaggerJSONFactory.translateEntity(SwaggerDefinitionsJSON.cardAttributeCommons)
|
||||
val translateCaseClassToSwaggerFormatString: String =
|
||||
SwaggerJSONFactory.translateEntity(
|
||||
SwaggerDefinitionsJSON.cardAttributeCommons
|
||||
)
|
||||
logger.debug("{" + translateCaseClassToSwaggerFormatString + "}")
|
||||
translateCaseClassToSwaggerFormatString should not include ("""/definitions/Val""")
|
||||
}
|
||||
}
|
||||
feature("Test all V300, V220 and V210, exampleRequestBodies and successResponseBodies and all the case classes in SwaggerDefinitionsJSON") {
|
||||
feature(
|
||||
"Test all V300, V220 and V210, exampleRequestBodies and successResponseBodies and all the case classes in SwaggerDefinitionsJSON"
|
||||
) {
|
||||
scenario("Test all the case classes") {
|
||||
val resourceDocList: ArrayBuffer[ResourceDoc] = ArrayBuffer.empty
|
||||
OBPAPI6_0_0.allResourceDocs ++
|
||||
OBPAPI5_1_0.allResourceDocs ++
|
||||
OBPAPI5_0_0.allResourceDocs ++
|
||||
OBPAPI4_0_0.allResourceDocs ++
|
||||
OBPAPI3_1_0.allResourceDocs ++
|
||||
OBPAPI3_0_0.allResourceDocs ++
|
||||
OBPAPI2_2_0.allResourceDocs ++
|
||||
OBPAPI6_0_0.allResourceDocs ++
|
||||
OBPAPI5_1_0.allResourceDocs ++
|
||||
OBPAPI5_0_0.allResourceDocs ++
|
||||
OBPAPI4_0_0.allResourceDocs ++
|
||||
OBPAPI3_1_0.allResourceDocs ++
|
||||
OBPAPI3_0_0.allResourceDocs ++
|
||||
OBPAPI2_2_0.allResourceDocs ++
|
||||
OBPAPI2_1_0.allResourceDocs
|
||||
|
||||
//Translate every entity(JSON Case Class) in a list to appropriate swagger format
|
||||
// Translate every entity(JSON Case Class) in a list to appropriate swagger format
|
||||
val listOfExampleRequestBodyDefinition =
|
||||
for (e <- resourceDocList if e.exampleRequestBody != null)
|
||||
yield {
|
||||
@ -79,13 +107,15 @@ class SwaggerFactoryUnitTest extends V140ServerSetup with MdcLoggable {
|
||||
SwaggerJSONFactory.translateEntity(e.successResponseBody)
|
||||
}
|
||||
|
||||
val listNestedMissingDefinition: List[String] = SwaggerDefinitionsJSON.allFields
|
||||
.map(SwaggerJSONFactory.translateEntity)
|
||||
.toList
|
||||
val listNestedMissingDefinition: List[String] =
|
||||
SwaggerDefinitionsJSON.allFields
|
||||
.map(SwaggerJSONFactory.translateEntity)
|
||||
.toList
|
||||
|
||||
val allStrings = listOfExampleRequestBodyDefinition ++ listOfSuccessRequestBodyDefinition ++ listNestedMissingDefinition
|
||||
//All of the following are invalid value in Swagger, if any of them exist,
|
||||
//need check how you create the case class object in SwaggerDefinitionsJSON.json.
|
||||
val allStrings =
|
||||
listOfExampleRequestBodyDefinition ++ listOfSuccessRequestBodyDefinition ++ listNestedMissingDefinition
|
||||
// All of the following are invalid value in Swagger, if any of them exist,
|
||||
// need check how you create the case class object in SwaggerDefinitionsJSON.json.
|
||||
allStrings.toString() should not include ("Nil$")
|
||||
allStrings.toString() should not include ("JArray")
|
||||
allStrings.toString() should not include ("JBool")
|
||||
@ -98,5 +128,66 @@ class SwaggerFactoryUnitTest extends V140ServerSetup with MdcLoggable {
|
||||
logger.debug(allStrings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
feature("Test JSON escaping robustness in Swagger generation") {
|
||||
scenario("Test quotes in example values are properly escaped") {
|
||||
val testObj = TestWithQuotes(
|
||||
name = "Test with \"quotes\"",
|
||||
description = "Has 'single' and \"double\" quotes"
|
||||
)
|
||||
val result = SwaggerJSONFactory.translateEntity(testObj)
|
||||
noException should be thrownBy {
|
||||
net.liftweb.json.parse("{" + result + "}")
|
||||
}
|
||||
result should include("\\\"")
|
||||
}
|
||||
|
||||
scenario("Test newlines and special chars are properly escaped") {
|
||||
val testObj = TestWithNewlines(text = "Line 1\nLine 2\tTab")
|
||||
val result = SwaggerJSONFactory.translateEntity(testObj)
|
||||
noException should be thrownBy {
|
||||
net.liftweb.json.parse("{" + result + "}")
|
||||
}
|
||||
result should include("\\n")
|
||||
}
|
||||
|
||||
scenario("Test ABAC rule-like strings with escaped quotes") {
|
||||
val testObj = AbacRule(rule = """user.emailAddress.contains(\"admin\")""")
|
||||
val result = SwaggerJSONFactory.translateEntity(testObj)
|
||||
noException should be thrownBy {
|
||||
net.liftweb.json.parse("{" + result + "}")
|
||||
}
|
||||
}
|
||||
|
||||
scenario("Test error messages with special characters") {
|
||||
import code.api.v1_4_0.JSONFactory1_4_0
|
||||
val mockResourceDoc = JSONFactory1_4_0.ResourceDocJson(
|
||||
operation_id = "testOp",
|
||||
implemented_by = JSONFactory1_4_0.ImplementedByJson("1.0.0", "test"),
|
||||
request_verb = "GET",
|
||||
request_url = "/test",
|
||||
summary = "Test",
|
||||
description = "Test desc",
|
||||
description_markdown = "Test desc",
|
||||
example_request_body = null,
|
||||
success_response_body = SwaggerDefinitionsJSON.bankJSON,
|
||||
error_response_bodies = List("OBP-10000"),
|
||||
tags = List("Test"),
|
||||
typed_request_body = net.liftweb.json.JNothing,
|
||||
typed_success_response_body = net.liftweb.json.JNothing,
|
||||
roles = Some(List()),
|
||||
is_featured = false,
|
||||
special_instructions = "",
|
||||
specified_url = "/obp/v4.0.0/test",
|
||||
connector_methods = List(),
|
||||
created_by_bank_id = None
|
||||
)
|
||||
noException should be thrownBy {
|
||||
SwaggerJSONFactory.loadDefinitions(
|
||||
List(mockResourceDoc),
|
||||
SwaggerDefinitionsJSON.allFields.take(10)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
package code.api.v5_0_0
|
||||
|
||||
/*
|
||||
* CardTest is completely commented out due to initialization issues.
|
||||
*
|
||||
* The problem: When this test class is loaded during test discovery, it triggers initialization of
|
||||
* V500ServerSetupAsync which tries to start a test server. This causes port binding issues and
|
||||
* initialization errors that abort the entire test suite.
|
||||
*
|
||||
* Additional issues:
|
||||
* - createPhysicalCardJsonV500 causes circular dependency chain
|
||||
* - ExampleValue$ → Glossary$ → Helper$.ObpS → cglib proxy creation fails
|
||||
* - NoClassDefFoundError when running on Java 17 with Java 11 project configuration
|
||||
* - Port 8018 binding conflicts
|
||||
*
|
||||
* TODO: Fix the initialization order, move createPhysicalCardJsonV500 call inside test methods,
|
||||
* and resolve server setup issues before re-enabling this test.
|
||||
*/
|
||||
|
||||
/*
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
|
||||
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{createPhysicalCardJsonV500}
|
||||
import code.api.util.ApiRole
|
||||
@ -19,19 +37,6 @@ import org.scalatest.{Ignore, Tag}
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* CardTest is temporarily disabled due to initialization issues with createPhysicalCardJsonV500.
|
||||
*
|
||||
* The problem: When this test class is loaded, it triggers initialization of createPhysicalCardJsonV500
|
||||
* at line 37, which causes a circular dependency chain:
|
||||
* - createPhysicalCardJsonV500 → ExampleValue$ → Glossary$ → Helper$.ObpS → cglib proxy creation
|
||||
*
|
||||
* This fails with NoClassDefFoundError when running on Java 17 with Java 11 project configuration.
|
||||
* The error occurs because cglib cannot create proxies due to module access restrictions.
|
||||
*
|
||||
* TODO: Fix the initialization order or move createPhysicalCardJsonV500 call inside test methods
|
||||
* instead of at class initialization time (line 37).
|
||||
*/
|
||||
@Ignore
|
||||
class CardTest extends V500ServerSetupAsync with DefaultUsers {
|
||||
|
||||
@ -41,8 +46,8 @@ class CardTest extends V500ServerSetupAsync with DefaultUsers {
|
||||
|
||||
|
||||
feature("test Card APIs") {
|
||||
scenario("We will create Card with many error cases",
|
||||
ApiEndpointAddCardForBank,
|
||||
scenario("We will create Card with many error cases",
|
||||
ApiEndpointAddCardForBank,
|
||||
VersionOfApi
|
||||
) {
|
||||
Given("The test bank and test account")
|
||||
@ -61,7 +66,7 @@ class CardTest extends V500ServerSetupAsync with DefaultUsers {
|
||||
|
||||
val properCardJson = dummyCard.copy(account_id = testAccount.value, issue_number = "123", customer_id = customerId)
|
||||
|
||||
val requestAnonymous = (v5_0_0_Request / "management"/"banks" / testBank.value / "cards" ).POST
|
||||
val requestAnonymous = (v5_0_0_Request / "management"/"banks" / testBank.value / "cards" ).POST
|
||||
val requestWithAuthUser = (v5_0_0_Request / "management" /"banks" / testBank.value / "cards" ).POST <@ (user1)
|
||||
|
||||
Then(s"We test with anonymous user.")
|
||||
@ -99,7 +104,7 @@ class CardTest extends V500ServerSetupAsync with DefaultUsers {
|
||||
responseWithWrongVlaueForAllows.body.toString contains(AllowedValuesAre++ CardAction.availableValues.mkString(", "))
|
||||
|
||||
Then(s"We call the authentication user, but wrong card.replacement value")
|
||||
val wrongCardReplacementReasonJson = dummyCard.copy(replacement = Some(ReplacementJSON(new Date(),"Wrong"))) // The replacement must be Enum of `CardReplacementReason`
|
||||
val wrongCardReplacementReasonJson = dummyCard.copy(replacement = Some(ReplacementJSON(new Date(),"Wrong"))) // The replacement must be Enum of `CardReplacementReason`
|
||||
val responseWrongCardReplacementReasonJson = makePostRequest(requestWithAuthUser, write(wrongCardReplacementReasonJson))
|
||||
And(s"We should get 400 and get the error message")
|
||||
responseWrongCardReplacementReasonJson.code should equal(400)
|
||||
@ -169,4 +174,5 @@ class CardTest extends V500ServerSetupAsync with DefaultUsers {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@ -46,7 +46,7 @@ class MetricsTest extends V500ServerSetup {
|
||||
override def afterAll(): Unit = {
|
||||
super.afterAll()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
@ -57,14 +57,17 @@ class MetricsTest extends V500ServerSetup {
|
||||
object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString)
|
||||
object ApiEndpoint1 extends Tag(nameOf(Implementations5_0_0.getMetricsAtBank))
|
||||
|
||||
lazy val apiEndpointName = nameOf(Implementations5_0_0.getMetricsAtBank)
|
||||
lazy val versionName = ApiVersion.v5_0_0.toString
|
||||
|
||||
lazy val bankId = testBankId1.value
|
||||
|
||||
def getMetrics(consumerAndToken: Option[(Consumer, Token)], bankId: String): APIResponse = {
|
||||
val request = v5_0_0_Request / "management" / "metrics" / "banks" / bankId <@(consumerAndToken)
|
||||
makeGetRequest(request)
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
|
||||
|
||||
feature(s"test $apiEndpointName version $versionName - Unauthorized access") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When(s"We make a request $ApiEndpoint1")
|
||||
val response400 = getMetrics(None, bankId)
|
||||
@ -73,7 +76,7 @@ class MetricsTest extends V500ServerSetup {
|
||||
response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access") {
|
||||
feature(s"test $apiEndpointName version $versionName - Authorized access") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When(s"We make a request $ApiEndpoint1")
|
||||
val response400 = getMetrics(user1, bankId)
|
||||
@ -82,7 +85,7 @@ class MetricsTest extends V500ServerSetup {
|
||||
response400.body.extract[ErrorMessage].message contains (UserHasMissingRoles + CanGetMetricsAtOneBank) should be (true)
|
||||
}
|
||||
}
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with proper Role") {
|
||||
feature(s"test $apiEndpointName version $versionName - Authorized access with proper Role") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When(s"We make a request $ApiEndpoint1")
|
||||
Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanGetMetricsAtOneBank.toString)
|
||||
@ -92,5 +95,5 @@ class MetricsTest extends V500ServerSetup {
|
||||
response400.body.extract[MetricsJson]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
559
run_all_tests.sh
Executable file
559
run_all_tests.sh
Executable file
@ -0,0 +1,559 @@
|
||||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# OBP-API Test Runner Script
|
||||
#
|
||||
# What it does:
|
||||
# 1. Changes terminal to blue background with "Tests Running" in title
|
||||
# 2. Runs: mvn clean test
|
||||
# 3. Shows all test output in real-time
|
||||
# 4. Updates title bar with: phase, time elapsed, pass/fail counts
|
||||
# 5. Saves detailed log and summary to test-results/
|
||||
# 6. Restores terminal to normal when done
|
||||
#
|
||||
# Usage:
|
||||
# ./run_all_tests.sh - Run full test suite
|
||||
# ./run_all_tests.sh --summary-only - Regenerate summary from existing log
|
||||
################################################################################
|
||||
|
||||
set -e
|
||||
|
||||
################################################################################
|
||||
# PARSE COMMAND LINE ARGUMENTS
|
||||
################################################################################
|
||||
|
||||
SUMMARY_ONLY=false
|
||||
if [ "$1" = "--summary-only" ]; then
|
||||
SUMMARY_ONLY=true
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# TERMINAL STYLING FUNCTIONS
|
||||
################################################################################
|
||||
|
||||
# Set terminal to "test mode" - blue background, special title
|
||||
set_terminal_style() {
|
||||
local phase="${1:-Running}"
|
||||
echo -ne "\033]0;OBP-API Tests ${phase}...\007" # Title
|
||||
echo -ne "\033]11;#001f3f\007" # Dark blue background
|
||||
echo -ne "\033]10;#ffffff\007" # White text
|
||||
# Print header bar
|
||||
printf "\033[44m\033[1;37m%-$(tput cols)s\r OBP-API TEST RUNNER ACTIVE - ${phase} \n%-$(tput cols)s\033[0m\n" " " " "
|
||||
}
|
||||
|
||||
# Update title bar with progress: "Testing: DynamicEntityTest - Scenario name [5m 23s]"
|
||||
update_terminal_title() {
|
||||
local phase="$1" # Starting, Building, Testing, Complete
|
||||
local elapsed="${2:-}" # Time elapsed (e.g. "5m 23s")
|
||||
local counts="${3:-}" # Module counts (e.g. "obp-commons:+38 obp-api:+245")
|
||||
local suite="${4:-}" # Current test suite name
|
||||
local scenario="${5:-}" # Current scenario name
|
||||
|
||||
local title="OBP-API ${phase}"
|
||||
[ -n "$suite" ] && title="${title}: ${suite}"
|
||||
[ -n "$scenario" ] && title="${title} - ${scenario}"
|
||||
title="${title}..."
|
||||
[ -n "$elapsed" ] && title="${title} [${elapsed}]"
|
||||
[ -n "$counts" ] && title="${title} ${counts}"
|
||||
|
||||
echo -ne "\033]0;${title}\007"
|
||||
}
|
||||
|
||||
# Restore terminal to normal (black background, default title)
|
||||
restore_terminal_style() {
|
||||
echo -ne "\033]0;Terminal\007\033]11;#000000\007\033]10;#ffffff\007\033[0m"
|
||||
}
|
||||
|
||||
# Cleanup function: stop monitor, restore terminal, remove flag files
|
||||
cleanup_on_exit() {
|
||||
# Stop background monitor if running
|
||||
if [ -n "${MONITOR_PID:-}" ]; then
|
||||
kill $MONITOR_PID 2>/dev/null || true
|
||||
wait $MONITOR_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove monitor flag file
|
||||
rm -f "${LOG_DIR}/monitor.flag" 2>/dev/null || true
|
||||
|
||||
# Restore terminal
|
||||
restore_terminal_style
|
||||
}
|
||||
|
||||
# Always cleanup on exit (Ctrl+C, errors, or normal completion)
|
||||
trap cleanup_on_exit EXIT INT TERM
|
||||
|
||||
################################################################################
|
||||
# CONFIGURATION
|
||||
################################################################################
|
||||
|
||||
LOG_DIR="test-results"
|
||||
DETAIL_LOG="${LOG_DIR}/last_run.log" # Full Maven output
|
||||
SUMMARY_LOG="${LOG_DIR}/last_run_summary.log" # Summary only
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
# If summary-only mode, skip to summary generation
|
||||
if [ "$SUMMARY_ONLY" = true ]; then
|
||||
if [ ! -f "${DETAIL_LOG}" ]; then
|
||||
echo "ERROR: No log file found at ${DETAIL_LOG}"
|
||||
echo "Please run tests first without --summary-only flag"
|
||||
exit 1
|
||||
fi
|
||||
echo "Regenerating summary from existing log: ${DETAIL_LOG}"
|
||||
# Skip cleanup and jump to summary generation
|
||||
START_TIME=0
|
||||
END_TIME=0
|
||||
DURATION=0
|
||||
DURATION_MIN=0
|
||||
DURATION_SEC=0
|
||||
else
|
||||
# Delete old log files and stale flag files from previous run
|
||||
echo "Cleaning up old files..."
|
||||
if [ -f "${DETAIL_LOG}" ]; then
|
||||
rm -f "${DETAIL_LOG}"
|
||||
echo " - Removed old detail log"
|
||||
fi
|
||||
if [ -f "${SUMMARY_LOG}" ]; then
|
||||
rm -f "${SUMMARY_LOG}"
|
||||
echo " - Removed old summary log"
|
||||
fi
|
||||
if [ -f "${LOG_DIR}/monitor.flag" ]; then
|
||||
rm -f "${LOG_DIR}/monitor.flag"
|
||||
echo " - Removed stale monitor flag"
|
||||
fi
|
||||
if [ -f "${LOG_DIR}/warning_analysis.tmp" ]; then
|
||||
rm -f "${LOG_DIR}/warning_analysis.tmp"
|
||||
echo " - Removed stale warning analysis"
|
||||
fi
|
||||
if [ -f "${LOG_DIR}/recent_lines.tmp" ]; then
|
||||
rm -f "${LOG_DIR}/recent_lines.tmp"
|
||||
echo " - Removed stale temp file"
|
||||
fi
|
||||
fi # End of if [ "$SUMMARY_ONLY" = true ]
|
||||
|
||||
################################################################################
|
||||
# HELPER FUNCTIONS
|
||||
################################################################################
|
||||
|
||||
# Log message to terminal and summary file
|
||||
log_message() {
|
||||
echo "$1"
|
||||
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" >> "${SUMMARY_LOG}"
|
||||
}
|
||||
|
||||
# Print section header
|
||||
print_header() {
|
||||
echo ""
|
||||
echo "================================================================================"
|
||||
echo "$1"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Analyze warnings and return top contributors
|
||||
analyze_warnings() {
|
||||
local log_file="$1"
|
||||
local temp_file="${LOG_DIR}/warning_analysis.tmp"
|
||||
|
||||
# Extract and categorize warnings from last 5000 lines (for performance)
|
||||
# This gives good coverage without scanning entire multi-MB log file
|
||||
tail -n 5000 "${log_file}" 2>/dev/null | grep -i "warning" | \
|
||||
# Normalize patterns to group similar warnings
|
||||
sed -E 's/line [0-9]+/line XXX/g' | \
|
||||
sed -E 's/[0-9]+ warnings?/N warnings/g' | \
|
||||
sed -E 's/\[WARNING\] .*(src|test)\/[^ ]+/[WARNING] <source-file>/g' | \
|
||||
sed -E 's/version [0-9]+\.[0-9]+(\.[0-9]+)?/version X.X/g' | \
|
||||
# Extract the core warning message
|
||||
sed -E 's/^.*\[WARNING\] *//' | \
|
||||
sort | uniq -c | sort -rn > "${temp_file}"
|
||||
|
||||
# Return the temp file path for further processing
|
||||
echo "${temp_file}"
|
||||
}
|
||||
|
||||
# Format and display top warning factors
|
||||
display_warning_factors() {
|
||||
local analysis_file="$1"
|
||||
local max_display="${2:-10}"
|
||||
|
||||
if [ ! -f "${analysis_file}" ] || [ ! -s "${analysis_file}" ]; then
|
||||
log_message " No detailed warning analysis available"
|
||||
return
|
||||
fi
|
||||
|
||||
local total_warning_types=$(wc -l < "${analysis_file}")
|
||||
local displayed=0
|
||||
|
||||
log_message "Top Warning Factors:"
|
||||
log_message "-------------------"
|
||||
|
||||
while IFS= read -r line && [ $displayed -lt $max_display ]; do
|
||||
# Extract count and message
|
||||
local count=$(echo "$line" | awk '{print $1}')
|
||||
local message=$(echo "$line" | sed -E 's/^[[:space:]]*[0-9]+[[:space:]]*//')
|
||||
|
||||
# Truncate long messages
|
||||
if [ ${#message} -gt 80 ]; then
|
||||
message="${message:0:77}..."
|
||||
fi
|
||||
|
||||
# Format with count prominence
|
||||
printf " %4d x %s\n" "$count" "$message" | tee -a "${SUMMARY_LOG}" > /dev/tty
|
||||
|
||||
displayed=$((displayed + 1))
|
||||
done < "${analysis_file}"
|
||||
|
||||
if [ $total_warning_types -gt $max_display ]; then
|
||||
local remaining=$((total_warning_types - max_display))
|
||||
log_message " ... and ${remaining} more warning type(s)"
|
||||
fi
|
||||
|
||||
# Clean up temp file
|
||||
rm -f "${analysis_file}"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# GENERATE SUMMARY FUNCTION (DRY)
|
||||
################################################################################
|
||||
|
||||
generate_summary() {
|
||||
local detail_log="$1"
|
||||
local summary_log="$2"
|
||||
local start_time="${3:-0}"
|
||||
local end_time="${4:-0}"
|
||||
|
||||
# Calculate duration
|
||||
local duration=$((end_time - start_time))
|
||||
local duration_min=$((duration / 60))
|
||||
local duration_sec=$((duration % 60))
|
||||
|
||||
# If no timing info (summary-only mode), extract from log
|
||||
if [ $duration -eq 0 ] && grep -q "Total time:" "$detail_log"; then
|
||||
local time_str=$(grep "Total time:" "$detail_log" | tail -1)
|
||||
duration_min=$(echo "$time_str" | grep -oP '\d+(?= min)' || echo "0")
|
||||
duration_sec=$(echo "$time_str" | grep -oP '\d+(?=\.\d+ s)' || echo "0")
|
||||
fi
|
||||
|
||||
print_header "Test Results Summary"
|
||||
|
||||
# Extract test statistics from ScalaTest output (with UNKNOWN fallback if extraction fails)
|
||||
# ScalaTest outputs across multiple lines:
|
||||
# Run completed in X seconds.
|
||||
# Total number of tests run: N
|
||||
# Suites: completed M, aborted 0
|
||||
# Tests: succeeded N, failed 0, canceled 0, ignored 0, pending 0
|
||||
# All tests passed.
|
||||
# We need to extract the stats from the last test run (in case there are multiple modules)
|
||||
SCALATEST_SECTION=$(grep -A 4 "Run completed" "${detail_log}" | tail -5)
|
||||
if [ -n "$SCALATEST_SECTION" ]; then
|
||||
TOTAL_TESTS=$(echo "$SCALATEST_SECTION" | grep -oP "Total number of tests run: \K\d+" || echo "UNKNOWN")
|
||||
SUCCEEDED=$(echo "$SCALATEST_SECTION" | grep -oP "succeeded \K\d+" || echo "UNKNOWN")
|
||||
FAILED=$(echo "$SCALATEST_SECTION" | grep -oP "failed \K\d+" || echo "UNKNOWN")
|
||||
ERRORS=$(echo "$SCALATEST_SECTION" | grep -oP "errors \K\d+" || echo "0")
|
||||
SKIPPED=$(echo "$SCALATEST_SECTION" | grep -oP "ignored \K\d+" || echo "UNKNOWN")
|
||||
else
|
||||
TOTAL_TESTS="UNKNOWN"
|
||||
SUCCEEDED="UNKNOWN"
|
||||
FAILED="UNKNOWN"
|
||||
ERRORS="0"
|
||||
SKIPPED="UNKNOWN"
|
||||
fi
|
||||
WARNINGS=$(grep -c "WARNING" "${detail_log}" || echo "UNKNOWN")
|
||||
|
||||
# Determine build status
|
||||
if grep -q "BUILD SUCCESS" "${detail_log}"; then
|
||||
BUILD_STATUS="SUCCESS"
|
||||
BUILD_COLOR=""
|
||||
elif grep -q "BUILD FAILURE" "${detail_log}"; then
|
||||
BUILD_STATUS="FAILURE"
|
||||
BUILD_COLOR=""
|
||||
else
|
||||
BUILD_STATUS="UNKNOWN"
|
||||
BUILD_COLOR=""
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
log_message "Test Run Summary"
|
||||
log_message "================"
|
||||
log_message "Timestamp: $(date)"
|
||||
log_message "Duration: ${duration_min}m ${duration_sec}s"
|
||||
log_message "Build Status: ${BUILD_STATUS}"
|
||||
log_message ""
|
||||
log_message "Test Statistics:"
|
||||
log_message " Total: ${TOTAL_TESTS}"
|
||||
log_message " Succeeded: ${SUCCEEDED}"
|
||||
log_message " Failed: ${FAILED}"
|
||||
log_message " Errors: ${ERRORS}"
|
||||
log_message " Skipped: ${SKIPPED}"
|
||||
log_message " Warnings: ${WARNINGS}"
|
||||
log_message ""
|
||||
|
||||
# Analyze and display warning factors if warnings exist
|
||||
if [ "${WARNINGS}" != "0" ] && [ "${WARNINGS}" != "UNKNOWN" ]; then
|
||||
warning_analysis=$(analyze_warnings "${detail_log}")
|
||||
display_warning_factors "${warning_analysis}" 10
|
||||
log_message ""
|
||||
fi
|
||||
|
||||
# Show failed tests if any (only actual test failures, not application ERROR logs)
|
||||
if [ "${FAILED}" != "0" ] && [ "${FAILED}" != "UNKNOWN" ]; then
|
||||
log_message "Failed Tests:"
|
||||
# Look for ScalaTest failure markers, not application ERROR logs
|
||||
grep -E "\*\*\* FAILED \*\*\*|\*\*\* RUN ABORTED \*\*\*" "${detail_log}" | head -50 >> "${summary_log}"
|
||||
log_message ""
|
||||
elif [ "${ERRORS}" != "0" ] && [ "${ERRORS}" != "UNKNOWN" ]; then
|
||||
log_message "Test Errors:"
|
||||
grep -E "\*\*\* FAILED \*\*\*|\*\*\* RUN ABORTED \*\*\*" "${detail_log}" | head -50 >> "${summary_log}"
|
||||
log_message ""
|
||||
fi
|
||||
|
||||
# Final result
|
||||
print_header "Test Run Complete"
|
||||
|
||||
if [ "${BUILD_STATUS}" = "SUCCESS" ] && [ "${FAILED}" = "0" ] && [ "${ERRORS}" = "0" ]; then
|
||||
log_message "[PASS] All tests passed!"
|
||||
return 0
|
||||
else
|
||||
log_message "[FAIL] Tests failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# SUMMARY-ONLY MODE
|
||||
################################################################################
|
||||
|
||||
if [ "$SUMMARY_ONLY" = true ]; then
|
||||
# Just regenerate the summary and exit
|
||||
rm -f "${SUMMARY_LOG}"
|
||||
if generate_summary "${DETAIL_LOG}" "${SUMMARY_LOG}" 0 0; then
|
||||
log_message ""
|
||||
log_message "Summary regenerated:"
|
||||
log_message " ${SUMMARY_LOG}"
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# START TEST RUN
|
||||
################################################################################
|
||||
|
||||
set_terminal_style "Starting"
|
||||
|
||||
# Start the test run
|
||||
print_header "OBP-API Test Suite"
|
||||
log_message "Starting test run at $(date)"
|
||||
log_message "Detail log: ${DETAIL_LOG}"
|
||||
log_message "Summary log: ${SUMMARY_LOG}"
|
||||
echo ""
|
||||
|
||||
# Set Maven options for tests
|
||||
# The --add-opens flags tell Java 17 to allow Kryo serialization library to access
|
||||
# the internal java.lang.invoke and java.lang modules, which fixes the InaccessibleObjectException
|
||||
export MAVEN_OPTS="-Xss128m -Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED"
|
||||
log_message "Maven Options: ${MAVEN_OPTS}"
|
||||
echo ""
|
||||
|
||||
# Ensure test properties file exists
|
||||
PROPS_FILE="obp-api/src/main/resources/props/test.default.props"
|
||||
PROPS_TEMPLATE="${PROPS_FILE}.template"
|
||||
|
||||
if [ -f "${PROPS_FILE}" ]; then
|
||||
log_message "[OK] Found test.default.props"
|
||||
else
|
||||
log_message "[WARNING] test.default.props not found - creating from template"
|
||||
if [ -f "${PROPS_TEMPLATE}" ]; then
|
||||
cp "${PROPS_TEMPLATE}" "${PROPS_FILE}"
|
||||
log_message "[OK] Created test.default.props"
|
||||
else
|
||||
log_message "ERROR: ${PROPS_TEMPLATE} not found!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# CHECK AND CLEANUP TEST SERVER PORTS
|
||||
# Port 8018 is used by the embedded Jetty test server (configured in test.default.props)
|
||||
################################################################################
|
||||
|
||||
print_header "Checking Test Server Ports"
|
||||
log_message "Checking if test server port 8018 is available..."
|
||||
|
||||
# Check if port 8018 is in use
|
||||
if lsof -i :8018 >/dev/null 2>&1; then
|
||||
log_message "[WARNING] Port 8018 is in use - attempting to kill process"
|
||||
# Try to kill the process using the port
|
||||
PORT_PID=$(lsof -t -i :8018 2>/dev/null)
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
kill -9 $PORT_PID 2>/dev/null || true
|
||||
sleep 2
|
||||
log_message "[OK] Killed process $PORT_PID using port 8018"
|
||||
fi
|
||||
else
|
||||
log_message "[OK] Port 8018 is available"
|
||||
fi
|
||||
|
||||
# Also check for any stale Java test processes
|
||||
STALE_TEST_PROCS=$(ps aux | grep -E "TestServer|ScalaTest.*obp-api" | grep -v grep | awk '{print $2}' || true)
|
||||
if [ -n "$STALE_TEST_PROCS" ]; then
|
||||
log_message "[WARNING] Found stale test processes - cleaning up"
|
||||
echo "$STALE_TEST_PROCS" | xargs kill -9 2>/dev/null || true
|
||||
sleep 2
|
||||
log_message "[OK] Cleaned up stale test processes"
|
||||
else
|
||||
log_message "[OK] No stale test processes found"
|
||||
fi
|
||||
|
||||
log_message ""
|
||||
|
||||
################################################################################
|
||||
# CLEAN METRICS DATABASE
|
||||
################################################################################
|
||||
|
||||
print_header "Cleaning Metrics Database"
|
||||
log_message "Checking for test database files..."
|
||||
|
||||
# Only delete specific test database files to prevent accidental data loss
|
||||
# The test configuration uses test_only_lift_proto.db as the database filename
|
||||
TEST_DB_PATTERNS=(
|
||||
"./test_only_lift_proto.db"
|
||||
"./test_only_lift_proto.db.mv.db"
|
||||
"./test_only_lift_proto.db.trace.db"
|
||||
"./obp-api/test_only_lift_proto.db"
|
||||
"./obp-api/test_only_lift_proto.db.mv.db"
|
||||
"./obp-api/test_only_lift_proto.db.trace.db"
|
||||
)
|
||||
|
||||
FOUND_FILES=false
|
||||
for dbfile in "${TEST_DB_PATTERNS[@]}"; do
|
||||
if [ -f "$dbfile" ]; then
|
||||
FOUND_FILES=true
|
||||
rm -f "$dbfile"
|
||||
log_message " [OK] Deleted: $dbfile"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FOUND_FILES" = false ]; then
|
||||
log_message "No old test database files found"
|
||||
fi
|
||||
|
||||
log_message ""
|
||||
|
||||
################################################################################
|
||||
# RUN TESTS
|
||||
################################################################################
|
||||
|
||||
print_header "Running Tests"
|
||||
update_terminal_title "Building"
|
||||
log_message "Executing: mvn clean test"
|
||||
echo ""
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
export START_TIME
|
||||
|
||||
# Create flag file to signal background process to stop
|
||||
MONITOR_FLAG="${LOG_DIR}/monitor.flag"
|
||||
touch "${MONITOR_FLAG}"
|
||||
|
||||
# Background process: Monitor log file and update title bar with progress
|
||||
(
|
||||
# Wait for log file to be created and have Maven output
|
||||
while [ ! -f "${DETAIL_LOG}" ] || [ ! -s "${DETAIL_LOG}" ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
phase="Building"
|
||||
in_testing=false
|
||||
|
||||
# Keep monitoring until flag file is removed
|
||||
while [ -f "${MONITOR_FLAG}" ]; do
|
||||
# Use tail to look at recent lines only (last 500 lines for performance)
|
||||
# This ensures O(1) performance regardless of log file size
|
||||
recent_lines=$(tail -n 500 "${DETAIL_LOG}" 2>/dev/null)
|
||||
|
||||
# Switch to "Testing" phase when tests start
|
||||
if ! $in_testing && echo "$recent_lines" | grep -q "Run starting" 2>/dev/null; then
|
||||
phase="Testing"
|
||||
in_testing=true
|
||||
fi
|
||||
|
||||
# Extract current running test suite and scenario from recent lines
|
||||
suite=""
|
||||
scenario=""
|
||||
if $in_testing; then
|
||||
# Find the most recent test suite name (pattern like "SomeTest:")
|
||||
# Pipe directly to avoid temp file I/O
|
||||
suite=$(echo "$recent_lines" | grep -E "Test:" | tail -1 | sed 's/\x1b\[[0-9;]*m//g' | sed 's/:$//' | tr -d '\n\r')
|
||||
|
||||
# Find the most recent scenario name (pattern like " Scenario: ..." or "- Scenario: ...")
|
||||
scenario=$(echo "$recent_lines" | grep -i "scenario:" | tail -1 | sed 's/\x1b\[[0-9;]*m//g' | sed 's/^[[:space:]]*-*[[:space:]]*//' | sed -E 's/^[Ss]cenario:[[:space:]]*//' | tr -d '\n\r')
|
||||
|
||||
# Truncate scenario if too long (max 50 chars)
|
||||
if [ -n "$scenario" ] && [ ${#scenario} -gt 50 ]; then
|
||||
scenario="${scenario:0:47}..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Calculate elapsed time
|
||||
duration=$(($(date +%s) - START_TIME))
|
||||
minutes=$((duration / 60))
|
||||
seconds=$((duration % 60))
|
||||
elapsed=$(printf "%dm %ds" $minutes $seconds)
|
||||
|
||||
# Update title: "Testing: DynamicEntityTest - Scenario name [5m 23s]"
|
||||
update_terminal_title "$phase" "$elapsed" "" "$suite" "$scenario"
|
||||
|
||||
sleep 5
|
||||
done
|
||||
) &
|
||||
MONITOR_PID=$!
|
||||
|
||||
# Run Maven (all output goes to terminal AND log file)
|
||||
if mvn clean test 2>&1 | tee "${DETAIL_LOG}"; then
|
||||
TEST_RESULT="SUCCESS"
|
||||
RESULT_COLOR=""
|
||||
else
|
||||
TEST_RESULT="FAILURE"
|
||||
RESULT_COLOR=""
|
||||
fi
|
||||
|
||||
# Stop background monitor by removing flag file
|
||||
rm -f "${MONITOR_FLAG}"
|
||||
sleep 1
|
||||
kill $MONITOR_PID 2>/dev/null || true
|
||||
wait $MONITOR_PID 2>/dev/null || true
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
DURATION_MIN=$((DURATION / 60))
|
||||
DURATION_SEC=$((DURATION % 60))
|
||||
|
||||
# Update title with final results (no suite/scenario name for Complete phase)
|
||||
FINAL_ELAPSED=$(printf "%dm %ds" $DURATION_MIN $DURATION_SEC)
|
||||
# Build final counts with module context
|
||||
FINAL_COMMONS=$(sed -n '/Building Open Bank Project Commons/,/Building Open Bank Project API/{/Tests: succeeded/p;}' "${DETAIL_LOG}" 2>/dev/null | grep -oP "succeeded \K\d+" | head -1)
|
||||
FINAL_API=$(sed -n '/Building Open Bank Project API/,/OBP Http4s Runner/{/Tests: succeeded/p;}' "${DETAIL_LOG}" 2>/dev/null | grep -oP "succeeded \K\d+" | tail -1)
|
||||
FINAL_COUNTS=""
|
||||
[ -n "$FINAL_COMMONS" ] && FINAL_COUNTS="commons:+${FINAL_COMMONS}"
|
||||
[ -n "$FINAL_API" ] && FINAL_COUNTS="${FINAL_COUNTS:+${FINAL_COUNTS} }api:+${FINAL_API}"
|
||||
update_terminal_title "Complete" "$FINAL_ELAPSED" "$FINAL_COUNTS" "" ""
|
||||
|
||||
################################################################################
|
||||
# GENERATE SUMMARY (using DRY function)
|
||||
################################################################################
|
||||
|
||||
if generate_summary "${DETAIL_LOG}" "${SUMMARY_LOG}" "$START_TIME" "$END_TIME"; then
|
||||
EXIT_CODE=0
|
||||
else
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
log_message ""
|
||||
log_message "Logs saved to:"
|
||||
log_message " ${DETAIL_LOG}"
|
||||
log_message " ${SUMMARY_LOG}"
|
||||
echo ""
|
||||
|
||||
exit ${EXIT_CODE}
|
||||
Loading…
Reference in New Issue
Block a user