From 03dca9ca9414b58033c14fcefbe6c33fbbacd0ca Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Fri, 3 Nov 2023 13:28:45 +0100 Subject: [PATCH] Feature/8 implement methods (#27) #8 - Implement method ideas * Implemented full expected methods for Log 'ResponseAction'. * Updated log message to keep same format - on debug level. * Migrate several Set to Seq to keep order of elements which is required by logic of test design. * Introduces Enums instead of constants. * Make response perform methods private. * Review of code warnings. * Remove no more used object LoggerConfig. * Remove no more used class and object TestResults.scala. * Rename of variable in code to fit to real usage. * Move Factory classes to dedicated package. * Moved Suite related models classes to new suite package in model package. * Rename Suite to TestSet. * Rename SuiteAround to SuitePreAndPostProcessing. * Rename SuiteBefore to BeforeTestSet * Rename SuiteAfter to AfterTestSet * Rename SuiteBundle to Suite * Rename ResponsePerformer to ResponseActions * Move ResponseAction related file into new package action * Rename "endpoint" to "name" as this parameters is used as name for suite not as endpoint. * Log action methods returns Unit. * Remove usage of Success from Action implementations. * Rename and move Type to String implicit conversion as they are used only in tests. * Fix correct usage clue calls in assert (not assertEquals) method. * Fix - Apply Scala CamelCase to enumeration contants. --- .github/workflows/ci-check-jacoco.yml | 1 + project/Dependencies.scala | 2 + .../main/resources/schema/after.schema.json | 2 +- .../main/resources/schema/before.schema.json | 2 +- .../main/resources/schema/suite.schema.json | 12 +- .../absa/testing/scapi/Exceptions.scala | 23 +- .../absa/testing/scapi/ScAPIRunner.scala | 13 +- .../scapi/config/ScAPIRunnerConfig.scala | 8 +- .../scapi/json/ReferenceResolver.scala | 32 +- .../absa/testing/scapi/json/Requestable.scala | 6 +- .../{ => factory}/EnvironmentFactory.scala | 3 +- .../json/{ => factory}/SuiteFactory.scala | 94 ++-- .../json/schema/JsonSchemaValidator.scala | 12 +- .../testing/scapi/model/SuiteResults.scala | 81 --- .../testing/scapi/model/TestResults.scala | 42 -- .../AfterTestSet.scala} | 8 +- .../BeforeTestSet.scala} | 8 +- .../scapi/model/{ => suite}/Method.scala | 8 +- .../{SuiteBundle.scala => suite/Suite.scala} | 4 +- .../SuitePreAndPostProcessing.scala} | 6 +- .../scapi/model/suite/SuiteResult.scala | 50 ++ .../model/{ => suite}/SuiteTestScenario.scala | 10 +- .../{Suite.scala => suite/TestSet.scala} | 16 +- .../model/suite/types/SuiteResultType.scala | 25 + .../scapi/reporter/StdOutReporter.scala | 26 +- .../model/CookieValue.scala} | 6 +- .../scapi/rest/request/RequestBody.scala | 8 +- .../scapi/rest/request/RequestHeaders.scala | 8 +- .../request/sender/ScAPIRequestSender.scala | 37 +- .../response/AssertionResponseAction.scala | 109 ----- .../rest/response/LogResponseAction.scala | 78 --- .../scapi/rest/response/Response.scala | 71 ++- .../action/AssertionResponseAction.scala | 444 +++++++++++++++++ .../ExtractJsonResponseAction.scala | 73 +-- .../response/action/LogResponseAction.scala | 113 +++++ .../ResponseActions.scala} | 9 +- .../types/AssertResponseActionType.scala | 55 +++ .../types/ExtractJsonResponseActionType.scala | 32 ++ .../action/types/LogResponseActionType.scala | 32 ++ .../types/ResponseActionGroupType.scala | 33 ++ .../scapi/suite/runner/SuiteRunner.scala | 191 ++++---- .../scapi/utils/cache/RuntimeCache.scala | 3 +- .../utils/validation/ContentValidator.scala | 27 +- .../undefinedConstantIssue.suite.json | 4 +- .../gui-controller/deleteQuestion.suite.json | 4 +- .../gui-controller/getUserCurrent.after.json | 2 +- .../gui-controller/getUserCurrent.before.json | 2 +- .../gui-controller/getUserCurrent.suite.json | 31 +- .../gui-controller/postQuestion.suite.json | 4 +- .../gui-controller/putQuestion.suite.json | 4 +- .../absa/testing/scapi/ScAPIRunnerTest.scala | 2 +- .../scapi/json/EnvironmentFactoryTest.scala | 7 +- .../testing/scapi/json/EnvironmentTest.scala | 13 +- .../testing/scapi/json/RequestBodyTest.scala | 14 +- .../scapi/json/RequestHeadersTest.scala | 20 +- .../scapi/json/RequestParamsTest.scala | 12 +- .../testing/scapi/json/SuiteFactoryTest.scala | 55 ++- .../scapi/reporter/StdOutReporterTest.scala | 89 ++-- .../testing/scapi/rest/RestClientTest.scala | 18 +- .../response/ResponseAssertionsTest.scala | 461 ++++++++++++++++-- .../rest/response/ResponseExtractTest.scala | 208 ++++---- .../scapi/rest/response/ResponseLogTest.scala | 69 ++- .../scapi/rest/response/ResponseTest.scala | 49 -- .../suite/runner/FakeScAPIRequestSender.scala | 7 +- .../scapi/suite/runner/SuiteRunnerTest.scala | 125 ++--- .../scapi/utils/cache/RuntimeCacheTest.scala | 64 +-- .../scapi/utils/file/FileUtilsTest.scala | 28 +- .../scapi/utils/file/JsonUtilsTest.scala | 4 +- .../validation/ContentValidatorTest.scala | 8 +- 69 files changed, 2062 insertions(+), 1065 deletions(-) rename testApi/src/main/scala/africa/absa/testing/scapi/json/{ => factory}/EnvironmentFactory.scala (96%) rename testApi/src/main/scala/africa/absa/testing/scapi/json/{ => factory}/SuiteFactory.scala (78%) delete mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteResults.scala delete mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/model/TestResults.scala rename testApi/src/main/scala/africa/absa/testing/scapi/model/{SuiteAfter.scala => suite/AfterTestSet.scala} (81%) rename testApi/src/main/scala/africa/absa/testing/scapi/model/{SuiteBefore.scala => suite/BeforeTestSet.scala} (81%) rename testApi/src/main/scala/africa/absa/testing/scapi/model/{ => suite}/Method.scala (88%) rename testApi/src/main/scala/africa/absa/testing/scapi/model/{SuiteBundle.scala => suite/Suite.scala} (86%) rename testApi/src/main/scala/africa/absa/testing/scapi/model/{SuiteAround.scala => suite/SuitePreAndPostProcessing.scala} (82%) create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteResult.scala rename testApi/src/main/scala/africa/absa/testing/scapi/model/{ => suite}/SuiteTestScenario.scala (87%) rename testApi/src/main/scala/africa/absa/testing/scapi/model/{Suite.scala => suite/TestSet.scala} (67%) create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/model/suite/types/SuiteResultType.scala rename testApi/src/main/scala/africa/absa/testing/scapi/{logging/LoggerConfig.scala => rest/model/CookieValue.scala} (85%) delete mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/AssertionResponseAction.scala delete mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/LogResponseAction.scala create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/AssertionResponseAction.scala rename testApi/src/main/scala/africa/absa/testing/scapi/rest/response/{ => action}/ExtractJsonResponseAction.scala (68%) create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/LogResponseAction.scala rename testApi/src/main/scala/africa/absa/testing/scapi/rest/response/{ResponsePerformer.scala => action/ResponseActions.scala} (80%) create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/AssertResponseActionType.scala create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ExtractJsonResponseActionType.scala create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/LogResponseActionType.scala create mode 100644 testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ResponseActionGroupType.scala delete mode 100644 testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseTest.scala diff --git a/.github/workflows/ci-check-jacoco.yml b/.github/workflows/ci-check-jacoco.yml index 2d15347..ac5c5ec 100644 --- a/.github/workflows/ci-check-jacoco.yml +++ b/.github/workflows/ci-check-jacoco.yml @@ -28,6 +28,7 @@ jobs: min-coverage-changed-files: 80.0 title: JaCoCo code coverage report - ScAPI update-comment: true + debug-mode: true - name: Get the Coverage info run: | echo "Total coverage ${{ steps.jacoco.outputs.coverage-overall }}" diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5d99c25..ef4de28 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -26,6 +26,7 @@ object Dependencies { private val commonsIoVersion = "2.13.0" private val requestsVersion = "0.8.0" private val loggerVersion = "2.14.1" + private val scalaXmlVersion = "1.3.0" def getScalaDependency(scalaVersion: String): ModuleID = "org.scala-lang" % "scala-library" % scalaVersion @@ -40,6 +41,7 @@ object Dependencies { "com.lihaoyi" %% "requests" % requestsVersion, "org.apache.logging.log4j" % "log4j-core" % loggerVersion, "org.apache.logging.log4j" % "log4j-api" % loggerVersion, + "org.scala-lang.modules" %% "scala-xml" % scalaXmlVersion, // test "org.scalameta" %% "munit" % munitVersion % Test diff --git a/testApi/src/main/resources/schema/after.schema.json b/testApi/src/main/resources/schema/after.schema.json index 1a181a6..cf5a563 100644 --- a/testApi/src/main/resources/schema/after.schema.json +++ b/testApi/src/main/resources/schema/after.schema.json @@ -124,7 +124,7 @@ "properties": { "method": { "type": "string", - "enum": ["assert.status-code", "assert.body-contains", "log.info", "extractJson.string-from-list"], + "enum": ["assert.response-time-is-below", "assert.response-time-is-above", "assert.status-code-equals", "assert.status-code-is-success", "assert.status-code-is-client-error", "assert.status-code-is-server-error", "assert.header-exists", "assert.header-value-equals", "assert.content-type-is-json", "assert.content-type-is-xml", "assert.content-type-is-html", "assert.cookie-exists", "assert.cookie-value-equals", "assert.cookie-is-secured", "assert.cookie-is-not-secured", "assert.body-contains-text", "log.error", "log.warn", "log.info", "log.debug", "extractJson.string-from-list"], "description": "The method to be used for the response action. Restricted to specific values." } }, diff --git a/testApi/src/main/resources/schema/before.schema.json b/testApi/src/main/resources/schema/before.schema.json index cd9eada..4676c84 100644 --- a/testApi/src/main/resources/schema/before.schema.json +++ b/testApi/src/main/resources/schema/before.schema.json @@ -122,7 +122,7 @@ "properties": { "method": { "type": "string", - "enum": ["assert.status-code", "assert.body-contains", "log.info", "extractJson.string-from-list"], + "enum": ["assert.response-time-is-below", "assert.response-time-is-above", "assert.status-code-equals", "assert.status-code-is-success", "assert.status-code-is-client-error", "assert.status-code-is-server-error", "assert.header-exists", "assert.header-value-equals", "assert.content-type-is-json", "assert.content-type-is-xml", "assert.content-type-is-html", "assert.cookie-exists", "assert.cookie-value-equals", "assert.cookie-is-secured", "assert.cookie-is-not-secured", "assert.body-contains-text", "log.error", "log.warn", "log.info", "log.debug", "extractJson.string-from-list"], "description": "The method to be used for the response action." } }, diff --git a/testApi/src/main/resources/schema/suite.schema.json b/testApi/src/main/resources/schema/suite.schema.json index 5d4fee2..ea01388 100644 --- a/testApi/src/main/resources/schema/suite.schema.json +++ b/testApi/src/main/resources/schema/suite.schema.json @@ -7,9 +7,9 @@ "type": "object", "additionalProperties": true, "properties": { - "endpoint": { + "name": { "type": "string", - "description": "The user endpoint name which suite cover by tests." + "description": "The suite name." }, "tests": { "type": "array", @@ -20,7 +20,7 @@ } }, "required": [ - "endpoint", + "name", "tests" ], "title": "suite", @@ -133,7 +133,7 @@ "properties": { "method": { "type": "string", - "enum": ["assert.status-code", "assert.body-contains", "log.info", "extractJson.string-from-list"], + "enum": ["assert.response-time-is-below", "assert.response-time-is-above", "assert.status-code-equals", "assert.status-code-is-success", "assert.status-code-is-client-error", "assert.status-code-is-server-error", "assert.header-exists", "assert.header-value-equals", "assert.content-type-is-json", "assert.content-type-is-xml", "assert.content-type-is-html", "assert.cookie-exists", "assert.cookie-value-equals", "assert.cookie-is-secured", "assert.cookie-is-not-secured", "assert.body-contains-text", "log.error", "log.warn", "log.info", "log.debug", "extractJson.string-from-list"], "description": "The method to be used for the response action." } }, @@ -156,7 +156,7 @@ { "properties": { "method": { - "const": "assert.status-code" + "const": "assert.status-code-equals" } }, "required": ["code"] @@ -190,7 +190,7 @@ { "properties": { "method": { - "const": "assert.status-code" + "const": "assert.status-code-equals" } } }, diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/Exceptions.scala b/testApi/src/main/scala/africa/absa/testing/scapi/Exceptions.scala index 78b1227..a74d999 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/Exceptions.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/Exceptions.scala @@ -20,23 +20,30 @@ import com.networknt.schema.ValidationMessage import scala.collection.mutable -case class UndefinedConstantsInProperties(undefinedConstants: Set[String], source: String) +case class UndefinedConstantsInPropertiesException(undefinedConstants: Set[String], source: String) extends Exception(s"Undefined constant(s): '${undefinedConstants.mkString(", ")}' in '$source'.") -case class PropertyNotFound(property: String) extends Exception(s"Property not found: '$property'.") +case class PropertyNotFoundException(property: String) extends Exception(s"Property not found: '$property'.") -case class JsonInvalidSchema(filePath: String, messages: mutable.Set[ValidationMessage]) +case class JsonInvalidSchemaException(filePath: String, messages: mutable.Set[ValidationMessage]) extends Exception(s"Json file '$filePath' not valid to defined json schema. " + messages.mkString("\n")) -case class ProjectLoadFailed() extends Exception("Problems during project loading.") +case class ProjectLoadFailedException() extends Exception("Problems during project loading.") -case class SuiteLoadFailed(detail: String) extends Exception(s"Problems during project loading. Details: $detail") +case class SuiteLoadFailedException(detail: String) + extends Exception(s"Problems during project loading. Details: $detail") -case class UndefinedHeaderType(undefinedType: String) +case class SuiteBeforeFailedException(detail: String) + extends Exception(s"Problems during running before suite logic. Details: $detail") + +case class UndefinedHeaderTypeException(undefinedType: String) extends Exception(s"Undefined Header content type: '$undefinedType'") -case class UndefinedResponseActionType(undefinedType: String) +case class UndefinedResponseActionTypeException(undefinedType: String) extends Exception(s"Undefined response action content type: '$undefinedType'") -case class ContentValidationFailed(value: String, message: String) +case class ContentValidationFailedException(value: String, message: String) extends Exception(s"Content validation failed for value: '$value': $message") + +case class AssertionException(message: String) + extends Exception(s"Assertion failed: $message") diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/ScAPIRunner.scala b/testApi/src/main/scala/africa/absa/testing/scapi/ScAPIRunner.scala index faeef59..328ea4f 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/ScAPIRunner.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/ScAPIRunner.scala @@ -17,9 +17,10 @@ package africa.absa.testing.scapi import africa.absa.testing.scapi.config.ScAPIRunnerConfig -import africa.absa.testing.scapi.json.{Environment, EnvironmentFactory, SuiteFactory} +import africa.absa.testing.scapi.json.factory.{EnvironmentFactory, SuiteFactory} +import africa.absa.testing.scapi.json.Environment import africa.absa.testing.scapi.logging.Logger -import africa.absa.testing.scapi.model.{SuiteBundle, SuiteResults} +import africa.absa.testing.scapi.model.suite.{Suite, SuiteResult} import africa.absa.testing.scapi.reporter.StdOutReporter import africa.absa.testing.scapi.rest.RestClient import africa.absa.testing.scapi.rest.request.sender.ScAPIRequestSender @@ -47,11 +48,11 @@ object ScAPIRunner { Logger.setLevel(if (cmd.debug) Level.DEBUG else Level.INFO) cmd.logConfigInfo() - if (!Files.exists(Paths.get(cmd.testRootPath, "suites"))) throw SuiteLoadFailed("'suites' directory have to exist in project root.") + if (!Files.exists(Paths.get(cmd.testRootPath, "suites"))) throw SuiteLoadFailedException("'suites' directory have to exist in project root.") // jsons to objects val environment: Environment = EnvironmentFactory.fromFile(cmd.envPath) - val suiteBundles: Set[SuiteBundle] = SuiteFactory.fromFiles(environment, cmd.testRootPath, cmd.filter, cmd.fileFormat) + val suiteBundles: Set[Suite] = SuiteFactory.fromFiles(environment, cmd.testRootPath, cmd.filter, cmd.fileFormat) SuiteFactory.validateSuiteContent(suiteBundles) // run tests and result reporting - use categories for test filtering @@ -59,8 +60,8 @@ object ScAPIRunner { Logger.info("Validate only => end run.") } else { Logger.info("Running tests") - val testResults: List[SuiteResults] = SuiteRunner.runSuites(suiteBundles, environment, () => new RestClient(ScAPIRequestSender)) - StdOutReporter.printReport(testResults) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suiteBundles, environment, () => new RestClient(ScAPIRequestSender)) + StdOutReporter.printReport(suiteResults) } } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/config/ScAPIRunnerConfig.scala b/testApi/src/main/scala/africa/absa/testing/scapi/config/ScAPIRunnerConfig.scala index 87f9be9..46cd9fa 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/config/ScAPIRunnerConfig.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/config/ScAPIRunnerConfig.scala @@ -115,23 +115,23 @@ object ScAPIRunnerConfig { opt[String]("filter") .optional() .action((value, config) => { config.copy(filter = value) }) - .text(s"Filter rule to select test definitions file (recursive) to include into test suite. Default is all '${DefaultFilter}'") + .text(s"Filter rule to select test definitions file (recursive) to include into test suite. Default is all '$DefaultFilter'") opt[Seq[String]]("categories") .optional() .valueName(",") .action((value, config) => { config.copy(categories = value.toSet) }) - .text(s"Select which test categories will be included into test suite. Default is all '${DefaultCategories}'") + .text(s"Select which test categories will be included into test suite. Default is all '$DefaultCategories'") opt[Int]("thread-count") .optional() .action((value, config) => { config.copy(threadCount = value) }) - .text(s"Maximum count of thread used to run test suite. Default is '${DefaultThreadCount}'") + .text(s"Maximum count of thread used to run test suite. Default is '$DefaultThreadCount'") opt[String]("file-format") .optional() .action((value, config) => { config.copy(fileFormat = value) }) - .text(s"Format of definition files. Default is all '${DefaultFileFormat}'") + .text(s"Format of definition files. Default is all '$DefaultFileFormat'") opt[String]("report") .optional() 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 c15845e..47101e7 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 @@ -16,8 +16,9 @@ package africa.absa.testing.scapi.json +import africa.absa.testing.scapi.rest.response.action.types.ResponseActionGroupType.ResponseActionGroupType import africa.absa.testing.scapi.utils.cache.RuntimeCache -import africa.absa.testing.scapi.{PropertyNotFound, UndefinedConstantsInProperties} +import africa.absa.testing.scapi.{PropertyNotFoundException, UndefinedConstantsInPropertiesException} import scala.util.matching.Regex @@ -56,10 +57,10 @@ sealed protected trait ReferenceResolver { * If there are any unresolved references, it throws an exception. * * @param notResolvedReferences A set of unresolved reference keys. - * @throws UndefinedConstantsInProperties If there are any unresolved references. + * @throws UndefinedConstantsInPropertiesException If there are any unresolved references. */ private def notResolved(notResolvedReferences: Set[String]): Unit = - if (notResolvedReferences.nonEmpty) throw UndefinedConstantsInProperties(notResolvedReferences, s"'${getClass.getSimpleName}' action.") + if (notResolvedReferences.nonEmpty) throw UndefinedConstantsInPropertiesException(notResolvedReferences, s"'${getClass.getSimpleName}' action.") /** * Resolve a map of references to their actual values. It iteratively updates the map with resolved values. @@ -114,7 +115,7 @@ case class Environment private(constants: Map[String, String], properties: Map[S * @param key The key to retrieve the value for. * @return The value corresponding to the key. */ - def apply(key: String): String = properties.getOrElse(key, constants.getOrElse(key, throw PropertyNotFound(key))) + def apply(key: String): String = properties.getOrElse(key, constants.getOrElse(key, throw PropertyNotFoundException(key))) /** * Method to resolve all the references in the environment's properties. @@ -198,23 +199,20 @@ case class Action private(methodName: String, url: String, body: Option[String] } /** - * Case class that represents ResponseAction. - * This class is used to hold test response action. - * It implements the `ReferenceResolver` trait to support resolution of reference constants. + * Represents a `ResponseAction` case class. + * + *

This class encapsulates the details of a test response action and provides + * functionality to resolve reference constants through the `ReferenceResolver` trait.

* - * @constructor create a new ResponseAction with a name and value. - * @param method the "group and name" of the response action. - * @param params the map containing the parameters of the response action. Each key-value pair in the map - * represents a parameter name and its corresponding value. + * @param group The type of the response action group. + * @param name The name that identifies the response action. + * @param params A map containing the parameters of the response action. Each entry in the map + * corresponds to a parameter name and its associated value. */ -case class ResponseAction private(method: String, +case class ResponseAction private(group: ResponseActionGroupType, + name: String, params: Map[String, String]) extends ReferenceResolver { - private def splitter: Seq[String] = method.split("\\.").toSeq - - def group : String = splitter.head - def name : String = splitter.tail.head - /** * Method to resolve references. * diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/json/Requestable.scala b/testApi/src/main/scala/africa/absa/testing/scapi/json/Requestable.scala index 9830ae3..dd59d1e 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/json/Requestable.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/json/Requestable.scala @@ -19,9 +19,9 @@ package africa.absa.testing.scapi.json trait Requestable { def name: String - def headers: Set[Header] + def headers: Seq[Header] - def actions: Set[Action] + def actions: Seq[Action] - def responseActions: Set[ResponseAction] + def responseActions: Seq[ResponseAction] } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/json/EnvironmentFactory.scala b/testApi/src/main/scala/africa/absa/testing/scapi/json/factory/EnvironmentFactory.scala similarity index 96% rename from testApi/src/main/scala/africa/absa/testing/scapi/json/EnvironmentFactory.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/json/factory/EnvironmentFactory.scala index ee9b9b8..157a54e 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/json/EnvironmentFactory.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/json/factory/EnvironmentFactory.scala @@ -14,8 +14,9 @@ * limitations under the License. */ -package africa.absa.testing.scapi.json +package africa.absa.testing.scapi.json.factory +import africa.absa.testing.scapi.json.Environment import africa.absa.testing.scapi.json.schema.{JsonSchemaValidator, ScAPIJsonSchema} import africa.absa.testing.scapi.utils.file.JsonUtils import spray.json._ diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/json/SuiteFactory.scala b/testApi/src/main/scala/africa/absa/testing/scapi/json/factory/SuiteFactory.scala similarity index 78% rename from testApi/src/main/scala/africa/absa/testing/scapi/json/SuiteFactory.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/json/factory/SuiteFactory.scala index 0ee31e2..aabe8da 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/json/SuiteFactory.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/json/factory/SuiteFactory.scala @@ -14,14 +14,17 @@ * limitations under the License. */ -package africa.absa.testing.scapi.json +package africa.absa.testing.scapi.json.factory import africa.absa.testing.scapi._ +import africa.absa.testing.scapi.json._ import africa.absa.testing.scapi.json.schema.{JsonSchemaValidator, ScAPIJsonSchema} import africa.absa.testing.scapi.logging.Logger import africa.absa.testing.scapi.model._ +import africa.absa.testing.scapi.model.suite.{Method, TestSet, AfterTestSet, SuitePreAndPostProcessing, BeforeTestSet, Suite, SuiteTestScenario} import africa.absa.testing.scapi.rest.request.{RequestBody, RequestHeaders, RequestParams} import africa.absa.testing.scapi.rest.response.Response +import africa.absa.testing.scapi.rest.response.action.types.ResponseActionGroupType import africa.absa.testing.scapi.utils.file.{FileUtils, JsonUtils} import spray.json._ @@ -42,10 +45,10 @@ object SuiteFactory { * @param format The format of suite JSON files. * @return Set of Suite instances. */ - def fromFiles(environment: Environment, testRootPath: String, filter: String, format: String): Set[SuiteBundle] = { + def fromFiles(environment: Environment, testRootPath: String, filter: String, format: String): Set[Suite] = { // NOTE: format not used as json is only supported format in time od development - val suiteLoadingResults: Map[String, Try[SuiteBundle]] = { + val suiteLoadingResults: Map[String, Try[Suite]] = { val suiteJsonFiles = findSuiteJsonFiles(testRootPath, filter) val suiteTries = suiteJsonFiles.map { file => val suiteTry = Try(loadJsonSuiteBundle(file, environment.asMap())) @@ -56,7 +59,7 @@ object SuiteFactory { if (suiteLoadingResults.values.forall(_.isSuccess)) { Logger.info("All suites loaded.") - val suiteBundles: Set[SuiteBundle] = suiteLoadingResults.values.collect { + val suiteBundles: Set[Suite] = suiteLoadingResults.values.collect { case Success(suiteBundle) => suiteBundle }.toSet @@ -71,7 +74,7 @@ object SuiteFactory { failedSuites.foreach { case (key, value) => Logger.error(s"$key => $value") } - throw ProjectLoadFailed() + throw ProjectLoadFailedException() } } @@ -86,8 +89,8 @@ object SuiteFactory { * @param suiteBundles The set of SuiteBundles to be filtered. * @return A set of filtered SuiteBundles based on 'only' attribute. */ - def filterOnlyOrAll(suiteBundles: Set[SuiteBundle]): Set[SuiteBundle] = { - val (suitesWithOnlyTest, others) = suiteBundles.foldLeft((List.empty[SuiteBundle], List.empty[SuiteBundle])) { + def filterOnlyOrAll(suiteBundles: Set[Suite]): Set[Suite] = { + val (suitesWithOnlyTest, others) = suiteBundles.foldLeft((List.empty[Suite], List.empty[Suite])) { case ((onlySuites, normalSuites), suiteBundle) => val suite = suiteBundle.suite val onlyTests = suite.tests.filter(_.only.getOrElse(false)) @@ -95,7 +98,7 @@ object SuiteFactory { case 0 => (onlySuites, suiteBundle :: normalSuites) // No 'only' test case 1 => (suiteBundle.copy(suite = suite.copy(tests = onlyTests)) :: onlySuites, normalSuites) // Exactly one 'only' test case _ => - Logger.error(s"Suite ${suite.endpoint} has more than one test marked as only.") + Logger.error(s"Suite ${suite.name} has more than one test marked as only.") (onlySuites, normalSuites) // More than one 'only' test in a suite is an error } } @@ -104,9 +107,9 @@ object SuiteFactory { case 0 => others.toSet // If no suite with 'only' test(s), return all other suites case 1 => suitesWithOnlyTest.toSet // Only one 'only' test across all suites case _ => // More than one 'only' test across all suites is an error - val testNames = suitesWithOnlyTest.flatMap(suiteBundle => suiteBundle.suite.tests.map(test => s"${suiteBundle.suite.endpoint}.${test.name}")).mkString(", ") + val testNames = suitesWithOnlyTest.flatMap(suiteBundle => suiteBundle.suite.tests.map(test => s"${suiteBundle.suite.name}.${test.name}")).mkString(", ") Logger.error(s"Detected more than one test with defined only option. Tests: $testNames") - Set.empty[SuiteBundle] + Set.empty[Suite] } } @@ -126,7 +129,7 @@ object SuiteFactory { * @param environmentMap The map containing environment variables. * @return A SuiteBundle instance. */ - def loadJsonSuiteBundle(suitePath: String, environmentMap: Map[String, String]): SuiteBundle = { + private def loadJsonSuiteBundle(suitePath: String, environmentMap: Map[String, String]): Suite = { val (suiteFilePath, suiteFileName) = FileUtils.splitPathAndFileName(suitePath) val suiteName = suiteFileName.stripSuffix(".suite.json") @@ -134,7 +137,7 @@ object SuiteFactory { // TODO - code proposal - will be solved in #4 // val functions: Map[String, String] = loadJsonSuiteFunctions(suiteFilePath, environmentMap) - val beforeActions: Option[SuiteBefore] = loadJsonSuite[SuiteBefore]( + val beforeActions: Option[BeforeTestSet] = loadJsonSuite[BeforeTestSet]( suiteFilePath, suiteName, environmentMap ++ suiteConstants.constants, @@ -142,7 +145,7 @@ object SuiteFactory { "before", parseToSuiteBefore ) - val afterActions: Option[SuiteAfter] = loadJsonSuite[SuiteAfter]( + val afterActions: Option[AfterTestSet] = loadJsonSuite[AfterTestSet]( suiteFilePath, suiteName, environmentMap ++ suiteConstants.constants, @@ -153,9 +156,9 @@ object SuiteFactory { JsonSchemaValidator.validate(suitePath, ScAPIJsonSchema.SUITE) val jsonString: String = JsonUtils.stringFromPath(suitePath) - val notResolvedSuite: Suite = parseToSuite(jsonString) - val resolvedSuite: Suite = notResolvedSuite.resolveReferences(environmentMap ++ suiteConstants.constants) - SuiteBundle(resolvedSuite, beforeActions, afterActions) + val notResolvedSuite: TestSet = parseToSuite(jsonString) + val resolvedSuite: TestSet = notResolvedSuite.resolveReferences(environmentMap ++ suiteConstants.constants) + Suite(resolvedSuite, beforeActions, afterActions) } /** @@ -188,12 +191,13 @@ object SuiteFactory { * @param extension The file extension. * @param parser The parser function used to parse JSON string. * @return A Suite instance. - */ def loadJsonSuite[T <: SuiteAround](suiteFilePath: String, - suiteName: String, - properties: Map[String, String], - jsonSchema: URL, - extension: String, - parser: String => T): Option[T] = { + */ + private def loadJsonSuite[T <: SuitePreAndPostProcessing](suiteFilePath: String, + suiteName: String, + properties: Map[String, String], + jsonSchema: URL, + extension: String, + parser: String => T): Option[T] = { val filePath: Path = Paths.get(suiteFilePath, s"$suiteName.$extension.json") if (!Files.exists(filePath)) { None @@ -211,7 +215,7 @@ object SuiteFactory { * @param jsonString The JSON string to be parsed. * @return A SuiteConstants instance. */ - def parseToSuiteConstant(jsonString: String): SuiteConstants = { + private def parseToSuiteConstant(jsonString: String): SuiteConstants = { import SuiteConstantJsonProtocol.suiteConstantFormat jsonString.parseJson.convertTo[SuiteConstants] } @@ -222,9 +226,9 @@ object SuiteFactory { * @param jsonString The JSON string to be parsed. * @return A SuiteBefore instance. */ - def parseToSuiteBefore(jsonString: String): SuiteBefore = { + private def parseToSuiteBefore(jsonString: String): BeforeTestSet = { import SuiteBeforeJsonProtocol.suiteBeforeFormat - jsonString.parseJson.convertTo[SuiteBefore] + jsonString.parseJson.convertTo[BeforeTestSet] } /** @@ -233,9 +237,9 @@ object SuiteFactory { * @param jsonString The JSON string to be parsed. * @return A SuiteAfter instance. */ - def parseToSuiteAfter(jsonString: String): SuiteAfter = { + private def parseToSuiteAfter(jsonString: String): AfterTestSet = { import SuiteAfterJsonProtocol.suiteAfterFormat - jsonString.parseJson.convertTo[SuiteAfter] + jsonString.parseJson.convertTo[AfterTestSet] } /** @@ -244,9 +248,9 @@ object SuiteFactory { * @param jsonString The JSON string to be parsed. * @return A Suite instance. */ - def parseToSuite(jsonString: String): Suite = { + private def parseToSuite(jsonString: String): TestSet = { import SuiteJsonProtocol.suiteFormat - jsonString.parseJson.convertTo[Suite] + jsonString.parseJson.convertTo[TestSet] } /** @@ -255,7 +259,7 @@ object SuiteFactory { * * @param suiteBundles The set of SuiteBundles to be validated. */ - def validateSuiteContent(suiteBundles: Set[SuiteBundle]): Unit = suiteBundles.foreach(validateSuiteContent) + def validateSuiteContent(suiteBundles: Set[Suite]): Unit = suiteBundles.foreach(validateSuiteContent) /** * This method validates the content of a SuiteBundle. @@ -263,8 +267,8 @@ object SuiteFactory { * * @param suiteBundle The SuiteBundle to be validated. */ - def validateSuiteContent(suiteBundle: SuiteBundle): Unit = { - Logger.debug(s"Validation content of suite: ${suiteBundle.suite.endpoint}") + def validateSuiteContent(suiteBundle: Suite): Unit = { + Logger.debug(s"Validation content of suite: ${suiteBundle.suite.name}") suiteBundle.suite.tests.foreach(test => { test.headers.foreach(header => RequestHeaders.validateContent(header)) RequestBody.validateContent(test.actions.head.body) @@ -284,7 +288,7 @@ object SuiteJsonProtocol extends DefaultJsonProtocol { implicit val responseActionsFormat: RootJsonFormat[ResponseAction] = ResponseActionJsonProtocol.ResponseActionJsonFormat implicit val suiteTestFormat: RootJsonFormat[SuiteTestScenario] = jsonFormat6(SuiteTestScenario) implicit val methodFormat: RootJsonFormat[Method] = jsonFormat4(Method) - implicit val suiteFormat: RootJsonFormat[Suite] = jsonFormat2(Suite) + implicit val suiteFormat: RootJsonFormat[TestSet] = jsonFormat2(TestSet) } /** @@ -303,7 +307,7 @@ object SuiteBeforeJsonProtocol extends DefaultJsonProtocol { implicit val testActionFormat: RootJsonFormat[Action] = jsonFormat4(Action) implicit val responseActionFormat: RootJsonFormat[ResponseAction] = ResponseActionJsonProtocol.ResponseActionJsonFormat implicit val methodFormat: RootJsonFormat[Method] = jsonFormat4(Method) - implicit val suiteBeforeFormat: RootJsonFormat[SuiteBefore] = jsonFormat2(SuiteBefore) + implicit val suiteBeforeFormat: RootJsonFormat[BeforeTestSet] = jsonFormat2(BeforeTestSet) } /** @@ -315,7 +319,7 @@ object SuiteAfterJsonProtocol extends DefaultJsonProtocol { implicit val testActionFormat: RootJsonFormat[Action] = jsonFormat4(Action) implicit val responseActionFormat: RootJsonFormat[ResponseAction] = ResponseActionJsonProtocol.ResponseActionJsonFormat implicit val methodFormat: RootJsonFormat[Method] = jsonFormat4(Method) - implicit val suiteAfterFormat: RootJsonFormat[SuiteAfter] = jsonFormat2(SuiteAfter) + implicit val suiteAfterFormat: RootJsonFormat[AfterTestSet] = jsonFormat2(AfterTestSet) } /** @@ -324,16 +328,26 @@ object SuiteAfterJsonProtocol extends DefaultJsonProtocol { object ResponseActionJsonProtocol extends DefaultJsonProtocol { implicit object ResponseActionJsonFormat extends RootJsonFormat[ResponseAction] { - def write(a: ResponseAction): JsObject = JsObject( - ("method" -> JsString(a.method)) +: - a.params.view.mapValues(JsString(_)).toSeq: _* - ) + def write(a: ResponseAction): JsObject = { + val fixedFields = Seq( + "group" -> JsString(a.group.toString), + "name" -> JsString(a.name) + ) + + val paramFields = a.params.view.mapValues(JsString(_)).toSeq + + JsObject(fixedFields ++ paramFields: _*) + } def read(value: JsValue): ResponseAction = { value.asJsObject.getFields("method") match { case Seq(JsString(method)) => + val splitter: Seq[String] = method.split("\\.").toSeq + val group = ResponseActionGroupType.fromString(splitter.head).getOrElse( + throw new IllegalArgumentException(s"Invalid action group: ${splitter.head}")) + val name = splitter.tail.head val params = value.asJsObject.fields.view.toMap - ResponseAction(method, params.map { case (k, v) => k -> v.convertTo[String] }) + ResponseAction(group, name, params.map { case (k, v) => k -> v.convertTo[String] }) case _ => throw DeserializationException("Assertion expected") } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/json/schema/JsonSchemaValidator.scala b/testApi/src/main/scala/africa/absa/testing/scapi/json/schema/JsonSchemaValidator.scala index 29e98a2..1531dd7 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/json/schema/JsonSchemaValidator.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/json/schema/JsonSchemaValidator.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.json.schema -import africa.absa.testing.scapi.JsonInvalidSchema +import africa.absa.testing.scapi.JsonInvalidSchemaException import africa.absa.testing.scapi.utils.file.JsonUtils import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.networknt.schema.{JsonSchema, JsonSchemaFactory, SpecVersion} @@ -31,28 +31,28 @@ import java.net.URL * @param schemaPath Path to the JSON Schema file. */ case class JsonSchemaValidator(jsonPath: String, schemaPath: URL) { - protected val mapper: ObjectMapper = new ObjectMapper() + private val mapper: ObjectMapper = new ObjectMapper() /** * Method to read JSON data from the specified file path. * * @return JsonNode object representing the JSON data. */ - def jsonNode: JsonNode = mapper.readTree(JsonUtils.stringFromPath(jsonPath)) + private def jsonNode: JsonNode = mapper.readTree(JsonUtils.stringFromPath(jsonPath)) /** * Method to create a JsonSchemaFactory instance. * * @return JsonSchemaFactory instance. */ - def jsonSchemaFactory: JsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012) + private def jsonSchemaFactory: JsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012) /** * Method to generate a JsonSchema from the provided schema file path. * * @return JsonSchema object representing the JSON schema. */ - def jsonSchema: JsonSchema = jsonSchemaFactory.getSchema(JsonUtils.stringFromPath(schemaPath)) + private def jsonSchema: JsonSchema = jsonSchemaFactory.getSchema(JsonUtils.stringFromPath(schemaPath)) /** * Method to validate the JSON data against the provided schema. @@ -61,7 +61,7 @@ case class JsonSchemaValidator(jsonPath: String, schemaPath: URL) { val errors = jsonSchema.validate(jsonNode) import scala.jdk.CollectionConverters._ - if (!errors.isEmpty) throw JsonInvalidSchema(jsonPath, errors.asScala) + if (!errors.isEmpty) throw JsonInvalidSchemaException(jsonPath, errors.asScala) } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteResults.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteResults.scala deleted file mode 100644 index 41f6eaf..0000000 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteResults.scala +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.model - -/** - * Case class representing the results of a suite test. - * - * @param resultType The type of the result (e.g. Before, Test, After) - * @param suiteName The name of the suite - * @param name The name of the test or method - * @param status The status of the result (e.g. Success, Failure) - * @param duration The duration of the test, if applicable - * @param errMessage The error message, if any - * @param categories The categories of the test, if any - */ -case class SuiteResults(resultType: String, - suiteName: String, - name: String, - status: String, - duration: Option[Long], - errMessage: Option[String] = None, - categories: Option[String] = None) { - /** - * Checks if the suite result was a success. - * - * @return true if the suite result was a success, false otherwise - */ - def isSuccess: Boolean = this.status == SuiteResults.Success -} - -object SuiteResults { - val Success: String = "Success" - val Failure: String = "Failure" - - val RESULT_TYPE_BEFORE_METHOD = "Before" - val RESULT_TYPE_TEST = "Test" - val RESULT_TYPE_AFTER_METHOD = "After" - - /** - * Creates and returns a `SuiteResults` object for a method setup operation. This is typically used for storing test setup operation results. - * - * @param resultType Type of result. - * @param suiteName Name of the suite that this setup operation belongs to. - * @param name Name of the setup operation. - * @param status Boolean indicating the success (true) or failure (false) of the setup operation. - * @param duration Optional duration of the setup operation in milliseconds. None if not available. - * @param errMessage Optional error message if the setup operation failed. None if not available. - * @param categories Optional string representing categories the setup operation belongs to. None if not available. - * @return A `SuiteResults` object containing the provided details of the setup operation. - */ - def withBooleanStatus(resultType: String, - suiteName: String, - name: String, - status: Boolean, - duration: Option[Long], - errMessage: Option[String] = None, - categories: Option[String] = None): SuiteResults = { - SuiteResults(resultType, - suiteName, - name, - if (status) Success else Failure, - duration, - errMessage, - categories - ) - } -} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/TestResults.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/TestResults.scala deleted file mode 100644 index d78474c..0000000 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/TestResults.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.model - -case class TestResults(suiteName: String, - testName: String, - status: String, - duration: Option[Long] = None, - errMessage: Option[String] = None, - categories: Option[String] = None) - -object TestResults { - val Success: String = "Success" - val Failure: String = "Failure" - - def withBooleanStatus(suiteName: String, - testName: String, - status: Boolean, - duration: Option[Long] = None, - errMessage: Option[String] = None, - categories: Option[String] = None): TestResults = - TestResults(suiteName = suiteName, - testName = testName, - status = if (status) Success else Failure, - duration = duration, - errMessage = errMessage, - categories = categories) -} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAfter.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/AfterTestSet.scala similarity index 81% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAfter.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/AfterTestSet.scala index 36d6bde..cb85c2d 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAfter.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/AfterTestSet.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite /** * Case class that represents a suite after methods. @@ -22,9 +22,9 @@ package africa.absa.testing.scapi.model * @param name The name of the after methods. * @param methods The set of suite after methods. */ -case class SuiteAfter(name: String, methods: Set[Method]) extends SuiteAround(name, methods) { - override def resolveReferences(references: Map[String, String]): SuiteAround = { - SuiteAfter( +case class AfterTestSet(name: String, methods: Set[Method]) extends SuitePreAndPostProcessing(name, methods) { + override def resolveReferences(references: Map[String, String]): SuitePreAndPostProcessing = { + AfterTestSet( name, methods.map(c => c.resolveReferences(references)) ) diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBefore.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/BeforeTestSet.scala similarity index 81% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBefore.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/BeforeTestSet.scala index e8fb895..d641532 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBefore.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/BeforeTestSet.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite /** * Case class that represents a suite before methods. @@ -22,9 +22,9 @@ package africa.absa.testing.scapi.model * @param name The name of the before methods. * @param methods The set of suite before methods. */ -case class SuiteBefore(name: String, methods: Set[Method]) extends SuiteAround(name, methods) { - override def resolveReferences(references: Map[String, String]): SuiteAround = { - SuiteBefore( +case class BeforeTestSet(name: String, methods: Set[Method]) extends SuitePreAndPostProcessing(name, methods) { + override def resolveReferences(references: Map[String, String]): SuitePreAndPostProcessing = { + BeforeTestSet( name, methods.map(c => c.resolveReferences(references)) ) diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/Method.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Method.scala similarity index 88% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/Method.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Method.scala index 224702d..9e2a742 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/Method.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Method.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite import africa.absa.testing.scapi.json.{Action, Header, Requestable, ResponseAction} @@ -27,9 +27,9 @@ import africa.absa.testing.scapi.json.{Action, Header, Requestable, ResponseActi * @param responseActions The set of responseAction objects for the method. */ case class Method(name: String, - headers: Set[Header], - actions: Set[Action], - responseActions: Set[ResponseAction]) extends Requestable { + headers: Seq[Header], + actions: Seq[Action], + responseActions: Seq[ResponseAction]) extends Requestable { /** * Method to resolve references within the Method instance. * diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBundle.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Suite.scala similarity index 86% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBundle.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Suite.scala index d7de8d2..d072c75 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteBundle.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/Suite.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite /** * Represents a suite of tests, with optional "before" and "after" setup/teardown. @@ -24,4 +24,4 @@ package africa.absa.testing.scapi.model * @param suiteBefore An optional SuiteBefore object, representing any setup actions to be run before the suite. * @param suiteAfter An optional SuiteAfter object, representing any teardown actions to be run after the suite. */ -case class SuiteBundle(suite: Suite, suiteBefore: Option[SuiteBefore] = None, suiteAfter: Option[SuiteAfter] = None) +case class Suite(suite: TestSet, suiteBefore: Option[BeforeTestSet] = None, suiteAfter: Option[AfterTestSet] = None) diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAround.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuitePreAndPostProcessing.scala similarity index 82% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAround.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuitePreAndPostProcessing.scala index 388ebd8..a654095 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteAround.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuitePreAndPostProcessing.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite /** * Abstract class that represents a suite support methods. @@ -22,12 +22,12 @@ package africa.absa.testing.scapi.model * @param name The name of the after methods. * @param methods The set of suite after methods. */ -abstract class SuiteAround(name: String, methods: Set[Method]) { +abstract class SuitePreAndPostProcessing(name: String, methods: Set[Method]) { /** * Method to resolve references within the before methods instance. * * @param references A map containing the references to be resolved. * @return A new SuiteBefore instance where all references are resolved. */ - def resolveReferences(references: Map[String, String]): SuiteAround + def resolveReferences(references: Map[String, String]): SuitePreAndPostProcessing } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteResult.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteResult.scala new file mode 100644 index 0000000..db71d5a --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteResult.scala @@ -0,0 +1,50 @@ +/* + * 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.model.suite + +import africa.absa.testing.scapi.model.suite.types.SuiteResultType.SuiteResultType + +import scala.util.{Failure, Try} + +/** + * Represents the results of a suite test. + * + * @param resultType The type of the result (e.g. Before, Test, After) + * @param suiteName The name of the test suite + * @param name The specific name of the test or method within the suite + * @param result The outcome of the test (e.g. Success, Failure) + * @param duration The time taken to execute the test, if applicable (in milliseconds) + * @param categories The categories or tags associated with the test, if any + */ +case class SuiteResult(resultType: SuiteResultType, + suiteName: String, + name: String, + result: Try[Unit], + duration: Option[Long], + categories: Option[String] = None) { + /** + * Checks if the suite result was a success. + * + * @return true if the suite result was a success, false otherwise + */ + def isSuccess: Boolean = result.isSuccess + + def errorMsg: Option[String] = result match { + case Failure(t) => Some(t.getMessage) + case _ => None + } +} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteTestScenario.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteTestScenario.scala similarity index 87% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteTestScenario.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteTestScenario.scala index b01efa7..9c58482 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/SuiteTestScenario.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/SuiteTestScenario.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite import africa.absa.testing.scapi.json.{Action, Header, Requestable, ResponseAction} @@ -29,10 +29,10 @@ import africa.absa.testing.scapi.json.{Action, Header, Requestable, ResponseActi * @param only The control if test should be only be running when set to true. */ case class SuiteTestScenario(name: String, - categories: Set[String], - headers: Set[Header], - actions: Set[Action], - responseActions: Set[ResponseAction], + categories: Seq[String], + headers: Seq[Header], + actions: Seq[Action], + responseActions: Seq[ResponseAction], only: Option[Boolean] = Some(false)) extends Requestable { /** * Method to resolve references within the SuiteTestScenario instance. diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/Suite.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/TestSet.scala similarity index 67% rename from testApi/src/main/scala/africa/absa/testing/scapi/model/Suite.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/model/suite/TestSet.scala index ea4092d..086a9a2 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/model/Suite.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/TestSet.scala @@ -14,16 +14,16 @@ * limitations under the License. */ -package africa.absa.testing.scapi.model +package africa.absa.testing.scapi.model.suite /** - * A suite case class that represents a collection of test scenarios for a specific endpoint. + * A suite case class that represents a collection of test scenarios. * - * @param endpoint The endpoint which the suite of tests is targeting. - * @param tests A set of `SuiteTestScenario` which define the tests in this suite. - * @constructor Creates a new instance of a Suite. + * @param name The name of the suite. + * @param tests A set of `SuiteTestScenario` which define the tests in this suite. + * @constructor Creates a new instance of a Suite. */ -case class Suite(endpoint: String, tests: Set[SuiteTestScenario]) { +case class TestSet(name: String, tests: Set[SuiteTestScenario]) { /** * Method to resolve references in the test scenarios using a provided map of references. @@ -31,7 +31,7 @@ case class Suite(endpoint: String, tests: Set[SuiteTestScenario]) { * @param references A map of string keys and values which will be used to resolve references in the test scenarios. * @return Returns a new Suite with resolved references in its test scenarios. */ - def resolveReferences(references: Map[String, String]): Suite = { - Suite(endpoint, tests.map(c => c.resolveReferences(references))) + def resolveReferences(references: Map[String, String]): TestSet = { + TestSet(name, tests.map(c => c.resolveReferences(references))) } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/types/SuiteResultType.scala b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/types/SuiteResultType.scala new file mode 100644 index 0000000..543997b --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/model/suite/types/SuiteResultType.scala @@ -0,0 +1,25 @@ +/* + * 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.model.suite.types + +object SuiteResultType extends Enumeration { + type SuiteResultType = Value + + val BeforeTestSet: SuiteResultType.Value = Value("before-test-set") + val TestSet: SuiteResultType.Value = Value("test-set") + val AfterTestSet: SuiteResultType.Value = Value("after-test-set") +} 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 5dfde82..9413b1e 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 @@ -16,7 +16,8 @@ package africa.absa.testing.scapi.reporter -import africa.absa.testing.scapi.model.SuiteResults +import africa.absa.testing.scapi.model.suite.SuiteResult +import africa.absa.testing.scapi.model.suite.types.SuiteResultType /** * A singleton object to manage the standard output reporting of test results. @@ -27,7 +28,7 @@ object StdOutReporter { * * @param testResults The set of test suite results to be reported. */ - def printReport(testResults: List[SuiteResults]): Unit = { + def printReport(testResults: List[SuiteResult]): Unit = { def createFormattedLine(line: Option[String] = None, maxChars: Int = 80, repeatChar: Char = '*'): String = line match { case Some(text) => s"${repeatChar.toString * ((maxChars - text.length - 2) / 2)} $text ${repeatChar.toString * ((maxChars - text.length - 2) / 2)}" @@ -58,8 +59,8 @@ object StdOutReporter { printHeader("Simple Text Report") - val successCount = testResults.count(r => r.status == SuiteResults.Success && r.resultType == SuiteResults.RESULT_TYPE_TEST) - val failureCount = testResults.count(r => r.status == SuiteResults.Failure && r.resultType == SuiteResults.RESULT_TYPE_TEST) + val successCount = testResults.count(r => r.isSuccess && r.resultType == SuiteResultType.TestSet) + val failureCount = testResults.count(r => !r.isSuccess && r.resultType == SuiteResultType.TestSet) println(s"Number of tests run: ${successCount + failureCount}") println(s"Number of successful tests: $successCount") @@ -67,10 +68,10 @@ object StdOutReporter { if (testResults.nonEmpty) { val suiteSummary = testResults - .filter(_.resultType == SuiteResults.RESULT_TYPE_TEST) + .filter(_.resultType == SuiteResultType.TestSet) .groupBy(_.suiteName).map { case (suiteName, results) => - (suiteName, results.size, results.count(_.status == SuiteResults.Success)) + (suiteName, results.size, results.count(_.isSuccess)) } printInnerHeader("Suites Summary") @@ -83,10 +84,15 @@ object StdOutReporter { printTableRowSplitter() println(s"| %-${maxSuiteLength}s | %-${maxTestLength}s | %-13s | %-7s | %-${maxTestCategoriesLength}s | ".format("Suite Name", "Test Name", "Duration (ms)", "Status", "Categories")) printTableRowSplitter() - val resultsList = testResults.filter(_.resultType == SuiteResults.RESULT_TYPE_TEST) + val resultsList = testResults.filter(_.resultType == SuiteResultType.TestSet) resultsList.zipWithIndex.foreach { case (result, index) => val duration = result.duration.map(_.toString).getOrElse("NA") - println(s"| %-${maxSuiteLength}s | %-${maxTestLength}s | %13s | %-7s | %-${maxTestCategoriesLength}s | ".format(result.suiteName, result.name, duration, result.status, result.categories.getOrElse(""))) + println(s"| %-${maxSuiteLength}s | %-${maxTestLength}s | %13s | %-7s | %-${maxTestCategoriesLength}s | ".format( + result.suiteName, + result.name, + duration, + if (result.isSuccess) "Success" else "Failure", + result.categories.getOrElse(""))) // Check if the index + 1 is divisible by 4 (since index is 0-based) if ((index + 1) % 3 == 0) printTableRowSplitter() @@ -94,10 +100,10 @@ object StdOutReporter { if (failureCount > 0) { printInnerHeader("Details of failed tests") - testResults.filter(_.status == SuiteResults.Failure).sortBy(_.name).foreach { result => + testResults.filter(!_.isSuccess).sortBy(_.name).foreach { result => println(s"Suite: ${result.suiteName}") println(s"Test: ${result.name}") - println(s"Error: ${result.errMessage.getOrElse("No details available")}") + println(s"Error: ${result.errorMsg.getOrElse("No details available")}") println(s"Duration: ${result.duration.getOrElse("NA")} ms") println(s"Category: ${result.categories}") println() diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/logging/LoggerConfig.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/model/CookieValue.scala similarity index 85% rename from testApi/src/main/scala/africa/absa/testing/scapi/logging/LoggerConfig.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/rest/model/CookieValue.scala index db00150..12ee53d 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/logging/LoggerConfig.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/model/CookieValue.scala @@ -14,8 +14,6 @@ * limitations under the License. */ -package africa.absa.testing.scapi.logging +package africa.absa.testing.scapi.rest.model -object LoggerConfig { - var logLevel: String = _ -} +case class CookieValue(value: String, secured: Boolean) diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestBody.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestBody.scala index e75a92c..11c9e9e 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestBody.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestBody.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.rest.request -import africa.absa.testing.scapi.ContentValidationFailed +import africa.absa.testing.scapi.ContentValidationFailedException import africa.absa.testing.scapi.utils.cache.RuntimeCache import spray.json._ @@ -33,7 +33,7 @@ object RequestBody { * * @param jsonBody An optional string containing the JSON body. If None or empty string, an empty JSON object ("{}") is returned. * @return A string representing the HTTP request body. - * @throws ContentValidationFailed if JSON body parsing to a string fails at runtime. + * @throws ContentValidationFailedException if JSON body parsing to a string fails at runtime. */ def buildBody(jsonBody: Option[String] = None): String = { jsonBody match { @@ -47,7 +47,7 @@ object RequestBody { * If the JSON body cannot be parsed, a ContentValidationFailed exception is thrown. * * @param jsonBody An optional string containing the JSON body to validate. If None or empty, the method simply returns. - * @throws ContentValidationFailed if the provided JSON body cannot be parsed. + * @throws ContentValidationFailedException if the provided JSON body cannot be parsed. */ def validateContent(jsonBody: Option[String]): Unit = { jsonBody match { @@ -57,7 +57,7 @@ object RequestBody { body.parseJson } match { case Failure(e) => - throw ContentValidationFailed(body, s"Received value cannot be parsed to json: ${e.getMessage}") + throw ContentValidationFailedException(body, s"Received value cannot be parsed to json: ${e.getMessage}") case _ => () } case _ => () diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestHeaders.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestHeaders.scala index 7f9a575..21eeb88 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestHeaders.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/RequestHeaders.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.rest.request -import africa.absa.testing.scapi.UndefinedHeaderType +import africa.absa.testing.scapi.UndefinedHeaderTypeException import africa.absa.testing.scapi.json.Header import africa.absa.testing.scapi.utils.cache.RuntimeCache import africa.absa.testing.scapi.utils.validation.ContentValidator @@ -35,7 +35,7 @@ object RequestHeaders { * @param headersSet A set of Header objects to be processed. * @return A Map where the key is the header name (lowercase) and the value is the resolved header value. */ - def buildHeaders(headersSet: Set[Header]): Map[String, String] = { + def buildHeaders(headersSet: Seq[Header]): Map[String, String] = { headersSet.foldLeft(Map.empty[String, String]) { (acc, header) => header.name.toLowerCase match { case CONTENT_TYPE => acc + (header.name -> RuntimeCache.resolve(header.value)) @@ -52,13 +52,13 @@ object RequestHeaders { * For any other header type, it throws an UndefinedHeaderType exception. * * @param header The Header object to be validated. - * @throws UndefinedHeaderType If an undefined header type is encountered. + * @throws UndefinedHeaderTypeException If an undefined header type is encountered. */ def validateContent(header: Header): Unit = { header.name.toLowerCase match { case CONTENT_TYPE => ContentValidator.validateNonEmptyString(header.value, s"Header.${header.name}") case AUTHORIZATION => ContentValidator.validateNonEmptyString(header.value, s"Header.${header.name}") - case _ => throw UndefinedHeaderType(header.name) + case _ => throw UndefinedHeaderTypeException(header.name) } } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/sender/ScAPIRequestSender.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/sender/ScAPIRequestSender.scala index 31e7355..80456a2 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/sender/ScAPIRequestSender.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/request/sender/ScAPIRequestSender.scala @@ -16,13 +16,38 @@ package africa.absa.testing.scapi.rest.request.sender +import africa.absa.testing.scapi.rest.model.CookieValue import africa.absa.testing.scapi.rest.response.Response +import java.net.HttpCookie + /** * ScAPIRequestSender is an implementation of the RequestSender interface. * It provides the capability to send different types of HTTP requests including GET, POST, PUT, and DELETE. */ object ScAPIRequestSender extends RequestSender { + + private def sendRequest(requestFunc: => requests.Response): Response = { + val startTime = System.nanoTime() + val response = requestFunc + val endTime = System.nanoTime() + + val extractedCookies: Map[String, CookieValue] = response.cookies.view.map { + case (name, cookie: HttpCookie) => + (name, CookieValue(cookie.getValue, cookie.getSecure)) + }.toMap + + Response( + response.statusCode, + response.text(), + url = response.url, + statusMessage = response.statusMessage, + response.headers, + cookies = extractedCookies, + (endTime - startTime) / 1_000_000 + ) + } + /** * Sends a GET request to the provided URL with the given headers, SSL certificate verification setting, data, and parameters. * @@ -34,8 +59,7 @@ object ScAPIRequestSender extends RequestSender { * @return Response Returns the response from the GET request. */ override def get(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { - val response = requests.get(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params) - Response(response.statusCode, response.text(), response.headers.toMap) + sendRequest(requests.get(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params)) } /** @@ -49,8 +73,7 @@ object ScAPIRequestSender extends RequestSender { * @return Response Returns the response from the POST request. */ override def post(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { - val response = requests.post(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params) - Response(response.statusCode, response.text(), response.headers.toMap) + sendRequest(requests.post(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params)) } /** @@ -64,8 +87,7 @@ object ScAPIRequestSender extends RequestSender { * @return Response Returns the response from the PUT request. */ override def put(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { - val response = requests.put(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params) - Response(response.statusCode, response.text(), response.headers.toMap) + sendRequest(requests.put(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params)) } /** @@ -79,7 +101,6 @@ object ScAPIRequestSender extends RequestSender { * @return Response Returns the response from the DELETE request. */ override def delete(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { - val response = requests.delete(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params) - Response(response.statusCode, response.text(), response.headers.toMap) + sendRequest(requests.delete(url = url, headers = headers, verifySslCerts = verifySslCerts, data = data, params = params)) } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/AssertionResponseAction.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/AssertionResponseAction.scala deleted file mode 100644 index 9669b41..0000000 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/AssertionResponseAction.scala +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.rest.response - -import africa.absa.testing.scapi.UndefinedResponseActionType -import africa.absa.testing.scapi.json.ResponseAction -import africa.absa.testing.scapi.logging.Logger -import africa.absa.testing.scapi.utils.validation.ContentValidator - -/** - * Object that validates and performs various assertion response actions on the response received. - * It extends the functionality of ResponsePerformer. - */ -object AssertionResponseAction extends ResponsePerformer { - - val STATUS_CODE = "status-code" - val BODY_CONTAINS = "body-contains" - - /** - * Validates the content of an assertion response action object depending on its type. - * - * @param responseAction The response action object to be validated. - * @throws UndefinedResponseActionType If the response action type is not recognized. - */ - def validateContent(responseAction: ResponseAction): Unit = { - responseAction.name.toLowerCase match { - case STATUS_CODE => - responseAction.params.get("code") match { - case code => ContentValidator.validateIntegerString(code.get, s"ResponseAssertion.$STATUS_CODE.code") - case None => throw new IllegalArgumentException(s"Missing required 'code' parameter for assertion $STATUS_CODE logic.") - } - case BODY_CONTAINS => - responseAction.params.get("body") match { - case body => ContentValidator.validateNonEmptyString(body.get, s"ResponseAssertion.$BODY_CONTAINS.body") - case None => throw new IllegalArgumentException(s"Missing required 'body' parameter for assertion $BODY_CONTAINS logic.") - } - case _ => throw UndefinedResponseActionType(responseAction.name) - } - } - - /** - * Performs assertion actions on a response depending on the type of assertion method provided. - * - * @param response The response to perform the assertions on. - * @param responseAction The assertion response action to perform on the response. - * @return Boolean value indicating whether the assertion passed or failed. - * @throws IllegalArgumentException If the assertion type is not supported. - */ - def performResponseAction(response: Response, responseAction: ResponseAction): Boolean = { - responseAction.name match { - case STATUS_CODE => - val code = responseAction.params("code") - assertStatusCode(response, code) - case BODY_CONTAINS => - val body = responseAction.params("body") - assertBodyContains(response, body) - case _ => throw new IllegalArgumentException(s"Unsupported assertion method [group: assert]: ${responseAction.name}") - } - } - - /* - dedicated actions - */ - - /** - * Asserts that the status code of the response matches the expected status code. - * - * @param response The response whose status code is to be checked. - * @param expectedCode The expected status code as a string. - * @return A Boolean indicating whether the response's status code matches the expected code. Returns true if they match, false otherwise. - */ - def assertStatusCode(response: Response, expectedCode: String): Boolean = { - val iExpectedCode: Int = expectedCode.toInt - - val isSuccess: Boolean = response.statusCode == iExpectedCode - if (!isSuccess) { - Logger.error(s"Expected $iExpectedCode, but got ${response.statusCode}") - } - isSuccess - } - - /** - * Asserts that the body of the response contains the expected content. - * - * @param response The HTTP response to check the body of. - * @param expectedContent The expected content present in the response body as a string. - * @return A Boolean indicating whether the expected content is present in the response body or not. - */ - def assertBodyContains(response: Response, expectedContent: String): Boolean = { - val isSuccess: Boolean = response.body.contains(expectedContent) - if (!isSuccess) - Logger.error(s"Expected body to contain $expectedContent") - isSuccess - } -} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/LogResponseAction.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/LogResponseAction.scala deleted file mode 100644 index ad1aaf2..0000000 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/LogResponseAction.scala +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.rest.response - -import africa.absa.testing.scapi.UndefinedResponseActionType -import africa.absa.testing.scapi.json.ResponseAction -import africa.absa.testing.scapi.logging.Logger -import africa.absa.testing.scapi.utils.validation.ContentValidator - -/** - * Singleton object `ResponseLog` that extends the `ResponsePerformer` trait. - * It provides utilities for validating and performing logging messages. - */ -object LogResponseAction extends ResponsePerformer { - - val INFO = "info" - - /** - * Validates the content of an log response action object depending on its type. - * - * @param responseAction The response action to be validated. - * @throws UndefinedResponseActionType if the response action's name is not recognized. - */ - def validateContent(responseAction: ResponseAction): Unit = { - responseAction.name.toLowerCase match { - case INFO => - responseAction.params.get("message") match { - case message => ContentValidator.validateNonEmptyString(message.get, s"ResponseLog.$INFO.message") - case None => throw new IllegalArgumentException(s"Missing required 'message' for assertion $INFO logic.") - } - case _ => throw UndefinedResponseActionType(responseAction.name) - } - } - - /** - * Performs log actions on a response depending on the type of log method provided. - * - * @param response The response on which the response action are to be performed. - * @param responseAction The responseAction to be performed on the response. - * @throws IllegalArgumentException if the response action's name is not recognized. - */ - def performResponseAction(response: Response, responseAction: ResponseAction): Boolean = { - responseAction.name match { - case INFO => - val message = responseAction.params("message") - logInfo(message) - case _ => throw new IllegalArgumentException(s"Unsupported log method [group: log]: ${responseAction.name}") - } - } - - /* - dedicated actions - */ - - /** - * This method logs a message at the INFO level. - * - * @param message The message to be logged. - */ - def logInfo(message: String): Boolean = { - Logger.info(message) - true - } -} 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 df74355..f3ce594 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 @@ -17,18 +17,26 @@ package africa.absa.testing.scapi.rest.response import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.logging.Logger +import africa.absa.testing.scapi.rest.model.CookieValue +import africa.absa.testing.scapi.rest.response.action.types.ResponseActionGroupType +import africa.absa.testing.scapi.rest.response.action.{AssertionResponseAction, ExtractJsonResponseAction, LogResponseAction} -case class Response(statusCode: Int, body: String, headers: Map[String, Seq[String]]) +import scala.util.{Failure, Success, Try} + +case class Response(statusCode: Int, + body: String, + url: String, + statusMessage: String, + headers: Map[String, Seq[String]], + cookies: Map[String, CookieValue], + duration: Long) /** * A singleton object that is responsible for managing and handling responses. */ object Response { - val GROUP_ASSERT: String = "assert" - val GROUP_EXTRACT_JSON: String = "extractJson" - val GROUP_LOG: String = "log" - /** * Validates an ResponseAction based on its group type. * Calls the appropriate group's validateContent method based on group type. @@ -38,9 +46,9 @@ object Response { */ def validate(responseAction: ResponseAction): Unit = { responseAction.group match { - case GROUP_ASSERT => AssertionResponseAction.validateContent(responseAction) - case GROUP_EXTRACT_JSON => ExtractJsonResponseAction.validateContent(responseAction) - case GROUP_LOG => LogResponseAction.validateContent(responseAction) + case ResponseActionGroupType.Assert => AssertionResponseAction.validateContent(responseAction) + case ResponseActionGroupType.ExtractJson => ExtractJsonResponseAction.validateContent(responseAction) + case ResponseActionGroupType.Log => LogResponseAction.validateContent(responseAction) case _ => throw new IllegalArgumentException(s"Unsupported assertion group: ${responseAction.group}") } } @@ -53,18 +61,47 @@ object Response { * * @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. - * @return Boolean indicating whether all response actions passed (true) or any response action failed (false). + * @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: Set[ResponseAction]): Boolean = { - responseAction.forall { assertion => + def perform(response: Response, responseAction: 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 baseLog = + s""" + |Parameters received: + | Required Response-Action: + | Group->'${resolvedResponseAction.group}', + | Method->'${resolvedResponseAction.name}', + | Params->'$filteredParams', + | Actual Response: + | $response""".stripMargin + val exceptionLog = exception.map(e => s"\nException: ${e.getMessage}").getOrElse("") + Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - error details:$baseLog$exceptionLog") + } + + responseAction.iterator.map { assertion => val resolvedResponseAction: ResponseAction = assertion.resolveByRuntimeCache() - resolvedResponseAction.group match { - case GROUP_ASSERT => AssertionResponseAction.performResponseAction(response, assertion) - case GROUP_EXTRACT_JSON => ExtractJsonResponseAction.performResponseAction(response, assertion) - case GROUP_LOG => LogResponseAction.performResponseAction(response, assertion) - case _ => throw new IllegalArgumentException(s"Unsupported assertion group: ${assertion.group}") + 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}")) } - } + + res match { + case Success(_) => + Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - completed successfully.") + case Failure(e: IllegalArgumentException) => + Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - failed with exception.") + logParameters(response, resolvedResponseAction, Some(e)) + case Failure(e) => + Logger.debug(s"Response-${resolvedResponseAction.group}: '${resolvedResponseAction.name}' - failed with unexpected exception.") + logParameters(response, resolvedResponseAction, Some(e)) + } + res + }.find(_.isFailure).getOrElse(Success(())) } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/AssertionResponseAction.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/AssertionResponseAction.scala new file mode 100644 index 0000000..84dae75 --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/AssertionResponseAction.scala @@ -0,0 +1,444 @@ +/* + * 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.rest.response.action + +import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.logging.Logger +import africa.absa.testing.scapi.rest.response.Response +import africa.absa.testing.scapi.rest.response.action.types.AssertResponseActionType._ +import africa.absa.testing.scapi.utils.validation.ContentValidator +import africa.absa.testing.scapi.{AssertionException, UndefinedResponseActionTypeException} +import spray.json._ + +import scala.util.{Failure, Try} +import scala.xml.XML + +/** + * Object that validates and performs various assertion response actions on the response received. + * It extends the functionality of ResponsePerformer. + */ +object AssertionResponseAction extends ResponseActions { + + /** + * Validates the content of an assertion response action object depending on its type. + * + * @param responseAction The response action object to be validated. + * @throws UndefinedResponseActionTypeException If the response action type is not recognized. + */ + def validateContent(responseAction: ResponseAction): Unit = { + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + action match { + + // response-time-... + case ResponseTimeIsBelow | ResponseTimeIsAbove => + responseAction.params.getOrElse("limit", None) match { + case limit: String => ContentValidator.validateLongString(limit, s"ResponseAssertion.${responseAction.name}.limit") + case None => throw new IllegalArgumentException(s"Missing required 'limit' parameter for assertion ${responseAction.name} logic.") + } + + // status-code-... + case StatusCodeEquals => + responseAction.params.getOrElse("code", None) match { + case code: String => ContentValidator.validateIntegerString(code, s"ResponseAssertion.$StatusCodeEquals.code") + case None => throw new IllegalArgumentException(s"Missing required 'code' parameter for assertion $StatusCodeEquals logic.") + } + case StatusCodeIsSuccess | StatusCodeIsClientError | StatusCodeIsServerError => () + + // header-... + case HeaderExists | HeaderValueEquals => + responseAction.params.getOrElse("headerName", None) match { + case headerName: String => ContentValidator.validateNonEmptyString(headerName, s"ResponseAssertion.${responseAction.name}.headerName") + case None => throw new IllegalArgumentException(s"Missing required 'headerName' parameter for assertion ${responseAction.name} logic.") + } + action match { + case HeaderValueEquals => + responseAction.params.getOrElse("expectedValue", None) match { + case expectedValue: String => ContentValidator.validateNonEmptyString(expectedValue, s"ResponseAssertion.$HeaderValueEquals.expectedValue") + case None => throw new IllegalArgumentException(s"Missing required 'expectedValue' parameter for assertion $HeaderValueEquals logic.") + } + case _ => () + } + + // content-type-... + case ContentTypeIsJson | ContentTypeIsXml | ContentTypeIsHtml => () + + // cookies-... + case CookieExists | CookieValueEquals | CookieIsSecured | CookieIsNotSecured => + responseAction.params.getOrElse("cookieName", None) match { + case cookieName: String => ContentValidator.validateNonEmptyString(cookieName, s"ResponseAssertion.${responseAction.name}.cookieName") + case None => throw new IllegalArgumentException(s"Missing required 'cookieName' parameter for assertion ${responseAction.name} logic.") + } + action match { + case CookieValueEquals => + responseAction.params.getOrElse("expectedValue", None) match { + case expectedValue: String => ContentValidator.validateNonEmptyString(expectedValue, s"ResponseAssertion.$CookieValueEquals.expectedValue") + case None => throw new IllegalArgumentException(s"Missing required 'expectedValue' parameter for assertion $CookieValueEquals logic.") + } + case _ => () + } + + // body-... + case BodyContainsText => + responseAction.params.getOrElse("text", None) match { + case text: String => ContentValidator.validateNonEmptyString(text, s"ResponseAssertion.$BodyContainsText.text") + case None => throw new IllegalArgumentException(s"Missing required 'text' parameter for assertion $BodyContainsText logic.") + } + case _ => throw UndefinedResponseActionTypeException(responseAction.name) + } + } + + /** + * Performs assertion actions on a response depending on the type of assertion method provided. + * + * @param response The response on which the assertions are to be performed. + * @param responseAction The assertion response action to be performed on the response. + * @throws UndefinedResponseActionTypeException If the assertion type is not supported. + * @return A Try[Unit] indicating the success of the assertion operation. + */ + def performResponseAction(response: Response, responseAction: ResponseAction): Try[Unit] = { + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + action match { + + // response-time-... + case ResponseTimeIsBelow | ResponseTimeIsAbove => + val limit = responseAction.params("limit") + action match { + case ResponseTimeIsBelow => assertResponseTimeIsBelow(response, limit) + case ResponseTimeIsAbove => assertResponseTimeIsAbove(response, limit) + } + + // status-code-... + case StatusCodeEquals => + val code = responseAction.params("code") + assertStatusCodeEquals(response, code) + case StatusCodeIsSuccess => assertStatusCodeSuccess(response) + case StatusCodeIsClientError => assertStatusCodeIsClientError(response) + case StatusCodeIsServerError => assertStatusCodeIsServerError(response) + + // header-... + case HeaderExists | HeaderValueEquals => + val headerName = responseAction.params("headerName") + action match { + case HeaderExists => assertHeaderExists(response, headerName) + case HeaderValueEquals => + val expectedValue = responseAction.params("expectedValue") + assertHeaderValueEquals(response, headerName, expectedValue) + } + + // content-type-... + case ContentTypeIsJson => assertContentTypeIsJson(response) + case ContentTypeIsXml => assertContentTypeIsXml(response) + case ContentTypeIsHtml => assertContentTypeIsHtml(response) + + // cookies-... + case CookieExists | CookieValueEquals | CookieIsSecured | CookieIsNotSecured => + val cookieName = responseAction.params("cookieName") + action match { + case CookieExists => assertCookieExists(response, cookieName) + case CookieValueEquals => + val expectedValue = responseAction.params("expectedValue") + assertCookieValueEquals(response, cookieName, expectedValue) + case CookieIsSecured => assertCookieIsSecured(response, cookieName) + case CookieIsNotSecured => assertCookieIsNotSecured(response, cookieName) + } + + // body-... + case BodyContainsText => + val text = responseAction.params("text") + assertBodyContainsText(response, text) + + case _ => Failure(UndefinedResponseActionTypeException(s"Unsupported assertion method [group: assert]: ${responseAction.name}")) + } + } + + /* + dedicated actions + */ + + /** + * Asserts that the response duration is below the specified maximum time. + * + * @param response The response object containing the duration to be checked. + * @param maxTimeMillis The maximum allowed duration in milliseconds, provided as a string. + * @return A Try[Unit] that succeeds if the response's duration is below the specified maximum time, and fails with an AssertionException otherwise. + */ + private def assertResponseTimeIsBelow(response: Response, maxTimeMillis: String): Try[Unit] = { + Try { + val lMaxTimeMillis: Long = maxTimeMillis.toLong + + if (response.duration > lMaxTimeMillis) { + throw AssertionException(s"Expected maximal length '$lMaxTimeMillis' is smaller then received '${response.duration}' one.") + } + } + } + + /** + * Asserts that the response duration is greater than or equal to the specified minimum time. + * + * @param response The response object containing the duration to be checked. + * @param minTimeMillis The minimum required duration in milliseconds, provided as a string. + * @return A Try[Unit] that is a Success if the response's duration is greater than or equal to the specified minimum time, and a Failure with an AssertionException otherwise. + */ + private def assertResponseTimeIsAbove(response: Response, minTimeMillis: String): Try[Unit] = { + Try { + val lMinTimeMillis: Long = minTimeMillis.toLong + + if (response.duration < lMinTimeMillis) { + throw AssertionException(s"Expected minimal length '$lMinTimeMillis' is bigger then received '${response.duration}' one.") + } + } + } + + /** + * Compares the status code of the given response with the expected status code. + * + * @param response The HTTP response object to be evaluated. + * @param expectedCode The expected HTTP status code as a string. + * @return A Try[Unit] that is successful if the response's status code matches the expected code, and contains an exception otherwise. + * @throws AssertionException if the response's status code does not match the expected code. + */ + private def assertStatusCodeEquals(response: Response, expectedCode: String): Try[Unit] = { + Try { + val iExpectedCode: Int = expectedCode.toInt + + if (response.statusCode != iExpectedCode) { + throw AssertionException(s"Expected $iExpectedCode, but got ${response.statusCode}") + } + } + } + + /** + * Asserts if the status code of the given response is within the success range (200-299). + * + * @param response The HTTP response object containing the status code. + * @return A Try[Unit] that is a Success if the status code is within the range 200-299, and a Failure with an AssertionException otherwise. + */ + private def assertStatusCodeSuccess(response: Response): Try[Unit] = { + Try { + if (!(response.statusCode >= 200 && response.statusCode <= 299)) { + throw AssertionException(s"Received status code '${response.statusCode}' is not in expected range (200 - 299).") + } + } + } + + /** + * Asserts that the status code of the given response is within the client error range (400-499). + * + * @param response The response object containing the status code. + * @return A Try[Unit] that is a Success if the status code is within the range 400-499, and a Failure with an AssertionException otherwise. + */ + private def assertStatusCodeIsClientError(response: Response): Try[Unit] = { + Try { + if (!(response.statusCode >= 400 && response.statusCode <= 499)) { + throw AssertionException(s"Received status code '${response.statusCode}' is not in expected range (400 - 499).") + } + } + } + + /** + * Asserts that the status code of the given response is within the server error range (500-599). + * + * @param response The response object containing the status code. + * @return A Try[Unit] that is a Success if the status code is within the range 500-599, and a Failure with an AssertionException otherwise. + */ + private def assertStatusCodeIsServerError(response: Response): Try[Unit] = { + Try { + if (!(response.statusCode >= 500 && response.statusCode <= 599)) { + throw AssertionException(s"Received status code '${response.statusCode}' is not in expected range (500 - 599).") + } + } + } + + /** + * Asserts that the specified header exists in the given response. + * + * @param response The response object containing the headers. + * @param headerName The name of the header to check for. + * @return A Try[Unit] that is a Success if the header exists in the response, and a Failure with an AssertionException otherwise. + */ + private def assertHeaderExists(response: Response, headerName: String): Try[Unit] = { + Try { + if (!response.headers.contains(headerName.toLowerCase)) { + throw AssertionException(s"Expected header '$headerName' not found.") + } + } + } + + /** + * Asserts that the value of the specified header in the given response matches the expected value. + * + * @param response The response object containing the headers. + * @param headerName The name of the header to check. + * @param expectedValue The expected value of the header. + * @return A Try[Unit] that is a Success if the header value matches the expected value, and a Failure with an AssertionException otherwise. + */ + private def assertHeaderValueEquals(response: Response, headerName: String, expectedValue: String): Try[Unit] = { + Try { + if (assertHeaderExists(response, headerName).isFailure) { + throw AssertionException(s"Expected header '$headerName' not found.") + } else if (!expectedValue.equals(response.headers(headerName.toLowerCase).head)) { + throw AssertionException(s"Expected header '$headerName' value '$expectedValue' is not equal to " + + s"received header value '${response.headers(headerName.toLowerCase).head}'.") + } + } + } + + /** + * Asserts that the value of the "Content-Type" header in the given response is "application/json". + * + * @param response The response object containing the headers. + * @return A Try[Unit] that is a Success if the "Content-Type" header value is "application/json", and a Failure with an AssertionException otherwise. + */ + private def assertContentTypeIsJson(response: Response): Try[Unit] = { + Try { + val isContentTypeJson = assertHeaderValueEquals(response, "content-type", "application/json") + val isBodyJson = try { + response.body.parseJson + true + } catch { + case _: JsonParser.ParsingException => false + } + + if (!isContentTypeJson.isSuccess || !isBodyJson) { + throw AssertionException("Received content is not JSON type.") + } + } + } + + /** + * Asserts that the value of the "Content-Type" header in the given response is "application/xml". + * + * @param response The response object containing the headers. + * @return A Try[Unit] that is a Success if the "Content-Type" header value is "application/xml", and a Failure with an AssertionException otherwise. + */ + private def assertContentTypeIsXml(response: Response): Try[Unit] = { + Try { + val isContentTypeXml = assertHeaderValueEquals(response, "content-type", "application/xml") + val isBodyXml = try { + XML.loadString(response.body) + true + } catch { + case _: Exception => false + } + + if (!isContentTypeXml.isSuccess || !isBodyXml) { + throw AssertionException("Received content is not XML type.") + } + } + } + + /** + * Asserts that the value of the "Content-Type" header in the given response is "text/html". + * + * @param response The response object containing the headers. + * @return A Try[Unit] that is a Success if the "Content-Type" header value is "text/html", and a Failure with an AssertionException otherwise. + */ + private def assertContentTypeIsHtml(response: Response): Try[Unit] = { + Try { + assertHeaderValueEquals(response, "content-type", "text/html") match { + case Failure(exception) => + throw AssertionException(s"Received content is not HTML type. Details: ${exception.getMessage}") + case _ => // Do nothing for Success + } + } + } + + // cookies-... + + /** + * Asserts that the specified cookie exists in the given response. + * + * @param response The response object containing the cookies. + * @param cookieName The name of the cookie to check for existence. + * @return A Try[Unit] that is a Success if the specified cookie exists in the response, and a Failure with an AssertionException otherwise. + */ + private def assertCookieExists(response: Response, cookieName: String): Try[Unit] = { + Try { + if (!response.cookies.contains(cookieName)) { + throw AssertionException(s"Cookie '$cookieName' does not exist in the response.") + } + } + } + + /** + * Asserts that the value of the specified cookie in the given response equals the expected value. + * + * @param response The response object containing the cookies. + * @param cookieName The name of the cookie to check. + * @param expectedValue The expected value of the cookie. + * @return A Try[Unit] that is a Success if the value of the specified cookie matches the expected value, and a Failure with an AssertionException otherwise. + */ + private def assertCookieValueEquals(response: Response, cookieName: String, expectedValue: String): Try[Unit] = { + assertCookieExists(response, cookieName).flatMap { _ => + Try { + if (!(response.cookies(cookieName).value == expectedValue)) { + throw AssertionException(s"Cookie '$cookieName' value does not match expected value '$expectedValue'.") + } + } + } + } + + /** + * Asserts that the specified cookie in the given response is secured. + * + * @param response The response object containing the cookies. + * @param cookieName The name of the cookie to check. + * @return A Try[Unit] that is a Success if the specified cookie is secured, and a Failure with an AssertionException otherwise. + */ + private def assertCookieIsSecured(response: Response, cookieName: String): Try[Unit] = { + assertCookieExists(response, cookieName).flatMap { _ => + Try { + if (!response.cookies(cookieName).secured) { + throw AssertionException(s"Cookie '$cookieName' is not secured.") + } + } + } + } + + /** + * Asserts that the specified cookie in the given response is not secured. + * + * @param response The response object containing the cookies. + * @param cookieName The name of the cookie to check. + * @return A Try[Unit] that is a Success if the specified cookie is not secured, and a Failure with an AssertionException otherwise. + */ + private def assertCookieIsNotSecured(response: Response, cookieName: String): Try[Unit] = { + assertCookieExists(response, cookieName).flatMap { _ => + Try { + if (response.cookies(cookieName).secured) { + throw AssertionException(s"Cookie '$cookieName' is secured.") + } + } + } + } + + /** + * Asserts that the body of the response contains the expected content. + * + * @param response The HTTP response to check the body of. + * @param text The expected text present in the response body as a string. + * @return A Try[Unit] that is a Success if the body contains the expected text, and a Failure with an AssertionException otherwise. + */ + private def assertBodyContainsText(response: Response, text: String): Try[Unit] = { + Try { + if (!response.body.contains(text)) { + Logger.error(s"Expected body to contain $text") + throw AssertionException(s"Body does not contain expected text '$text'.") + } + } + } +} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ExtractJsonResponseAction.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ExtractJsonResponseAction.scala similarity index 68% rename from testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ExtractJsonResponseAction.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ExtractJsonResponseAction.scala index 610a104..e3cd7d9 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ExtractJsonResponseAction.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ExtractJsonResponseAction.scala @@ -14,53 +14,57 @@ * limitations under the License. */ -package africa.absa.testing.scapi.rest.response +package africa.absa.testing.scapi.rest.response.action -import africa.absa.testing.scapi.UndefinedResponseActionType import africa.absa.testing.scapi.json.ResponseAction import africa.absa.testing.scapi.logging.Logger +import africa.absa.testing.scapi.rest.response.Response +import africa.absa.testing.scapi.rest.response.action.types.ExtractJsonResponseActionType._ import africa.absa.testing.scapi.utils.cache.RuntimeCache import africa.absa.testing.scapi.utils.validation.ContentValidator +import africa.absa.testing.scapi.{AssertionException, UndefinedResponseActionTypeException} import spray.json._ +import scala.util.Try + /** * ExtractJsonResponseAction is an object that extends ResponsePerformer. * It is designed to extract specific data from a JSON response and perform validations. */ -object ExtractJsonResponseAction extends ResponsePerformer { - - val STRING_FROM_LIST = "string-from-list" +object ExtractJsonResponseAction extends ResponseActions { /** * Validates the content of an extract response action object depending on its type. * * @param responseAction The ResponseAction instance to be validated. - * @throws UndefinedResponseActionType if an unsupported assertion type is encountered. + * @throws UndefinedResponseActionTypeException if an unsupported assertion type is encountered. */ def validateContent(responseAction: ResponseAction): Unit = { - responseAction.name.toLowerCase match { - case STRING_FROM_LIST => validateStringFromList(responseAction) - case _ => throw UndefinedResponseActionType(responseAction.name) + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + action match { + case StringFromList => validateStringFromList(responseAction) + case _ => throw UndefinedResponseActionTypeException(responseAction.name) } } /** * Performs extract actions on a response depending on the type of assertion method provided. * - * @param response The Response instance to perform response action on. + * @param response The Response instance to perform response action on. * @param responseAction The ResponseAction instance containing the response action details. - * @throws IllegalArgumentException if an unsupported response action name is encountered. + * @throws UndefinedResponseActionTypeException if an unsupported response action name is encountered. */ - def performResponseAction(response: Response, responseAction: ResponseAction): Boolean = { - responseAction.name match { - case STRING_FROM_LIST => + def performResponseAction(response: Response, responseAction: ResponseAction): Try[Unit] = { + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + action match { + case StringFromList => val cacheKey = responseAction.params("cacheKey") val listIndex = responseAction.params("listIndex").toInt val jsonKey = responseAction.params("jsonKey") val cacheLevel = responseAction.params("cacheLevel") stringFromList(response, cacheKey, listIndex, jsonKey, cacheLevel) - case _ => throw new IllegalArgumentException(s"Unsupported assertion[group: extract]: ${responseAction.name}") + case _ => throw UndefinedResponseActionTypeException(s"Unsupported assertion[group: extract]: ${responseAction.name}") } } @@ -77,17 +81,17 @@ object ExtractJsonResponseAction extends ResponsePerformer { * @param listIndex The index in the JSON array from which to extract the string. * @param jsonKey The key in the JSON object from which to extract the string. * @param runtimeCacheLevel The expiration level to use when storing the extracted string in the runtime cache. - * @return Boolean indicating whether the string extraction and caching operation was successful. + * @return A Try[Unit] indicating whether the string extraction and caching operation was successful or not. */ - def stringFromList(response: Response, cacheKey: String, listIndex: Int, jsonKey: String, runtimeCacheLevel: String): Boolean = { - try { + private def stringFromList(response: Response, cacheKey: String, listIndex: Int, jsonKey: String, runtimeCacheLevel: String): Try[Unit] = { + Try { val jsonAst = response.body.parseJson val objects = jsonAst match { case JsArray(array) => array case _ => Logger.error("Expected a JSON array") - return false + throw AssertionException("Expected a JSON array in the response.") } // Extract "jsonKey" from the object at the given index @@ -96,15 +100,14 @@ object ExtractJsonResponseAction extends ResponsePerformer { case Seq(JsNumber(value)) => value.toString() case _ => Logger.error(s"Expected '$jsonKey' field not found in provided json.") - return false + throw AssertionException(s"Expected '$jsonKey' field not found in provided json.") } RuntimeCache.put(key = cacheKey, value = value, RuntimeCache.determineLevel(runtimeCacheLevel)) - true - } catch { + } recover { case e: spray.json.JsonParser.ParsingException => Logger.error(s"Expected json string in response body. JSON parsing error: ${e.getMessage}") - false + throw AssertionException(s"Expected json string in response body. JSON parsing error: ${e.getMessage}") } } @@ -115,18 +118,18 @@ object ExtractJsonResponseAction extends ResponsePerformer { * * @param assertion The ResponseAction instance containing the response action details. */ - def validateStringFromList(assertion: ResponseAction): Unit = { - val cacheKey = assertion.params.getOrElse("cacheKey", throw new IllegalArgumentException(s"Missing required 'cacheKey' parameter for extract $STRING_FROM_LIST logic")) - val listIndex = assertion.params.getOrElse("listIndex", throw new IllegalArgumentException(s"Missing required 'listIndex' parameter for extract $STRING_FROM_LIST logic")) - val jsonKey = assertion.params.getOrElse("jsonKey", throw new IllegalArgumentException(s"Missing required 'jsonKey' parameter for extract $STRING_FROM_LIST logic")) - val cacheLevel = assertion.params.getOrElse("cacheLevel", throw new IllegalArgumentException(s"Missing required 'cacheLevel' parameter for extract $STRING_FROM_LIST logic")) - - ContentValidator.validateNonEmptyString(cacheKey, s"ExtractJson.$STRING_FROM_LIST.cacheKey") - ContentValidator.validateNonEmptyString(listIndex, s"ExtractJson.$STRING_FROM_LIST.listIndex") - ContentValidator.validateNonEmptyString(jsonKey, s"ExtractJson.$STRING_FROM_LIST.jsonKey") - ContentValidator.validateNonEmptyString(cacheLevel, s"ExtractJson.$STRING_FROM_LIST.cacheLevel") - - ContentValidator.validateIntegerString(listIndex, s"ExtractJson.$STRING_FROM_LIST.listIndex") + private def validateStringFromList(assertion: ResponseAction): Unit = { + val cacheKey = assertion.params.getOrElse("cacheKey", throw new IllegalArgumentException(s"Missing required 'cacheKey' parameter for extract $StringFromList logic")) + val listIndex = assertion.params.getOrElse("listIndex", throw new IllegalArgumentException(s"Missing required 'listIndex' parameter for extract $StringFromList logic")) + val jsonKey = assertion.params.getOrElse("jsonKey", throw new IllegalArgumentException(s"Missing required 'jsonKey' parameter for extract $StringFromList logic")) + val cacheLevel = assertion.params.getOrElse("cacheLevel", throw new IllegalArgumentException(s"Missing required 'cacheLevel' parameter for extract $StringFromList logic")) + + ContentValidator.validateNonEmptyString(cacheKey, s"ExtractJson.$StringFromList.cacheKey") + ContentValidator.validateNonEmptyString(listIndex, s"ExtractJson.$StringFromList.listIndex") + ContentValidator.validateNonEmptyString(jsonKey, s"ExtractJson.$StringFromList.jsonKey") + ContentValidator.validateNonEmptyString(cacheLevel, s"ExtractJson.$StringFromList.cacheLevel") + + ContentValidator.validateIntegerString(listIndex, s"ExtractJson.$StringFromList.listIndex") } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/LogResponseAction.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/LogResponseAction.scala new file mode 100644 index 0000000..c2f278e --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/LogResponseAction.scala @@ -0,0 +1,113 @@ +/* + * 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.rest.response.action + +import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.logging.Logger +import africa.absa.testing.scapi.rest.response.Response +import africa.absa.testing.scapi.rest.response.action.types.LogResponseActionType._ +import africa.absa.testing.scapi.utils.validation.ContentValidator +import africa.absa.testing.scapi.{PropertyNotFoundException, UndefinedResponseActionTypeException} + +import scala.util.{Failure, Try} + +/** + * Singleton object `ResponseLog` that extends the `ResponsePerformer` trait. + * It provides utilities for validating and performing logging messages. + */ +object LogResponseAction extends ResponseActions { + + /** + * Validates the content of an log response action object depending on its type. + * + * @param responseAction The response action to be validated. + * @throws UndefinedResponseActionTypeException if the response action's name is not recognized. + */ + def validateContent(responseAction: ResponseAction): Unit = { + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + action match { + case Error | Warn | Info | Debug => + responseAction.params.get("message") match { + case Some(message) => ContentValidator.validateNonEmptyString(message, s"ResponseLog.${responseAction.name}.message") + case None => throw new IllegalArgumentException(s"Missing required 'message' for assertion ${responseAction.name} logic.") + } + case _ => throw UndefinedResponseActionTypeException(responseAction.name) + } + } + + /** + * Performs log actions on a response depending on the type of log method provided. + * + * @param response The response on which the response action is to be performed. + * @param responseAction The response action to be performed on the response. + * @throws UndefinedResponseActionTypeException if the response action's name is not recognized. + * @throws PropertyNotFoundException if the required 'message' parameter is missing. + */ + def performResponseAction(response: Response, responseAction: ResponseAction): Try[Unit] = { + val message = responseAction.params.getOrElse("message", return Failure(PropertyNotFoundException("Missing 'message' parameter"))) + val action = fromString(responseAction.name.toLowerCase).getOrElse(None) + Try { + action match { + case Error => logError(message) + case Warn => logWarn(message) + case Info => logInfo(message) + case Debug => logDebug(message) + case _ => Failure(UndefinedResponseActionTypeException(s"Unsupported log method [group: log]: ${responseAction.name}")) + } + } + } + + /* + dedicated actions + */ + + /** + * Logs a message at the ERROR level. + * + * @param message The message to be logged. + */ + private def logError(message: String): Unit = { + Logger.error(message) + } + + /** + * Logs a message at the WARN level. + * + * @param message The message to be logged. + */ + private def logWarn(message: String): Unit = { + Logger.warn(message) + } + + /** + * Logs a message at the INFO level. + * + * @param message The message to be logged. + */ + private def logInfo(message: String): Unit = { + Logger.info(message) + } + + /** + * Logs a message at the DEBUG level. + * + * @param message The message to be logged. + */ + private def logDebug(message: String): Unit = { + Logger.debug(message) + } +} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ResponsePerformer.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ResponseActions.scala similarity index 80% rename from testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ResponsePerformer.scala rename to testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ResponseActions.scala index e430305..8f3bcf9 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/ResponsePerformer.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/ResponseActions.scala @@ -14,11 +14,14 @@ * limitations under the License. */ -package africa.absa.testing.scapi.rest.response +package africa.absa.testing.scapi.rest.response.action import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.rest.response.Response -trait ResponsePerformer { +import scala.util.Try + +trait ResponseActions { def validateContent(responseAction: ResponseAction): Unit - def performResponseAction(response: Response, responseAction: ResponseAction): Boolean + def performResponseAction(response: Response, responseAction: ResponseAction): Try[Unit] } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/AssertResponseActionType.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/AssertResponseActionType.scala new file mode 100644 index 0000000..fb91c9e --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/AssertResponseActionType.scala @@ -0,0 +1,55 @@ +/* + * 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.rest.response.action.types + +import scala.language.implicitConversions + +object AssertResponseActionType extends Enumeration { + type AssertResponseActionType = Value + + // response-time-... + val ResponseTimeIsBelow: AssertResponseActionType.Value = Value("response-time-is-below") + val ResponseTimeIsAbove: AssertResponseActionType.Value = Value("response-time-is-above") + + // status-code-... + val StatusCodeEquals: AssertResponseActionType.Value = Value("status-code-equals") + val StatusCodeIsSuccess: AssertResponseActionType.Value = Value("status-code-is-success") + val StatusCodeIsClientError: AssertResponseActionType.Value = Value("status-code-is-client-error") + val StatusCodeIsServerError: AssertResponseActionType.Value = Value("status-code-is-server-error") + + // header-... + val HeaderExists: AssertResponseActionType.Value = Value("header-exists") + val HeaderValueEquals: AssertResponseActionType.Value = Value("header-value-equals") + + // content-type-... + val ContentTypeIsJson: AssertResponseActionType.Value = Value("content-type-is-json") + val ContentTypeIsXml: AssertResponseActionType.Value = Value("content-type-is-xml") + val ContentTypeIsHtml: AssertResponseActionType.Value = Value("content-type-is-html") + + // cookies-... + val CookieExists: AssertResponseActionType.Value = Value("cookie-exists") + val CookieValueEquals: AssertResponseActionType.Value = Value("cookie-value-equals") + val CookieIsSecured: AssertResponseActionType.Value = Value("cookie-is-secured") + val CookieIsNotSecured: AssertResponseActionType.Value = Value("cookie-is-not-secured") + + // body-... + val BodyContainsText: AssertResponseActionType.Value = Value("body-contains-text") + + private val stringToValueMap = values.map(v => v.toString -> v).toMap + + def fromString(s: String): Option[AssertResponseActionType] = stringToValueMap.get(s) +} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ExtractJsonResponseActionType.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ExtractJsonResponseActionType.scala new file mode 100644 index 0000000..99b82eb --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ExtractJsonResponseActionType.scala @@ -0,0 +1,32 @@ +/* + * 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.rest.response.action.types + +import scala.language.implicitConversions + +object ExtractJsonResponseActionType extends Enumeration { + type ExtractJsonResponseActionType = Value + + val StringFromList: ExtractJsonResponseActionType.Value = Value("string-from-list") + + private val stringToValueMap = values.map(v => v.toString -> v).toMap + + def fromString(s: String): Option[ExtractJsonResponseActionType] = stringToValueMap.get(s) +} + + + diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/LogResponseActionType.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/LogResponseActionType.scala new file mode 100644 index 0000000..9c91945 --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/LogResponseActionType.scala @@ -0,0 +1,32 @@ +/* + * 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.rest.response.action.types + +import scala.language.implicitConversions + +object LogResponseActionType extends Enumeration { + type LogResponseActionType = Value + + val Error: LogResponseActionType.Value = Value("error") + val Warn: LogResponseActionType.Value = Value("warn") + val Info: LogResponseActionType.Value = Value("info") + val Debug: LogResponseActionType.Value = Value("debug") + + private val stringToValueMap = values.map(v => v.toString -> v).toMap + + def fromString(s: String): Option[LogResponseActionType] = stringToValueMap.get(s) +} diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ResponseActionGroupType.scala b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ResponseActionGroupType.scala new file mode 100644 index 0000000..1ed1b70 --- /dev/null +++ b/testApi/src/main/scala/africa/absa/testing/scapi/rest/response/action/types/ResponseActionGroupType.scala @@ -0,0 +1,33 @@ +/* + * 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.rest.response.action.types + +object ResponseActionGroupType extends Enumeration { + val Assert, ExtractJson, Log = Value + type ResponseActionGroupType = Value + + private val stringToValueMap = Map( + "assert" -> Assert, + "extractJson" -> ExtractJson, + "log" -> Log + ) + + def fromString(s: String): Option[ResponseActionGroupType] = stringToValueMap.get(s) +} + + + 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 3a9ae08..05bc8be 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 @@ -16,19 +16,24 @@ package africa.absa.testing.scapi.suite.runner +import africa.absa.testing.scapi.SuiteBeforeFailedException import africa.absa.testing.scapi.json.{Environment, Requestable} import africa.absa.testing.scapi.logging.Logger -import africa.absa.testing.scapi.model.{Method, SuiteBundle, SuiteResults, SuiteTestScenario} +import africa.absa.testing.scapi.model.suite.types.SuiteResultType +import africa.absa.testing.scapi.model.suite.types.SuiteResultType.SuiteResultType +import africa.absa.testing.scapi.model.suite.{Method, Suite, SuiteResult, SuiteTestScenario} import africa.absa.testing.scapi.rest.RestClient import africa.absa.testing.scapi.rest.request.{RequestBody, RequestHeaders, RequestParams} import africa.absa.testing.scapi.rest.response.Response import africa.absa.testing.scapi.utils.cache.{RuntimeCache, SuiteLevel, TestLevel} +import scala.util.{Failure, Try} + /** * Main object handling the running of test suites. */ object SuiteRunner { - type RestClientCreator = () => RestClient + private type RestClientCreator = () => RestClient /** * Run a set of test suites. @@ -37,105 +42,98 @@ object SuiteRunner { * @param environment The current environment. * @return Set of SuiteResults. */ - def runSuites(suiteBundles: Set[SuiteBundle], environment: Environment, restClientCreator: RestClientCreator): List[SuiteResults] = { - suiteBundles.foldLeft(List[SuiteResults]()) { (resultList, suiteBundle) => - Logger.debug(s"Running Suite: ${suiteBundle.suite.endpoint}") + def runSuites(suiteBundles: Set[Suite], environment: Environment, restClientCreator: RestClientCreator): List[SuiteResult] = { + suiteBundles.foldLeft(List[SuiteResult]()) { (resultList, suiteBundle) => + Logger.debug(s"Suite: ${suiteBundle.suite.name} - Started") - val resultSuiteBefore: List[SuiteResults] = suiteBundle.suiteBefore.toList.flatMap { suiteBefore => - suiteBefore.methods.map { method => runSuiteBefore(suiteBundle.suite.endpoint, suiteBefore.name, method, environment, restClientCreator) } + val suiteBeforeResult: List[SuiteResult] = suiteBundle.suiteBefore.toList.flatMap { suiteBefore => + suiteBefore.methods.map { method => runSuiteBefore(suiteBundle.suite.name, suiteBefore.name, method, environment, restClientCreator) } } - var resultSuite: List[SuiteResults] = List.empty - var resultSuiteAfter: List[SuiteResults] = List.empty - if (!resultSuiteBefore.forall(_.isSuccess)) { - Logger.error(s"Suite-Before for Suite: ${suiteBundle.suite.endpoint} has failed methods. Not executing main tests and Suite-After.") - resultSuite = resultSuite :+ SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_TEST, - suiteName = suiteBundle.suite.endpoint, + var suiteResult: List[SuiteResult] = List.empty + var suiteAfterResult: List[SuiteResult] = List.empty + if (!suiteBeforeResult.forall(_.isSuccess)) { + val errorMsg = s"Suite-Before for Suite: ${suiteBundle.suite.name} has failed methods. Not executing main tests and Suite-After." + Logger.error(errorMsg) + + // add failed Test suite result instance and it will not be started + suiteResult = suiteResult :+ SuiteResult( + resultType = SuiteResultType.TestSet, + suiteName = suiteBundle.suite.name, name = "SKIPPED", - status = false, + result = Failure(SuiteBeforeFailedException(errorMsg)), duration = Some(0L), categories = Some("SKIPPED")) } else { - resultSuite = suiteBundle.suite.tests.toList.map(test => - this.runSuiteTest(suiteBundle.suite.endpoint, test, environment, restClientCreator)) + suiteResult = suiteBundle.suite.tests.toList.map(test => + this.runSuiteTest(suiteBundle.suite.name, test, environment, restClientCreator)) - resultSuiteAfter = suiteBundle.suiteAfter.toList.flatMap { suiteAfter => - suiteAfter.methods.map { method => runSuiteAfter(suiteBundle.suite.endpoint, suiteAfter.name, method, environment, restClientCreator) } + suiteAfterResult = suiteBundle.suiteAfter.toList.flatMap { suiteAfter => + suiteAfter.methods.map { method => runSuiteAfter(suiteBundle.suite.name, suiteAfter.name, method, environment, restClientCreator) } } } RuntimeCache.expire(SuiteLevel) - resultList ++ resultSuiteBefore ++ resultSuite ++ resultSuiteAfter + resultList ++ suiteBeforeResult ++ suiteResult ++ suiteAfterResult } } /** * Runs all the suite-before methods for a given test suite. * - * @param suiteEndpoint Suite's endpoint. + * @param suiteName Suite's name. * @param suiteBeforeName SuiteBefore's name. * @param method Method to execute. * @param environment The current environment. * @return SuiteResults after the execution of the suite-before method. */ - private def runSuiteBefore(suiteEndpoint: String, suiteBeforeName: String, method: Method, environment: Environment, restClientCreator: RestClientCreator): SuiteResults = { - Logger.debug(s"Running Suite-Before: ${suiteBeforeName}") + private def runSuiteBefore(suiteName: String, suiteBeforeName: String, method: Method, environment: Environment, restClientCreator: RestClientCreator): SuiteResult = { + Logger.debug(s"Suite-Before: $suiteBeforeName - Started") val testStartTime: Long = System.currentTimeMillis() try { - val response: Response = sendRequest(method, environment, restClientCreator) - val isSuccess: Boolean = Response.perform( - response = response, - responseAction = method.responseActions - ) - + val result: Try[Unit] = processRequest(method, environment, restClientCreator) val testEndTime: Long = System.currentTimeMillis() - Logger.debug(s"Before method '${method.name}' finished. Response statusCode is '${response.statusCode}'") - SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_BEFORE_METHOD, - suiteName = suiteEndpoint, + Logger.debug(s"Suite-Before: method '${method.name}' - ${if (result.isSuccess) "completed successfully" else "failed"}.") + SuiteResult( + resultType = SuiteResultType.BeforeTestSet, + suiteName = suiteName, name = method.name, - status = isSuccess, + result = result, duration = Some(testEndTime - testStartTime) ) } catch { - case e: Exception => handleException(e, suiteEndpoint, suiteBeforeName, testStartTime, "Before") + case e: Exception => handleException(e, suiteName, suiteBeforeName, testStartTime, SuiteResultType.BeforeTestSet) } } /** * Runs all the suite-tests methods for a given test suite. * - * @param suiteEndpoint Suite's endpoint. + * @param suiteName Suite's name. * @param test The test to run. * @param environment The current environment. * @return SuiteResults after the execution of the suite-test. */ - private def runSuiteTest(suiteEndpoint: String, test: SuiteTestScenario, environment: Environment, restClientCreator: RestClientCreator): SuiteResults = { - Logger.debug(s"Running Suite-Test: ${test.name}") + private def runSuiteTest(suiteName: String, test: SuiteTestScenario, environment: Environment, restClientCreator: RestClientCreator): SuiteResult = { + Logger.debug(s"Suite-Test: ${test.name} - Started") val testStartTime: Long = System.currentTimeMillis() try { - val response: Response = sendRequest(test, environment, restClientCreator) - val isSuccess: Boolean = Response.perform( - response = response, - responseAction = test.responseActions - ) - + val result: Try[Unit] = processRequest(test, environment, restClientCreator) val testEndTime: Long = System.currentTimeMillis() - Logger.debug(s"Test '${test.name}' finished. Response statusCode is '${response.statusCode}'") - SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_TEST, - suiteName = suiteEndpoint, + Logger.debug(s"Suite-Test: '${test.name}' - ${if (result.isSuccess) "completed successfully" else "failed"}.") + SuiteResult( + resultType = SuiteResultType.TestSet, + suiteName = suiteName, name = test.name, - status = isSuccess, + result = result, duration = Some(testEndTime - testStartTime), categories = Some(test.categories.mkString(",")) ) } catch { - case e: Exception => handleException(e, suiteEndpoint, test.name, testStartTime, "Test", Some(test.categories.mkString(","))) + case e: Exception => handleException(e, suiteName, test.name, testStartTime, SuiteResultType.TestSet, Some(test.categories.mkString(","))) } finally { RuntimeCache.expire(TestLevel) } @@ -144,34 +142,29 @@ object SuiteRunner { /** * Runs all the suite-after methods for a given test suite. * - * @param suiteEndpoint Suite's endpoint. + * @param suiteName Suite's name. * @param suiteAfterName SuiteAfter's name. * @param method Method to execute. * @param environment The current environment. * @return SuiteResults after the execution of the suite-after method. */ - private def runSuiteAfter(suiteEndpoint: String, suiteAfterName: String, method: Method, environment: Environment, restClientCreator: RestClientCreator): SuiteResults = { - Logger.debug(s"Running Suite-After: ${suiteAfterName}") + private def runSuiteAfter(suiteName: String, suiteAfterName: String, method: Method, environment: Environment, restClientCreator: RestClientCreator): SuiteResult = { + Logger.debug(s"Suite-After: $suiteAfterName - Started") val testStartTime: Long = System.currentTimeMillis() try { - val response: Response = sendRequest(method, environment, restClientCreator) - val isSuccess: Boolean = Response.perform( - response = response, - responseAction = method.responseActions - ) - + val result: Try[Unit] = processRequest(method, environment, restClientCreator) val testEndTime: Long = System.currentTimeMillis() - Logger.debug(s"After method '${method.name}' finished. Response statusCode is '${response.statusCode}'") - SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_AFTER_METHOD, - suiteName = suiteEndpoint, + Logger.debug(s"After method '${method.name}' ${if (result.isSuccess) "completed successfully" else "failed"}.") + SuiteResult( + resultType = SuiteResultType.AfterTestSet, + suiteName = suiteName, name = method.name, - status = isSuccess, + result = result, duration = Some(testEndTime - testStartTime) ) } catch { - case e: Exception => handleException(e, suiteEndpoint, suiteAfterName, testStartTime, "After") + case e: Exception => handleException(e, suiteName, suiteAfterName, testStartTime, SuiteResultType.AfterTestSet) } } @@ -193,48 +186,42 @@ object SuiteRunner { ) } + /** + * Process the request and perform the associated response actions. + * + * @param requestable The request-able method containing the actions and response actions. + * @param environment The current environment. + * @param restClientCreator A creator function for the REST client. + * @return A Try containing the result of the response actions. + */ + 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 = response, + responseAction = requestable.responseActions + ) + result + } + /** * Handles exceptions occurring during suite running. * - * @param e The exception to handle. - * @param suiteEndpoint Suite's endpoint. - * @param name The name of the suite or test. - * @param testStartTime The starting time of the suite or test. - * @param resultType The type of the suite or test ("Before", "Test", or "After"). + * @param e The exception to handle. + * @param suiteName Suite's name. + * @param name The name of the suite or test. + * @param testStartTime The starting time of the suite or test. + * @param suiteResultType The type of the suite or test ("Before", "Test", or "After"). * @return SuiteResults after the exception handling. */ - private def handleException(e: Throwable, suiteEndpoint: String, name: String, testStartTime: Long, resultType: String, categories: Option[String] = None): SuiteResults = { + private def handleException(e: Throwable, suiteName: String, name: String, testStartTime: Long, suiteResultType: SuiteResultType, categories: Option[String] = None): SuiteResult = { val testEndTime = System.currentTimeMillis() - val message = e match { - case _ => s"Request exception occurred while running suite: ${suiteEndpoint}, ${resultType}: ${name}. Exception: ${e.getMessage}" - } - Logger.error(message) - resultType match { - case "Before" => SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_BEFORE_METHOD, - suiteName = suiteEndpoint, - name = name, - status = false, - errMessage = Some(e.getMessage), - duration = Some(testEndTime - testStartTime)) - - case "Test" => SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_TEST, - suiteName = suiteEndpoint, - name = name, - status = false, - errMessage = Some(e.getMessage), - duration = Some(testEndTime - testStartTime), - categories = categories - ) - - case "After" => SuiteResults.withBooleanStatus( - resultType = SuiteResults.RESULT_TYPE_AFTER_METHOD, - suiteName = suiteEndpoint, - name = name, - status = false, - errMessage = Some(e.getMessage), - duration = Some(testEndTime - testStartTime)) - } + Logger.error(s"Request exception occurred while running suite: $suiteName, $suiteResultType: $name. Exception: ${e.getMessage}") + + SuiteResult( + resultType = suiteResultType, + suiteName = suiteName, + name = name, + result = Failure(e), + duration = Some(testEndTime - testStartTime)) } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/utils/cache/RuntimeCache.scala b/testApi/src/main/scala/africa/absa/testing/scapi/utils/cache/RuntimeCache.scala index 99c6287..aaec997 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/utils/cache/RuntimeCache.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/utils/cache/RuntimeCache.scala @@ -130,10 +130,9 @@ object RuntimeCache { case "global" => GlobalLevel case "suite" => SuiteLevel case "test" => TestLevel - case _ => { + case _ => Logger.warn(s"Not known expiration cache level: '$level'. Used default TEST level.") TestLevel - } } } diff --git a/testApi/src/main/scala/africa/absa/testing/scapi/utils/validation/ContentValidator.scala b/testApi/src/main/scala/africa/absa/testing/scapi/utils/validation/ContentValidator.scala index 4193b67..94e5e81 100644 --- a/testApi/src/main/scala/africa/absa/testing/scapi/utils/validation/ContentValidator.scala +++ b/testApi/src/main/scala/africa/absa/testing/scapi/utils/validation/ContentValidator.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.utils.validation -import africa.absa.testing.scapi.ContentValidationFailed +import africa.absa.testing.scapi.ContentValidationFailedException import scala.util.{Failure, Success, Try} /** @@ -28,12 +28,25 @@ object ContentValidator { * Validates that a string can be parsed to an integer. Throws an exception if the string cannot be parsed. * * @param input The string to be validated. - * @throws ContentValidationFailed if the input string cannot be parsed to an integer. + * @throws ContentValidationFailedException if the input string cannot be parsed to an integer. */ def validateIntegerString(input: String, param: String): Unit = { Try(input.toInt) match { case Success(_) => // Do nothing - case Failure(e) => throw ContentValidationFailed(input, s"Received value of '$param' cannot be parsed to an integer: ${e.getMessage}") + case Failure(e) => throw ContentValidationFailedException(input, s"Received value of '$param' cannot be parsed to an integer: ${e.getMessage}") + } + } + + /** + * Validates that a string can be parsed to a long. Throws an exception if the string cannot be parsed. + * + * @param input The string to be validated. + * @throws ContentValidationFailedException if the input string cannot be parsed to a long. + */ + def validateLongString(input: String, param: String): Unit = { + Try(input.toLong) match { + case Success(_) => // Do nothing + case Failure(e) => throw ContentValidationFailedException(input, s"Received value of '$param' cannot be parsed to a long: ${e.getMessage}") } } @@ -41,11 +54,11 @@ object ContentValidator { * Validates that a string is not empty. Throws an exception if the string is empty. * * @param input The string to be validated. - * @throws ContentValidationFailed if the input string is empty. + * @throws ContentValidationFailedException if the input string is empty. */ def validateNonEmptyString(input: String, param: String): Unit = { if (input.isEmpty) { - throw ContentValidationFailed(input, s"Received string value of '$param' is empty.") + throw ContentValidationFailedException(input, s"Received string value of '$param' is empty.") } } @@ -54,12 +67,12 @@ object ContentValidator { * * @param input The Option[String] to be validated. * @param paramName The name of the parameter, used in error messaging. - * @throws ContentValidationFailed if the input Option[String] is None. + * @throws ContentValidationFailedException if the input Option[String] is None. */ def validateNotNone(input: Option[String], paramName: String): Unit = { input match { case Some(_) => // do nothing, input is valid - case None => throw new ContentValidationFailed(paramName, "Input cannot be None") + case None => throw ContentValidationFailedException(paramName, "Input cannot be None") } } } diff --git a/testApi/src/test/resources/project_with_issues/suites/aulgui-controller/undefinedConstantIssue.suite.json b/testApi/src/test/resources/project_with_issues/suites/aulgui-controller/undefinedConstantIssue.suite.json index 59b4c26..0e2fc01 100644 --- a/testApi/src/test/resources/project_with_issues/suites/aulgui-controller/undefinedConstantIssue.suite.json +++ b/testApi/src/test/resources/project_with_issues/suites/aulgui-controller/undefinedConstantIssue.suite.json @@ -1,5 +1,5 @@ { - "endpoint" : "getUserCurrent", + "name" : "getUserCurrent", "tests": [ { "name" : "test-name-1", @@ -18,7 +18,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/deleteQuestion.suite.json b/testApi/src/test/resources/test_project/suites/gui-controller/deleteQuestion.suite.json index 9f5839e..adc4c21 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/deleteQuestion.suite.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/deleteQuestion.suite.json @@ -1,5 +1,5 @@ { - "endpoint" : "deleteQuestion", + "name" : "deleteQuestion", "tests": [ { "name" : "test-name-1", @@ -22,7 +22,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.after.json b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.after.json index ac88f32..6861887 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.after.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.after.json @@ -21,7 +21,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.before.json b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.before.json index e6faad2..3bc7069 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.before.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.before.json @@ -21,7 +21,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.suite.json b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.suite.json index 0ec190d..15039e7 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.suite.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/getUserCurrent.suite.json @@ -1,5 +1,5 @@ { - "endpoint" : "getUserCurrent", + "name" : "getUserCurrent", "tests": [ { "name" : "get current user", @@ -22,8 +22,35 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" + }, + { + "method": "assert.status-code-is-success" + }, + { + "method": "log.error", + "message": "Dummy error log message." + }, + { + "method": "log.warn", + "message": "Dummy warn log message." + }, + { + "method": "log.info", + "message": "Dummy info log message." + }, + { + "method": "log.debug", + "message": "Dummy debug log message." + }, + { + "method": "assert.response-time-is-below", + "limit": "600000" + }, + { + "method": "assert.response-time-is-above", + "limit": "1" } ] } diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/postQuestion.suite.json b/testApi/src/test/resources/test_project/suites/gui-controller/postQuestion.suite.json index cc7039c..fe6cfe7 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/postQuestion.suite.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/postQuestion.suite.json @@ -1,5 +1,5 @@ { - "endpoint" : "postQuestion", + "name" : "postQuestion", "tests": [ { "name" : "test-name-1", @@ -29,7 +29,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/resources/test_project/suites/gui-controller/putQuestion.suite.json b/testApi/src/test/resources/test_project/suites/gui-controller/putQuestion.suite.json index 9a36a7e..c6ca547 100644 --- a/testApi/src/test/resources/test_project/suites/gui-controller/putQuestion.suite.json +++ b/testApi/src/test/resources/test_project/suites/gui-controller/putQuestion.suite.json @@ -1,5 +1,5 @@ { - "endpoint" : "putQuestion", + "name" : "putQuestion", "tests": [ { "name" : "test-name-1", @@ -22,7 +22,7 @@ ], "responseActions": [ { - "method": "assert.status-code", + "method": "assert.status-code-equals", "code": "200" } ] diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/ScAPIRunnerTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/ScAPIRunnerTest.scala index fc3f432..07f02d7 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/ScAPIRunnerTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/ScAPIRunnerTest.scala @@ -64,7 +64,7 @@ class ScAPIRunnerTest extends FunSuite { "--env", "localhost.env.json", "--test-root-path", "/random/path/without/suite") - interceptMessage[SuiteLoadFailed]("Problems during project loading. Details: 'suites' directory have to exist in project root.") { + interceptMessage[SuiteLoadFailedException]("Problems during project loading. Details: 'suites' directory have to exist in project root.") { ScAPIRunner.main(args) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentFactoryTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentFactoryTest.scala index dc65c6a..f143072 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentFactoryTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentFactoryTest.scala @@ -16,8 +16,9 @@ package africa.absa.testing.scapi.json +import africa.absa.testing.scapi.json.factory.EnvironmentFactory import africa.absa.testing.scapi.json.schema.{JsonSchemaValidator, ScAPIJsonSchema} -import africa.absa.testing.scapi.{JsonInvalidSchema, UndefinedConstantsInProperties} +import africa.absa.testing.scapi.{JsonInvalidSchemaException, UndefinedConstantsInPropertiesException} import munit.FunSuite import java.net.URL @@ -61,7 +62,7 @@ class EnvironmentFactoryTest extends FunSuite { } test("fromFile - missing referenced constant") { - intercept[UndefinedConstantsInProperties] { + interceptMessage[UndefinedConstantsInPropertiesException]("Undefined constant(s): 'errPort' in ''Environment' action.'.") { val envPath: String = getClass.getResource("/missing_constant_env.json").getPath EnvironmentFactory.fromFile(envPath) } @@ -79,7 +80,7 @@ class EnvironmentFactoryTest extends FunSuite { def validateEnvJson(name: String, resourcePath: String): Unit = { test(name) { - intercept[JsonInvalidSchema] { + intercept[JsonInvalidSchemaException] { val envSchemaPath: URL = ScAPIJsonSchema.ENVIRONMENT val envPath: String = getClass.getResource(resourcePath).getPath diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentTest.scala index 4cf3b3c..d708fc1 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/EnvironmentTest.scala @@ -16,7 +16,8 @@ package africa.absa.testing.scapi.json -import africa.absa.testing.scapi.PropertyNotFound +import africa.absa.testing.scapi.PropertyNotFoundException +import africa.absa.testing.scapi.json.factory.EnvironmentFactory import munit.FunSuite class EnvironmentTest extends FunSuite { @@ -52,13 +53,13 @@ class EnvironmentTest extends FunSuite { val actual_value_from_properties: String = env("url") val actual_value_from_constants: String = env("server") - assertEquals(clue(expected_value_from_properties), clue(actual_value_from_properties)) - assertEquals(clue(expected_value_from_constants), clue(actual_value_from_constants)) + assert(clue(expected_value_from_properties) == clue(actual_value_from_properties)) + assert(clue(expected_value_from_constants) == clue(actual_value_from_constants)) } test("apply - properties does not exist") { val env: Environment = Environment(constants, propertiesResolved) - interceptMessage[PropertyNotFound]("Property not found: 'no_exist'.") { + interceptMessage[PropertyNotFoundException]("Property not found: 'no_exist'.") { env("no_exist") } } @@ -70,7 +71,7 @@ class EnvironmentTest extends FunSuite { val expected: String = propertyPort val actual: String = env("port") - assertEquals(clue(expected), clue(actual)) + assert(clue(expected) == clue(actual)) } /* @@ -94,6 +95,6 @@ class EnvironmentTest extends FunSuite { val actual: Environment = notResolverEnv.resolveReferences - assertEquals(clue(expected), clue(actual)) + assert(clue(expected) == clue(actual)) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestBodyTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestBodyTest.scala index 1cfc274..2bb8e66 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestBodyTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestBodyTest.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.json -import africa.absa.testing.scapi.ContentValidationFailed +import africa.absa.testing.scapi.ContentValidationFailedException import africa.absa.testing.scapi.rest.request.RequestBody import munit.FunSuite import spray.json.JsonParser.ParsingException @@ -30,32 +30,32 @@ class RequestBodyTest extends FunSuite { test("buildBody - return string representation of JSON when jsonBody is not empty") { val jsonBody = Some("""{"key":"value"}""") val result = RequestBody.buildBody(jsonBody) - assertEquals(result, """{"key":"value"}""") + assertEquals("""{"key":"value"}""", result) } test("buildBody - throw exception when non json string received") { val jsonBody = Some("""not json string""") - intercept[ParsingException] { + interceptMessage[ParsingException]("Unexpected character 'o' at input index 0 (line 1, position 1), expected JSON Value:\nnot json string\n^\n") { RequestBody.buildBody(jsonBody) } } test("buildBody - return empty string representation of JSON when no body received") { val result = RequestBody.buildBody() - assertEquals(result, """{}""") + assertEquals("""{}""", result) } test("buildBody - return empty JSON object string when jsonBody is empty") { val jsonBody = Some("") val result = RequestBody.buildBody(jsonBody) - assertEquals(result, "{}") + assertEquals("{}", result) } test("buildBody - return empty JSON object string when jsonBody is None") { val jsonBody: Option[String] = None val result = RequestBody.buildBody(jsonBody) - assertEquals(result, "{}") + assertEquals("{}", result) } /* @@ -79,7 +79,7 @@ class RequestBodyTest extends FunSuite { test("validateContent - fail when body is a non json string") { val jsonBody = Some("""not json string""") - intercept[ContentValidationFailed] { + interceptMessage[ContentValidationFailedException]("Content validation failed for value: 'not json string': Received value cannot be parsed to json: Unexpected character 'o' at input index 0 (line 1, position 1), expected JSON Value:\nnot json string\n^\n") { RequestBody.validateContent(jsonBody) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestHeadersTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestHeadersTest.scala index b432db3..43eb358 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestHeadersTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestHeadersTest.scala @@ -17,7 +17,7 @@ package africa.absa.testing.scapi.json import africa.absa.testing.scapi.rest.request.RequestHeaders -import africa.absa.testing.scapi.{ContentValidationFailed, UndefinedHeaderType} +import africa.absa.testing.scapi.{ContentValidationFailedException, UndefinedHeaderTypeException} import munit.FunSuite class RequestHeadersTest extends FunSuite { @@ -27,7 +27,7 @@ class RequestHeadersTest extends FunSuite { */ test("buildHeaders - should correctly build headers map") { - val headersSet = Set( + val headersSeq = Seq( Header("Content-Type", "application/json"), Header("Authorization", "Bearer abcdefg12345"), Header("Custom-Header", "customValue") @@ -39,16 +39,16 @@ class RequestHeadersTest extends FunSuite { "Custom-Header" -> "customValue" ) - val actualMap = RequestHeaders.buildHeaders(headersSet) - assertEquals(actualMap, expectedMap) + val actualMap = RequestHeaders.buildHeaders(headersSeq) + assert(clue(expectedMap) == clue(actualMap)) } test("buildHeaders - should return an empty map if no headers are provided") { - val headersSet = Set.empty[Header] + val headersSeq = Seq.empty[Header] val expectedMap = Map.empty[String, String] - val actualMap = RequestHeaders.buildHeaders(headersSet) - assertEquals(actualMap, expectedMap) + val actualMap = RequestHeaders.buildHeaders(headersSeq) + assert(clue(expectedMap) == clue(actualMap)) } /* @@ -62,7 +62,7 @@ class RequestHeadersTest extends FunSuite { test("validateContent - CONTENT_TYPE header - empty") { val header = Header(RequestHeaders.CONTENT_TYPE, "") - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'Header.content-type' is empty.") { + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'Header.content-type' is empty.") { RequestHeaders.validateContent(header) } } @@ -74,14 +74,14 @@ class RequestHeadersTest extends FunSuite { test("validateContent - AUTHORIZATION header - empty") { val header = Header(RequestHeaders.AUTHORIZATION, "") - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'Header.authorization' is empty.") { + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'Header.authorization' is empty.") { RequestHeaders.validateContent(header) } } test("validateContent - Unsupported header type") { val header = Header("unsupported-header", "value") - intercept[UndefinedHeaderType] { + interceptMessage[UndefinedHeaderTypeException]("Undefined Header content type: 'unsupported-header'") { RequestHeaders.validateContent(header) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestParamsTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestParamsTest.scala index e4f62df..39f74d8 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestParamsTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/RequestParamsTest.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.json -import africa.absa.testing.scapi.ContentValidationFailed +import africa.absa.testing.scapi.ContentValidationFailedException import africa.absa.testing.scapi.rest.request.RequestParams import munit.FunSuite @@ -29,25 +29,25 @@ class RequestParamsTest extends FunSuite { test("buildParams - no params") { val paramsSet: Option[Set[Param]] = None val result: Map[String, String] = RequestParams.buildParams(paramsSet) - assertEquals(result, Map.empty[String, String]) + assert(clue(Map.empty[String, String]) == clue(result)) } test("buildParams - single valid param") { val paramsSet: Option[Set[Param]] = Some(Set(Param("name", "value"))) val result: Map[String, String] = RequestParams.buildParams(paramsSet) - assertEquals(result, Map("name" -> "value")) + assert(clue(Map("name" -> "value")) == clue(result)) } test("buildParams - multiple valid params") { val paramsSet: Option[Set[Param]] = Some(Set(Param("name1", "value1"), Param("name2", "value2"))) val result: Map[String, String] = RequestParams.buildParams(paramsSet) - assertEquals(result, Map("name1" -> "value1", "name2" -> "value2")) + assert(clue(Map("name1" -> "value1", "name2" -> "value2")) == clue(result)) } test("buildParams - params with empty name or value should be ignored") { val paramsSet: Option[Set[Param]] = Some(Set(Param("name", ""), Param("", "value"), Param("name2", "value2"))) val result: Map[String, String] = RequestParams.buildParams(paramsSet) - assertEquals(result, Map("name2" -> "value2")) + assert(clue(Map("name2" -> "value2")) == clue(result)) } /* @@ -61,7 +61,7 @@ class RequestParamsTest extends FunSuite { test("validateContent - should throw exception for empty name") { val params = Set(Param("", "value")) - intercept[ContentValidationFailed] { + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'Param.' is empty.") { RequestParams.validateContent(Some(params)) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/json/SuiteFactoryTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/json/SuiteFactoryTest.scala index 2814931..579ef7a 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/json/SuiteFactoryTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/json/SuiteFactoryTest.scala @@ -16,8 +16,9 @@ package africa.absa.testing.scapi.json -import africa.absa.testing.scapi.{ProjectLoadFailed, UndefinedConstantsInProperties} -import africa.absa.testing.scapi.model.{Suite, SuiteBundle, SuiteTestScenario} +import africa.absa.testing.scapi.json.factory.SuiteFactory +import africa.absa.testing.scapi.model.suite.{TestSet, Suite, SuiteTestScenario} +import africa.absa.testing.scapi.{ProjectLoadFailedException, UndefinedConstantsInPropertiesException} import munit.FunSuite import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext @@ -66,11 +67,11 @@ class SuiteFactoryTest extends FunSuite { initTestLogger() try { - val caught = intercept[ProjectLoadFailed] { + val caught = intercept[ProjectLoadFailedException] { SuiteFactory.fromFiles(environment, testRootPath, "(.*)", "json") } - assert(caught.isInstanceOf[ProjectLoadFailed]) + assert(caught.isInstanceOf[ProjectLoadFailedException]) assert(out.toString.contains("Undefined constant(s): 'constants.no_provided' in ''Header' action.")) assert(out.toString.contains("Not all suites loaded. Failed suites:")) } finally { @@ -100,7 +101,7 @@ class SuiteFactoryTest extends FunSuite { val actual: Map[String, String] = SuiteFactory.loadJsonSuiteConstants(suiteFilePath, suiteName, properties).constants - assertEquals(clue(expected), clue(actual)) + assert(clue(expected) == clue(actual)) } test("loadJsonSuite - no constants file exist") { @@ -111,7 +112,7 @@ class SuiteFactoryTest extends FunSuite { val actual: Map[String, String] = SuiteFactory.loadJsonSuiteConstants(suiteFilePath, suiteName, properties).constants - assertEquals(clue(expected), clue(actual)) + assert(clue(expected) == clue(actual)) } test("loadJsonSuite - not all references resolved") { @@ -119,7 +120,7 @@ class SuiteFactoryTest extends FunSuite { val suiteName = "getUserCurrent" val properties: Map[String, String] = Map.empty - intercept[UndefinedConstantsInProperties] { + interceptMessage[UndefinedConstantsInPropertiesException]("Undefined constant(s): 'env.basic_token' in ''SuiteConstants' action.'.") { SuiteFactory.loadJsonSuiteConstants(suiteFilePath, suiteName, properties).constants } } @@ -129,43 +130,43 @@ class SuiteFactoryTest extends FunSuite { */ test("filterOnlyOrAll - only used - once") { val suitesBundles = Set( - SuiteBundle(suite = Suite(endpoint = "endpoint1", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(false)), - SuiteTestScenario(name = "test2", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(true)) + Suite(suite = TestSet(name = "name1", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(false)), + SuiteTestScenario(name = "test2", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(true)) ))), - SuiteBundle(suite = Suite(endpoint = "endpoint1", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(false)), - SuiteTestScenario(name = "test2", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(true)) + Suite(suite = TestSet(name = "name1", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(false)), + SuiteTestScenario(name = "test2", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(true)) ))), - SuiteBundle(suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(false)), + Suite(suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(false)), )))) - val filteredSuiteBundles: Set[SuiteBundle] = SuiteFactory.filterOnlyOrAll(suitesBundles) + val filteredSuiteBundles: Set[Suite] = SuiteFactory.filterOnlyOrAll(suitesBundles) assertEquals(filteredSuiteBundles.size, 1) val filteredSuite = filteredSuiteBundles.head.suite - assertEquals(filteredSuite.endpoint, "endpoint1") - assertEquals(filteredSuite.tests.size, 1) + assert("name1" == clue(filteredSuite.name)) + assert(1 == clue(filteredSuite.tests.size)) val filteredTest = filteredSuite.tests.head - assertEquals(filteredTest.name, "test2") - assertEquals(filteredTest.only, Some(true)) + assert("test2" == clue(filteredTest.name)) + assert(clue(Some(true)) == clue(filteredTest.only)) } test("fromFile - only used - twice") { val suitesBundles = Set( - SuiteBundle(suite = Suite(endpoint = "endpoint1", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(false)), - SuiteTestScenario(name = "test2", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(true)) + Suite(suite = TestSet(name = "name1", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(false)), + SuiteTestScenario(name = "test2", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(true)) ))), - SuiteBundle(suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), headers = Set.empty, actions = Set.empty, responseActions = Set.empty, only = Some(true)), + Suite(suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), headers = Seq.empty, actions = Seq.empty, responseActions = Seq.empty, only = Some(true)), )))) - val filteredSuiteBundles: Set[SuiteBundle] = SuiteFactory.filterOnlyOrAll(suitesBundles) + val filteredSuiteBundles: Set[Suite] = SuiteFactory.filterOnlyOrAll(suitesBundles) - assertEquals(filteredSuiteBundles.size, 0) + assert(0 == clue(filteredSuiteBundles.size)) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/reporter/StdOutReporterTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/reporter/StdOutReporterTest.scala index e1b4018..606a15c 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/reporter/StdOutReporterTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/reporter/StdOutReporterTest.scala @@ -16,53 +16,55 @@ package africa.absa.testing.scapi.reporter -import africa.absa.testing.scapi.model.SuiteResults +import africa.absa.testing.scapi.AssertionException +import africa.absa.testing.scapi.model.suite.SuiteResult +import africa.absa.testing.scapi.model.suite.types.SuiteResultType import munit.FunSuite import java.io.ByteArrayOutputStream +import scala.util.{Failure, Success} class StdOutReporterTest extends FunSuite { - val successTestResults: List[SuiteResults] = List( - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + val successTestResults: List[SuiteResult] = List( + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 1", name = "Test 1", - status = true, + result = Success(()), duration = Some(100L), categories = Some("Category 1")), - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 1", name = "Test 2", - status = true, + result = Success(()), duration = Some(200L), categories = Some("Category 2") ), - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 2", name = "Test 1", - status = true, + result = Success(()), duration = Some(50L), categories = Some("Category 3")) ) - val mixedSuccessTestResults: List[SuiteResults] = List( - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + val mixedSuccessTestResults: List[SuiteResult] = List( + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 1", name = "Test 1", - status = true, + result = Success(()), duration = Some(100L), categories = Some("Category 1")), - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 1", name = "Test 2", - status = false, + result = Failure(AssertionException("Error message")), duration = Some(200L), - categories = Some("Category 2"), - errMessage = Some("Error message")), - SuiteResults.withBooleanStatus(SuiteResults.RESULT_TYPE_TEST, + categories = Some("Category 2")), + SuiteResult(SuiteResultType.TestSet, suiteName = "Suite 2", name = "Test 1", - status = true, + result = Success(()), duration = Some(50L), categories = Some("Category 3")) ) @@ -81,11 +83,11 @@ class StdOutReporterTest extends FunSuite { val output = baos.toString // Assertions - assertEquals(clue(output.contains("Simple Text Report")), true) - assertEquals(clue(output.contains("Number of tests run: 0")), true) - assertEquals(clue(output.contains("Number of successful tests: 0")), true) - assertEquals(clue(output.contains("Number of failed tests: 0")), true) - assertEquals(clue(output.contains("End Report")), true) + assert(clue(output.contains("Simple Text Report"))) + assert(clue(output.contains("Number of tests run: 0"))) + assert(clue(output.contains("Number of successful tests: 0"))) + assert(clue(output.contains("Number of failed tests: 0"))) + assert(clue(output.contains("End Report"))) } test("full results with failed") { @@ -96,7 +98,6 @@ class StdOutReporterTest extends FunSuite { min 2 Suites min 1 suites with min 2 tests */ - var failedTestResults = successTestResults val baos = new ByteArrayOutputStream() @@ -109,24 +110,24 @@ class StdOutReporterTest extends FunSuite { // Assertions // report header & tail - assertEquals(clue(output.contains("Simple Text Report")), true) - assertEquals(clue(output.contains("Number of tests run: 3")), true) - assertEquals(clue(output.contains("Number of successful tests: 2")), true) - assertEquals(clue(output.contains("Number of failed tests: 1")), true) - assertEquals(clue(output.contains("End Report")), true) + assert(clue(output.contains("Simple Text Report"))) + assert(clue(output.contains("Number of tests run: 3"))) + assert(clue(output.contains("Number of successful tests: 2"))) + assert(clue(output.contains("Number of failed tests: 1"))) + assert(clue(output.contains("End Report"))) // suite summary - assertEquals(clue(output.contains("Suite: Suite 1, Total tests: 2, Successful: 1, Failed: 1")), true) - assertEquals(clue(output.contains("Suite: Suite 2, Total tests: 1, Successful: 1, Failed: 0")), true) + assert(clue(output.contains("Suite: Suite 1, Total tests: 2, Successful: 1, Failed: 1"))) + assert(clue(output.contains("Suite: Suite 2, Total tests: 1, Successful: 1, Failed: 0"))) // summary of all tests val updatedOutput = output.replace(" ", "") - assertEquals(clue(updatedOutput.contains("|Suite1|Test1|100|Success|Category1|")), true) - assertEquals(clue(updatedOutput.contains("|Suite1|Test2|200|Failure|Category2|")), true) - assertEquals(clue(updatedOutput.contains("|Suite2|Test1|50|Success|Category3|")), true) + assert(clue(updatedOutput.contains("|Suite1|Test1|100|Success|Category1|"))) + assert(clue(updatedOutput.contains("|Suite1|Test2|200|Failure|Category2|"))) + assert(clue(updatedOutput.contains("|Suite2|Test1|50|Success|Category3|"))) // error from detail - assertEquals(clue(output.contains("Error: Error message")), true) + assert(clue(output.contains("Assertion failed: Error message"))) } test("results all success") { @@ -141,20 +142,20 @@ class StdOutReporterTest extends FunSuite { // Assertions // report header & tail - assertEquals(clue(output.contains("Simple Text Report")), true) - assertEquals(clue(output.contains("Number of tests run: 3")), true) - assertEquals(clue(output.contains("Number of successful tests: 3")), true) - assertEquals(clue(output.contains("Number of failed tests: 0")), true) - assertEquals(clue(output.contains("End Report")), true) + assert(clue(output.contains("Simple Text Report"))) + assert(clue(output.contains("Number of tests run: 3"))) + assert(clue(output.contains("Number of successful tests: 3"))) + assert(clue(output.contains("Number of failed tests: 0"))) + assert(clue(output.contains("End Report"))) // suite summary - assertEquals(clue(output.contains("Suite: Suite 1, Total tests: 2, Successful: 2, Failed: 0")), true) - assertEquals(clue(output.contains("Suite: Suite 2, Total tests: 1, Successful: 1, Failed: 0")), true) + assert(clue(output.contains("Suite: Suite 1, Total tests: 2, Successful: 2, Failed: 0"))) + assert(clue(output.contains("Suite: Suite 2, Total tests: 1, Successful: 1, Failed: 0"))) // summary of all tests val updatedOutput = output.replace(" ", "") - assertEquals(clue(updatedOutput.contains("|Suite1|Test1|100|Success|Category1|")), true) - assertEquals(clue(updatedOutput.contains("|Suite1|Test2|200|Success|Category2|")), true) - assertEquals(clue(updatedOutput.contains("|Suite2|Test1|50|Success|Category3|")), true) + assert(clue(updatedOutput.contains("|Suite1|Test1|100|Success|Category1|"))) + assert(clue(updatedOutput.contains("|Suite1|Test2|200|Success|Category2|"))) + assert(clue(updatedOutput.contains("|Suite2|Test1|50|Success|Category3|"))) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/rest/RestClientTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/rest/RestClientTest.scala index 97f62ad..eefb604 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/rest/RestClientTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/rest/RestClientTest.scala @@ -30,7 +30,7 @@ class RestClientTest extends FunSuite { assert(data == "testData") assert(params == Map("param1" -> "value1")) - Response(200, "test response", Map.empty[String, Seq[String]]) + Response(200, "test response", "", "", Map.empty[String, Seq[String]], Map.empty, 100) } override def get(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { @@ -58,32 +58,32 @@ class RestClientTest extends FunSuite { val restClient: RestClient = new RestClient(mockRequestSender) val response: Response = restClient.sendRequest("get", "testUrl", "testData", Map("Authorization" -> "Bearer testToken"), Map("param1" -> "value1"), false) - assert(response.statusCode == 200) - assert(response.body == "test response") + assertEquals(200, response.statusCode) + assertEquals("test response", response.body) } test("sendRequest - call post with correct parameters") { val restClient: RestClient = new RestClient(mockRequestSender) val response: Response = restClient.sendRequest("post", "testUrl", "testData", Map("Authorization" -> "Bearer testToken"), Map("param1" -> "value1"), false) - assert(response.statusCode == 200) - assert(response.body == "test response") + assertEquals(200, response.statusCode) + assertEquals("test response", response.body) } test("sendRequest - call put with correct parameters") { val restClient: RestClient = new RestClient(mockRequestSender) val response: Response = restClient.sendRequest("put", "testUrl", "testData", Map("Authorization" -> "Bearer testToken"), Map("param1" -> "value1"), false) - assert(response.statusCode == 200) - assert(response.body == "test response") + assertEquals(200, response.statusCode) + assertEquals("test response", response.body) } test("sendRequest - call delete with correct parameters") { val restClient: RestClient = new RestClient(mockRequestSender) val response: Response = restClient.sendRequest("delete", "testUrl", "testData", Map("Authorization" -> "Bearer testToken"), Map("param1" -> "value1"), false) - assert(response.statusCode == 200) - assert(response.body == "test response") + assertEquals(200, response.statusCode) + assertEquals("test response", response.body) } test("sendRequest - call not supported action") { diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseAssertionsTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseAssertionsTest.scala index eedd11e..cf9217e 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseAssertionsTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseAssertionsTest.scala @@ -17,78 +17,479 @@ package africa.absa.testing.scapi.rest.response import africa.absa.testing.scapi.json.ResponseAction -import africa.absa.testing.scapi.{ContentValidationFailed, UndefinedResponseActionType} +import africa.absa.testing.scapi.rest.model.CookieValue +import africa.absa.testing.scapi.rest.response.action.AssertionResponseAction +import africa.absa.testing.scapi.rest.response.action.types.AssertResponseActionType.AssertResponseActionType +import africa.absa.testing.scapi.rest.response.action.types.{AssertResponseActionType, ResponseActionGroupType} +import africa.absa.testing.scapi.{ContentValidationFailedException, UndefinedResponseActionTypeException} import munit.FunSuite +import scala.language.implicitConversions + class ResponseAssertionsTest extends FunSuite { + implicit def assertResponseActionType2String(value: AssertResponseActionType): String = value.toString + /* validateContent */ + + // response-time-... + + test("validateContent - response time is below - limit is integer string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsBelow, Map("limit" -> "200")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - response time is below - limit is not integer string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsBelow, Map("limit" -> "not_integer")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: 'not_integer': Received value of 'ResponseAssertion.response-time-is-below.limit' cannot be parsed to a long: For input string: \"not_integer\"") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - response time is above - limit is integer string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsAbove, Map("limit" -> "200")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - response time is above - limit is not integer string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsAbove, Map("limit" -> "not_integer")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: 'not_integer': Received value of 'ResponseAssertion.response-time-is-above.limit' cannot be parsed to a long: For input string: \"not_integer\"") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - response time is above - missing limit parameter") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsAbove, Map.empty) + interceptMessage[IllegalArgumentException]("Missing required 'limit' parameter for assertion response-time-is-above logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + // status-code-... + test("validateContent - valid status code string") { - val assertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.${AssertionResponseAction.STATUS_CODE}", Map("code" -> "200")) - AssertionResponseAction.validateContent(assertion) + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals, Map("code" -> "200")) + AssertionResponseAction.validateContent(responseAction) } test("validateContent - invalid status code string") { - intercept[ContentValidationFailed] { - AssertionResponseAction.validateContent(ResponseAction(method = s"${Response.GROUP_ASSERT}.${AssertionResponseAction.STATUS_CODE}", Map("code" -> "not an integer"))) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: 'not an integer': Received value of 'ResponseAssertion.status-code-equals.code' cannot be parsed to an integer: For input string: \"not an integer\"") { + AssertionResponseAction.validateContent(ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals, Map("code" -> "not an integer"))) + } + } + + test("validateContent - status code equals - missing code parameter") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals, Map.empty) + interceptMessage[IllegalArgumentException]("Missing required 'code' parameter for assertion status-code-equals logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + // header-... + + test("validateContent - header exists - valid header name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderExists, Map("headerName" -> "content-type")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - header exists - invalid header name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderExists, Map("headerName" -> "")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.header-exists.headerName' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - header exists - missing header name parameter") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderExists, Map.empty) + interceptMessage[IllegalArgumentException]("Missing required 'headerName' parameter for assertion header-exists logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - header value equals - valid header name and value strings") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "Content-Type", "expectedValue" -> "application/json")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - header value equals - invalid header name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "", "expectedValue" -> "application/json")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.header-value-equals.headerName' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - header value equals - invalid expected value string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "Content-Type", "expectedValue" -> "")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.header-value-equals.expectedValue' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - header value equals - missing header name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("expectedValue" -> "application/json")) + interceptMessage[IllegalArgumentException]("Missing required 'headerName' parameter for assertion header-value-equals logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - header value equals - missing header value string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "Content-Type")) + interceptMessage[IllegalArgumentException]("Missing required 'expectedValue' parameter for assertion header-value-equals logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + // cookies-... + + test("validateContent - cookie exists - valid cookie name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieExists, Map("cookieName" -> "testCookie")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - cookie exists - invalid cookie name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieExists, Map("cookieName" -> "")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.cookie-exists.cookieName' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - cookie exists - missing cookie name parameter") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieExists, Map()) + interceptMessage[IllegalArgumentException]("Missing required 'cookieName' parameter for assertion cookie-exists logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - cookie value equals - valid cookie name and value strings") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + AssertionResponseAction.validateContent(responseAction) + } + + test("validateContent - cookie value equals - invalid cookie name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "", "expectedValue" -> "cookieValue")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.cookie-value-equals.cookieName' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - cookie value equals - invalid cookie value string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie", "expectedValue" -> "")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.cookie-value-equals.expectedValue' is empty.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - cookie value equals - missing cookie name string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("expectedValue" -> "cookieValue")) + interceptMessage[IllegalArgumentException]("Missing required 'cookieName' parameter for assertion cookie-value-equals logic.") { + AssertionResponseAction.validateContent(responseAction) + } + } + + test("validateContent - cookie value equals - missing cookie value string") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie")) + interceptMessage[IllegalArgumentException]("Missing required 'expectedValue' parameter for assertion cookie-value-equals logic.") { + AssertionResponseAction.validateContent(responseAction) } } - test("validateContent - body is not empty") { - val assertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.${AssertionResponseAction.BODY_CONTAINS}", Map("body" -> "test content")) - AssertionResponseAction.validateContent(assertion) + // body-... + + test("validateContent - body contains text - body is not empty") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.BodyContainsText, Map("text" -> "test content")) + AssertionResponseAction.validateContent(responseAction) } - test("validateContent - body is empty") { - intercept[ContentValidationFailed] { - AssertionResponseAction.validateContent(ResponseAction(method = s"${Response.GROUP_ASSERT}.${AssertionResponseAction.BODY_CONTAINS}", Map("body" -> ""))) + test("validateContent - body contains text - body is empty") { + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ResponseAssertion.body-contains-text.text' is empty.") { + AssertionResponseAction.validateContent(ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.BodyContainsText, Map("text" -> ""))) + } + } + + test("validateContent - body contains text - body parameter is missing") { + interceptMessage[IllegalArgumentException]("Missing required 'text' parameter for assertion body-contains-text logic.") { + AssertionResponseAction.validateContent(ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.BodyContainsText, Map.empty)) } } test("validateContent - unsupported response action") { - intercept[UndefinedResponseActionType] { - AssertionResponseAction.validateContent(ResponseAction(method = s"${Response.GROUP_ASSERT}.unsupported", Map("body" -> "value"))) + interceptMessage[UndefinedResponseActionTypeException]("Undefined response action content type: 'unsupported'") { + AssertionResponseAction.validateContent(ResponseAction(group = ResponseActionGroupType.Assert, name = "unsupported", Map("body" -> "value"))) } } /* performResponseAction */ + + // response-time-... + + test("performAssertions - response time is below limit - success") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsBelow, Map("limit" -> "100")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 99) + + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isSuccess) + } + + test("performAssertions - response time is below limit - failed") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsBelow, Map("limit" -> "100")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 101) + + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isFailure) + } + + test("performAssertions - response time is above limit - success") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsAbove, Map("limit" -> "100")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 101) + + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isSuccess) + } + + test("performAssertions - response time is above limit - failed") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ResponseTimeIsAbove, Map("limit" -> "100")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 99) + + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isFailure) + } + + // status-code-... + test("performAssertions - status code assertion - equals") { - val statusCodeAssertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.status-code", Map("code" -> "200")) - val response = Response(200, "Dummy Body", Map.empty) + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals, Map("code" -> "200")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) - assert(AssertionResponseAction.performResponseAction(response, statusCodeAssertion)) + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isSuccess) } test("performAssertions - status code assertion - not equals") { - val statusCodeAssertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.status-code", Map("code" -> "200")) - val response = Response(500, "Dummy Body", Map.empty) + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals, Map("code" -> "200")) + val response = Response(500, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, statusCodeResponseAction).isFailure) + } + + test("performAssertions - status code - is success") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsSuccess, Map.empty) + val response200 = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response299 = Response(299, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response200, statusCodeResponseAction).isSuccess) + assert(AssertionResponseAction.performResponseAction(response299, statusCodeResponseAction).isSuccess) + } + + test("performAssertions - status code - is not success") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsSuccess, Map.empty) + val response199 = Response(199, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response300 = Response(300, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response500 = Response(500, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response199, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response300, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response500, statusCodeResponseAction).isFailure) + } + + test("performAssertions - status code - is client error") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsClientError, Map.empty) + val response400 = Response(400, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response499 = Response(499, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response400, statusCodeResponseAction).isSuccess) + assert(AssertionResponseAction.performResponseAction(response499, statusCodeResponseAction).isSuccess) + } + + test("performAssertions - status code - is not client error") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsClientError, Map.empty) + val response399 = Response(399, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response500 = Response(500, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response200 = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response399, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response500, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response200, statusCodeResponseAction).isFailure) + } + + test("performAssertions - status code - is server error") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsServerError, Map.empty) + val response500 = Response(500, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response599 = Response(599, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response500, statusCodeResponseAction).isSuccess) + assert(AssertionResponseAction.performResponseAction(response599, statusCodeResponseAction).isSuccess) + } + + test("performAssertions - status code - is not server error") { + val statusCodeResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeIsServerError, Map.empty) + val response499 = Response(499, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response600 = Response(600, "Dummy Body", "", "", Map.empty, Map.empty, 100) + val response200 = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response499, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response600, statusCodeResponseAction).isFailure) + assert(AssertionResponseAction.performResponseAction(response200, statusCodeResponseAction).isFailure) + } + + // header-... + + test("performAssertions - header exists") { + val headerExistsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderExists, Map("headerName" -> "Content-Type")) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, headerExistsResponseAction).isSuccess) + } + + test("performAssertions - header does not exists") { + val headerExistsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderExists, Map("headerName" -> "headerValue")) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) - assert(!AssertionResponseAction.performResponseAction(response, statusCodeAssertion)) + assert(AssertionResponseAction.performResponseAction(response, headerExistsResponseAction).isFailure) } + test("performAssertions - header value is equals") { + val headerValueEqualsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "Content-Type", "expectedValue" -> "application/json")) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, headerValueEqualsResponseAction).isSuccess) + } + + test("performAssertions - header value is not equals") { + val headerValueEqualsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.HeaderValueEquals, Map("headerName" -> "someName", "expectedValue" -> "someValue")) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, headerValueEqualsResponseAction).isFailure) + } + + // content-type-... + + test("performAssertions - content type is json") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsJson, Map.empty) + val response = Response(200, "{}", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isSuccess) + } + + test("performAssertions - content type is not json") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsJson, Map.empty) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/xml")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isFailure) + } + + test("performAssertions - content type is xml") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsXml, Map.empty) + val response = Response(200, """QADevDon't forget to test it!""", "", "", Map("content-type" -> Seq("application/xml")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isSuccess) + } + + test("performAssertions - content type is not xml") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsXml, Map.empty) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/json")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isFailure) + } + + test("performAssertions - content type is html") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsHtml, Map.empty) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("text/html")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isSuccess) + } + + test("performAssertions - content type is not html") { + val contentTypeIsJsonResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.ContentTypeIsHtml, Map.empty) + val response = Response(200, "Dummy Body", "", "", Map("content-type" -> Seq("application/xml")), Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, contentTypeIsJsonResponseAction).isFailure) + } + + // cookies-... + + test("performAssertions - cookie exists") { + val cookieExistsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieExists, Map("cookieName" -> "testCookie")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("testCookie" -> CookieValue(value = "", secured = false)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieExistsResponseAction).isSuccess) + } + + test("performAssertions - cookie does not exists") { + val cookieExistsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieExists, Map("cookieName" -> "anotherCookie")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("testCookie" -> CookieValue(value = "", secured = false)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieExistsResponseAction).isFailure) + } + + test("performAssertions - cookie value is equals") { + val cookieValueEqualsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("testCookie" -> CookieValue(value = "cookieValue", secured = false)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieValueEqualsResponseAction).isSuccess) + } + + test("performAssertions - cookie value is not equals") { + val cookieValueEqualsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("testCookie" -> CookieValue(value = "anotherValue", secured = false)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieValueEqualsResponseAction).isFailure) + } + + test("performAssertions - cookie value equals - cookie does not exist") { + val cookieValueEqualsResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieValueEquals, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieValueEqualsResponseAction).isFailure) + } + + test("performAssertions - cookie is secured") { + val cookieIsSecuredResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieIsSecured, Map("cookieName" -> "securedCookie")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("securedCookie" -> CookieValue(value = "someValue", secured = true)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieIsSecuredResponseAction).isSuccess) + } + + test("performAssertions - cookie is secured - cookie does not exist") { + val cookieIsSecuredResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieIsSecured, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieIsSecuredResponseAction).isFailure) + } + + test("performAssertions - cookie is not secured") { + val cookieIsNotSecuredResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieIsNotSecured, Map("cookieName" -> "notSecuredCookie")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map("notSecuredCookie" -> CookieValue(value = "someValue", secured = false)), 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieIsNotSecuredResponseAction).isSuccess) + } + + test("performAssertions - cookie is not secured - cookie does not exist") { + val cookieIsNotSecuredResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.CookieIsNotSecured, Map("cookieName" -> "testCookie", "expectedValue" -> "cookieValue")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) + + assert(AssertionResponseAction.performResponseAction(response, cookieIsNotSecuredResponseAction).isFailure) + } + + // body-... + test("performAssertions - body contains assertion") { - val bodyContainsAssertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.body-contains", Map("body" -> "dummy")) - val response = Response(200, "This is a dummy body", Map.empty) - assert(AssertionResponseAction.performResponseAction(response, bodyContainsAssertion)) + val bodyContainsTextResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.BodyContainsText, Map("text" -> "dummy")) + val response = Response(200, "This is a dummy body", "", "", Map.empty, Map.empty, 100) + assert(AssertionResponseAction.performResponseAction(response, bodyContainsTextResponseAction).isSuccess) } test("performAssertions - body does not contains assertion") { - val bodyContainsAssertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.body-contains", Map("body" -> "dummies")) - val response = Response(200, "This is a dummy body", Map.empty) + val bodyContainsTextResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.BodyContainsText, Map("text" -> "dummies")) + val response = Response(200, "This is a dummy body", "", "", Map.empty, Map.empty, 100) - assert(!AssertionResponseAction.performResponseAction(response, bodyContainsAssertion)) + assert(AssertionResponseAction.performResponseAction(response, bodyContainsTextResponseAction).isFailure) } + // unsupported + test("performAssertions - unsupported assertion") { - val unsupportedAssertion = ResponseAction(method = s"${Response.GROUP_ASSERT}.unsupported-assertion", Map("nonsense" -> "value")) - val response = Response(200, "Dummy Body", Map.empty) + val unsupportedResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = "unsupported-assertion", Map("nonsense" -> "value")) + val response = Response(200, "Dummy Body", "", "", Map.empty, Map.empty, 100) - interceptMessage[IllegalArgumentException]("Unsupported assertion method [group: assert]: unsupported-assertion") { - AssertionResponseAction.performResponseAction(response, unsupportedAssertion) - } + assert(AssertionResponseAction.performResponseAction(response, unsupportedResponseAction).isFailure) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseExtractTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseExtractTest.scala index fd8eb2d..a834885 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseExtractTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseExtractTest.scala @@ -16,28 +16,51 @@ package africa.absa.testing.scapi.rest.response -import africa.absa.testing.scapi.{ContentValidationFailed, UndefinedResponseActionType} +import africa.absa.testing.scapi.{AssertionException, ContentValidationFailedException, UndefinedResponseActionTypeException} import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.rest.response.action.ExtractJsonResponseAction +import africa.absa.testing.scapi.rest.response.action.types.ExtractJsonResponseActionType.ExtractJsonResponseActionType +import africa.absa.testing.scapi.rest.response.action.types.{ExtractJsonResponseActionType, ResponseActionGroupType} import africa.absa.testing.scapi.utils.cache.RuntimeCache import munit.FunSuite +import scala.language.implicitConversions +import scala.util.{Failure, Try} + class ResponseExtractTest extends FunSuite { - val assertionStringFromList: ResponseAction = ResponseAction(method = s"${Response.GROUP_EXTRACT_JSON}.${ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "question_id", "listIndex" -> "1", "jsonKey" -> "id", "cacheLevel" -> "suite")) - val assertionUnsupported: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.Unsupported", Map("cacheKey" -> "key", "listIndex" -> "200", "jsonKey" -> "jsonKey", "cacheLevel" -> "Test")) + implicit def extractJsonResponseActionType2String(value: ExtractJsonResponseActionType): String = value.toString + + val assertionStringFromList: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "question_id", "listIndex" -> "1", "jsonKey" -> "id", "cacheLevel" -> "suite")) + val assertionUnsupported: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = "Unsupported", Map("cacheKey" -> "key", "listIndex" -> "200", "jsonKey" -> "jsonKey", "cacheLevel" -> "Test")) val responseWithID: Response = Response( 200, "[{\"id\":\"efa01eeb-34cb-42da-b150-ca6dbe52xxx1\",\"domainName\":\"Domain1\"},{\"id\":\"382be85a-1f00-4c15-b607-cbda03ccxxx2\",\"domainName\":\"Domain2\"},{\"id\":\"65173a5b-b13c-4db0-bd1b-24b3e3abxxx3\",\"domainName\":\"Domain3\"}]", - Map("Content-Type" -> Seq("application/json"))) + "", + "", + Map("Content-Type" -> Seq("application/json")), + Map.empty, + 100 + ) val responseNoJsonBody: Response = Response( 200, "no json here", - Map("Content-Type" -> Seq("application/json"))) + "", + "", + Map("Content-Type" -> Seq("application/json")), + Map.empty, + 100 + ) val responseJsonNoArrayBody: Response = Response( 200, "{\"id\":\"efa01eeb-34cb-42da-b150-ca6dbe52xxx1\",\"domainName\":\"Domain1\"}", - Map("Content-Type" -> Seq("application/json"))) + "", + "", + Map("Content-Type" -> Seq("application/json")), + Map.empty, + 100 + ) /* validateContent @@ -47,124 +70,117 @@ class ResponseExtractTest extends FunSuite { ExtractJsonResponseAction.validateContent(assertionStringFromList) } - test("validateContent - unsupported option") { - intercept[UndefinedResponseActionType] { - ExtractJsonResponseAction.validateContent(assertionUnsupported) + test("validateContent - validateStringFromList - not integer in string") { + val notValidAssertionStringFromList: ResponseAction = assertionStringFromList.copy(params = assertionStringFromList.params.updated("listIndex", "x")) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: 'x': Received value of 'ExtractJson.string-from-list.listIndex' cannot be parsed to an integer: For input string: \"x\"") { + ExtractJsonResponseAction.validateContent(notValidAssertionStringFromList) } } - /* - performResponseAction - */ - - test("performAssertion - STRING_FROM_LIST") { - val result: Boolean = ExtractJsonResponseAction.performResponseAction(responseWithID, assertionStringFromList) - assert(result) - assertEquals("382be85a-1f00-4c15-b607-cbda03ccxxx2", RuntimeCache.get("question_id").get) - } + test("validateContent - validateStringFromList - None parameters") { + val assertion1None: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "", "listIndex" -> "", "jsonKey" -> "")) + val assertion2None: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "", "listIndex" -> "")) + val assertion3None: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "")) + val assertion4None: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map.empty) - test("performAssertion - unsupported assertion") { - intercept[IllegalArgumentException] { - ExtractJsonResponseAction.performResponseAction(responseWithID, assertionUnsupported) + interceptMessage[IllegalArgumentException]("Missing required 'cacheKey' parameter for extract string-from-list logic") { + ExtractJsonResponseAction.validateContent(assertion4None) + } + interceptMessage[IllegalArgumentException]("Missing required 'listIndex' parameter for extract string-from-list logic") { + ExtractJsonResponseAction.validateContent(assertion3None) + } + interceptMessage[IllegalArgumentException]("Missing required 'jsonKey' parameter for extract string-from-list logic") { + ExtractJsonResponseAction.validateContent(assertion2None) + } + interceptMessage[IllegalArgumentException]("Missing required 'cacheLevel' parameter for extract string-from-list logic") { + ExtractJsonResponseAction.validateContent(assertion1None) } } - /* - stringFromList - */ - - // positive test "stringFromList - correct parameters" - tested during "performAssertion - STRING_FROM_LIST" + test("validateContent - validateStringFromList - empty parameters") { + val assertionParam1: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "", "listIndex" -> "", "jsonKey" -> "", "cacheLevel" -> "")) + val assertionParam2: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "1", "listIndex" -> "", "jsonKey" -> "", "cacheLevel" -> "")) + val assertionParam3: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "1", "listIndex" -> "x", "jsonKey" -> "", "cacheLevel" -> "")) + val assertionParam4: ResponseAction = ResponseAction(group = ResponseActionGroupType.ExtractJson, name = ExtractJsonResponseActionType.StringFromList, Map("cacheKey" -> "1", "listIndex" -> "x", "jsonKey" -> "y", "cacheLevel" -> "")) - test("stringFromList - incorrect parameters - wrong list index") { - val cacheKey = "question_id" - val listIndex = 10 - val jsonKey = "id" - val runtimeCacheLevel = "Test" - interceptMessage[IndexOutOfBoundsException]("10 is out of bounds (min 0, max 2)") { - ExtractJsonResponseAction.stringFromList(responseWithID, cacheKey, listIndex, jsonKey, runtimeCacheLevel) + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.cacheKey' is empty.") { + ExtractJsonResponseAction.validateContent(assertionParam1) + } + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.listIndex' is empty.") { + ExtractJsonResponseAction.validateContent(assertionParam2) + } + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.jsonKey' is empty.") { + ExtractJsonResponseAction.validateContent(assertionParam3) + } + interceptMessage[ContentValidationFailedException]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.cacheLevel' is empty.") { + ExtractJsonResponseAction.validateContent(assertionParam4) } } - test("stringFromList - incorrect parameters - wrong jsonKey") { - val cacheKey = "question_id" - val listIndex = 0 - val jsonKey = "ids" - val runtimeCacheLevel = "Test" - val result = ExtractJsonResponseAction.stringFromList(responseWithID, cacheKey, listIndex, jsonKey, runtimeCacheLevel) - assert(!result) + test("validateContent - unsupported option") { + interceptMessage[UndefinedResponseActionTypeException]("Undefined response action content type: 'Unsupported'") { + ExtractJsonResponseAction.validateContent(assertionUnsupported) + } } - test("stringFromList - incorrect parameters - no json body in response") { - val cacheKey = "question_id" - val listIndex = 0 - val jsonKey = "id" - val runtimeCacheLevel = "Test" + /* + performResponseAction + */ - val result = ExtractJsonResponseAction.stringFromList(responseNoJsonBody, cacheKey, listIndex, jsonKey, runtimeCacheLevel) - assert(!result) + test("performAssertion - STRING_FROM_LIST") { + val result: Try[Unit] = ExtractJsonResponseAction.performResponseAction(responseWithID, assertionStringFromList) + assert(result.isSuccess) + assertEquals("382be85a-1f00-4c15-b607-cbda03ccxxx2", RuntimeCache.get("question_id").get) } - test("stringFromList - incorrect parameters - no json arrays in response body") { - val cacheKey = "question_id" - val listIndex = 0 - val jsonKey = "id" - val runtimeCacheLevel = "Test" + // string-from-... - val result = ExtractJsonResponseAction.stringFromList(responseJsonNoArrayBody, cacheKey, listIndex, jsonKey, runtimeCacheLevel) - assert(!result) - } + test("performAssertion - stringFromList - incorrect parameters - wrong list index") { + val notValidAssertionStringFromList: ResponseAction = assertionStringFromList.copy(params = assertionStringFromList.params.updated("listIndex", "10")) + val res = ExtractJsonResponseAction.performResponseAction(responseWithID, notValidAssertionStringFromList) - /* - validateStringFromList - */ + assert(res.isFailure) - // positive test "stringFromList - correct parameters" - tested during "validateContent - STRING_FROM_LIST" + res match { + case Failure(exception: IndexOutOfBoundsException) => + assert(exception.getMessage == "10 is out of bounds (min 0, max 2)") + case _ => + fail("Expected a Failure with the specific exception type and message.") + } + } - test("validateStringFromList - None parameters") { - val assertion1None: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "", "listIndex" -> "", "jsonKey" -> "")) - val assertion2None: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "", "listIndex" -> "")) - val assertion3None: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "")) - val assertion4None: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map()) + test("performAssertion - stringFromList - incorrect parameters - wrong jsonKey") { + val notValidAssertionStringFromList: ResponseAction = assertionStringFromList.copy(params = assertionStringFromList.params.updated("jsonKey", "ids")) + val res = ExtractJsonResponseAction.performResponseAction(responseWithID, notValidAssertionStringFromList) - interceptMessage[IllegalArgumentException]("Missing required 'cacheKey' parameter for extract string-from-list logic") { - ExtractJsonResponseAction.validateStringFromList(assertion4None) - } - interceptMessage[IllegalArgumentException]("Missing required 'listIndex' parameter for extract string-from-list logic") { - ExtractJsonResponseAction.validateStringFromList(assertion3None) - } - interceptMessage[IllegalArgumentException]("Missing required 'jsonKey' parameter for extract string-from-list logic") { - ExtractJsonResponseAction.validateStringFromList(assertion2None) - } - interceptMessage[IllegalArgumentException]("Missing required 'cacheLevel' parameter for extract string-from-list logic") { - ExtractJsonResponseAction.validateStringFromList(assertion1None) + assert(res.isFailure) + + res match { + case Failure(exception: AssertionException) => + assert(exception.getMessage.contains("Expected 'ids' field not found in provided json.")) + case _ => + fail("Expected a Failure with the specific exception type and message.") } } - test("validateStringFromList - empty parameters") { - val assertionParam1: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "", "listIndex" -> "", "jsonKey" -> "", "cacheLevel" -> "")) - val assertionParam2: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "1", "listIndex" -> "", "jsonKey" -> "", "cacheLevel" -> "")) - val assertionParam3: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "1", "listIndex" -> "x", "jsonKey" -> "", "cacheLevel" -> "")) - val assertionParam4: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "1", "listIndex" -> "x", "jsonKey" -> "y", "cacheLevel" -> "")) + test("performAssertion - stringFromList - incorrect parameters - no json arrays in response body") { + val notValidAssertionStringFromList: ResponseAction = assertionStringFromList.copy(params = assertionStringFromList.params.updated("listIndex", "0")) + val res = ExtractJsonResponseAction.performResponseAction(responseNoJsonBody, notValidAssertionStringFromList) - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.cacheKey' is empty.") { - ExtractJsonResponseAction.validateStringFromList(assertionParam1) - } - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.listIndex' is empty.") { - ExtractJsonResponseAction.validateStringFromList(assertionParam2) - } - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.jsonKey' is empty.") { - ExtractJsonResponseAction.validateStringFromList(assertionParam3) - } - interceptMessage[ContentValidationFailed]("Content validation failed for value: '': Received string value of 'ExtractJson.string-from-list.cacheLevel' is empty.") { - ExtractJsonResponseAction.validateStringFromList(assertionParam4) + assert(res.isFailure) + + res match { + case Failure(exception: AssertionException) => + assert(exception.getMessage.contains("Expected json string in response body. JSON parsing error:")) + assert(exception.getMessage.contains("Unexpected character 'o' at input index 0")) + case _ => + fail("Expected a Failure with the specific exception type and message.") } } - test("validateStringFromList - not integer in string") { - val assertion: ResponseAction = ResponseAction(method = s"{Response.GROUP_EXTRACT_JSON}.&{ExtractJsonResponseAction.STRING_FROM_LIST}", Map("cacheKey" -> "key", "listIndex" -> "x", "jsonKey" -> "y", "cacheLevel" -> "y")) - interceptMessage[ContentValidationFailed]("Content validation failed for value: 'x': Received value of 'ExtractJson.string-from-list.listIndex' cannot be parsed to an integer: For input string: \"x\"") { - ExtractJsonResponseAction.validateStringFromList(assertion) + test("performAssertion - unsupported assertion") { + interceptMessage[UndefinedResponseActionTypeException]("Undefined response action content type: 'Unsupported assertion[group: extract]: Unsupported'") { + ExtractJsonResponseAction.performResponseAction(responseWithID, assertionUnsupported) } - } } 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 fbbc80b..25cf4c5 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 @@ -16,24 +16,49 @@ package africa.absa.testing.scapi.rest.response -import africa.absa.testing.scapi.UndefinedResponseActionType import africa.absa.testing.scapi.json.ResponseAction +import africa.absa.testing.scapi.rest.response.action.LogResponseAction +import africa.absa.testing.scapi.rest.response.action.types.LogResponseActionType.LogResponseActionType +import africa.absa.testing.scapi.rest.response.action.types.{ResponseActionGroupType, LogResponseActionType => LogType} import munit.FunSuite +import scala.language.implicitConversions + + class ResponseLogTest extends FunSuite { + implicit def logResponseActionType2String(value: LogResponseActionType): String = value.toString + /* validateContent */ + test("validateContent - ERROR supported") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Error, Map("message" -> "Non-empty string")) + // no exception thrown, meaning validation passed + LogResponseAction.validateContent(responseAction) + } + + test("validateContent - WARN supported") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Warn, Map("message" -> "Non-empty string")) + // no exception thrown, meaning validation passed + LogResponseAction.validateContent(responseAction) + } + test("validateContent - INFO supported") { - val responseActionInfo = ResponseAction(method = s"${Response.GROUP_LOG}.${LogResponseAction.INFO}", Map("message" -> "Non-empty string")) + val responseAction = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Info, Map("message" -> "Non-empty string")) // no exception thrown, meaning validation passed - LogResponseAction.validateContent(responseActionInfo) + LogResponseAction.validateContent(responseAction) } - test("validateContent - not supported validation type") { - val responseAction = ResponseAction(method = s"${Response.GROUP_LOG}.not_info", Map("message" -> "Some string")) - intercept[UndefinedResponseActionType] { + test("validateContent - DEBUG supported") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Debug, Map("message" -> "Non-empty string")) + // no exception thrown, meaning validation passed + LogResponseAction.validateContent(responseAction) + } + + test("validateContent - no message provided") { + val responseAction = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Info, Map.empty) + interceptMessage[IllegalArgumentException]("Missing required 'message' for assertion info logic.") { LogResponseAction.validateContent(responseAction) } } @@ -42,25 +67,27 @@ class ResponseLogTest extends FunSuite { performResponseAction */ - test("performAssertion - INFO supported") { - val assertion = ResponseAction(method = s"${Response.GROUP_LOG}.${LogResponseAction.INFO}", Map("message" -> "info message")) - val response = Response(200, "OK", Map("Content-Type" -> Seq("application/json"))) - assertEquals(LogResponseAction.performResponseAction(response, assertion), true) + test("performAssertion - ERROR supported") { + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Error, Map("message" -> "info message")) + val response = Response(500, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) + assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) } - test("performAssertion - not supported validation type") { - val assertion = ResponseAction(method = s"${Response.GROUP_LOG}.not_info", Map("message" -> "info message")) - val response = Response(200, "OK", Map("Content-Type" -> Seq("application/json"))) - intercept[IllegalArgumentException] { - LogResponseAction.performResponseAction(response, assertion) - } + test("performAssertion - WARN supported") { + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Warn, Map("message" -> "info message")) + val response = Response(401, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) + assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) } - /* - logInfo - */ + test("performAssertion - INFO supported") { + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Info, Map("message" -> "info message")) + val response = Response(200, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) + assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) + } - test("logInfo") { - assertEquals(LogResponseAction.logInfo("log message"), true) + test("performAssertion - DEBUG supported") { + val assertion = ResponseAction(group = ResponseActionGroupType.Log, name = LogType.Debug, Map("message" -> "info message")) + val response = Response(200, "OK", "", "", Map("Content-Type" -> Seq("application/json")), Map.empty, 100) + assert(LogResponseAction.performResponseAction(response, assertion).isSuccess) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseTest.scala deleted file mode 100644 index d166efa..0000000 --- a/testApi/src/test/scala/africa/absa/testing/scapi/rest/response/ResponseTest.scala +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.rest.response - -import africa.absa.testing.scapi.json.ResponseAction -import munit.FunSuite - -class ResponseTest extends FunSuite { - - /* - validate - */ - - test("validate - unsupported group") { - val unsupportedAssertion = ResponseAction(method = "unsupportedGroup.not needed", Map("param 1" -> "")) - - intercept[IllegalArgumentException] { - Response.validate(unsupportedAssertion) - } - } - - /* - perform - */ - - test("perform - unsupported group") { - val unsupportedAssertion = ResponseAction(method = "unsupportedGroup.not needed", Map("param 1" -> "")) - val response = Response(200, "OK", Map.empty) - - intercept[IllegalArgumentException] { - Response.perform(response, Set(unsupportedAssertion)) - } - } - -} diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/FakeScAPIRequestSender.scala b/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/FakeScAPIRequestSender.scala index e400a43..6e2a001 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/FakeScAPIRequestSender.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/FakeScAPIRequestSender.scala @@ -26,7 +26,12 @@ object FakeScAPIRequestSender extends RequestSender { var mockResponse: Response = Response( 200, "[{\"id\":\"efa01eeb-34cb-42da-b150-ca6dbe52xxx1\",\"domainName\":\"Domain1\"},{\"id\":\"382be85a-1f00-4c15-b607-cbda03ccxxx2\",\"domainName\":\"Domain2\"},{\"id\":\"65173a5b-b13c-4db0-bd1b-24b3e3abxxx3\",\"domainName\":\"Domain3\"}]", - Map("Content-Type" -> Seq("application/json"))) + "no url", + "fake status messsage", + Map("Content-Type" -> Seq("application/json")), + Map.empty, + 100 + ) override def get(url: String, headers: Map[String, String], verifySslCerts: Boolean, data: String, params: Map[String, String]): Response = { mockResponse diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/SuiteRunnerTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/SuiteRunnerTest.scala index a2960d6..ddc62ad 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/SuiteRunnerTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/suite/runner/SuiteRunnerTest.scala @@ -18,10 +18,11 @@ package africa.absa.testing.scapi.suite.runner import africa.absa.testing.scapi.json.{Action, Environment, Header, ResponseAction} import africa.absa.testing.scapi.logging.Logger -import africa.absa.testing.scapi.model._ +import africa.absa.testing.scapi.model.suite._ +import africa.absa.testing.scapi.model.suite.types.SuiteResultType import africa.absa.testing.scapi.rest.RestClient import africa.absa.testing.scapi.rest.request.RequestHeaders -import africa.absa.testing.scapi.rest.response.{AssertionResponseAction, Response} +import africa.absa.testing.scapi.rest.response.action.types.{AssertResponseActionType, ResponseActionGroupType} import africa.absa.testing.scapi.utils.cache.RuntimeCache import munit.FunSuite import org.apache.logging.log4j.Level @@ -31,50 +32,50 @@ class SuiteRunnerTest extends FunSuite { val header: Header = Header(name = RequestHeaders.AUTHORIZATION, value = "Basic abcdefg") val action: Action = Action(methodName = "get", url = "nice url") val actionNotSupported: Action = Action(methodName = "wrong", url = "nice url") - val responseAction: ResponseAction = ResponseAction(method = s"${Response.GROUP_ASSERT}.${AssertionResponseAction.STATUS_CODE}", Map("code" -> "200")) - val method: Method = Method(name = "test", headers = Set(header), actions = Set(action), responseActions = Set(responseAction)) - val methodNotSupported: Method = Method(name = "test", headers = Set(header), actions = Set(actionNotSupported), responseActions = Set(responseAction)) - - val suitesBundles: Set[SuiteBundle] = Set( - SuiteBundle(suite = Suite(endpoint = "endpoint1", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(true)), - SuiteTestScenario(name = "test2", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(true)) + val responseAction: ResponseAction = ResponseAction(group = ResponseActionGroupType.Assert, name = AssertResponseActionType.StatusCodeEquals.toString, Map("code" -> "200")) + val method: Method = Method(name = "test", headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction)) + val methodNotSupported: Method = Method(name = "test", headers = Seq(header), actions = Seq(actionNotSupported), responseActions = Seq(responseAction)) + + val suitesBundles: Set[Suite] = Set( + Suite(suite = TestSet(name = "name1", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(true)), + SuiteTestScenario(name = "test2", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(true)) ))), - SuiteBundle( - suiteBefore = Some(SuiteBefore(name = "suiteBefore", methods = Set(method))), - suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(false)), + Suite( + suiteBefore = Some(BeforeTestSet(name = "suiteBefore", methods = Set(method))), + suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(false)), )), - suiteAfter = Some(SuiteAfter(name = "suiteAfter", methods = Set(method))), + suiteAfter = Some(AfterTestSet(name = "suiteAfter", methods = Set(method))), )) - val suitesBundleNoBefore: Set[SuiteBundle] = Set( - SuiteBundle( - suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(false)), + val suitesBundleNoBefore: Set[Suite] = Set( + Suite( + suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(false)), )), - suiteAfter = Some(SuiteAfter(name = "suiteAfter", methods = Set(method))), + suiteAfter = Some(AfterTestSet(name = "suiteAfter", methods = Set(method))), )) - val suitesBundleAfterMethodNotSupported: Set[SuiteBundle] = Set( - SuiteBundle( - suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(false)), + val suitesBundleAfterMethodNotSupported: Set[Suite] = Set( + Suite( + suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(false)), )), - suiteAfter = Some(SuiteAfter(name = "suiteAfter", methods = Set(methodNotSupported))), + suiteAfter = Some(AfterTestSet(name = "suiteAfter", methods = Set(methodNotSupported))), )) - val suitesBundleNoAfter: Set[SuiteBundle] = Set( - SuiteBundle( - suiteBefore = Some(SuiteBefore(name = "suiteBefore", methods = Set(method))), - suite = Suite(endpoint = "endpoint2", tests = Set( - SuiteTestScenario(name = "test1", categories = Set("SMOKE"), - headers = Set(header), actions = Set(action), responseActions = Set(responseAction), only = Some(false)), + val suitesBundleNoAfter: Set[Suite] = Set( + Suite( + suiteBefore = Some(BeforeTestSet(name = "suiteBefore", methods = Set(method))), + suite = TestSet(name = "name2", tests = Set( + SuiteTestScenario(name = "test1", categories = Seq("SMOKE"), + headers = Seq(header), actions = Seq(action), responseActions = Seq(responseAction), only = Some(false)), )))) val constants: Map[String, String] = Map( @@ -101,55 +102,55 @@ class SuiteRunnerTest extends FunSuite { */ test("runSuite - SuiteBefore exists") { - val suiteResults: List[SuiteResults] = SuiteRunner.runSuites(suitesBundles, environment, () => new RestClient(FakeScAPIRequestSender)) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suitesBundles, environment, () => new RestClient(FakeScAPIRequestSender)) - val beforeSuiteResult: SuiteResults = suiteResults.find(result => - result.resultType == SuiteResults.RESULT_TYPE_BEFORE_METHOD && result.suiteName == "endpoint2").get + val beforeSuiteResult: SuiteResult = suiteResults.find(result => + result.resultType == SuiteResultType.BeforeTestSet && result.suiteName == "name2").get - assertEquals(5, suiteResults.size) - assertEquals("test", beforeSuiteResult.name) - assertEquals("Success", beforeSuiteResult.status) + assert(5 == clue(suiteResults.size)) + assert("test" == clue(beforeSuiteResult.name)) + assert(clue(beforeSuiteResult.isSuccess)) } test("runSuite - SuiteBefore empty") { - val suiteResults: List[SuiteResults] = SuiteRunner.runSuites(suitesBundleNoBefore, environment, () => new RestClient(FakeScAPIRequestSender)) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suitesBundleNoBefore, environment, () => new RestClient(FakeScAPIRequestSender)) - val beforeSuiteResult: Option[SuiteResults] = suiteResults.find(result => - result.resultType == SuiteResults.RESULT_TYPE_BEFORE_METHOD && result.suiteName == "endpoint2") + val beforeSuiteResult: Option[SuiteResult] = suiteResults.find(result => + result.resultType == SuiteResultType.BeforeTestSet && result.suiteName == "name2") - assertEquals(2, suiteResults.size) + assert(2 == clue(suiteResults.size)) assert(beforeSuiteResult.isEmpty) } test("runSuite - SuiteAfter exists") { - val suiteResults: List[SuiteResults] = SuiteRunner.runSuites(suitesBundles, environment, () => new RestClient(FakeScAPIRequestSender)) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suitesBundles, environment, () => new RestClient(FakeScAPIRequestSender)) - val afterSuiteResult: SuiteResults = suiteResults.find(result => - result.resultType == SuiteResults.RESULT_TYPE_AFTER_METHOD && result.suiteName == "endpoint2").get + val afterSuiteResult: SuiteResult = suiteResults.find(result => + result.resultType == SuiteResultType.AfterTestSet && result.suiteName == "name2").get - assertEquals(5, suiteResults.size) - assertEquals("test", afterSuiteResult.name) - assertEquals("Success", afterSuiteResult.status) + assert(5 == clue(suiteResults.size)) + assert("test" == clue(afterSuiteResult.name)) + assert(clue(afterSuiteResult.isSuccess)) } test("runSuite - SuiteAfter empty") { - val suiteResults: List[SuiteResults] = SuiteRunner.runSuites(suitesBundleNoAfter, environment, () => new RestClient(FakeScAPIRequestSender)) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suitesBundleNoAfter, environment, () => new RestClient(FakeScAPIRequestSender)) - val afterSuiteResult: Option[SuiteResults] = suiteResults.find(result => - result.resultType == SuiteResults.RESULT_TYPE_AFTER_METHOD && result.suiteName == "endpoint2") + val afterSuiteResult: Option[SuiteResult] = suiteResults.find(result => + result.resultType == SuiteResultType.AfterTestSet && result.suiteName == "name2") - assertEquals(2, suiteResults.size) + assert(2 == clue(suiteResults.size)) assert(afterSuiteResult.isEmpty) } test("runSuite - SuiteAfter empty methods") { - val suiteResults: List[SuiteResults] = SuiteRunner.runSuites(suitesBundleAfterMethodNotSupported, environment, () => new RestClient(FakeScAPIRequestSender)) + val suiteResults: List[SuiteResult] = SuiteRunner.runSuites(suitesBundleAfterMethodNotSupported, environment, () => new RestClient(FakeScAPIRequestSender)) - val afterSuiteResult: SuiteResults = suiteResults.find(result => - result.resultType == SuiteResults.RESULT_TYPE_AFTER_METHOD && result.suiteName == "endpoint2").get + val afterSuiteResult: SuiteResult = suiteResults.find(result => + result.resultType == SuiteResultType.AfterTestSet && result.suiteName == "name2").get - assertEquals(2, suiteResults.size) - assertEquals("Failure", afterSuiteResult.status) - assertEquals("RestClient:sendRequest - unexpected action method called", afterSuiteResult.errMessage.get) + assert(2 == clue(suiteResults.size)) + assert(clue(!afterSuiteResult.isSuccess)) + assert("RestClient:sendRequest - unexpected action method called" == afterSuiteResult.errorMsg.get) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/utils/cache/RuntimeCacheTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/utils/cache/RuntimeCacheTest.scala index 03a0e63..7e90eb9 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/utils/cache/RuntimeCacheTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/utils/cache/RuntimeCacheTest.scala @@ -31,28 +31,28 @@ class RuntimeCacheTest extends FunSuite { test("put") { RuntimeCache.put("key", "value") - assertEquals(RuntimeCache.get("key"), Some("value")) + assert(clue(Some("value")) == clue(RuntimeCache.get("key"))) } test("put - with level") { RuntimeCache.put("g", "g", GlobalLevel) RuntimeCache.put("s", "s", SuiteLevel) RuntimeCache.put("t", "t", TestLevel) - assertEquals(RuntimeCache.get("g"), Some("g")) - assertEquals(RuntimeCache.get("s"), Some("s")) - assertEquals(RuntimeCache.get("t"), Some("t")) + assert(clue(Some("g")) == clue(RuntimeCache.get("g"))) + assert(clue(Some("s")) == clue(RuntimeCache.get("s"))) + assert(clue(Some("t")) == clue(RuntimeCache.get("t"))) } test("put - already exists - on same level") { RuntimeCache.put("key", "valueA") RuntimeCache.put("key", "valueB") - assertEquals(RuntimeCache.get("key"), Some("valueA")) + assert(clue(Some("valueA")) == clue(RuntimeCache.get("key"))) } test("put - already exists - on different level") { RuntimeCache.put("key", "valueA", TestLevel) RuntimeCache.put("key", "valueB", SuiteLevel) - assertEquals(RuntimeCache.get("key"), Some("valueA")) + assert(clue(Some("valueA")) == clue(RuntimeCache.get("key"))) } /* @@ -61,7 +61,7 @@ class RuntimeCacheTest extends FunSuite { // smoke possitive tested during put tests - skipped here test("get - nonexistent key") { - assertEquals(None, RuntimeCache.get("nonexistent")) + assert(clue(RuntimeCache.get("nonexistent")).isEmpty) } /* @@ -71,11 +71,11 @@ class RuntimeCacheTest extends FunSuite { test("update") { RuntimeCache.put("key", "value") RuntimeCache.update("key", "newValue") - assertEquals(RuntimeCache.get("key"), Some("newValue")) + assert(clue(Some("newValue")) == clue(RuntimeCache.get("key"))) } test("update - nonexistent key") { - intercept[NoSuchElementException] { + interceptMessage[NoSuchElementException]("Key nonexistent not found in cache") { RuntimeCache.update("nonexistent", "value") } } @@ -86,7 +86,7 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.expire(TestLevel) - assertEquals(RuntimeCache.get("key"), Some("newValue")) + assert(clue(Some("newValue")) == clue(RuntimeCache.get("key"))) } test("update - with level down") { @@ -95,7 +95,7 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.expire(TestLevel) - assertEquals(None, RuntimeCache.get("nonexistent")) + assert(clue(RuntimeCache.get("nonexistent")).isEmpty) } /* @@ -106,7 +106,7 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.put("key", "value") RuntimeCache.remove("key") - assertEquals(None, RuntimeCache.get("nonexistent")) + assert(RuntimeCache.get("nonexistent").isEmpty) } test("remove - key no exist") { @@ -125,9 +125,9 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.expire(GlobalLevel) - assertEquals(None, RuntimeCache.get("key1")) - assertEquals(None, RuntimeCache.get("key2")) - assertEquals(None, RuntimeCache.get("key3")) + assert(clue(RuntimeCache.get("key1")).isEmpty) + assert(clue(RuntimeCache.get("key2")).isEmpty) + assert(clue(RuntimeCache.get("key3")).isEmpty) } test("expire - suite level") { @@ -136,8 +136,8 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.expire(SuiteLevel) - assertEquals(None, RuntimeCache.get("key1")) - assertEquals(None, RuntimeCache.get("key2")) + assert(clue(RuntimeCache.get("key1")).isEmpty) + assert(clue(RuntimeCache.get("key2")).isEmpty) } test("expire - test level") { @@ -146,8 +146,8 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.expire(TestLevel) - assertEquals(None, RuntimeCache.get("key2")) - assertEquals(RuntimeCache.get("key1"), Some("value1")) + assert(clue(RuntimeCache.get("key2")).isEmpty) + assert(clue(Some("value1")) == clue(RuntimeCache.get("key1"))) } /* @@ -161,9 +161,9 @@ class RuntimeCacheTest extends FunSuite { RuntimeCache.reset() - assertEquals(None, RuntimeCache.get("key1")) - assertEquals(None, RuntimeCache.get("key2")) - assertEquals(None, RuntimeCache.get("key3")) + assert(clue(RuntimeCache.get("key1")).isEmpty) + assert(clue(RuntimeCache.get("key2")).isEmpty) + assert(clue(RuntimeCache.get("key3")).isEmpty) } /* @@ -171,10 +171,10 @@ class RuntimeCacheTest extends FunSuite { */ test("determineLevel") { - assertEquals(RuntimeCache.determineLevel("global"), GlobalLevel) - assertEquals(RuntimeCache.determineLevel("suite"), SuiteLevel) - assertEquals(RuntimeCache.determineLevel("test"), TestLevel) - assertEquals(RuntimeCache.determineLevel("unknown"), TestLevel) + assert(GlobalLevel == clue(RuntimeCache.determineLevel("global"))) + assert(SuiteLevel == clue(RuntimeCache.determineLevel("suite"))) + assert(TestLevel == clue(RuntimeCache.determineLevel("test"))) + assert(TestLevel == clue(RuntimeCache.determineLevel("unknown"))) } /* @@ -183,11 +183,11 @@ class RuntimeCacheTest extends FunSuite { test("resolve") { RuntimeCache.put("key", "value") - assertEquals(RuntimeCache.resolve("{{ cache.key }}"), "value") + assert("value".==(clue(RuntimeCache.resolve("{{ cache.key }}")))) } test("resolve - key not exist") { - intercept[NoSuchElementException] { + interceptMessage[NoSuchElementException]("Key not found in cache: notExist") { RuntimeCache.resolve("{{ cache.notExist }}") } } @@ -201,22 +201,22 @@ class RuntimeCacheTest extends FunSuite { test("resolve - no placeholder to resolve") { RuntimeCache.put("key", "value") - assertEquals(RuntimeCache.resolve("cache.key"), "cache.key") + assert("cache.key" == clue(RuntimeCache.resolve("cache.key"))) } test("resolve - mixed placeholders") { RuntimeCache.put("key", "value") - assertEquals(RuntimeCache.resolve("{{ cache.key }} and {{ not.cache.key }}"), "value and {{ not.cache.key }}") + assert("value and {{ not.cache.key }}" == clue(RuntimeCache.resolve("{{ cache.key }} and {{ not.cache.key }}"))) } test("resolve - empty key") { - intercept[NoSuchElementException] { + interceptMessage[NoSuchElementException]("Key not found in cache: ") { RuntimeCache.resolve("{{ cache. }}") } } test("resolve - empty value") { RuntimeCache.put("key", "") - assertEquals(RuntimeCache.resolve("{{ cache.key }}"), "") + assert("".==(clue(RuntimeCache.resolve("{{ cache.key }}")))) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/FileUtilsTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/FileUtilsTest.scala index 6347568..be65da7 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/FileUtilsTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/FileUtilsTest.scala @@ -45,7 +45,7 @@ class FileUtilsTest extends FunSuite { val matchingFiles = FileUtils.findMatchingFiles(tmpDir.toString) - assertEquals(clue(expectedFiles), clue(matchingFiles)) + assert(clue(expectedFiles) == clue(matchingFiles)) } test("find matching files - custom filter") { @@ -54,7 +54,7 @@ class FileUtilsTest extends FunSuite { val matchingFiles = FileUtils.findMatchingFiles(tmpDir.toString, "(.*).json") - assertEquals(clue(expectedFiles), clue(matchingFiles)) + assert(clue(expectedFiles) == clue(matchingFiles)) } test("find matching files - no found") { @@ -63,7 +63,7 @@ class FileUtilsTest extends FunSuite { val matchingFiles = FileUtils.findMatchingFiles(tmpDir.toString, "(.*).nonsense") - assertEquals(clue(expectedFiles), clue(matchingFiles)) + assert(clue(expectedFiles) == clue(matchingFiles)) } test("find matching files - subdirectories") { @@ -72,7 +72,7 @@ class FileUtilsTest extends FunSuite { val matchingFiles = FileUtils.findMatchingFiles(tmpDir.toString, "(.*).xml") - assertEquals(clue(expectedFiles), clue(matchingFiles)) + assert(clue(expectedFiles) == clue(matchingFiles)) } /* @@ -85,8 +85,8 @@ class FileUtilsTest extends FunSuite { val (actualPath, actualFileName) = FileUtils.splitPathAndFileName(filePath) - assertEquals(clue(expectedPath), clue(actualPath)) - assertEquals(clue(expectedFileName), clue(actualFileName)) + assert(clue(expectedPath) == clue(actualPath)) + assert(clue(expectedFileName) == clue(actualFileName)) } test("split to path and fileName - empty input") { @@ -94,8 +94,8 @@ class FileUtilsTest extends FunSuite { val (actualPath, actualFileName) = FileUtils.splitPathAndFileName(filePath) - assertEquals("", clue(actualPath)) - assertEquals("", clue(actualFileName)) + assert("".==(clue(actualPath))) + assert("".==(clue(actualFileName))) } test("split to path and fileName - fileName only") { @@ -103,8 +103,8 @@ class FileUtilsTest extends FunSuite { val (path, fileName) = FileUtils.splitPathAndFileName(filePath) - assertEquals(clue(path), "") - assertEquals(clue(fileName), "filename.txt") + assert("".==(clue(path))) + assert("filename.txt".==(clue(fileName))) } test("split to path and fileName - dir path only") { @@ -115,9 +115,9 @@ class FileUtilsTest extends FunSuite { val (actualPath, actualFileName) = FileUtils.splitPathAndFileName(filePath) val (actualPathEndSlash, actualFileNameEndSlash) = FileUtils.splitPathAndFileName(filePathEndSlash) - assertEquals(clue(expectedPath), clue(actualPath)) - assertEquals(clue(expectedPath), clue(actualPathEndSlash)) - assertEquals("to", clue(actualFileName)) - assertEquals("to", clue(actualFileNameEndSlash)) + assert(clue(expectedPath) == clue(actualPath)) + assert(clue(expectedPath) == clue(actualPathEndSlash)) + assert("to".==(clue(actualFileName))) + assert("to".==(clue(actualFileNameEndSlash))) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/JsonUtilsTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/JsonUtilsTest.scala index daf99ed..2d6b513 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/JsonUtilsTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/utils/file/JsonUtilsTest.scala @@ -36,7 +36,7 @@ class JsonUtilsTest extends FunSuite { emptyFile.deleteOnExit() val result = JsonUtils.stringFromPath(emptyFile.getAbsolutePath) - assertEquals("", clue(result)) + assert("" == clue(result)) } test("jsonStringFromPath with valid JSON file") { @@ -48,6 +48,6 @@ class JsonUtilsTest extends FunSuite { } val result = JsonUtils.stringFromPath(jsonFile.getAbsolutePath) - assertEquals("""{"name": "test"}""", clue(result)) + assert("""{"name": "test"}""" == clue(result)) } } diff --git a/testApi/src/test/scala/africa/absa/testing/scapi/utils/validation/ContentValidatorTest.scala b/testApi/src/test/scala/africa/absa/testing/scapi/utils/validation/ContentValidatorTest.scala index 0c0c1d9..f27edd5 100644 --- a/testApi/src/test/scala/africa/absa/testing/scapi/utils/validation/ContentValidatorTest.scala +++ b/testApi/src/test/scala/africa/absa/testing/scapi/utils/validation/ContentValidatorTest.scala @@ -16,7 +16,7 @@ package africa.absa.testing.scapi.utils.validation -import africa.absa.testing.scapi.ContentValidationFailed +import africa.absa.testing.scapi.ContentValidationFailedException import munit.FunSuite class ContentValidatorTest extends FunSuite { @@ -30,13 +30,13 @@ class ContentValidatorTest extends FunSuite { } test("validateIntegerString - fails with non-integer string") { - intercept[ContentValidationFailed] { + intercept[ContentValidationFailedException] { ContentValidator.validateIntegerString("not an integer", "unitTest") } } test("validateIntegerString - fails with empty string") { - intercept[ContentValidationFailed] { + intercept[ContentValidationFailedException] { ContentValidator.validateIntegerString("", "unitTest") } } @@ -50,7 +50,7 @@ class ContentValidatorTest extends FunSuite { } test("validateNonEmptyString - fails with empty string") { - intercept[ContentValidationFailed] { + intercept[ContentValidationFailedException] { ContentValidator.validateNonEmptyString("", "unitTest") } }