Get banks on this API instance
\nReturns a list of banks supported on this server:
User Authentication is Optional. The User need not be logged in.
\nJSON response body fields:
\n\n\n\nfull_name: full name string
\nid: d8839721-ad8f-45dd-9f78-2080414b93f9
\nlogo: logo url
\nscheme: OBP
\n\nwebsite: www.openbankproject.com
\n", + description_markdown = + "Get banks on this API instance\nReturns a list of banks supported on this server:\n\n* ID used as parameter in URLs\n* Short and full name of bank\n* Logo URL\n* Website\n\nUser Authentication is Optional. The User need not be logged in.\n\n\n**JSON response body fields:**\n\n\n\n[**address**](/glossary#address): \n\n\n\n[**bank_routing**](/glossary#bank_routing): \n\n\n\n[**banks**](/glossary#banks): \n\n\n\n[**full_name**](/glossary#full_name): full name string\n\n\n\n[**id**](/glossary#id): d8839721-ad8f-45dd-9f78-2080414b93f9\n\n\n\n[**logo**](/glossary#logo): logo url\n\n\n\n[**scheme**](/glossary#scheme): OBP\n\n\n\n[**short_name**](/glossary#short_name): \n\n\n\n[**website**](/glossary#website): www.openbankproject.com\n\n\n", + success_response_body = Some( + parseJson( + """{"banks":[{"id":"gh.29.uk","short_name":"short_name ","full_name":"full_name","logo":"logo","website":"www.openbankproject.com","bank_routing":{"scheme":"OBP","address":"gh.29.uk"}}]}""" + ) + ), + error_response_bodies = List("OBP-50000: Unknown Error."), + tags = List("Bank", "Account Information Service (AIS)", "PSD2"), + typed_success_response_body = Some( + parseJson( + """{"type":"object","properties":{"banks":{"type":"array","items":{"type":"object","properties":{"website":{"type":"string"},"logo":{"type":"string"},"bank_routing":{"type":"object","properties":{"address":{"type":"string"},"scheme":{"type":"string"}}},"short_name":{"type":"string"},"id":{"type":"string"},"full_name":{"type":"string"}}}}}}""" + ) + ), + is_featured = false, + special_instructions = "", + specified_url = "/obp/v3.1.0/banks", + connector_methods = List("obp.getBanks", "obp.getBankAccountsForUser") + ) + + // Verify structure + assert(getBanksDocJson.operation_id == "OBPv3.0.0-getBanks") + assert(getBanksDocJson.request_verb == "GET") + assert(getBanksDocJson.request_url == "/obp/v3.0.0/banks") + assert(getBanksDocJson.summary == "Get Banks") + assert(getBanksDocJson.implemented_by.version == "OBPv3.0.0") + assert(getBanksDocJson.implemented_by.function == "getBanks") + + val docTags = getBanksDocJson.tags + assert(docTags.contains("Bank")) + assert(docTags.contains("Account Information Service (AIS)")) + assert(docTags.contains("PSD2")) + + assert(getBanksDocJson.error_response_bodies.contains("OBP-50000: Unknown Error.")) + assert(getBanksDocJson.connector_methods.contains("obp.getBanks")) + assert(getBanksDocJson.connector_methods.contains("obp.getBankAccountsForUser")) + assert(getBanksDocJson.specified_url == "/obp/v3.1.0/banks") + } + + test("GetBanks ResourceDocJson JSON output should match expected JSON string") { + val getBanksDocJson = OBPResourceDocJson( + operation_id = "OBPv3.0.0-getBanks", + implemented_by = ImplementedByJson(version = "OBPv3.0.0", function = "getBanks"), + request_verb = "GET", + request_url = "/obp/v3.0.0/banks", + summary = "Get Banks", + description = + "Get banks on this API instance
\nReturns a list of banks supported on this server:
User Authentication is Optional. The User need not be logged in.
\nJSON response body fields:
\n\n\n\nfull_name: full name string
\nid: d8839721-ad8f-45dd-9f78-2080414b93f9
\nlogo: logo url
\nscheme: OBP
\n\nwebsite: www.openbankproject.com
\n", + description_markdown = + "Get banks on this API instance\nReturns a list of banks supported on this server:\n\n* ID used as parameter in URLs\n* Short and full name of bank\n* Logo URL\n* Website\n\nUser Authentication is Optional. The User need not be logged in.\n\n\n**JSON response body fields:**\n\n\n\n[**address**](/glossary#address): \n\n\n\n[**bank_routing**](/glossary#bank_routing): \n\n\n\n[**banks**](/glossary#banks): \n\n\n\n[**full_name**](/glossary#full_name): full name string\n\n\n\n[**id**](/glossary#id): d8839721-ad8f-45dd-9f78-2080414b93f9\n\n\n\n[**logo**](/glossary#logo): logo url\n\n\n\n[**scheme**](/glossary#scheme): OBP\n\n\n\n[**short_name**](/glossary#short_name): \n\n\n\n[**website**](/glossary#website): www.openbankproject.com\n\n\n", + success_response_body = Some( + parseJson( + """{"banks":[{"id":"gh.29.uk","short_name":"short_name ","full_name":"full_name","logo":"logo","website":"www.openbankproject.com","bank_routing":{"scheme":"OBP","address":"gh.29.uk"}}]}""" + ) + ), + error_response_bodies = List("OBP-50000: Unknown Error."), + tags = List("Bank", "Account Information Service (AIS)", "PSD2"), + typed_success_response_body = Some( + parseJson( + """{"type":"object","properties":{"banks":{"type":"array","items":{"type":"object","properties":{"website":{"type":"string"},"logo":{"type":"string"},"bank_routing":{"type":"object","properties":{"address":{"type":"string"},"scheme":{"type":"string"}}},"short_name":{"type":"string"},"id":{"type":"string"},"full_name":{"type":"string"}}}}}}""" + ) + ), + is_featured = false, + special_instructions = "", + specified_url = "/obp/v3.1.0/banks", + connector_methods = List("obp.getBanks", "obp.getBankAccountsForUser") + ) + + assert(getBanksDocJson.operation_id == "OBPv3.0.0-getBanks") + assert(getBanksDocJson.implemented_by.version == "OBPv3.0.0") + assert(getBanksDocJson.implemented_by.function == "getBanks") + assert(getBanksDocJson.request_verb == "GET") + assert(getBanksDocJson.request_url == "/obp/v3.0.0/banks") + assert(getBanksDocJson.summary == "Get Banks") + assert(getBanksDocJson.specified_url == "/obp/v3.1.0/banks") + assert(getBanksDocJson.tags.length == 3) + assert(getBanksDocJson.error_response_bodies.length == 1) + assert(getBanksDocJson.connector_methods.length == 2) + } +} diff --git a/src/test/scala/com/openbankproject/resourcedocs/core/model/OBPResourceDocJsonSpec.scala b/src/test/scala/com/openbankproject/resourcedocs/core/model/OBPResourceDocJsonSpec.scala new file mode 100644 index 0000000..a1e4b80 --- /dev/null +++ b/src/test/scala/com/openbankproject/resourcedocs/core/model/OBPResourceDocJsonSpec.scala @@ -0,0 +1,70 @@ +package com.openbankproject.resourcedocs.core.model + +import io.circe.{Json => CirceJson} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import json._ + +case class SampleRequest(id: String, quantity: Int, tags: List[String]) +case class SampleResponse(success: Boolean, message: Option[String]) + +class OBPResourceDocJsonSpec extends AnyFlatSpec with Matchers { + + // Define implicits using the macro + implicit val sampleRequestSchema: Schema[SampleRequest] = Json.schema[SampleRequest] + implicit val sampleResponseSchema: Schema[SampleResponse] = Json.schema[SampleResponse] + + "OBPResourceDocJson.generateSchema" should "generate correct Draft-04 schema for a case class" in { + val schema: CirceJson = OBPResourceDocJson.generateSchema[SampleRequest] + + val schemaObj = schema.asObject.get + + // Handle Schema with Definitions (Draft-04 style) + // The root might contain $ref, and properties are in definitions + val properties = if (schemaObj.contains("definitions")) { + val definitions = schemaObj("definitions").flatMap(_.asObject).get + // Assuming only one definition is generated for the simple case class + definitions.values.head.asObject.get("properties").flatMap(_.asObject).get + } else { + schemaObj("properties").flatMap(_.asObject).get + } + + properties("id").flatMap(_.asObject).flatMap(_("type").flatMap(_.asString)) should be(Some("string")) + properties("quantity").flatMap(_.asObject).flatMap(_("type").flatMap(_.asString)) should be(Some("integer")) + + // Verify array handling + val tagsProp = properties("tags").flatMap(_.asObject).get + tagsProp("type").flatMap(_.asString) should be(Some("array")) + tagsProp("items").flatMap(_.asObject).flatMap(_("type").flatMap(_.asString)) should be(Some("string")) + + // Verify required fields + // If using definitions, 'required' is inside the definition + val requiredSource = if (schemaObj.contains("definitions")) { + val definitions = schemaObj("definitions").flatMap(_.asObject).get + definitions.values.head.asObject.get + } else { + schemaObj + } + val required = requiredSource("required").flatMap(_.asArray).getOrElse(Vector.empty).flatMap(_.asString) + required should contain allOf ("id", "quantity", "tags") + } + + it should "handle Option types as optional fields in schema" in { + val schema: CirceJson = OBPResourceDocJson.generateSchema[SampleResponse] + + val schemaObj = schema.asObject.get + + val requiredSource = if (schemaObj.contains("definitions")) { + val definitions = schemaObj("definitions").flatMap(_.asObject).get + definitions.values.head.asObject.get + } else { + schemaObj + } + + val required = requiredSource("required").flatMap(_.asArray).map(_.flatMap(_.asString)).getOrElse(Vector.empty) + + // 'success' should be required, 'message' should NOT be in required list + required should contain("success") + required should not contain "message" + } +} diff --git a/src/test/scala/com/openbankproject/resourcedocs/exporter/MarkdownExporterSpec.scala b/src/test/scala/com/openbankproject/resourcedocs/exporter/MarkdownExporterSpec.scala new file mode 100644 index 0000000..88c73d1 --- /dev/null +++ b/src/test/scala/com/openbankproject/resourcedocs/exporter/MarkdownExporterSpec.scala @@ -0,0 +1,81 @@ +package com.openbankproject.resourcedocs.exporter + +import com.openbankproject.resourcedocs.core.model.{ImplementedByJson, OBPResourceDocJson} +import io.circe.Json +import io.circe.parser.parse +import org.scalatest.funsuite.AnyFunSuite + +class MarkdownExporterSpec extends AnyFunSuite { + + private def parseJson(value: String): Json = + parse(value).fold(throw _, identity) + + private val getBanksDescriptionHtml: String = + "Get banks on this API instance
\nReturns a list of banks supported on this server:
User Authentication is Optional. The User need not be logged in.
\nJSON response body fields:
\n\nbank_routings: bank routing in form of (scheme, address)
\n\nfull_name: full name string
\nid: d8839721-ad8f-45dd-9f78-2080414b93f9
\nlogo: logo url
\nname: ACCOUNT_MANAGEMENT_FEE
\nscheme: OBP
\n\nvalue: 5987953
\nwebsite: www.openbankproject.com
\nattributes: attribute value in form of (name, value)
\n" + + private val getBanksDescriptionMarkdown: String = + "Get banks on this API instance\nReturns a list of banks supported on this server:\n\n* ID used as parameter in URLs\n* Short and full name of bank\n* Logo URL\n* Website\n\nUser Authentication is Optional. The User need not be logged in.\n\n\n**JSON response body fields:**\n\n\n\n[**address**](/glossary#address): \n\n\n\n[**bank_routings**](/glossary#bank_routings): bank routing in form of (scheme, address)\n\n\n\n[**banks**](/glossary#banks): \n\n\n\n[**full_name**](/glossary#full_name): full name string\n\n\n\n[**id**](/glossary#id): d8839721-ad8f-45dd-9f78-2080414b93f9\n\n\n\n[**logo**](/glossary#logo): logo url\n\n\n\n[**name**](/glossary#name): ACCOUNT_MANAGEMENT_FEE\n\n\n\n[**scheme**](/glossary#scheme): OBP\n\n\n\n[**short_name**](/glossary#short_name): \n\n\n\n[**value**](/glossary#): 5987953\n\n\n\n[**website**](/glossary#website): www.openbankproject.com\n\n\n\n[attributes](/glossary#attributes): attribute value in form of (name, value)\n\n\n" + + private val getBanksSuccessResponse = + parseJson( + """{"banks":[{"id":"gh.29.uk","short_name":"short_name ","full_name":"full_name","logo":"logo","website":"www.openbankproject.com","bank_routings":[{"scheme":"OBP","address":"gh.29.uk"}],"attributes":[{"name":"ACCOUNT_MANAGEMENT_FEE","value":"5987953"}]}]}""" + ) + + private val getBanksTypedSuccessResponse = + parseJson( + """{"type":"object","properties":{"banks":{"type":"array","items":{"type":"object","properties":{"bank_routings":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"scheme":{"type":"string"}}}},"website":{"type":"string"},"logo":{"type":"string"},"attributes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"short_name":{"type":"string"},"id":{"type":"string"},"full_name":{"type":"string"}}}}}}""" + ) + + private def getBanksDoc( + operationId: String, + verb: String, + path: String, + summary: String, + tags: List[String] + ): OBPResourceDocJson = { + OBPResourceDocJson( + operation_id = operationId, + implemented_by = ImplementedByJson(version = "OBPv4.0.0", function = "getBanks"), + request_verb = verb, + request_url = path, + summary = summary, + description = getBanksDescriptionHtml, + description_markdown = getBanksDescriptionMarkdown, + success_response_body = Some(getBanksSuccessResponse), + error_response_bodies = List("OBP-50000: Unknown Error."), + tags = tags, + typed_success_response_body = Some(getBanksTypedSuccessResponse), + specified_url = "/obp/v5.1.0/banks" + ) + } + + test("export should order getBanks docs and list key metadata") { + val getBanksV3 = getBanksDoc( + operationId = "OBPv3.0.0-getBanks", + verb = "GET", + path = "/obp/v3.0.0/banks", + summary = "Get Banks V3", + tags = List("BankAccountTag1", "Bank") + ) + val getBanksV4 = getBanksDoc( + operationId = "OBPv4.0.0-getBanks", + verb = "GET", + path = "/obp/v4.0.0/banks", + summary = "Get Banks V4", + tags = List("PSD2", "Account Information Service (AIS)") + ) + + val markdown = MarkdownExporter.render(Seq(getBanksV4, getBanksV3)) // intentionally shuffled + + assert(markdown.contains("### OBPv3.0.0-getBanks")) + assert(markdown.contains("- Method: GET")) + assert(markdown.contains("- Path: /obp/v3.0.0/banks")) + assert(markdown.contains("- Summary: Get Banks V3")) + assert(markdown.contains("Get banks on this API instance")) + assert(markdown.contains("Tags: Bank, BankAccountTag1")) + + assert(markdown.contains("### OBPv4.0.0-getBanks")) + assert(markdown.contains("- Path: /obp/v4.0.0/banks")) + assert(markdown.indexOf("### OBPv3.0.0-getBanks") < markdown.indexOf("### OBPv4.0.0-getBanks")) + } +} diff --git a/src/test/scala/com/openbankproject/resourcedocs/exporter/OBPLikeJsonExporterSpec.scala b/src/test/scala/com/openbankproject/resourcedocs/exporter/OBPLikeJsonExporterSpec.scala new file mode 100644 index 0000000..390a05a --- /dev/null +++ b/src/test/scala/com/openbankproject/resourcedocs/exporter/OBPLikeJsonExporterSpec.scala @@ -0,0 +1,297 @@ +package com.openbankproject.resourcedocs.exporter + +import com.openbankproject.resourcedocs.core.model.{ImplementedByJson, OBPResourceDocJson, RoleInfoJson} +import io.circe.Json +import io.circe.parser.parse +import org.scalatest.funsuite.AnyFunSuite + +import java.time.Instant + +class OBPLikeJsonExporterSpec extends AnyFunSuite { + + private def parseJson(value: String): Json = + parse(value).fold(throw _, identity) + + private val getBanksDescriptionHtml: String = + "Get banks on this API instance
\nReturns a list of banks supported on this server:
User Authentication is Optional. The User need not be logged in.
\nJSON response body fields:
\n\nbank_routings: bank routing in form of (scheme, address)
\n\nfull_name: full name string
\nid: d8839721-ad8f-45dd-9f78-2080414b93f9
\nlogo: logo url
\nname: ACCOUNT_MANAGEMENT_FEE
\nscheme: OBP
\n\nvalue: 5987953
\nwebsite: www.openbankproject.com
\nattributes: attribute value in form of (name, value)
\n" + + private val getBanksDescriptionMarkdown: String = + "Get banks on this API instance\nReturns a list of banks supported on this server:\n\n* ID used as parameter in URLs\n* Short and full name of bank\n* Logo URL\n* Website\n\nUser Authentication is Optional. The User need not be logged in.\n\n\n**JSON response body fields:**\n\n\n\n[**address**](/glossary#address): \n\n\n\n[**bank_routings**](/glossary#bank_routings): bank routing in form of (scheme, address)\n\n\n\n[**banks**](/glossary#banks): \n\n\n\n[**full_name**](/glossary#full_name): full name string\n\n\n\n[**id**](/glossary#id): d8839721-ad8f-45dd-9f78-2080414b93f9\n\n\n\n[**logo**](/glossary#logo): logo url\n\n\n\n[**name**](/glossary#name): ACCOUNT_MANAGEMENT_FEE\n\n\n\n[**scheme**](/glossary#scheme): OBP\n\n\n\n[**short_name**](/glossary#short_name): \n\n\n\n[**value**](/glossary#): 5987953\n\n\n\n[**website**](/glossary#website): www.openbankproject.com\n\n\n\n[attributes](/glossary#attributes): attribute value in form of (name, value)\n\n\n" + + private val getBanksSuccessResponse = + parseJson( + """{ + | "banks": [ + | { + | "id": "gh.29.uk", + | "short_name": "short_name ", + | "full_name": "full_name", + | "logo": "logo", + | "website": "www.openbankproject.com", + | "bank_routings": [ + | { + | "scheme": "OBP", + | "address": "gh.29.uk" + | } + | ], + | "attributes": [ + | { + | "name": "ACCOUNT_MANAGEMENT_FEE", + | "value": "5987953" + | } + | ] + | } + | ] + |}""".stripMargin + ) + + private val getBanksTypedSuccessResponse = + parseJson( + """{ + | "type": "object", + | "properties": { + | "banks": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "bank_routings": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "address": { + | "type": "string" + | }, + | "scheme": { + | "type": "string" + | } + | } + | } + | }, + | "website": { + | "type": "string" + | }, + | "logo": { + | "type": "string" + | }, + | "attributes": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "name": { + | "type": "string" + | }, + | "value": { + | "type": "string" + | } + | } + | } + | }, + | "short_name": { + | "type": "string" + | }, + | "id": { + | "type": "string" + | }, + | "full_name": { + | "type": "string" + | } + | } + | } + | } + | } + |}""".stripMargin + ) + + private def baseDoc(operationId: String, request_url: String = "/obp/v4.0.0/banks") = OBPResourceDocJson( + operation_id = operationId, + implemented_by = ImplementedByJson(version = "OBPv4.0.0", function = "getBanks"), + request_verb = "GET", + request_url = request_url, + summary = "Get Banks", + description = getBanksDescriptionHtml, + description_markdown = getBanksDescriptionMarkdown, + success_response_body = Some(getBanksSuccessResponse), + error_response_bodies = List("OBP-50000: Unknown Error."), + tags = List( + "BankAccountTag1", + "BankAccountTag1", + "BankAccountTag1", + "Bank", + "Account Information Service (AIS)", + "PSD2" + ), + typed_success_response_body = Some(getBanksTypedSuccessResponse), + specified_url = "/obp/v5.1.0/banks", + special_instructions = "", + connector_methods = Nil + ) + + private def escapeForJson(value: String): String = + value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + + private def normalizeWhitespace(value: String): String = + value.filterNot(_.isWhitespace) + + test("export should match the official getBanks JSON payload exactly") { + val doc = baseDoc("OBPv4.0.0-getBanks") + + val json = OBPLikeJsonExporter.render(Seq(doc), Instant.parse("2025-11-16T21:57:26Z")) + + val expectedJson = + s""" + |{ + | "resource_docs": [ + | { + | "operation_id": "OBPv4.0.0-getBanks", + | "implemented_by": { + | "version": "OBPv4.0.0", + | "function": "getBanks" + | }, + | "request_verb": "GET", + | "request_url": "/obp/v4.0.0/banks", + | "summary": "Get Banks", + | "description": "${escapeForJson(getBanksDescriptionHtml)}", + | "description_markdown": "${escapeForJson(getBanksDescriptionMarkdown)}", + | "success_response_body": { + | "banks": [ + | { + | "id": "gh.29.uk", + | "short_name": "short_name ", + | "full_name": "full_name", + | "logo": "logo", + | "website": "www.openbankproject.com", + | "bank_routings": [ + | { + | "scheme": "OBP", + | "address": "gh.29.uk" + | } + | ], + | "attributes": [ + | { + | "name": "ACCOUNT_MANAGEMENT_FEE", + | "value": "5987953" + | } + | ] + | } + | ] + | }, + | "error_response_bodies": [ + | "OBP-50000: Unknown Error." + | ], + | "tags": [ + | "BankAccountTag1", + | "BankAccountTag1", + | "BankAccountTag1", + | "Bank", + | "Account Information Service (AIS)", + | "PSD2" + | ], + | "typed_success_response_body": { + | "type": "object", + | "properties": { + | "banks": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "bank_routings": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "address": { + | "type": "string" + | }, + | "scheme": { + | "type": "string" + | } + | } + | } + | }, + | "website": { + | "type": "string" + | }, + | "logo": { + | "type": "string" + | }, + | "attributes": { + | "type": "array", + | "items": { + | "type": "object", + | "properties": { + | "name": { + | "type": "string" + | }, + | "value": { + | "type": "string" + | } + | } + | } + | }, + | "short_name": { + | "type": "string" + | }, + | "id": { + | "type": "string" + | }, + | "full_name": { + | "type": "string" + | } + | } + | } + | } + | } + | }, + | "is_featured": false, + | "special_instructions": "", + | "specified_url": "/obp/v5.1.0/banks", + | "connector_methods": [] + | } + | ], + | "meta": { + | "response_date": "2025-11-16T21:57:26Z", + | "count": 1 + | } + |} + |""".stripMargin.stripPrefix("\n") + + assert(normalizeWhitespace(json) == normalizeWhitespace(expectedJson)) + } + + test("export should keep duplicates, render roles, and order by operation id") { + val docWithRoles = baseDoc("BX-getCustomers").copy( + roles = Some(List(RoleInfoJson("CanRead"))), + tags = List("A", "A", "B") + ) + val laterDoc = baseDoc("CX-getBanks").copy( + success_response_body = None, + typed_success_response_body = None, + connector_methods = Nil + ) + + val json = OBPLikeJsonExporter.render(Seq(laterDoc, docWithRoles), Instant.parse("2025-01-01T00:00:00Z")) + + val firstIdx = json.indexOf("BX-getCustomers") + val secondIdx = json.indexOf("CX-getBanks") + assert(firstIdx >= 0 && secondIdx > firstIdx) + assert(json.contains(""""tags": [ + | "A", + | "A", + | "B" + | ],""".stripMargin)) + assert(json.contains(""""roles": [ + | { + | "role": "CanRead", + | "requires_bank_id": false + | } + | ]""".stripMargin)) + } +} diff --git a/src/test/scala/com/openbankproject/resourcedocs/exporter/OpenApiLikeJsonExporterSpec.scala b/src/test/scala/com/openbankproject/resourcedocs/exporter/OpenApiLikeJsonExporterSpec.scala new file mode 100644 index 0000000..2b5dc3e --- /dev/null +++ b/src/test/scala/com/openbankproject/resourcedocs/exporter/OpenApiLikeJsonExporterSpec.scala @@ -0,0 +1,98 @@ +package com.openbankproject.resourcedocs.exporter + +import com.openbankproject.resourcedocs.core.model.{ImplementedByJson, OBPResourceDocJson, RoleInfoJson} +import io.circe.Json +import io.circe.parser.parse +import org.scalatest.funsuite.AnyFunSuite + +class OpenApiLikeJsonExporterSpec extends AnyFunSuite { + + private def parseJson(value: String): Json = + parse(value).fold(throw _, identity) + + private val getBanksDescriptionHtml: String = + "Get banks on this API instance
\nReturns a list of banks supported on this server:
User Authentication is Optional. The User need not be logged in.
\nJSON response body fields:
\n\nbank_routings: bank routing in form of (scheme, address)
\n\nfull_name: full name string
\nid: d8839721-ad8f-45dd-9f78-2080414b93f9
\nlogo: logo url
\nname: ACCOUNT_MANAGEMENT_FEE
\nscheme: OBP
\n\nvalue: 5987953
\nwebsite: www.openbankproject.com
\nattributes: attribute value in form of (name, value)
\n" + + private val getBanksDescriptionMarkdown: String = + "Get banks on this API instance\nReturns a list of banks supported on this server:\n\n* ID used as parameter in URLs\n* Short and full name of bank\n* Logo URL\n* Website\n\nUser Authentication is Optional. The User need not be logged in.\n\n\n**JSON response body fields:**\n\n\n\n[**address**](/glossary#address): \n\n\n\n[**bank_routings**](/glossary#bank_routings): bank routing in form of (scheme, address)\n\n\n\n[**banks**](/glossary#banks): \n\n\n\n[**full_name**](/glossary#full_name): full name string\n\n\n\n[**id**](/glossary#id): d8839721-ad8f-45dd-9f78-2080414b93f9\n\n\n\n[**logo**](/glossary#logo): logo url\n\n\n\n[**name**](/glossary#name): ACCOUNT_MANAGEMENT_FEE\n\n\n\n[**scheme**](/glossary#scheme): OBP\n\n\n\n[**short_name**](/glossary#short_name): \n\n\n\n[**value**](/glossary#): 5987953\n\n\n\n[**website**](/glossary#website): www.openbankproject.com\n\n\n\n[attributes](/glossary#attributes): attribute value in form of (name, value)\n\n\n" + + private val getBanksSuccessResponse = + parseJson( + """{"banks":[{"id":"gh.29.uk","short_name":"short_name ","full_name":"full_name","logo":"logo","website":"www.openbankproject.com","bank_routings":[{"scheme":"OBP","address":"gh.29.uk"}],"attributes":[{"name":"ACCOUNT_MANAGEMENT_FEE","value":"5987953"}]}]}""" + ) + + private val getBanksTypedSuccessResponse = + parseJson( + """{"type":"object","properties":{"banks":{"type":"array","items":{"type":"object","properties":{"bank_routings":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"scheme":{"type":"string"}}}},"website":{"type":"string"},"logo":{"type":"string"},"attributes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"short_name":{"type":"string"},"id":{"type":"string"},"full_name":{"type":"string"}}}}}}""" + ) + + private def getBanksDoc(operationId: String, verb: String, path: String, summary: String): OBPResourceDocJson = { + OBPResourceDocJson( + operation_id = operationId, + implemented_by = ImplementedByJson("OBPv4.0.0", "getBanks"), + request_verb = verb, + request_url = path, + summary = summary, + description = getBanksDescriptionHtml, + description_markdown = getBanksDescriptionMarkdown, + success_response_body = Some(getBanksSuccessResponse), + error_response_bodies = List("OBP-50000: Unknown Error."), + tags = List("Bank", "Account Information Service (AIS)", "PSD2"), + typed_success_response_body = Some(getBanksTypedSuccessResponse), + specified_url = "/obp/v5.1.0/banks" + ) + } + + test("export should group getBanks methods by path and verb") { + val getBanksV3 = getBanksDoc( + operationId = "OBPv3.0.0-getBanks", + verb = "GET", + path = "/obp/v3.0.0/banks", + summary = "Get Banks V3" + ) + val createBanks = getBanksDoc( + operationId = "OBPv4.0.0-createBank", + verb = "POST", + path = "/obp/v4.0.0/banks", + summary = "Create Bank" + ) + val getBankAttributes = getBanksDoc( + operationId = "OBPv4.0.0-getBankAttributes", + verb = "GET", + path = "/obp/v4.0.0/banks/{bankId}/attributes", + summary = "Get Bank Attributes" + ) + + val json = OpenApiLikeJsonExporter.render(Seq(getBankAttributes, createBanks, getBanksV3)) + + assert(json.contains("\"openapi\": \"3.0.0\"")) + assert(json.contains("\"/obp/v3.0.0/banks\"")) + assert(json.contains("\"/obp/v4.0.0/banks\"")) + assert(json.contains("\"post\"")) + assert(json.contains("\"get\"")) + assert(json.contains("\"operationId\": \"OBPv3.0.0-getBanks\"")) + assert(json.contains("\"operationId\": \"OBPv4.0.0-createBank\"")) + assert(json.contains("\"/obp/v4.0.0/banks/{bankId}/attributes\"")) + assert(json.contains("\"operationId\": \"OBPv4.0.0-getBankAttributes\"")) + } + + test("export should encode getBanks role expressions and include response codes") { + val securedDoc = getBanksDoc( + operationId = "OBPv5.1.0-getBanksSecure", + verb = "GET", + path = "/obp/v5.1.0/banks", + summary = "Get Banks Secure" + ).copy( + roles = Some(List(RoleInfoJson("CanGetBanks"), RoleInfoJson("CanReadBanks"))), + error_response_bodies = List("OBP-40300: Insufficient Privileges.", "OBP-50000: Unknown Error.") + ) + + val json = OpenApiLikeJsonExporter.render(Seq(securedDoc)) + + assert(json.contains("\"rolesAnyOf\"")) + assert(json.contains("CanGetBanks")) + assert(json.contains("CanReadBanks")) + assert(json.contains("\"40300\"")) + assert(json.contains("\"50000\"")) + } +}