From 69594e9c9106a9353c127f4b9474274ba8448a0d Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Thu, 9 Nov 2023 14:57:05 +0100 Subject: [PATCH] Fix/33 collected fixes and usage improvements (#34) # 33 - Collected fixes and usage improvements - Add missing logic to control debug regime. - Add missing logic for RuntimeCache data resolving. Improved related logic. --- .../scapi/json/ReferenceResolver.scala | 29 +++++ .../absa/testing/scapi/logging/Logger.scala | 17 +-- .../scapi/reporter/StdOutReporter.scala | 6 +- .../scapi/rest/response/Response.scala | 23 ++-- .../scapi/suite/runner/SuiteRunner.scala | 40 +++++-- .../suites/demo/getOwners.after.json | 2 +- .../suites/demo/getOwners.before.json | 8 +- .../suites/demo/getOwners.suite.json | 6 +- .../scapi/json/ReferenceResolverTest.scala | 106 ++++++++++++++++++ .../scapi/rest/response/ResponseLogTest.scala | 6 +- 10 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 testApi/src/test/scala/africa/absa/testing/scapi/json/ReferenceResolverTest.scala diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/json/ReferenceResolver.scala b/testApi/src/main/scala/africa/absa/testing/scapi/json/ReferenceResolver.scala index 47101e7..82e3135 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/json/ReferenceResolver.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/json/ReferenceResolver.scala @@ -171,6 +171,15 @@ case class Header private(name: String, value: String) extends ReferenceResolver * @return a new Header option instance with resolved references. */ def resolveReferences(references: Map[String, String]): Header = this.copy(value = getResolved(value, references)) + + /** + * Method to resolve references using Runtime Cache. This method is used when the resolution of a reference is not possible at compile-time. + * + * @return A new Header instance with resolved references. + */ + def resolveByRuntimeCache(): Header = this.copy( + value = RuntimeCache.resolve(this.value) + ) } /** @@ -196,6 +205,17 @@ case class Action private(methodName: String, url: String, body: Option[String] body = body.map(b => getResolved(b, references)), params = params.map(_.map(param => param.resolveReferences(references))) ) + + /** + * Method to resolve references using Runtime Cache. This method is used when the resolution of a reference is not possible at compile-time. + * + * @return A new Action instance with resolved references. + */ + def resolveByRuntimeCache(): Action = this.copy( + url = RuntimeCache.resolve(this.url), + body = this.body.fold(this.body)(body => Option(RuntimeCache.resolve(body))), + params = this.params.fold(this.params)(params => Option(params.map(param => param.resolveByRuntimeCache()))) + ) } /** @@ -250,4 +270,13 @@ case class Param private(name: String, value: String) extends ReferenceResolver * @return a new Param option instance with resolved references. */ def resolveReferences(references: Map[String, String]): Param = this.copy(value = getResolved(value, references)) + + /** + * Method to resolve references using Runtime Cache. This method is used when the resolution of a reference is not possible at compile-time. + * + * @return A new Param instance with resolved references. + */ + def resolveByRuntimeCache(): Param = this.copy( + value = RuntimeCache.resolve(value) + ) } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/logging/Logger.scala b/testApi/src/main/scala/africa/absa/testing/scapi/logging/Logger.scala index f85148a..6f02a8c 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/logging/Logger.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/logging/Logger.scala @@ -25,13 +25,16 @@ object Logger { level = newLevel } - private def log(level: Level, message: String): Unit = { - val callerClassName = new Exception().getStackTrace.drop(1) - .find(_.getClassName != getClass.getName) - .map(_.getClassName) - .getOrElse("unknown") - val logger = LogManager.getLogger(callerClassName) - if (logger.isEnabled(level)) logger.log(level, message) + private def log(messageLevel: Level, message: String): Unit = { + if (messageLevel.isMoreSpecificThan(level)) { + val callerClassName = new Exception().getStackTrace.drop(1) + .find(_.getClassName != getClass.getName) + .map(_.getClassName) + .getOrElse("unknown") + val logger = LogManager.getLogger(callerClassName) + + if (logger.isEnabled(messageLevel)) logger.log(messageLevel, message) + } } def debug(message: String): Unit = { diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/reporter/StdOutReporter.scala b/testApi/src/main/scala/africa/absa/testing/scapi/reporter/StdOutReporter.scala index 9413b1e..26e2630 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/reporter/StdOutReporter.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/reporter/StdOutReporter.scala @@ -102,7 +102,11 @@ object StdOutReporter { printInnerHeader("Details of failed tests") testResults.filter(!_.isSuccess).sortBy(_.name).foreach { result => println(s"Suite: ${result.suiteName}") - println(s"Test: ${result.name}") + result.resultType match { + case SuiteResultType.BeforeTestSet => println(s"Before: ${result.name}") + case SuiteResultType.TestSet => println(s"Test: ${result.name}") + case SuiteResultType.AfterTestSet => println(s"After: ${result.name}") + } println(s"Error: ${result.errorMsg.getOrElse("No details available")}") println(s"Duration: ${result.duration.getOrElse("NA")} ms") println(s"Category: ${result.categories}") diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/Response.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/Response.scala index f3ce594..973a582 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/Response.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/Response.scala @@ -60,13 +60,18 @@ object Response { * Returns true only if all assertions return true, and false as soon as any assertion returns false. * * @param response The response on which actions will be performed. - * @param responseAction The set of response actions that dictate what actions will be performed on the response. + * @param responseActions The set of response actions that dictate what actions will be performed on the response. * @return Boolean indicating whether all response actions passed (true) or any response action failed (false). TODO * @throws IllegalArgumentException If an response action group is not supported. */ - def perform(response: Response, responseAction: Seq[ResponseAction]): Try[Unit] = { + def perform(response: Response, responseActions: Seq[ResponseAction]): Try[Unit] = { def logParameters(response: Response, resolvedResponseAction: ResponseAction, exception: Option[Throwable] = None): Unit = { - val filteredParams = resolvedResponseAction.params.filter(_._1 != "method").map { case (k, v) => s"$k->$v" }.mkString(", ") + val filteredParams = resolvedResponseAction.params + .flatMap { + case (k, v) if k != "method" => Some(s"$k->$v") + case _ => None + } + .mkString(", ") val baseLog = s""" |Parameters received: @@ -80,15 +85,15 @@ object Response { Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - error details:$baseLog$exceptionLog") } - responseAction.iterator.map { assertion => - val resolvedResponseAction: ResponseAction = assertion.resolveByRuntimeCache() + responseActions.map { responseAction => + val resolvedResponseAction: ResponseAction = responseAction.resolveByRuntimeCache() Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - Started.") val res: Try[Unit] = resolvedResponseAction.group match { - case ResponseActionGroupType.Assert => AssertionResponseAction.performResponseAction(response, assertion) - case ResponseActionGroupType.ExtractJson => ExtractJsonResponseAction.performResponseAction(response, assertion) - case ResponseActionGroupType.Log => LogResponseAction.performResponseAction(response, assertion) - case _ => Failure(new IllegalArgumentException(s"Unsupported assertion group: ${assertion.group}")) + case ResponseActionGroupType.Assert => AssertionResponseAction.performResponseAction(response, resolvedResponseAction) + case ResponseActionGroupType.ExtractJson => ExtractJsonResponseAction.performResponseAction(response, resolvedResponseAction) + case ResponseActionGroupType.Log => LogResponseAction.performResponseAction(response, resolvedResponseAction) + case _ => Failure(new IllegalArgumentException(s"Unsupported assertion group: ${resolvedResponseAction.group}")) } res match { diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/suite/runner/SuiteRunner.scala b/testApi/src/main/scala/africa/absa/testing/scapi/suite/runner/SuiteRunner.scala index 255a4db..74652cf 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/suite/runner/SuiteRunner.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/suite/runner/SuiteRunner.scala @@ -46,8 +46,14 @@ object SuiteRunner { suiteBundles.foldLeft(List[SuiteResult]()) { (resultList, suiteBundle) => Logger.debug(s"Suite: ${suiteBundle.suite.name} - Started") + // TODO - One suite can define only one Before, so why iteration across list items??? val suiteBeforeResult: List[SuiteResult] = suiteBundle.suiteBefore.toList.flatMap { suiteBefore => - suiteBefore.methods.map { method => runSuiteBefore(suiteBundle.suite.name, suiteBefore.name, method, environment, restClientCreator) } + suiteBefore.methods.map { method => runSuiteBefore( + suiteBundle.suite.name, + suiteBefore.name, + method, + environment, + restClientCreator) } } var suiteResult: List[SuiteResult] = List.empty @@ -65,11 +71,22 @@ object SuiteRunner { duration = Some(0L), categories = Some("SKIPPED")) } else { + // TODO - One suite can define only one TestSuite, so why iteration across list items??? suiteResult = suiteBundle.suite.tests.toList.map(test => - this.runSuiteTest(suiteBundle.suite.name, test, environment, restClientCreator)) + this.runSuiteTest( + suiteBundle.suite.name, + test, + environment, + restClientCreator)) + // TODO - One suite can define only one After, so why iteration across list items??? suiteAfterResult = suiteBundle.suiteAfter.toList.flatMap { suiteAfter => - suiteAfter.methods.map { method => runSuiteAfter(suiteBundle.suite.name, suiteAfter.name, method, environment, restClientCreator) } + suiteAfter.methods.map { method => runSuiteAfter( + suiteBundle.suite.name, + suiteAfter.name, + method, + environment, + restClientCreator) } } } @@ -176,12 +193,14 @@ object SuiteRunner { * @return Response to the sent request. */ private def sendRequest(requestable: Requestable, environment: Environment, restClientCreator: RestClientCreator): Response = { + val resolvedAction = requestable.action.resolveByRuntimeCache() + restClientCreator().sendRequest( - method = requestable.action.methodName, - url = RuntimeCache.resolve(requestable.action.url), - headers = RequestHeaders.buildHeaders(requestable.headers), - body = RequestBody.buildBody(requestable.action.body), - params = RequestParams.buildParams(requestable.action.params), + method = resolvedAction.methodName, + url = RuntimeCache.resolve(resolvedAction.url), + headers = RequestHeaders.buildHeaders(requestable.headers.map(header => header.resolveByRuntimeCache())), + body = RequestBody.buildBody(resolvedAction.body), + params = RequestParams.buildParams(resolvedAction.params), verifySslCerts = Some(environment.constants.get("verifySslCerts").exists(_.toLowerCase == "true")).getOrElse(false) ) } @@ -196,11 +215,10 @@ object SuiteRunner { */ private def processRequest(requestable: Requestable, environment: Environment, restClientCreator: RestClientCreator): Try[Unit] = { val response: Response = sendRequest(requestable, environment, restClientCreator) - val result: Try[Unit] = Response.perform( + Response.perform( response = response, - responseAction = requestable.responseActions + responseActions = requestable.responseActions ) - result } /** diff --git a/testApi/src/test/resources/test_project/suites/demo/getOwners.after.json b/testApi/src/test/resources/test_project/suites/demo/getOwners.after.json index 8c4536f..0161412 100644 --- a/testApi/src/test/resources/test_project/suites/demo/getOwners.after.json +++ b/testApi/src/test/resources/test_project/suites/demo/getOwners.after.json @@ -1,5 +1,5 @@ { - "name" : "after.name.1", + "name" : "getOwners Demo After", "methods" : [ { "name" : "action-name-1", diff --git a/testApi/src/test/resources/test_project/suites/demo/getOwners.before.json b/testApi/src/test/resources/test_project/suites/demo/getOwners.before.json index c87f7ea..d92f050 100644 --- a/testApi/src/test/resources/test_project/suites/demo/getOwners.before.json +++ b/testApi/src/test/resources/test_project/suites/demo/getOwners.before.json @@ -1,8 +1,8 @@ { - "name" : "getOwner_5_demo", + "name" : "getOwners Demo Before", "methods" : [ { - "name" : "getOwner_5", + "name" : "Get Owner ID:5 data", "headers" : [ { "name": "authorization", @@ -76,6 +76,10 @@ { "method": "log.info", "message": "{{ cache.ownerId }}" + }, + { + "method": "log.info", + "message": "{{ cache.ownerName }}" } ] } diff --git a/testApi/src/test/resources/test_project/suites/demo/getOwners.suite.json b/testApi/src/test/resources/test_project/suites/demo/getOwners.suite.json index 6b63ba9..943fbe7 100644 --- a/testApi/src/test/resources/test_project/suites/demo/getOwners.suite.json +++ b/testApi/src/test/resources/test_project/suites/demo/getOwners.suite.json @@ -1,8 +1,8 @@ { - "name" : "getOwners_demo", + "name" : "getOwners Demo Suite", "tests": [ { - "name" : "getOwner5NotFound", + "name" : "Owner ID:5 not found", "categories": ["SMOKE"], "headers" : [], "action": { @@ -20,7 +20,7 @@ ] }, { - "name" : "getOwner5Found", + "name" : "Owner ID:5 found", "categories": ["SMOKE"], "headers" : [], "action": { diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/ReferenceResolverTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/ReferenceResolverTest.scala new file mode 100644 index 0000000..28b98a3 --- /dev/null +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/ReferenceResolverTest.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2023 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package africa.absa.testing.scapi.json + +import africa.absa.testing.scapi.rest.response.action.types.{AssertResponseActionType, ResponseActionGroupType} +import africa.absa.testing.scapi.utils.cache.{GlobalLevel, RuntimeCache} +import munit.FunSuite + +class ReferenceResolverTest extends FunSuite { + + val action: Action = Action( + methodName = "get", + url = "nice/{{ cache.urlValue }}", + body = Option("body {{ cache.surpriseValue }}"), + params = Option(Set(Param("message", "value nr.: {{ cache.numberValue }}"))) + ) + + val header: Header = Header( + name = "name", + value = "value+{{ cache.surpriseValue }}" + ) + + val responseAction: ResponseAction = ResponseAction( + group = ResponseActionGroupType.Assert, + name = AssertResponseActionType.ResponseTimeIsBelow.toString, + Map("limit" -> "{{ cache.responseTime }}")) + + /* + Header + */ + + test("Header - resolveByRuntimeCache - reference present") { + RuntimeCache.put("surpriseValue", "boo boo", GlobalLevel) + + val resolvedHeader = header.resolveByRuntimeCache() + + assert("value+boo boo" == clue(resolvedHeader.value)) + + RuntimeCache.reset() + } + + test("Header - resolveByRuntimeCache - reference not present") { + interceptMessage[NoSuchElementException]("Key not found in cache: surpriseValue") { + header.resolveByRuntimeCache() + } + } + + /* + Action + */ + + test("Action - resolveByRuntimeCache - reference present") { + RuntimeCache.put("urlValue", "url", GlobalLevel) + RuntimeCache.put("surpriseValue", "boo boo", GlobalLevel) + RuntimeCache.put("numberValue", "123", GlobalLevel) + + val resolvedAction = action.resolveByRuntimeCache() + + assert("nice/url" == clue(resolvedAction.url)) + assert("body boo boo" == clue(resolvedAction.body.get)) + assert("value nr.: 123" == clue(resolvedAction.params.get.head.value)) + + RuntimeCache.reset() + } + + test("Action - resolveByRuntimeCache - reference not present") { + interceptMessage[NoSuchElementException]("Key not found in cache: urlValue") { + action.resolveByRuntimeCache() + } + } + + /* + ResponseAction + */ + + test("ResponseAction - resolveByRuntimeCache - reference present") { + RuntimeCache.put("responseTime", "987", GlobalLevel) + + val resolvedResponseAction = responseAction.resolveByRuntimeCache() + + assert("987" == clue(resolvedResponseAction.params("limit"))) + + RuntimeCache.reset() + } + + test("ResponseAction - resolveByRuntimeCache - reference not present") { + interceptMessage[NoSuchElementException]("Key not found in cache: responseTime") { + responseAction.resolveByRuntimeCache() + } + } + +} diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseLogTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseLogTest.scala index 25cf4c5..13a62cc 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseLogTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseLogTest.scala @@ -68,13 +68,13 @@ class ResponseLogTest extends FunSuite { */ test("performAssertion - ERROR supported") { - val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Error, Map("message" -> "info message")) + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Error, Map("message" -> "error message")) val response = Response(500, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) } test("performAssertion - WARN supported") { - val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Warn, Map("message" -> "info message")) + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Warn, Map("message" -> "warn message")) val response = Response(401, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) } @@ -86,7 +86,7 @@ class ResponseLogTest extends FunSuite { } test("performAssertion - DEBUG supported") { - val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Debug, Map("message" -> "info message")) + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Debug, Map("message" -> "debug message")) val response = Response(200, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) }