From 914c058c17da206f038f72c94ee3bdd25321308f Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 28 Apr 2026 13:51:17 +0200 Subject: [PATCH 1/5] bugfix/implement v7.0.0 resource-docs aggregation to return all API versions endpoints --- .../scala/code/api/v7_0_0/Http4s700.scala | 90 ++- .../V7ResourceDocsAggregationTest.scala | 669 ++++++++++++++++++ 2 files changed, 758 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 8d448ef6f4..4b27bc152e 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -59,6 +59,89 @@ object Http4s700 { val versionStatus = ApiVersionStatus.STABLE.toString val resourceDocs = ArrayBuffer[ResourceDoc]() + /* + * IMPORTANT: Endpoint Exclusion Pattern + * + * excludeEndpoints is used to filter out old endpoints when v7.0.0 has a DIFFERENT URL pattern. + * + * WHEN TO EXCLUDE: + * - Old and new endpoints have DIFFERENT URLs (e.g., v4.0.0: /users/:username vs v7.0.0: /providers/:provider/users/:username) + * - The old endpoint should not be accessible via v7.0.0 at all + * + * WHEN NOT TO EXCLUDE: + * - Old and new endpoints have the SAME URL and HTTP method (e.g., GET /api/versions) + * - In this case, collectResourceDocs() automatically deduplicates by (URL, method) and keeps newest version + * - Excluding by function name would remove BOTH versions since they share the same name! + * + * Why? The routing works as follows: + * 1. allResourceDocs = collectResourceDocs() deduplicates docs by (URL, method), keeps newest + * 2. excludeEndpoints filters ResourceDocs by partialFunctionName (removes by name, not by version) + * 3. The filtered docs determine which endpoints are available + * + * Pattern: Add nameOf(Implementations{version}.endpointName) :: with a comment explaining why + * + * NOTE: Currently empty - no v7-specific exclusions have been identified yet. + * As v7.0.0 introduces endpoints with different URL patterns than previous versions, + * add those old endpoint names here with explanatory comments. + */ + lazy val excludeEndpoints: List[String] = + // Add exclusions here when v7.0.0 replaces old endpoints with different URLs + // Example: nameOf(Implementations3_0_0.getUserByUsername) :: // v7.0.0 uses /providers/:provider/users/:username + Nil + + /** + * Aggregated resource docs from all API versions (v7.0.0 + v6.0.0 + v5.1.0 + ... + v1.3.0) + * + * This method implements the resource docs aggregation pattern for v7.0.0: + * 1. Takes OBPAPI6_0_0.allResourceDocs (which already contains v6.0.0 + v5.1.0 + ... + v1.3.0) + * 2. Adds v7.0.0's own resourceDocs + * 3. Deduplicates by (requestUrl, requestVerb), keeping the newest version + * 4. Filters out explicitly excluded old endpoints + * + * Note: We cannot extend OBPRestHelper (Lift framework) in Http4s700 (Http4s framework) + * due to type incompatibilities. Instead, we implement the collectResourceDocs logic inline. + * + * The deduplication algorithm: + * - Sort all docs by API version (descending: v7.0.0, v6.0.0, v5.1.0, ...) + * - For each doc, check if (requestUrl, requestVerb) has been seen + * - If not seen, add to result (this keeps the newest version) + * - If seen, skip (this omits older versions of the same endpoint) + */ + def allResourceDocs: ArrayBuffer[ResourceDoc] = { + // Import v6.0.0's aggregated docs (v6.0.0 + v5.1.0 + ... + v1.3.0) + import code.api.v6_0_0.OBPAPI6_0_0 + + // Combine v6.0.0's aggregated docs with v7.0.0's docs + val allDocs = OBPAPI6_0_0.allResourceDocs ++ resourceDocs + + // Deduplicate by (requestUrl, requestVerb), keeping newest version + // Sort by API version (descending) so newer versions come first + implicit val ordering = new Ordering[ScannedApiVersion] { + override def compare(x: ScannedApiVersion, y: ScannedApiVersion): Int = + y.toString().compareTo(x.toString()) + } + + val sortedDocs = allDocs.sortBy(_.implementedInApiVersion) + + val result = ArrayBuffer[ResourceDoc]() + val urlAndMethods = scala.collection.mutable.Set[(String, String)]() + + for (doc <- sortedDocs) { + val urlAndMethod = (doc.requestUrl, doc.requestVerb) + if (!urlAndMethods.contains(urlAndMethod)) { + urlAndMethods.add(urlAndMethod) + result += doc + } + } + + // Filter out explicitly excluded old endpoints + if (excludeEndpoints.isEmpty) { + result + } else { + result.filterNot(it => it.partialFunctionName.matches(excludeEndpoints.mkString("|"))) + } + } + object Implementations7_0_0 { // Common prefix: /obp/v7.0.0 @@ -222,7 +305,12 @@ object Http4s700 { ) { ApiVersionUtils.valueOf(requestedApiVersionString) } - allDocs = ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil) + // Use aggregated docs for v7.0.0, version-specific docs for other versions + allDocs = if (requestedApiVersion == ApiVersion.v7_0_0) { + allResourceDocs.toList + } else { + ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil) + } filteredDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(allDocs, tags, functions) } yield JSONFactory1_4_0.createResourceDocsJson(filteredDocs, isVersion4OrHigher = true, localeParam, includeTechnology = true) } diff --git a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala new file mode 100644 index 0000000000..b0f3629f99 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala @@ -0,0 +1,669 @@ +package code.api.v7_0_0 + +import code.Http4sTestServer +import code.setup.ServerSetupWithTestData +import dispatch.Defaults._ +import dispatch._ +import net.liftweb.json.JValue +import net.liftweb.json.JsonAST.{JArray, JObject, JString} +import net.liftweb.json.JsonParser.parse +import org.scalatest.Tag + +import scala.collection.JavaConverters._ +import scala.concurrent.Await +import scala.concurrent.duration._ + +/** + * Bug Condition Exploration Test for v7 Resource Docs Aggregation Fix + * + * **CRITICAL**: This test MUST FAIL on unfixed code - failure confirms the bug exists + * **DO NOT attempt to fix the test or the code when it fails** + * **NOTE**: This test encodes the expected behavior - it will validate the fix when it passes after implementation + * **GOAL**: Surface counterexamples that demonstrate the bug exists + * + * **Validates: Requirements 1.1, 1.2, 1.3** + * + * Bug Description: + * The v7.0.0 resource-docs endpoint currently returns only v7.0.0's own endpoints (~10) + * instead of aggregating all versions (v7.0.0 + v6.0.0 + v5.1.0 + ... + v1.3.0) which should be 500+. + * + * Expected Behavior Properties: + * - Response includes v7.0.0 endpoints + * - Response includes v6.0.0 endpoints (e.g., getScannedApiVersions) + * - Response includes v5.1.0 and earlier endpoints + * - Deduplication works correctly (same URL+method keeps newest version) + */ +class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { + + object V7ResourceDocsAggregationTag extends Tag("V7ResourceDocsAggregation") + + // Use Http4sTestServer for full integration testing + private val http4sServer = Http4sTestServer + private val baseUrl = s"http://${http4sServer.host}:${http4sServer.port}" + + private def makeHttpRequest( + path: String, + headers: Map[String, String] = Map.empty + ): (Int, JValue, Map[String, String]) = { + val request = url(s"$baseUrl$path") + val requestWithHeaders = headers.foldLeft(request) { case (req, (key, value)) => + req.addHeader(key, value) + } + + try { + val response = Http.default( + requestWithHeaders.setHeader("Accept", "*/*") > as.Response(p => + (p.getStatusCode, p.getResponseBody, p.getHeaders.iterator().asScala.map(e => e.getKey -> e.getValue).toMap) + ) + ) + val (statusCode, body, responseHeaders) = Await.result(response, 30.seconds) + val json = if (body.trim.isEmpty) JObject(Nil) else parse(body) + (statusCode, json, responseHeaders) + } catch { + case e: java.util.concurrent.ExecutionException => + val statusPattern = """(\d{3})""".r + statusPattern.findFirstIn(e.getCause.getMessage) match { + case Some(code) => (code.toInt, JObject(Nil), Map.empty) + case None => throw e + } + case e: Exception => + throw e + } + } + + private def toFieldMap(fields: List[net.liftweb.json.JsonAST.JField]): Map[String, JValue] = + fields.map(field => field.name -> field.value).toMap + + private def extractResourceDocs(json: JValue): List[JObject] = { + json match { + case JObject(fields) => + toFieldMap(fields).get("resource_docs") match { + case Some(JArray(docs)) => + docs.collect { case obj: JObject => obj } + case _ => Nil + } + case _ => Nil + } + } + + private def getEndpointSignature(doc: JObject): Option[(String, String)] = { + val fieldMap = toFieldMap(doc.obj) + for { + JString(verb) <- fieldMap.get("request_verb") + JString(url) <- fieldMap.get("request_url") + } yield (verb, url) + } + + private def getOperationId(doc: JObject): Option[String] = { + val fieldMap = toFieldMap(doc.obj) + fieldMap.get("operation_id").collect { case JString(id) => id } + } + + feature("Bug Condition Exploration - V7 Resource Docs Aggregation") { + + scenario("Property 1: Bug Condition - V7 Resource Docs Returns Only V7 Endpoints (EXPECTED TO FAIL)", V7ResourceDocsAggregationTag) { + Given("The v7.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain aggregated docs from all versions (v7.0.0 + v6.0.0 + v5.1.0 + ... + v1.3.0)") + val resourceDocs = extractResourceDocs(json) + + // **Counterexample 1**: Expected 500+ endpoints, Actual: ~10 endpoints + info(s"Total endpoint count: ${resourceDocs.size}") + resourceDocs.size should be >= 500 + + And("Response should include v7.0.0 endpoints") + val v7Endpoints = resourceDocs.filter { doc => + getEndpointSignature(doc) match { + case Some((verb, url)) => + // v7.0.0 specific endpoints + (verb == "GET" && url.endsWith("/root")) || + (verb == "GET" && url.endsWith("/banks")) || + (verb == "GET" && url.endsWith("/cards")) + case None => false + } + } + info(s"V7.0.0 endpoints found: ${v7Endpoints.size}") + v7Endpoints should not be empty + + And("Response should include v6.0.0 endpoints (e.g., getScannedApiVersions)") + // **Counterexample 2**: Expected v6.0.0 endpoints discoverable, Actual: not found + val v6Endpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + // Known v6.0.0 endpoints + opId == "OBPv6.0.0-getScannedApiVersions" || + opId.startsWith("OBPv6.0.0-") + case None => false + } + } + info(s"V6.0.0 endpoints found: ${v6Endpoints.size}") + v6Endpoints should not be empty + + And("Response should include v5.1.0 and earlier endpoints") + val olderVersionEndpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + opId.startsWith("OBPv5.1.0-") || + opId.startsWith("OBPv5.0.0-") || + opId.startsWith("OBPv4.0.0-") || + opId.startsWith("OBPv3.1.0-") || + opId.startsWith("OBPv3.0.0-") || + opId.startsWith("OBPv2.2.0-") || + opId.startsWith("OBPv2.1.0-") || + opId.startsWith("OBPv2.0.0-") || + opId.startsWith("OBPv1.4.0-") || + opId.startsWith("OBPv1.3.0-") + case None => false + } + } + info(s"V5.1.0 and earlier endpoints found: ${olderVersionEndpoints.size}") + olderVersionEndpoints should not be empty + + And("Deduplication should work correctly (same URL+method keeps newest version)") + // Check for duplicate endpoints (same verb + url) + val endpointSignatures = resourceDocs.flatMap(getEndpointSignature) + val duplicates = endpointSignatures.groupBy(identity).filter(_._2.size > 1) + info(s"Duplicate endpoint signatures found: ${duplicates.size}") + duplicates shouldBe empty + + info("**Expected Outcome**: This test FAILS on unfixed code (proves bug exists)") + info("**Root Cause**: Missing allResourceDocs aggregation in Http4s700") + info("**Counterexamples**:") + info(s" - Expected: 500+ endpoints, Actual: ${resourceDocs.size} endpoints") + info(s" - Expected: v6.0.0 endpoints discoverable, Actual: ${v6Endpoints.size} found") + info(s" - Expected: v5.1.0+ endpoints discoverable, Actual: ${olderVersionEndpoints.size} found") + } + + scenario("Baseline - V6 Resource Docs Returns Aggregated Endpoints (SHOULD PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain aggregated docs from v6.0.0 and earlier versions") + val resourceDocs = extractResourceDocs(json) + info(s"V6.0.0 total endpoint count: ${resourceDocs.size}") + resourceDocs.size should be >= 400 + + And("Response should include v6.0.0 endpoints") + val v6Endpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => opId.startsWith("OBPv6.0.0-") + case None => false + } + } + info(s"V6.0.0 endpoints in v6 docs: ${v6Endpoints.size}") + v6Endpoints should not be empty + + And("Response should include v5.1.0 and earlier endpoints") + val olderVersionEndpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + opId.startsWith("OBPv5.1.0-") || + opId.startsWith("OBPv5.0.0-") || + opId.startsWith("OBPv4.0.0-") + case None => false + } + } + info(s"V5.1.0 and earlier endpoints in v6 docs: ${olderVersionEndpoints.size}") + olderVersionEndpoints should not be empty + + info("**Expected Outcome**: This test PASSES (establishes baseline behavior)") + info("**Purpose**: Confirms v6.0.0 aggregation works correctly") + } + + scenario("Cross-Version Query - V7 Endpoint Can Query V6 Docs (MAY FAIL)", V7ResourceDocsAggregationTag) { + Given("The v7.0.0 endpoint is used to query v6.0.0 resource-docs") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v7.0.0/resource-docs/v6.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v6.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain v6.0.0 aggregated docs") + val resourceDocs = extractResourceDocs(json) + info(s"V6.0.0 docs via v7 endpoint: ${resourceDocs.size}") + resourceDocs.size should be >= 400 + + info("**Expected Outcome**: This test should PASS (cross-version query works)") + info("**Purpose**: Verifies v7 endpoint can serve other versions' docs") + } + + scenario("Specific Endpoint Discovery - V6 getScannedApiVersions Through V7 (EXPECTED TO FAIL)", V7ResourceDocsAggregationTag) { + Given("The v7.0.0 resource-docs endpoint is queried for a specific v6.0.0 endpoint") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getScannedApiVersions request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getScannedApiVersions") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain the getScannedApiVersions endpoint") + val resourceDocs = extractResourceDocs(json) + info(s"getScannedApiVersions docs found: ${resourceDocs.size}") + + // **Counterexample 3**: Expected getScannedApiVersions discoverable, Actual: empty result + resourceDocs should not be empty + + val scannedApiVersionsEndpoint = resourceDocs.find { doc => + getEndpointSignature(doc) match { + case Some((verb, url)) => + verb == "GET" && url.contains("/api/versions") + case None => false + } + } + scannedApiVersionsEndpoint shouldBe defined + + info("**Expected Outcome**: This test FAILS on unfixed code") + info("**Counterexample**: getScannedApiVersions (v6.0.0) not discoverable through v7.0.0") + } + } + + /** + * Preservation Property Tests for v7 Resource Docs Aggregation Fix + * + * **CRITICAL**: These tests MUST PASS on unfixed code - passing confirms baseline behavior to preserve + * **GOAL**: Establish baseline behavior for non-buggy inputs (v4-v6 resource-docs endpoints) + * **PURPOSE**: Ensure the fix does not break existing v4-v6 resource-docs functionality + * + * **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5** + * + * Preservation Requirements: + * - v4.0.0, v5.1.0, and v6.0.0 resource-docs endpoints return aggregated docs + * - Query parameter filtering (tags, functions, locale) works correctly + * - Non-resource-docs v7.0.0 endpoints are unchanged + * - collectResourceDocs() deduplication by (URL, HTTP method) keeps newest version + */ + feature("Preservation Property Tests - Non-V7 Resource Docs Behavior") { + + scenario("Property 2.1: V6 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain aggregated docs from v6.0.0 and earlier versions") + val resourceDocs = extractResourceDocs(json) + info(s"V6.0.0 total endpoint count: ${resourceDocs.size}") + + // Baseline observation: v6.0.0 returns 400+ aggregated endpoints + resourceDocs.size should be >= 400 + + And("Response should include v6.0.0 endpoints") + val v6Endpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => opId.startsWith("OBPv6.0.0-") + case None => false + } + } + info(s"V6.0.0 endpoints: ${v6Endpoints.size}") + v6Endpoints should not be empty + + And("Response should include v5.1.0 and earlier endpoints") + val v5AndEarlierEndpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + opId.startsWith("OBPv5.1.0-") || + opId.startsWith("OBPv5.0.0-") || + opId.startsWith("OBPv4.0.0-") || + opId.startsWith("OBPv3.1.0-") || + opId.startsWith("OBPv3.0.0-") + case None => false + } + } + info(s"V5.1.0 and earlier endpoints: ${v5AndEarlierEndpoints.size}") + v5AndEarlierEndpoints should not be empty + + And("Deduplication should work for most endpoints (baseline behavior)") + val endpointSignatures = resourceDocs.flatMap(getEndpointSignature) + val duplicates = endpointSignatures.groupBy(identity).filter(_._2.size > 1) + info(s"Duplicate endpoint signatures: ${duplicates.size}") + // Note: Baseline behavior has 1 known duplicate (v1.4.0 resource-docs endpoint) + // This is acceptable for preservation testing - we're capturing actual behavior + duplicates.size should be <= 1 + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.1, 3.2 - v6.0.0 aggregation and deduplication work correctly") + } + + scenario("Property 2.2: V5.1 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v5.1.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v5.1.0/resource-docs/v5.1.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v5.1.0/resource-docs/v5.1.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain aggregated docs from v5.1.0 and earlier versions") + val resourceDocs = extractResourceDocs(json) + info(s"V5.1.0 total endpoint count: ${resourceDocs.size}") + + // Baseline observation: v5.1.0 returns 300+ aggregated endpoints + resourceDocs.size should be >= 300 + + And("Response should include v5.1.0 endpoints") + val v5_1Endpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => opId.startsWith("OBPv5.1.0-") + case None => false + } + } + info(s"V5.1.0 endpoints: ${v5_1Endpoints.size}") + v5_1Endpoints should not be empty + + And("Response should include v5.0.0 and earlier endpoints") + val v5AndEarlierEndpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + opId.startsWith("OBPv5.0.0-") || + opId.startsWith("OBPv4.0.0-") || + opId.startsWith("OBPv3.1.0-") + case None => false + } + } + info(s"V5.0.0 and earlier endpoints: ${v5AndEarlierEndpoints.size}") + v5AndEarlierEndpoints should not be empty + + And("Deduplication should work for most endpoints (baseline behavior)") + val endpointSignatures = resourceDocs.flatMap(getEndpointSignature) + val duplicates = endpointSignatures.groupBy(identity).filter(_._2.size > 1) + info(s"Duplicate endpoint signatures: ${duplicates.size}") + // Note: Baseline behavior has 1 known duplicate (v1.4.0 resource-docs endpoint) + // This is acceptable for preservation testing - we're capturing actual behavior + duplicates.size should be <= 1 + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.1, 3.2 - v5.1.0 aggregation and deduplication work correctly") + } + + scenario("Property 2.3: V4 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v4.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v4.0.0/resource-docs/v4.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v4.0.0/resource-docs/v4.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain aggregated docs from v4.0.0 and earlier versions") + val resourceDocs = extractResourceDocs(json) + info(s"V4.0.0 total endpoint count: ${resourceDocs.size}") + + // Baseline observation: v4.0.0 returns 200+ aggregated endpoints + resourceDocs.size should be >= 200 + + And("Response should include v4.0.0 endpoints") + val v4Endpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => opId.startsWith("OBPv4.0.0-") + case None => false + } + } + info(s"V4.0.0 endpoints: ${v4Endpoints.size}") + v4Endpoints should not be empty + + And("Response should include v3.1.0 and earlier endpoints") + val v3AndEarlierEndpoints = resourceDocs.filter { doc => + getOperationId(doc) match { + case Some(opId) => + opId.startsWith("OBPv3.1.0-") || + opId.startsWith("OBPv3.0.0-") || + opId.startsWith("OBPv2.2.0-") + case None => false + } + } + info(s"V3.1.0 and earlier endpoints: ${v3AndEarlierEndpoints.size}") + v3AndEarlierEndpoints should not be empty + + And("Deduplication should work for most endpoints (baseline behavior)") + val endpointSignatures = resourceDocs.flatMap(getEndpointSignature) + val duplicates = endpointSignatures.groupBy(identity).filter(_._2.size > 1) + info(s"Duplicate endpoint signatures: ${duplicates.size}") + // Note: Baseline behavior has 1 known duplicate (v1.4.0 resource-docs endpoint) + // This is acceptable for preservation testing - we're capturing actual behavior + duplicates.size should be <= 1 + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.1, 3.2 - v4.0.0 aggregation and deduplication work correctly") + } + + scenario("Property 2.4: Query Parameter Filtering Preserved - Functions (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called with functions filter") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp?functions=getScannedApiVersions request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp?functions=getScannedApiVersions") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain only the filtered endpoint") + val resourceDocs = extractResourceDocs(json) + info(s"Filtered endpoint count: ${resourceDocs.size}") + + // Baseline observation: functions filter returns specific endpoint(s) + resourceDocs should not be empty + resourceDocs.size should be <= 5 // Should be a small number of filtered results + + And("Filtered endpoint should match the requested function") + val matchingEndpoint = resourceDocs.find { doc => + getEndpointSignature(doc) match { + case Some((verb, url)) => + verb == "GET" && url.contains("/api/versions") + case None => false + } + } + matchingEndpoint shouldBe defined + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.3 - query parameter filtering works correctly") + } + + scenario("Property 2.5: Query Parameter Filtering Preserved - Tags (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called with tags filter") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp?tags=Account request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp?tags=Account") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain only endpoints tagged with Account") + val resourceDocs = extractResourceDocs(json) + info(s"Account-tagged endpoint count: ${resourceDocs.size}") + + // Baseline observation: tags filter returns subset of endpoints + resourceDocs should not be empty + resourceDocs.size should be < 500 // Should be less than total endpoints + + And("All returned endpoints should have Account tag") + val allHaveAccountTag = resourceDocs.forall { doc => + val fieldMap = toFieldMap(doc.obj) + fieldMap.get("tags") match { + case Some(JArray(tags)) => + tags.exists { + case JString(tag) => tag.toLowerCase.contains("account") + case _ => false + } + case _ => false + } + } + // Note: This assertion may be relaxed if tag filtering is case-sensitive or partial match + info(s"All endpoints have Account tag: $allHaveAccountTag") + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.3 - query parameter filtering works correctly") + } + + scenario("Property 2.6: Non-Resource-Docs V7 Endpoints Unchanged - Root (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("A non-resource-docs v7.0.0 endpoint is called") + + When("Making GET /obp/v7.0.0/root request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/root") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain expected root endpoint data") + json match { + case JObject(fields) => + val fieldMap = toFieldMap(fields) + // Root endpoint should return version info + fieldMap.get("version") shouldBe defined + fieldMap.get("git_commit") shouldBe defined + case _ => + fail("Expected JObject response from root endpoint") + } + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.5 - non-resource-docs v7.0.0 endpoints unchanged") + } + + scenario("Property 2.7: Non-Resource-Docs V7 Endpoints Unchanged - Banks (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("A non-resource-docs v7.0.0 endpoint is called") + + When("Making GET /obp/v7.0.0/banks request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/banks") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should contain expected banks data structure") + json match { + case JObject(fields) => + val fieldMap = toFieldMap(fields) + // Banks endpoint should return banks array + fieldMap.get("banks") shouldBe defined + case _ => + fail("Expected JObject response from banks endpoint") + } + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.5 - non-resource-docs v7.0.0 endpoints unchanged") + } + + scenario("Property 2.8: Deduplication Keeps Newest Version (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("For endpoints with same URL+method, deduplication should work for most endpoints") + val resourceDocs = extractResourceDocs(json) + + // Group by endpoint signature (verb + url) + val endpointsBySignature = resourceDocs.groupBy(getEndpointSignature) + + // Check that most signatures appear only once + val duplicateSignatures = endpointsBySignature.filter(_._2.size > 1) + info(s"Duplicate signatures found: ${duplicateSignatures.size}") + // Note: Baseline behavior has 1 known duplicate (v1.4.0 resource-docs endpoint) + // This is acceptable for preservation testing - we're capturing actual behavior + duplicateSignatures.size should be <= 1 + + // Verify that for common endpoints, the newest version is kept + // Example: GET /obp/vX.X.X/banks should be from v6.0.0, not older versions + val banksEndpoint = resourceDocs.find { doc => + getEndpointSignature(doc) match { + case Some((verb, url)) => + verb == "GET" && url.matches(".*/banks$") + case None => false + } + } + + banksEndpoint match { + case Some(doc) => + val opId = getOperationId(doc) + info(s"Banks endpoint operation_id: $opId") + // Should be from v6.0.0 or v5.1.0 (newest available) + opId match { + case Some(id) => + val isNewestVersion = id.startsWith("OBPv6.0.0-") || id.startsWith("OBPv5.1.0-") + info(s"Banks endpoint is from newest version: $isNewestVersion") + case None => + info("Banks endpoint has no operation_id") + } + case None => + info("Banks endpoint not found in v6.0.0 resource docs") + } + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.2 - collectResourceDocs() deduplication works correctly") + } + + scenario("Property 2.9: JSON Response Format Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { + Given("The v6.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("Response should have expected JSON structure") + json match { + case JObject(fields) => + val fieldMap = toFieldMap(fields) + + // Should have resource_docs array + fieldMap.get("resource_docs") shouldBe defined + + fieldMap.get("resource_docs") match { + case Some(JArray(docs)) => + info(s"Resource docs array size: ${docs.size}") + + // Each doc should have expected fields + docs.headOption match { + case Some(doc: JObject) => + val docFieldMap = toFieldMap(doc.obj) + + // Required fields in resource doc + docFieldMap.get("operation_id") shouldBe defined + docFieldMap.get("request_verb") shouldBe defined + docFieldMap.get("request_url") shouldBe defined + docFieldMap.get("summary") shouldBe defined + docFieldMap.get("description") shouldBe defined + + info("Resource doc has expected field structure") + case _ => + fail("Expected JObject in resource_docs array") + } + case _ => + fail("Expected JArray for resource_docs field") + } + case _ => + fail("Expected JObject response") + } + + info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") + info("**Validates**: Requirements 3.4 - JSON response format unchanged") + } + } +} From e9070146850c6f9c875c6c2edb2c8f85b0c34545 Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 28 Apr 2026 15:44:14 +0200 Subject: [PATCH 2/5] bugfix/add specifiedUrl fix for v7.0.0 resource-docs aggregation to use v7.0.0 version in all aggregated docs --- .../scala/code/api/v7_0_0/Http4s700.scala | 11 +++- .../V7ResourceDocsAggregationTest.scala | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 4b27bc152e..4ac0c225db 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -307,7 +307,16 @@ object Http4s700 { } // Use aggregated docs for v7.0.0, version-specific docs for other versions allDocs = if (requestedApiVersion == ApiVersion.v7_0_0) { - allResourceDocs.toList + // For v7.0.0, update requestUrl and specifiedUrl for all aggregated docs + // This mirrors the logic in ResourceDocsAPIMethods.getResourceDocsList + allResourceDocs.toList.map { doc => + // Save original requestUrl before modification (it's in short form like "/banks") + val originalRequestUrl = doc.requestUrl + doc.copy( + requestUrl = s"/${doc.implementedInApiVersion.urlPrefix}/${doc.implementedInApiVersion.vDottedApiVersion}${originalRequestUrl}", + specifiedUrl = Some(s"/${doc.implementedInApiVersion.urlPrefix}/${requestedApiVersion.vDottedApiVersion}${originalRequestUrl}") + ) + } } else { ResourceDocs140.ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(Nil) } diff --git a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala index b0f3629f99..7a9cff2fc9 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala @@ -665,5 +665,69 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { info("**Expected Outcome**: This test PASSES on unfixed code (baseline behavior)") info("**Validates**: Requirements 3.4 - JSON response format unchanged") } + + scenario("Property 2.10: V7 specifiedUrl Uses V7 Version for Aggregated Docs (MUST PASS AFTER FIX)", V7ResourceDocsAggregationTag) { + Given("The v7.0.0 resource-docs endpoint is called") + setPropsValues("resource_docs_requires_role" -> "false") + + When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp request") + val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp") + + Then("Response should be 200 OK") + statusCode shouldBe 200 + + And("All resource docs should have specifiedUrl with v7.0.0 version") + val resourceDocs = extractResourceDocs(json) + info(s"Total endpoint count: ${resourceDocs.size}") + + // Check that all docs have specifiedUrl field set + val docsWithSpecifiedUrl = resourceDocs.filter { doc => + val fieldMap = toFieldMap(doc.obj) + fieldMap.get("specified_url") match { + case Some(JString(url)) => url.nonEmpty + case _ => false + } + } + info(s"Docs with non-empty specifiedUrl: ${docsWithSpecifiedUrl.size}") + docsWithSpecifiedUrl.size shouldBe resourceDocs.size + + // Check that all specifiedUrl values use v7.0.0 version + val docsWithV7SpecifiedUrl = resourceDocs.filter { doc => + val fieldMap = toFieldMap(doc.obj) + fieldMap.get("specified_url") match { + case Some(JString(url)) => url.contains("/v7.0.0/") + case _ => false + } + } + info(s"Docs with v7.0.0 in specifiedUrl: ${docsWithV7SpecifiedUrl.size}") + docsWithV7SpecifiedUrl.size shouldBe resourceDocs.size + + // Verify a specific v6.0.0 endpoint has correct specifiedUrl + val v6Endpoint = resourceDocs.find { doc => + getOperationId(doc) match { + case Some(opId) => opId == "OBPv6.0.0-getScannedApiVersions" + case None => false + } + } + + v6Endpoint match { + case Some(doc) => + val fieldMap = toFieldMap(doc.obj) + fieldMap.get("specified_url") match { + case Some(JString(url)) => + info(s"getScannedApiVersions specifiedUrl: $url") + url should include("/v7.0.0/") + url should not include("/v6.0.0/") + case _ => + fail("Expected specifiedUrl field in getScannedApiVersions doc") + } + case None => + info("Warning: getScannedApiVersions endpoint not found in aggregated docs") + } + + info("**Expected Outcome**: This test PASSES after specifiedUrl fix") + info("**Validates**: specifiedUrl correctly uses v7.0.0 for all aggregated docs") + } } } + From 25c0ce94cd101de4648c86d56c9eb8d00d4c220a Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 28 Apr 2026 22:28:21 +0200 Subject: [PATCH 3/5] bugfix/use allResourceDocs instead of resourceDocs for v7.0.0 in getResourceDocsList to enable cross-version queries --- .../code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index e1d2572f7c..267fcc1529 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -122,7 +122,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth logger.debug(s"getResourceDocsList says requestedApiVersion is $requestedApiVersion") val resourceDocs = requestedApiVersion match { - case ApiVersion.v7_0_0 => code.api.v7_0_0.Http4s700.resourceDocs + case ApiVersion.v7_0_0 => code.api.v7_0_0.Http4s700.allResourceDocs // Use aggregated docs for v7.0.0 case ConstantsBG.`berlinGroupVersion2` => code.api.berlin.group.v2.Http4sBGv2.resourceDocs case ApiVersion.v6_0_0 => OBPAPI6_0_0.allResourceDocs case ApiVersion.v5_1_0 => OBPAPI5_1_0.allResourceDocs From 5d8250d633ae18aa6c0dad685afb75a9d5c745fb Mon Sep 17 00:00:00 2001 From: hongwei Date: Tue, 28 Apr 2026 22:51:52 +0200 Subject: [PATCH 4/5] performance/change allResourceDocs from def to lazy val to cache aggregation result and improve performance --- obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index 1c962840bd..a757cf307f 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -106,8 +106,10 @@ object Http4s700 { * - For each doc, check if (requestUrl, requestVerb) has been seen * - If not seen, add to result (this keeps the newest version) * - If seen, skip (this omits older versions of the same endpoint) + * + * Performance: Computed once and cached (lazy val) to avoid recomputation on every request. */ - def allResourceDocs: ArrayBuffer[ResourceDoc] = { + lazy val allResourceDocs: ArrayBuffer[ResourceDoc] = { // Import v6.0.0's aggregated docs (v6.0.0 + v5.1.0 + ... + v1.3.0) import code.api.v6_0_0.OBPAPI6_0_0 From eb0b974b2a5444575386b9ec459d7816e0091e91 Mon Sep 17 00:00:00 2001 From: hongwei Date: Wed, 29 Apr 2026 09:11:49 +0200 Subject: [PATCH 5/5] refactor/eliminate string literal duplication in V7ResourceDocsAggregationTest to fix SonarQube warnings --- .../V7ResourceDocsAggregationTest.scala | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala index 7a9cff2fc9..edd80254a7 100644 --- a/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala +++ b/obp-api/src/test/scala/code/api/v7_0_0/V7ResourceDocsAggregationTest.scala @@ -37,6 +37,14 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { object V7ResourceDocsAggregationTag extends Tag("V7ResourceDocsAggregation") + // Test message constants (to avoid SonarCube duplication warnings) + private val MSG_RESPONSE_200_OK = "Response should be 200 OK" + private val MSG_GIVEN_V7_ENDPOINT = "The v7.0.0 resource-docs endpoint is called" + private val MSG_GIVEN_V6_ENDPOINT = "The v6.0.0 resource-docs endpoint is called" + private val MSG_GIVEN_V5_ENDPOINT = "The v5.1.0 resource-docs endpoint is called" + private val MSG_GIVEN_V4_ENDPOINT = "The v4.0.0 resource-docs endpoint is called" + private val MSG_GIVEN_NON_RESOURCE_DOCS = "A non-resource-docs v7.0.0 endpoint is called" + // Use Http4sTestServer for full integration testing private val http4sServer = Http4sTestServer private val baseUrl = s"http://${http4sServer.host}:${http4sServer.port}" @@ -102,13 +110,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { feature("Bug Condition Exploration - V7 Resource Docs Aggregation") { scenario("Property 1: Bug Condition - V7 Resource Docs Returns Only V7 Endpoints (EXPECTED TO FAIL)", V7ResourceDocsAggregationTag) { - Given("The v7.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V7_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain aggregated docs from all versions (v7.0.0 + v6.0.0 + v5.1.0 + ... + v1.3.0)") @@ -182,13 +190,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Baseline - V6 Resource Docs Returns Aggregated Endpoints (SHOULD PASS)", V7ResourceDocsAggregationTag) { - Given("The v6.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V6_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain aggregated docs from v6.0.0 and earlier versions") @@ -230,7 +238,7 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { When("Making GET /obp/v7.0.0/resource-docs/v6.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v6.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain v6.0.0 aggregated docs") @@ -249,7 +257,7 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getScannedApiVersions request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp?functions=getScannedApiVersions") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain the getScannedApiVersions endpoint") @@ -291,13 +299,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { feature("Preservation Property Tests - Non-V7 Resource Docs Behavior") { scenario("Property 2.1: V6 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("The v6.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V6_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain aggregated docs from v6.0.0 and earlier versions") @@ -345,13 +353,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.2: V5.1 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("The v5.1.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V5_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v5.1.0/resource-docs/v5.1.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v5.1.0/resource-docs/v5.1.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain aggregated docs from v5.1.0 and earlier versions") @@ -397,13 +405,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.3: V4 Resource Docs Aggregation Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("The v4.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V4_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v4.0.0/resource-docs/v4.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v4.0.0/resource-docs/v4.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain aggregated docs from v4.0.0 and earlier versions") @@ -455,7 +463,7 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp?functions=getScannedApiVersions request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp?functions=getScannedApiVersions") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain only the filtered endpoint") @@ -487,7 +495,7 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp?tags=Account request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp?tags=Account") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain only endpoints tagged with Account") @@ -518,12 +526,12 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.6: Non-Resource-Docs V7 Endpoints Unchanged - Root (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("A non-resource-docs v7.0.0 endpoint is called") + Given(MSG_GIVEN_NON_RESOURCE_DOCS) When("Making GET /obp/v7.0.0/root request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/root") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain expected root endpoint data") @@ -542,12 +550,12 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.7: Non-Resource-Docs V7 Endpoints Unchanged - Banks (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("A non-resource-docs v7.0.0 endpoint is called") + Given(MSG_GIVEN_NON_RESOURCE_DOCS) When("Making GET /obp/v7.0.0/banks request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/banks") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should contain expected banks data structure") @@ -565,13 +573,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.8: Deduplication Keeps Newest Version (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("The v6.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V6_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("For endpoints with same URL+method, deduplication should work for most endpoints") @@ -618,13 +626,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.9: JSON Response Format Preserved (MUST PASS)", V7ResourceDocsAggregationTag) { - Given("The v6.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V6_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v6.0.0/resource-docs/v6.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v6.0.0/resource-docs/v6.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("Response should have expected JSON structure") @@ -667,13 +675,13 @@ class V7ResourceDocsAggregationTest extends ServerSetupWithTestData { } scenario("Property 2.10: V7 specifiedUrl Uses V7 Version for Aggregated Docs (MUST PASS AFTER FIX)", V7ResourceDocsAggregationTag) { - Given("The v7.0.0 resource-docs endpoint is called") + Given(MSG_GIVEN_V7_ENDPOINT) setPropsValues("resource_docs_requires_role" -> "false") When("Making GET /obp/v7.0.0/resource-docs/v7.0.0/obp request") val (statusCode, json, _) = makeHttpRequest("/obp/v7.0.0/resource-docs/v7.0.0/obp") - Then("Response should be 200 OK") + Then(MSG_RESPONSE_200_OK) statusCode shouldBe 200 And("All resource docs should have specifiedUrl with v7.0.0 version")