From 53daff339fbc15f7c611bc8a663f09f89012c63f Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Thu, 28 Nov 2024 23:12:32 +0100 Subject: [PATCH 01/16] Implement an OpenAPIComparator --- .../validation/OpenAPIComparator.scala | 170 ++++++++++++ .../OpenAPICompatibilityIssue.scala | 88 +++++++ .../validation/OpenAPIComparatorTest.scala | 249 ++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala create mode 100644 openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala create mode 100644 openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala new file mode 100644 index 0000000..d5e0554 --- /dev/null +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -0,0 +1,170 @@ +package sttp.apispec.openapi.validation + +import sttp.apispec.{Schema, SchemaLike} +import sttp.apispec.openapi.{MediaType, OpenAPI, Operation, Parameter, PathItem} +import sttp.apispec.validation.SchemaComparator + +class OpenAPIComparator( + writerOpenAPI: OpenAPI, + readerOpenAPI: OpenAPI +) { + private val httpMethods = List("get", "post", "patch", "delete", "options", "trace", "head", "put") + + def compare(): List[OpenAPICompatibilityIssue] = { + writerOpenAPI.paths.pathItems.toList.flatMap { + case (pathName, writerPathItem) => + val readerPathItem = readerOpenAPI.paths.pathItems.get(pathName) + readerPathItem match { + case None => Some(MissingPath(pathName)) + case Some(readerPathItem) => + val pathIssues = checkPath(pathName, writerPathItem, readerPathItem) + if (pathIssues.isEmpty) + None + else + pathIssues + case _ => None + } + case _ => None + } + } + + private def checkPath( + pathName: String, + writerPathItem: PathItem, + readerPathItem: PathItem + ): Option[IncompatiblePath] = { + val issues = httpMethods.flatMap { httpMethod => + val writerOperation = getOperation(writerPathItem, httpMethod) + val readerOperation = getOperation(readerPathItem, httpMethod) + + (readerOperation, writerOperation) match { + case (None, Some(_)) => Some(MissingOperation(httpMethod)) + case (Some(readerOp), Some(writerOp)) => checkOperation(httpMethod, readerOp, writerOp) + case _ => None + } + } + if (issues.isEmpty) + None + else + Some(IncompatiblePath(pathName, issues)) + } + private def getOperation(pathItem: PathItem, httpMethod: String): Option[Operation] = httpMethod match { + case "get" => pathItem.get + case "patch" => pathItem.patch + case "delete" => pathItem.delete + case "options" => pathItem.options + case "trace" => pathItem.trace + case "head" => pathItem.head + case "post" => pathItem.post + case "put" => pathItem.put + case _ => None + } + + private def checkOperation( + httpMethod: String, + readerOperation: Operation, + writerOperation: Operation + ): Option[IncompatibleOperation] = { + val readerParameters = getOperationParameters(readerOperation) + val writerParameters = getOperationParameters(writerOperation) + + val issues = writerParameters.flatMap { writerParameter => + val readerParameter = readerParameters.find(_.name == writerParameter.name) + readerParameter match { + case None => Some(MissingParameter(writerParameter.name)) + case Some(readerParameter) => checkParameter(readerParameter, writerParameter) + } + } + + if (issues.isEmpty) + None + else + Some(IncompatibleOperation(httpMethod, issues)) + } + + private def checkParameter(readerParameter: Parameter, writerParameter: Parameter): Option[IncompatibleParameter] = { + val isCompatibleStyle = readerParameter.style == writerParameter.style + val isCompatibleExplode = readerParameter.explode == writerParameter.explode + val isCompatibleAllowEmptyValue = readerParameter.allowEmptyValue == writerParameter.allowEmptyValue + val isCompatibleAllowReserved = readerParameter.allowReserved == writerParameter.allowReserved + + val issues = + checkSchema(readerParameter.schema, writerParameter.schema).toList ++ + checkParameterContent(readerParameter, writerParameter).toList ++ + (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ + (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ + (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ + (if (!isCompatibleAllowReserved) Some(MissMatch("allowReserved")) else None).toList + + if (issues.isEmpty) + None + else + Some(IncompatibleParameter(writerParameter.name, issues)) + } + + private def checkParameterContent( + readerParameter: Parameter, + writerParameter: Parameter + ): Option[IncompatibleParameterContent] = { + val issues = writerParameter.content.flatMap { case (writerMediaType, writerMediaTypeDescription) => + val readerMediaTypeDescription = readerParameter.content.get(writerMediaType) + readerMediaTypeDescription match { + case None => Some(MissingMediaType(writerMediaType)) + case Some(readerMediaTypeDescription) => + checkMediaType(writerMediaType, writerMediaTypeDescription, readerMediaTypeDescription) + } + } + + if (issues.isEmpty) + None + else + Some(IncompatibleParameterContent(issues.toList)) + } + + private def checkMediaType( + mediaType: String, + writerMediaTypeDescription: MediaType, + readerMediaTypeDescription: MediaType + ): Option[IncompatibleMediaType] = { + val issues = checkSchema(writerMediaTypeDescription.schema, readerMediaTypeDescription.schema) + if (issues.nonEmpty) + Some(IncompatibleMediaType(mediaType, issues.toList)) + else + None + // TODO: encoding? + } + + private def checkSchema( + readerSchema: Option[SchemaLike], + writerSchema: Option[SchemaLike] + ): Option[OpenAPICompatibilityIssue] = { + (readerSchema, writerSchema) match { + case (Some(readerSchema: Schema), Some(writerSchema: Schema)) => + val readerSchemas = Map("readerSchema" -> readerSchema) + val writerSchemas = Map("writerSchema" -> writerSchema) + + val schemaComparator = new SchemaComparator(readerSchemas, writerSchemas) + val schemaIssues = schemaComparator.compare(readerSchema, writerSchema) + if (schemaIssues.nonEmpty) + Some(IncompatibleSchema(schemaIssues)) + else + None + case _ => None + + } + } + + private def getOperationParameters(operation: Operation): List[Parameter] = { + operation.parameters.flatMap { + case Right(parameter) => Some(parameter) + case Left(reference) => resolveParameterReference(readerOpenAPI, reference.$ref) + } + } + + private def resolveParameterReference(openAPI: OpenAPI, ref: String): Option[Parameter] = { + openAPI.components match { + case Some(component) => component.getLocalParameter(ref) + case None => None + } + } +} diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala new file mode 100644 index 0000000..4e09719 --- /dev/null +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -0,0 +1,88 @@ +package sttp.apispec.openapi.validation + +import sttp.apispec.validation.SchemaCompatibilityIssue + +sealed abstract class OpenAPICompatibilityIssue { + def description: String + + override def toString: String = description + protected def issuesRepr(issues: List[OpenAPICompatibilityIssue]): String = + issues.iterator + .map(i => s"- ${i.description.replace("\n", "\n ")}") // indent + .mkString("\n") +} + +sealed abstract class SubOpenAPICompatibilityIssue extends OpenAPICompatibilityIssue { + def subIssues: List[OpenAPICompatibilityIssue] +} + +case class MissingPath(pathName: String) extends OpenAPICompatibilityIssue { + def description: String = + s"missing path: $pathName" +} + +case class IncompatiblePath( + pathName: String, + subIssues: List[OpenAPICompatibilityIssue] +) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible path $pathName:\n${issuesRepr(subIssues)}" +} + +case class MissingOperation(httpMethod: String) extends OpenAPICompatibilityIssue { + def description: String = + s"missing operation for $httpMethod method" +} + +case class IncompatibleOperation( + httpMethod: String, + subIssues: List[OpenAPICompatibilityIssue] +) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible operation $httpMethod:\n${issuesRepr(subIssues)}" +} + +case class MissingParameter( + name: String +) extends OpenAPICompatibilityIssue { + def description: String = + s"missing parameter $name" +} + +case class IncompatibleParameter( + name: String, + subIssues: List[OpenAPICompatibilityIssue] +) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible parameter $name:\n${issuesRepr(subIssues)}" +} + +case class IncompatibleSchema( + schemaIssues: List[SchemaCompatibilityIssue] +) extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible schema:\n${schemaIssues}" +} + +case class IncompatibleParameterContent( + subIssues: List[OpenAPICompatibilityIssue] +) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible parameter content:\n${issuesRepr(subIssues)}" +} + +case class MissingMediaType(mediaType: String) extends OpenAPICompatibilityIssue { + def description: String = + s"missing media type $mediaType" +} + +case class IncompatibleMediaType(mediaType: String, subIssues: List[OpenAPICompatibilityIssue]) + extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible media type $mediaType:\n${issuesRepr(subIssues)}" +} + +case class MissMatch(name: String) extends OpenAPICompatibilityIssue { + def description: String = + s"miss match $name" +} diff --git a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala new file mode 100644 index 0000000..30ece33 --- /dev/null +++ b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -0,0 +1,249 @@ +package sttp.apispec.openapi.validation + +import org.scalatest.funsuite.AnyFunSuite +import sttp.apispec.{Schema, SchemaType} +import sttp.apispec.openapi.{ + Info, + MediaType, + OpenAPI, + Operation, + Parameter, + ParameterIn, + ParameterStyle, + PathItem, + Paths +} +import sttp.apispec.validation.TypeMismatch + +import scala.collection.immutable.ListMap + +class OpenAPIComparatorTest extends AnyFunSuite { + private val paths = Paths(pathItems = ListMap("/test" -> PathItem())) + private val pathItem = PathItem() + private val operation = Operation() + private val parameter = Parameter("test", ParameterIn.Path, schema = None) + private val mediaType = MediaType() + + test("missing path") { + val readerOpenAPI = OpenAPI(info = Info("", "")) + val writerOpenAPI = OpenAPI(info = Info("", ""), paths = paths) + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val expected = List(MissingPath("/test")) + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> missing operation") { + val readerOpenAPI = OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem)) + val writerOpenAPI = OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation))) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val operationIssue = MissingOperation("get") + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> missing parameter") { + val readerOpenAPI = + OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation))) + + val writerOpenAPI = + OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter)))) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val parameterIssue = MissingParameter("test") + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible parameter -> incompatible schema") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = + paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.schema(Schema(SchemaType.Integer))))) + ) + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = + paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.schema(Schema(SchemaType.String))))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val parameterIssue = IncompatibleParameter("test", List(schemaIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test( + "incompatible path -> incompatible operation -> incompatible parameter -> incompatible content -> missing media type" + ) { + val readerOpenAPI = + OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter)))) + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = + paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.addMediaType("test", mediaType)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val mediaTypeIssue = MissingMediaType("test") + val parameterContentIssue = IncompatibleParameterContent(List(mediaTypeIssue)) + val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test( + "incompatible path -> incompatible operation -> incompatible parameter -> incompatible content -> incompatible media type -> incompatible schema" + ) { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem( + "/test", + pathItem.get( + operation.addParameter(parameter.addMediaType("test", mediaType.schema(Schema(SchemaType.Integer)))) + ) + ) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem( + "/test", + pathItem.get( + operation.addParameter(parameter.addMediaType("test", mediaType.schema(Schema(SchemaType.String)))) + ) + ) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)))) + val mediaTypeIssue = IncompatibleMediaType("test", List(schemaMismatch)) + val parameterContentIssue = IncompatibleParameterContent(List(mediaTypeIssue)) + val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible parameter -> style miss match") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.style(ParameterStyle.Label)))) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.style(ParameterStyle.Simple)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val missMatchIssue = MissMatch("style") + val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible parameter -> explode miss match") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.explode(false)))) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.explode(true)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val missMatchIssue = MissMatch("explode") + val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible parameter -> allowEmptyValue miss match") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowEmptyValue(false)))) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowEmptyValue(true)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val missMatchIssue = MissMatch("allowEmptyValue") + val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible parameter -> allowReserved miss match") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowReserved(false)))) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowReserved(true)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val missMatchIssue = MissMatch("allowReserved") + val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } +} From ba352ba87af298863c55bcf33328ec655c8a20e0 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Fri, 29 Nov 2024 18:38:04 +0100 Subject: [PATCH 02/16] Implement compatibility checks for request body and responses --- .../validation/OpenAPIComparator.scala | 99 +++++++++++++++++-- .../OpenAPICompatibilityIssue.scala | 36 ++++++- .../validation/OpenAPIComparatorTest.scala | 85 +++++++++++++++- 3 files changed, 205 insertions(+), 15 deletions(-) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index d5e0554..ee98a39 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -1,9 +1,11 @@ package sttp.apispec.openapi.validation import sttp.apispec.{Schema, SchemaLike} -import sttp.apispec.openapi.{MediaType, OpenAPI, Operation, Parameter, PathItem} +import sttp.apispec.openapi.{Header, MediaType, OpenAPI, Operation, Parameter, PathItem, RequestBody, Response} import sttp.apispec.validation.SchemaComparator +import scala.collection.immutable.ListMap + class OpenAPIComparator( writerOpenAPI: OpenAPI, readerOpenAPI: OpenAPI @@ -68,7 +70,7 @@ class OpenAPIComparator( val readerParameters = getOperationParameters(readerOperation) val writerParameters = getOperationParameters(writerOperation) - val issues = writerParameters.flatMap { writerParameter => + val parametersIssue = writerParameters.flatMap { writerParameter => val readerParameter = readerParameters.find(_.name == writerParameter.name) readerParameter match { case None => Some(MissingParameter(writerParameter.name)) @@ -76,6 +78,26 @@ class OpenAPIComparator( } } + val requestBodyIssue = (writerOperation.requestBody, readerOperation.requestBody) match { + case (Some(Right(writerRequestBody)), Some(Right(readerRequestBody))) => + checkRequestBody(writerRequestBody, readerRequestBody) + case (Some(Right(_)), None) => Some(MissingRequestBody()) + case _ => None + } + + val responsesIssues = writerOperation.responses.responses.flatMap { + case (writerResponseKey, Right(writerResponse)) => + val readerResponse = readerOperation.responses.responses.get(writerResponseKey) + readerResponse match { + case Some(Right(readerResponse)) => checkResponse(writerResponse, readerResponse) + case None => Some(MissingResponse(writerResponseKey)) + case _ => None + } + case _ => None + } + + // TODO: callbacks, security? + val issues = parametersIssue ++ requestBodyIssue ++ responsesIssues if (issues.isEmpty) None else @@ -90,7 +112,7 @@ class OpenAPIComparator( val issues = checkSchema(readerParameter.schema, writerParameter.schema).toList ++ - checkParameterContent(readerParameter, writerParameter).toList ++ + checkContent(readerParameter.content, writerParameter.content).toList ++ (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ @@ -102,12 +124,12 @@ class OpenAPIComparator( Some(IncompatibleParameter(writerParameter.name, issues)) } - private def checkParameterContent( - readerParameter: Parameter, - writerParameter: Parameter - ): Option[IncompatibleParameterContent] = { - val issues = writerParameter.content.flatMap { case (writerMediaType, writerMediaTypeDescription) => - val readerMediaTypeDescription = readerParameter.content.get(writerMediaType) + private def checkContent( + readerContent: ListMap[String, MediaType], + writerContent: ListMap[String, MediaType] + ): Option[IncompatibleContent] = { + val issues = writerContent.flatMap { case (writerMediaType, writerMediaTypeDescription) => + val readerMediaTypeDescription = readerContent.get(writerMediaType) readerMediaTypeDescription match { case None => Some(MissingMediaType(writerMediaType)) case Some(readerMediaTypeDescription) => @@ -118,7 +140,7 @@ class OpenAPIComparator( if (issues.isEmpty) None else - Some(IncompatibleParameterContent(issues.toList)) + Some(IncompatibleContent(issues.toList)) } private def checkMediaType( @@ -167,4 +189,61 @@ class OpenAPIComparator( case None => None } } + + private def checkRequestBody( + writerRequestBody: RequestBody, + readerRequestBody: RequestBody + ): Option[IncompatibleRequestBody] = { + val contentIssues = checkContent(readerRequestBody.content, writerRequestBody.content).toList + if (contentIssues.nonEmpty) + Some(IncompatibleRequestBody(contentIssues)) + else + None + } + + private def checkResponse(writerResponse: Response, readerResponse: Response): Option[IncompatibleResponse] = { + val contentIssue = checkContent(readerResponse.content, writerResponse.content) + val headerIssues = writerResponse.headers.flatMap { + case (writerHeaderName, Right(writerHeader)) => + val readerHeader = readerResponse.headers.get(writerHeaderName) + readerHeader match { + case Some(Right(readerHeader)) => checkHeader(writerHeaderName, writerHeader, readerHeader) + case None => Some(MissingHeader(writerHeaderName)) + case _ => None + } + case _ => None + } + + val issues = contentIssue.toList ++ headerIssues + if (issues.nonEmpty) + Some(IncompatibleResponse(issues)) + else + None + } + + private def checkHeader( + headerName: String, + writerHeader: Header, + readerHeader: Header + ): Option[IncompatibleHeader] = { + val schemaIssues = checkSchema(readerHeader.schema, writerHeader.schema) + val contentIssue = checkContent(readerHeader.content, writerHeader.content) + val isCompatibleStyle = readerHeader.style == writerHeader.style + val isCompatibleExplode = readerHeader.explode == writerHeader.explode + val isCompatibleAllowEmptyValue = readerHeader.allowEmptyValue == writerHeader.allowEmptyValue + val isCompatibleAllowReserved = readerHeader.allowReserved == writerHeader.allowReserved + + val issues = + schemaIssues.toList ++ + contentIssue.toList ++ + (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ + (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ + (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ + (if (!isCompatibleAllowReserved) Some(MissMatch("allowReserved")) else None).toList + + if (issues.nonEmpty) + Some(IncompatibleHeader(headerName, issues)) + else + None + } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 4e09719..0ee0db4 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -1,5 +1,6 @@ package sttp.apispec.openapi.validation +import sttp.apispec.openapi.ResponsesKey import sttp.apispec.validation.SchemaCompatibilityIssue sealed abstract class OpenAPICompatibilityIssue { @@ -64,11 +65,11 @@ case class IncompatibleSchema( s"incompatible schema:\n${schemaIssues}" } -case class IncompatibleParameterContent( +case class IncompatibleContent( subIssues: List[OpenAPICompatibilityIssue] ) extends SubOpenAPICompatibilityIssue { def description: String = - s"incompatible parameter content:\n${issuesRepr(subIssues)}" + s"incompatible content:\n${issuesRepr(subIssues)}" } case class MissingMediaType(mediaType: String) extends OpenAPICompatibilityIssue { @@ -86,3 +87,34 @@ case class MissMatch(name: String) extends OpenAPICompatibilityIssue { def description: String = s"miss match $name" } + +case class MissingRequestBody() extends OpenAPICompatibilityIssue { + def description: String = + s"missing request body" +} + +case class IncompatibleRequestBody(subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible request body:\n${issuesRepr(subIssues)}" +} + +case class MissingResponse(responsesKey: ResponsesKey) extends OpenAPICompatibilityIssue { + def description: String = + s"missing response for $responsesKey" +} + +case class IncompatibleResponse(subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible response:\n${issuesRepr(subIssues)}" +} + +case class MissingHeader(headerName: String) extends OpenAPICompatibilityIssue { + def description: String = + s"missing header $headerName" +} + +case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompatibilityIssue]) + extends SubOpenAPICompatibilityIssue { + def description: String = + s"incompatible header $headerName:\n${issuesRepr(subIssues)}" +} diff --git a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 30ece33..3277227 100644 --- a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -3,6 +3,7 @@ package sttp.apispec.openapi.validation import org.scalatest.funsuite.AnyFunSuite import sttp.apispec.{Schema, SchemaType} import sttp.apispec.openapi.{ + Header, Info, MediaType, OpenAPI, @@ -11,7 +12,10 @@ import sttp.apispec.openapi.{ ParameterIn, ParameterStyle, PathItem, - Paths + Paths, + RequestBody, + Response, + ResponsesCodeKey } import sttp.apispec.validation.TypeMismatch @@ -23,6 +27,9 @@ class OpenAPIComparatorTest extends AnyFunSuite { private val operation = Operation() private val parameter = Parameter("test", ParameterIn.Path, schema = None) private val mediaType = MediaType() + private val requestBody = RequestBody() + private val response = Response() + private val header = Header() test("missing path") { val readerOpenAPI = OpenAPI(info = Info("", "")) @@ -104,7 +111,7 @@ class OpenAPIComparatorTest extends AnyFunSuite { val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) val mediaTypeIssue = MissingMediaType("test") - val parameterContentIssue = IncompatibleParameterContent(List(mediaTypeIssue)) + val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/test", List(operationIssue)) @@ -142,7 +149,7 @@ class OpenAPIComparatorTest extends AnyFunSuite { val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)))) val mediaTypeIssue = IncompatibleMediaType("test", List(schemaMismatch)) - val parameterContentIssue = IncompatibleParameterContent(List(mediaTypeIssue)) + val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/test", List(operationIssue)) @@ -246,4 +253,76 @@ class OpenAPIComparatorTest extends AnyFunSuite { assert(openAPIComparator.compare() == expected) } + + test("incompatible path -> incompatible operation -> missing request body") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation)) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.requestBody(requestBody))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val requestBodyIssue = MissingRequestBody() + val operationIssue = IncompatibleOperation("get", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible path -> incompatible operation -> incompatible responses -> missing response") { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation)) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val reponsesIssue = MissingResponse(ResponsesCodeKey(200)) + val operationIssue = IncompatibleOperation("get", List(reponsesIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test( + "incompatible path -> incompatible operation -> incompatible responses -> incompatible response -> missing header" + ) { + val readerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response))) + ) + + val writerOpenAPI = + OpenAPI( + info = Info("", ""), + paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response.addHeader("test", header)))) + ) + + val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) + + val headerIssue = MissingHeader("test") + val reponsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(reponsesIssue)) + val pathIssue = IncompatiblePath("/test", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } } From fefeb58d191f1b12f9cd197b47a525a6344ad3bb Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Fri, 29 Nov 2024 21:53:57 +0100 Subject: [PATCH 03/16] Ensure consistent parameter order across all methods --- .../validation/OpenAPIComparator.scala | 24 +++++++++---------- .../validation/OpenAPIComparatorTest.scala | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index ee98a39..ae7d0e0 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -41,7 +41,7 @@ class OpenAPIComparator( (readerOperation, writerOperation) match { case (None, Some(_)) => Some(MissingOperation(httpMethod)) - case (Some(readerOp), Some(writerOp)) => checkOperation(httpMethod, readerOp, writerOp) + case (Some(readerOp), Some(writerOp)) => checkOperation(httpMethod, writerOp, readerOp) case _ => None } } @@ -64,8 +64,8 @@ class OpenAPIComparator( private def checkOperation( httpMethod: String, - readerOperation: Operation, - writerOperation: Operation + writerOperation: Operation, + readerOperation: Operation ): Option[IncompatibleOperation] = { val readerParameters = getOperationParameters(readerOperation) val writerParameters = getOperationParameters(writerOperation) @@ -74,7 +74,7 @@ class OpenAPIComparator( val readerParameter = readerParameters.find(_.name == writerParameter.name) readerParameter match { case None => Some(MissingParameter(writerParameter.name)) - case Some(readerParameter) => checkParameter(readerParameter, writerParameter) + case Some(readerParameter) => checkParameter(writerParameter, readerParameter) } } @@ -104,15 +104,15 @@ class OpenAPIComparator( Some(IncompatibleOperation(httpMethod, issues)) } - private def checkParameter(readerParameter: Parameter, writerParameter: Parameter): Option[IncompatibleParameter] = { + private def checkParameter(writerParameter: Parameter, readerParameter: Parameter): Option[IncompatibleParameter] = { val isCompatibleStyle = readerParameter.style == writerParameter.style val isCompatibleExplode = readerParameter.explode == writerParameter.explode val isCompatibleAllowEmptyValue = readerParameter.allowEmptyValue == writerParameter.allowEmptyValue val isCompatibleAllowReserved = readerParameter.allowReserved == writerParameter.allowReserved val issues = - checkSchema(readerParameter.schema, writerParameter.schema).toList ++ - checkContent(readerParameter.content, writerParameter.content).toList ++ + checkSchema(writerParameter.schema, readerParameter.schema).toList ++ + checkContent(writerParameter.content, readerParameter.content).toList ++ (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ @@ -125,8 +125,8 @@ class OpenAPIComparator( } private def checkContent( - readerContent: ListMap[String, MediaType], - writerContent: ListMap[String, MediaType] + writerContent: ListMap[String, MediaType], + readerContent: ListMap[String, MediaType] ): Option[IncompatibleContent] = { val issues = writerContent.flatMap { case (writerMediaType, writerMediaTypeDescription) => val readerMediaTypeDescription = readerContent.get(writerMediaType) @@ -157,8 +157,8 @@ class OpenAPIComparator( } private def checkSchema( - readerSchema: Option[SchemaLike], - writerSchema: Option[SchemaLike] + writerSchema: Option[SchemaLike], + readerSchema: Option[SchemaLike] ): Option[OpenAPICompatibilityIssue] = { (readerSchema, writerSchema) match { case (Some(readerSchema: Schema), Some(writerSchema: Schema)) => @@ -226,7 +226,7 @@ class OpenAPIComparator( writerHeader: Header, readerHeader: Header ): Option[IncompatibleHeader] = { - val schemaIssues = checkSchema(readerHeader.schema, writerHeader.schema) + val schemaIssues = checkSchema(writerHeader.schema, readerHeader.schema) val contentIssue = checkContent(readerHeader.content, writerHeader.content) val isCompatibleStyle = readerHeader.style == writerHeader.style val isCompatibleExplode = readerHeader.explode == writerHeader.explode diff --git a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 3277227..1f4dca1 100644 --- a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -147,7 +147,7 @@ class OpenAPIComparatorTest extends AnyFunSuite { val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)))) + val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)))) val mediaTypeIssue = IncompatibleMediaType("test", List(schemaMismatch)) val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) From cc642e1f89287dfc578bbc60a1f8c44634916e23 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Thu, 5 Dec 2024 22:41:03 +0100 Subject: [PATCH 04/16] Fix issues and update tests --- build.sbt | 20 + .../petstore/basic-petstore-v_1_0_1.json | 63 +++ .../petstore/basic-petstore-v_1_0_2.json | 96 +++++ .../petstore/basic-petstore-v_1_0_3.json | 133 ++++++ .../petstore/basic-petstore-v_1_0_4.json | 148 +++++++ .../petstore/basic-petstore-v_1_0_5.json | 151 +++++++ .../petstore/basic-petstore-v_1_0_6.json | 151 +++++++ .../petstore/basic-petstore-v_1_0_7.json | 148 +++++++ .../petstore/basic-petstore-v_1_0_8.json | 149 +++++++ .../petstore/basic-petstore-v_1_0_9.json | 150 +++++++ .../petstore/basic-petstore-v_1_1_0.json | 151 +++++++ .../petstore/basic-petstore-v_1_1_1.json | 152 +++++++ .../petstore/basic-petstore-v_1_1_2.json | 175 ++++++++ .../petstore/basic-petstore-v_1_1_3.json | 175 ++++++++ .../petstore/basic-petstore-v_1_1_4.json | 178 ++++++++ .../petstore/basic-petstore-v_1_1_5.json | 183 ++++++++ .../petstore/basic-petstore-v_1_1_6.json | 183 ++++++++ .../petstore/basic-petstore-v_1_1_7.json | 191 +++++++++ .../petstore/basic-petstore-v_1_1_8.json | 191 +++++++++ .../petstore/basic-petstore-v_1_1_9.json | 195 +++++++++ .../petstore/basic-petstore-v_1_2_0.json | 195 +++++++++ .../petstore/basic-petstore-v_1_2_1.json | 196 +++++++++ .../petstore/basic-petstore-v_1_2_2.json | 197 +++++++++ .../petstore/basic-petstore-v_1_2_3.json | 198 +++++++++ .../petstore/basic-petstore-v_1_2_4.json | 199 +++++++++ .../validation/OpenAPIComparatorTest.scala | 395 ++++++++++++++++++ .../validation/OpenAPIComparator.scala | 201 +++++---- .../OpenAPICompatibilityIssue.scala | 17 + .../validation/OpenAPIComparatorTest.scala | 328 --------------- 29 files changed, 4498 insertions(+), 411 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json create mode 100644 openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala delete mode 100644 openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala diff --git a/build.sbt b/build.sbt index bb76542..5578452 100644 --- a/build.sbt +++ b/build.sbt @@ -262,3 +262,23 @@ lazy val asyncapiCirceYaml: ProjectMatrix = (projectMatrix in file("asyncapi-cir settings = commonJvmSettings ) .dependsOn(asyncapiCirce) + +lazy val openapiComparatorTests: ProjectMatrix = (projectMatrix in file("openapi-comparator-tests")) + .settings(commonSettings) + .settings( + name := "openapi-comparator-tests", + publish / skip := true + ) + .jvmPlatform( + scalaVersions = scalaJVMVersions, + settings = commonJvmSettings + ) + .jsPlatform( + scalaVersions = scalaJSVersions, + settings = commonJsSettings + ) + .nativePlatform( + scalaVersions = scalaNativeVersions, + settings = commonNativeSettings + ) + .dependsOn(openapiModel, openapiCirce, circeTestUtils % Test) \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json new file mode 100644 index 0000000..a7f22e5 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json @@ -0,0 +1,63 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.1" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json new file mode 100644 index 0000000..b202960 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json new file mode 100644 index 0000000..d840ecd --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json @@ -0,0 +1,133 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.3" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json new file mode 100644 index 0000000..ab2b215 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.4" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "schema": { + "type": "string", + "enum": [ + "available", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json new file mode 100644 index 0000000..b89c469 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.5" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "sold" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json new file mode 100644 index 0000000..fc268bb --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.6" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json new file mode 100644 index 0000000..319759b --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.7" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json new file mode 100644 index 0000000..df50c9f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json @@ -0,0 +1,149 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.8" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json new file mode 100644 index 0000000..e616bc2 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json @@ -0,0 +1,150 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.9" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json new file mode 100644 index 0000000..ec035d4 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.0" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json new file mode 100644 index 0000000..ed87cde --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json @@ -0,0 +1,152 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.1" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json new file mode 100644 index 0000000..9749b19 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json new file mode 100644 index 0000000..a7eb4e6 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json new file mode 100644 index 0000000..cff46ba --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json @@ -0,0 +1,178 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json new file mode 100644 index 0000000..4cd017f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json @@ -0,0 +1,183 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json new file mode 100644 index 0000000..f9fb574 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json @@ -0,0 +1,183 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json new file mode 100644 index 0000000..0ff614e --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "schema": { + "type": "integer" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json new file mode 100644 index 0000000..0d458b4 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json new file mode 100644 index 0000000..4a90228 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json new file mode 100644 index 0000000..e571041 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json new file mode 100644 index 0000000..b6e3332 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json @@ -0,0 +1,196 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json new file mode 100644 index 0000000..1269aca --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json @@ -0,0 +1,197 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json new file mode 100644 index 0000000..76dfd15 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json @@ -0,0 +1,198 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json new file mode 100644 index 0000000..d9e3872 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala new file mode 100644 index 0000000..c6f5429 --- /dev/null +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -0,0 +1,395 @@ +package sttp.apispec.openapi.validation + +import org.scalatest.funsuite.AnyFunSuite +import sttp.apispec.SchemaType +import sttp.apispec.openapi.{OpenAPI, ResponsesCodeKey} +import sttp.apispec.openapi.circe.openAPIDecoder +import sttp.apispec.test.ResourcePlatform +import sttp.apispec.validation.TypeMismatch + +class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { + override val basedir = "openapi-comparator-tests" + + test("identical") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + assert(openAPIComparator.compare().isEmpty) + } + + test("missing path") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_2.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_1.json").flatMap(_.as[OpenAPI]): @unchecked + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val expected = List(MissingPath("/pets/{petId}")) + + assert(openAPIComparator.compare() == expected) + } + + test("missing operation") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_3.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_2.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val operationIssue = MissingOperation("post") + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("missing parameter") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_4.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_3.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val parameterIssue = MissingParameter("status") + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible parameter schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_5.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_4.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.Array), List(SchemaType.String)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val parameterIssue = IncompatibleParameter("status", List(schemaIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("parameter content is missing media type") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_6.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_5.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val mediaTypeIssue = MissingMediaType("application/json") + val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val parameterIssue = IncompatibleParameter("status", List(parameterContentIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible media type parameter content schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_7.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_6.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Array)))) + val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaMismatch)) + val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val parameterIssue = IncompatibleParameter("status", List(parameterContentIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible parameter style") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_8.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_7.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("style") + val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible parameter explode") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_9.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_8.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("explode") + val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible parameter allowEmptyValue") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_0.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_9.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("allowEmptyValue") + val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible parameter allowReserved") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_0.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("allowReserved") + val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + +// test("missing request body") { +// val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked +// val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked +// +// val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) +// +// val requestBodyIssue = MissingRequestBody() +// val operationIssue = IncompatibleOperation("get", List(requestBodyIssue)) +// val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) +// val expected = List(pathIssue) +// +// assert(openAPIComparator.compare() == expected) +// } + + test("missing request body content media type") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val mediaTypeIssue = MissingMediaType("application/xml") + val requestBodyContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val requestBodyIssue = IncompatibleRequestBody(List(requestBodyContentIssue)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible request body content media type schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_3.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaIssue)) + val requestBodyContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val requestBodyIssue = IncompatibleRequestBody(List(requestBodyContentIssue)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("missing response") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_4.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_3.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val responsesIssue = MissingResponse(ResponsesCodeKey(500)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("missing response content media type") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_5.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_4.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val mediaTypeIssues = MissingMediaType("application/xml") + val responseContentIssues = IncompatibleContent(List(mediaTypeIssues)) + val responsesIssue = IncompatibleResponse(List(responseContentIssues)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible response content media type schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_6.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_5.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val mediaTypeIssues = IncompatibleMediaType("application/xml", List(schemaIssue)) + val responseContentIssues = IncompatibleContent(List(mediaTypeIssues)) + val responsesIssue = IncompatibleResponse(List(responseContentIssues)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("missing header") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_7.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_6.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val headerIssue = MissingHeader("X-Rate-Limit") + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_8.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_7.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(schemaIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("missing header content media type") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_9.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_8.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val mediaTypeIssues = MissingMediaType("application/json") + val contentIssues = IncompatibleContent(List(mediaTypeIssues)) + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(contentIssues)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header content media type schema") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_0.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_9.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val schemaTypeMismatch = TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)) + val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) + val mediaTypeIssues = IncompatibleMediaType("application/json", List(schemaIssue)) + val contentIssues = IncompatibleContent(List(mediaTypeIssues)) + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(contentIssues)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header style") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_1.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_0.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("style") + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header explode") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_2.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_1.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("explode") + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header allowEmptyValue") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_3.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_2.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("allowEmptyValue") + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } + + test("incompatible header allowReserved") { + val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_3.json").flatMap(_.as[OpenAPI]): @unchecked + + val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + + val missMatchIssue = MissMatch("allowReserved") + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(openAPIComparator.compare() == expected) + } +} diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index ae7d0e0..33ff4a1 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -7,19 +7,23 @@ import sttp.apispec.validation.SchemaComparator import scala.collection.immutable.ListMap class OpenAPIComparator( - writerOpenAPI: OpenAPI, - readerOpenAPI: OpenAPI + clientOpenAPI: OpenAPI, + serverOpenAPI: OpenAPI ) { private val httpMethods = List("get", "post", "patch", "delete", "options", "trace", "head", "put") + private var serverSchemas: Map[String, Schema] = Map.empty[String, Schema] + private var clientSchemas: Map[String, Schema] = Map.empty[String, Schema] def compare(): List[OpenAPICompatibilityIssue] = { - writerOpenAPI.paths.pathItems.toList.flatMap { - case (pathName, writerPathItem) => - val readerPathItem = readerOpenAPI.paths.pathItems.get(pathName) - readerPathItem match { + initSchemas() + + clientOpenAPI.paths.pathItems.toList.flatMap { + case (pathName, clientPathItem) => + val serverPathItem = serverOpenAPI.paths.pathItems.get(pathName) + serverPathItem match { case None => Some(MissingPath(pathName)) - case Some(readerPathItem) => - val pathIssues = checkPath(pathName, writerPathItem, readerPathItem) + case Some(serverPathItem) => + val pathIssues = checkPath(pathName, clientPathItem, serverPathItem) if (pathIssues.isEmpty) None else @@ -30,18 +34,38 @@ class OpenAPIComparator( } } + private def initSchemas(): Unit = { + clientSchemas = clientOpenAPI.components match { + case Some(components) => + components.schemas.flatMap { + case (key, schema: Schema) => Some(key, schema) + case _ => None + } + case _ => Map.empty[String, Schema] + } + + serverSchemas = serverOpenAPI.components match { + case Some(components) => + components.schemas.flatMap { + case (key, schema: Schema) => Some(key, schema) + case _ => None + } + case _ => Map.empty[String, Schema] + } + } + private def checkPath( pathName: String, - writerPathItem: PathItem, - readerPathItem: PathItem + clientPathItem: PathItem, + serverPathItem: PathItem ): Option[IncompatiblePath] = { val issues = httpMethods.flatMap { httpMethod => - val writerOperation = getOperation(writerPathItem, httpMethod) - val readerOperation = getOperation(readerPathItem, httpMethod) + val clientOperation = getOperation(clientPathItem, httpMethod) + val serverOperation = getOperation(serverPathItem, httpMethod) - (readerOperation, writerOperation) match { - case (None, Some(_)) => Some(MissingOperation(httpMethod)) - case (Some(readerOp), Some(writerOp)) => checkOperation(httpMethod, writerOp, readerOp) + (clientOperation, serverOperation) match { + case (Some(_), None) => Some(MissingOperation(httpMethod)) + case (Some(clientOp), Some(serverOp)) => checkOperation(httpMethod, clientOp, serverOp) case _ => None } } @@ -64,33 +88,42 @@ class OpenAPIComparator( private def checkOperation( httpMethod: String, - writerOperation: Operation, - readerOperation: Operation + clientOperation: Operation, + serverOperation: Operation ): Option[IncompatibleOperation] = { - val readerParameters = getOperationParameters(readerOperation) - val writerParameters = getOperationParameters(writerOperation) - - val parametersIssue = writerParameters.flatMap { writerParameter => - val readerParameter = readerParameters.find(_.name == writerParameter.name) - readerParameter match { - case None => Some(MissingParameter(writerParameter.name)) - case Some(readerParameter) => checkParameter(writerParameter, readerParameter) + val serverParameters = getOperationParameters(serverOperation) + val clientParameters = getOperationParameters(clientOperation) + + val parametersIssue = clientParameters.flatMap { clientParameter => + val serverParameter = serverParameters.find(_.name == clientParameter.name) + serverParameter match { + case None => Some(MissingParameter(clientParameter.name)) + case Some(serverParameter) => + if (clientParameter.required.getOrElse(false) && !serverParameter.required.getOrElse(false)) { + Some(IncompatibleRequiredParameter(clientParameter.name)) + } else { + checkParameter(clientParameter, serverParameter) + } } } - val requestBodyIssue = (writerOperation.requestBody, readerOperation.requestBody) match { - case (Some(Right(writerRequestBody)), Some(Right(readerRequestBody))) => - checkRequestBody(writerRequestBody, readerRequestBody) + val requestBodyIssue = (clientOperation.requestBody, serverOperation.requestBody) match { + case (Some(Right(clientRequestBody)), Some(Right(serverRequestBody))) => + if (clientRequestBody.required.getOrElse(false) && !serverRequestBody.required.getOrElse(false)) { + Some(IncompatibleRequiredRequestBody()) + } else { + checkRequestBody(clientRequestBody, serverRequestBody) + } case (Some(Right(_)), None) => Some(MissingRequestBody()) case _ => None } - val responsesIssues = writerOperation.responses.responses.flatMap { - case (writerResponseKey, Right(writerResponse)) => - val readerResponse = readerOperation.responses.responses.get(writerResponseKey) - readerResponse match { - case Some(Right(readerResponse)) => checkResponse(writerResponse, readerResponse) - case None => Some(MissingResponse(writerResponseKey)) + val responsesIssues = clientOperation.responses.responses.flatMap { + case (clientResponseKey, Right(clientResponse)) => + val serverResponse = serverOperation.responses.responses.get(clientResponseKey) + serverResponse match { + case Some(Right(serverResponse)) => checkResponse(clientResponse, serverResponse) + case None => Some(MissingResponse(clientResponseKey)) case _ => None } case _ => None @@ -104,15 +137,15 @@ class OpenAPIComparator( Some(IncompatibleOperation(httpMethod, issues)) } - private def checkParameter(writerParameter: Parameter, readerParameter: Parameter): Option[IncompatibleParameter] = { - val isCompatibleStyle = readerParameter.style == writerParameter.style - val isCompatibleExplode = readerParameter.explode == writerParameter.explode - val isCompatibleAllowEmptyValue = readerParameter.allowEmptyValue == writerParameter.allowEmptyValue - val isCompatibleAllowReserved = readerParameter.allowReserved == writerParameter.allowReserved + private def checkParameter(clientParameter: Parameter, serverParameter: Parameter): Option[IncompatibleParameter] = { + val isCompatibleStyle = serverParameter.style == clientParameter.style + val isCompatibleExplode = serverParameter.explode == clientParameter.explode + val isCompatibleAllowEmptyValue = serverParameter.allowEmptyValue == clientParameter.allowEmptyValue + val isCompatibleAllowReserved = serverParameter.allowReserved == clientParameter.allowReserved val issues = - checkSchema(writerParameter.schema, readerParameter.schema).toList ++ - checkContent(writerParameter.content, readerParameter.content).toList ++ + checkSchema(clientParameter.schema, serverParameter.schema).toList ++ + checkContent(clientParameter.content, serverParameter.content).toList ++ (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ @@ -121,19 +154,19 @@ class OpenAPIComparator( if (issues.isEmpty) None else - Some(IncompatibleParameter(writerParameter.name, issues)) + Some(IncompatibleParameter(clientParameter.name, issues)) } private def checkContent( - writerContent: ListMap[String, MediaType], - readerContent: ListMap[String, MediaType] + clientContent: ListMap[String, MediaType], + serverContent: ListMap[String, MediaType] ): Option[IncompatibleContent] = { - val issues = writerContent.flatMap { case (writerMediaType, writerMediaTypeDescription) => - val readerMediaTypeDescription = readerContent.get(writerMediaType) - readerMediaTypeDescription match { - case None => Some(MissingMediaType(writerMediaType)) - case Some(readerMediaTypeDescription) => - checkMediaType(writerMediaType, writerMediaTypeDescription, readerMediaTypeDescription) + val issues = clientContent.flatMap { case (clientMediaType, clientMediaTypeDescription) => + val serverMediaTypeDescription = serverContent.get(clientMediaType) + serverMediaTypeDescription match { + case None => Some(MissingMediaType(clientMediaType)) + case Some(serverMediaTypeDescription) => + checkMediaType(clientMediaType, clientMediaTypeDescription, serverMediaTypeDescription) } } @@ -145,10 +178,10 @@ class OpenAPIComparator( private def checkMediaType( mediaType: String, - writerMediaTypeDescription: MediaType, - readerMediaTypeDescription: MediaType + clientMediaTypeDescription: MediaType, + serverMediaTypeDescription: MediaType ): Option[IncompatibleMediaType] = { - val issues = checkSchema(writerMediaTypeDescription.schema, readerMediaTypeDescription.schema) + val issues = checkSchema(clientMediaTypeDescription.schema, serverMediaTypeDescription.schema) if (issues.nonEmpty) Some(IncompatibleMediaType(mediaType, issues.toList)) else @@ -157,16 +190,13 @@ class OpenAPIComparator( } private def checkSchema( - writerSchema: Option[SchemaLike], - readerSchema: Option[SchemaLike] + clientSchema: Option[SchemaLike], + serverSchema: Option[SchemaLike] ): Option[OpenAPICompatibilityIssue] = { - (readerSchema, writerSchema) match { - case (Some(readerSchema: Schema), Some(writerSchema: Schema)) => - val readerSchemas = Map("readerSchema" -> readerSchema) - val writerSchemas = Map("writerSchema" -> writerSchema) - - val schemaComparator = new SchemaComparator(readerSchemas, writerSchemas) - val schemaIssues = schemaComparator.compare(readerSchema, writerSchema) + (serverSchema, clientSchema) match { + case (Some(serverSchema), Some(clientSchema)) => + val schemaComparator = new SchemaComparator(clientSchemas, serverSchemas) + val schemaIssues = schemaComparator.compare(clientSchema, serverSchema) if (schemaIssues.nonEmpty) Some(IncompatibleSchema(schemaIssues)) else @@ -179,7 +209,7 @@ class OpenAPIComparator( private def getOperationParameters(operation: Operation): List[Parameter] = { operation.parameters.flatMap { case Right(parameter) => Some(parameter) - case Left(reference) => resolveParameterReference(readerOpenAPI, reference.$ref) + case Left(reference) => resolveParameterReference(serverOpenAPI, reference.$ref) } } @@ -191,25 +221,30 @@ class OpenAPIComparator( } private def checkRequestBody( - writerRequestBody: RequestBody, - readerRequestBody: RequestBody + clientRequestBody: RequestBody, + serverRequestBody: RequestBody ): Option[IncompatibleRequestBody] = { - val contentIssues = checkContent(readerRequestBody.content, writerRequestBody.content).toList + val contentIssues = checkContent(clientRequestBody.content, serverRequestBody.content).toList if (contentIssues.nonEmpty) Some(IncompatibleRequestBody(contentIssues)) else None } - private def checkResponse(writerResponse: Response, readerResponse: Response): Option[IncompatibleResponse] = { - val contentIssue = checkContent(readerResponse.content, writerResponse.content) - val headerIssues = writerResponse.headers.flatMap { - case (writerHeaderName, Right(writerHeader)) => - val readerHeader = readerResponse.headers.get(writerHeaderName) - readerHeader match { - case Some(Right(readerHeader)) => checkHeader(writerHeaderName, writerHeader, readerHeader) - case None => Some(MissingHeader(writerHeaderName)) - case _ => None + private def checkResponse(clientResponse: Response, serverResponse: Response): Option[IncompatibleResponse] = { + val contentIssue = checkContent(clientResponse.content, serverResponse.content) + val headerIssues = clientResponse.headers.flatMap { + case (clientHeaderName, Right(clientHeader)) => + val serverHeader = serverResponse.headers.get(clientHeaderName) + serverHeader match { + case Some(Right(serverHeader)) => + if (clientHeader.required.getOrElse(false) && !serverHeader.required.getOrElse(false)) { + Some(IncompatibleRequiredHeader(clientHeaderName)) + } else { + checkHeader(clientHeaderName, clientHeader, serverHeader) + } + case None => Some(MissingHeader(clientHeaderName)) + case _ => None } case _ => None } @@ -223,15 +258,15 @@ class OpenAPIComparator( private def checkHeader( headerName: String, - writerHeader: Header, - readerHeader: Header + clientHeader: Header, + serverHeader: Header ): Option[IncompatibleHeader] = { - val schemaIssues = checkSchema(writerHeader.schema, readerHeader.schema) - val contentIssue = checkContent(readerHeader.content, writerHeader.content) - val isCompatibleStyle = readerHeader.style == writerHeader.style - val isCompatibleExplode = readerHeader.explode == writerHeader.explode - val isCompatibleAllowEmptyValue = readerHeader.allowEmptyValue == writerHeader.allowEmptyValue - val isCompatibleAllowReserved = readerHeader.allowReserved == writerHeader.allowReserved + val schemaIssues = checkSchema(clientHeader.schema, serverHeader.schema) + val contentIssue = checkContent(clientHeader.content, serverHeader.content) + val isCompatibleStyle = serverHeader.style == clientHeader.style + val isCompatibleExplode = serverHeader.explode == clientHeader.explode + val isCompatibleAllowEmptyValue = serverHeader.allowEmptyValue == clientHeader.allowEmptyValue + val isCompatibleAllowReserved = serverHeader.allowReserved == clientHeader.allowReserved val issues = schemaIssues.toList ++ diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 0ee0db4..dbc4b94 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -50,6 +50,13 @@ case class MissingParameter( s"missing parameter $name" } +case class IncompatibleRequiredParameter( + name: String +) extends OpenAPICompatibilityIssue { + def description: String = + s"parameter '$name' is required by the client but optional on the server" +} + case class IncompatibleParameter( name: String, subIssues: List[OpenAPICompatibilityIssue] @@ -93,6 +100,11 @@ case class MissingRequestBody() extends OpenAPICompatibilityIssue { s"missing request body" } +case class IncompatibleRequiredRequestBody() extends OpenAPICompatibilityIssue { + def description: String = + s"request body is required by the client but optional on the server" +} + case class IncompatibleRequestBody(subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { def description: String = s"incompatible request body:\n${issuesRepr(subIssues)}" @@ -113,6 +125,11 @@ case class MissingHeader(headerName: String) extends OpenAPICompatibilityIssue { s"missing header $headerName" } +case class IncompatibleRequiredHeader(headerName: String) extends OpenAPICompatibilityIssue { + def description: String = + s"header '$headerName' is required by the client but optional on the server" +} + case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { def description: String = diff --git a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala deleted file mode 100644 index 1f4dca1..0000000 --- a/openapi-model/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ /dev/null @@ -1,328 +0,0 @@ -package sttp.apispec.openapi.validation - -import org.scalatest.funsuite.AnyFunSuite -import sttp.apispec.{Schema, SchemaType} -import sttp.apispec.openapi.{ - Header, - Info, - MediaType, - OpenAPI, - Operation, - Parameter, - ParameterIn, - ParameterStyle, - PathItem, - Paths, - RequestBody, - Response, - ResponsesCodeKey -} -import sttp.apispec.validation.TypeMismatch - -import scala.collection.immutable.ListMap - -class OpenAPIComparatorTest extends AnyFunSuite { - private val paths = Paths(pathItems = ListMap("/test" -> PathItem())) - private val pathItem = PathItem() - private val operation = Operation() - private val parameter = Parameter("test", ParameterIn.Path, schema = None) - private val mediaType = MediaType() - private val requestBody = RequestBody() - private val response = Response() - private val header = Header() - - test("missing path") { - val readerOpenAPI = OpenAPI(info = Info("", "")) - val writerOpenAPI = OpenAPI(info = Info("", ""), paths = paths) - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val expected = List(MissingPath("/test")) - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> missing operation") { - val readerOpenAPI = OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem)) - val writerOpenAPI = OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation))) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val operationIssue = MissingOperation("get") - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> missing parameter") { - val readerOpenAPI = - OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation))) - - val writerOpenAPI = - OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter)))) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val parameterIssue = MissingParameter("test") - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible parameter -> incompatible schema") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = - paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.schema(Schema(SchemaType.Integer))))) - ) - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = - paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.schema(Schema(SchemaType.String))))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val schemaTypeMismatch = TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)) - val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) - val parameterIssue = IncompatibleParameter("test", List(schemaIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test( - "incompatible path -> incompatible operation -> incompatible parameter -> incompatible content -> missing media type" - ) { - val readerOpenAPI = - OpenAPI(info = Info("", ""), paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter)))) - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = - paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.addMediaType("test", mediaType)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val mediaTypeIssue = MissingMediaType("test") - val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) - val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test( - "incompatible path -> incompatible operation -> incompatible parameter -> incompatible content -> incompatible media type -> incompatible schema" - ) { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem( - "/test", - pathItem.get( - operation.addParameter(parameter.addMediaType("test", mediaType.schema(Schema(SchemaType.Integer)))) - ) - ) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem( - "/test", - pathItem.get( - operation.addParameter(parameter.addMediaType("test", mediaType.schema(Schema(SchemaType.String)))) - ) - ) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)))) - val mediaTypeIssue = IncompatibleMediaType("test", List(schemaMismatch)) - val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) - val parameterIssue = IncompatibleParameter("test", List(parameterContentIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible parameter -> style miss match") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.style(ParameterStyle.Label)))) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.style(ParameterStyle.Simple)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val missMatchIssue = MissMatch("style") - val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible parameter -> explode miss match") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.explode(false)))) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.explode(true)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val missMatchIssue = MissMatch("explode") - val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible parameter -> allowEmptyValue miss match") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowEmptyValue(false)))) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowEmptyValue(true)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val missMatchIssue = MissMatch("allowEmptyValue") - val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible parameter -> allowReserved miss match") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowReserved(false)))) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addParameter(parameter.allowReserved(true)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val missMatchIssue = MissMatch("allowReserved") - val parameterIssue = IncompatibleParameter("test", List(missMatchIssue)) - val operationIssue = IncompatibleOperation("get", List(parameterIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> missing request body") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation)) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.requestBody(requestBody))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val requestBodyIssue = MissingRequestBody() - val operationIssue = IncompatibleOperation("get", List(requestBodyIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test("incompatible path -> incompatible operation -> incompatible responses -> missing response") { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation)) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val reponsesIssue = MissingResponse(ResponsesCodeKey(200)) - val operationIssue = IncompatibleOperation("get", List(reponsesIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } - - test( - "incompatible path -> incompatible operation -> incompatible responses -> incompatible response -> missing header" - ) { - val readerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response))) - ) - - val writerOpenAPI = - OpenAPI( - info = Info("", ""), - paths = paths.addPathItem("/test", pathItem.get(operation.addResponse(200, response.addHeader("test", header)))) - ) - - val openAPIComparator = new OpenAPIComparator(writerOpenAPI, readerOpenAPI) - - val headerIssue = MissingHeader("test") - val reponsesIssue = IncompatibleResponse(List(headerIssue)) - val operationIssue = IncompatibleOperation("get", List(reponsesIssue)) - val pathIssue = IncompatiblePath("/test", List(operationIssue)) - val expected = List(pathIssue) - - assert(openAPIComparator.compare() == expected) - } -} From f2dd44d938301aa6a5c2c46be8f42705637e6c0d Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Fri, 6 Dec 2024 23:07:16 +0100 Subject: [PATCH 05/16] fix issues & add some missing test cases --- build.sbt | 3 +- .../petstore-added-operation.json} | 0 .../petstore.json} | 0 ...re-added-parameter-content-mediatype.json} | 0 .../petstore.json} | 0 .../petstore-added-parameter.json} | 0 .../petstore/added-parameter/petstore.json | 133 +++++++ .../added-path/petstore-added-path.json | 96 +++++ .../petstore.json} | 0 ...-added-requestbody-content-mediatype.json} | 0 .../petstore.json} | 0 .../petstore-added-requestbody.json | 199 ++++++++++ .../petstore/added-requestbody/petstore.json | 184 +++++++++ ...ore-added-response-content-mediatype.json} | 0 .../petstore.json} | 0 ...ed-response-header-content-mediatype.json} | 0 .../petstore.json} | 0 .../petstore-added-response-header.json} | 0 .../petstore.json} | 0 .../petstore-added-response.json | 178 +++++++++ .../petstore.json} | 0 .../petstore-changed-metadata.json | 199 ++++++++++ .../petstore/changed-metadata/petstore.json | 199 ++++++++++ .../petstore-identical.json} | 0 .../petstore/identical/petstore.json | 199 ++++++++++ ...-updated-parameter-allow_empty_value.json} | 0 .../petstore.json} | 0 ...tore-updated-parameter-allow_reserved.json | 152 ++++++++ .../petstore.json | 151 ++++++++ ...d-parameter-content-mediatype-schema.json} | 0 .../petstore.json | 151 ++++++++ .../petstore-updated-parameter-explode.json | 150 ++++++++ .../petstore.json} | 0 .../petstore-updated-parameter-name.json | 199 ++++++++++ .../updated-parameter-name/petstore.json | 199 ++++++++++ .../petstore-updated-parameter-schema.json | 151 ++++++++ .../updated-parameter-schema/petstore.json | 148 ++++++++ .../petstore-updated-parameter-style.json | 149 ++++++++ .../updated-parameter-style/petstore.json | 148 ++++++++ ...-requestbody-content-mediatype-schema.json | 175 +++++++++ .../petstore.json | 175 +++++++++ ...ted-response-content-mediatype-schema.json | 183 +++++++++ .../petstore.json | 183 +++++++++ ...ed-response-header-allow_empty_value.json} | 0 .../petstore.json} | 0 ...pdated-response-header-allow_reserved.json | 199 ++++++++++ .../petstore.json | 198 ++++++++++ ...onse-header-content-mediatype-schema.json} | 0 .../petstore.json | 195 ++++++++++ ...store-updated-response-header-explode.json | 197 ++++++++++ .../petstore.json} | 0 ...tstore-updated-response-header-schema.json | 191 ++++++++++ .../petstore.json | 191 ++++++++++ ...etstore-updated-response-header-style.json | 196 ++++++++++ .../petstore.json | 195 ++++++++++ .../validation/OpenAPIComparatorTest.scala | 359 +++++++++++------- .../validation/OpenAPIComparator.scala | 71 ++-- 57 files changed, 5512 insertions(+), 184 deletions(-) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_3.json => added-operation/petstore-added-operation.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_2.json => added-operation/petstore.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_6.json => added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_5.json => added-parameter-content-mediatype/petstore.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_4.json => added-parameter/petstore-added-parameter.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-path/petstore-added-path.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_1.json => added-path/petstore.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_2.json => added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_1.json => added-requestbody-content-mediatype/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore-added-requestbody.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_5.json => added-response-content-mediatype/petstore-added-response-content-mediatype.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_4.json => added-response-content-mediatype/petstore.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_9.json => added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_8.json => added-response-header-content-mediatype/petstore.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_7.json => added-response-header/petstore-added-response-header.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_6.json => added-response-header/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response/petstore-added-response.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_3.json => added-response/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_2_4.json => identical/petstore-identical.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/identical/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_1_0.json => updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_9.json => updated-parameter-allow_empty_value/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_7.json => updated-parameter-content-mediatype-schema/petstore-updated-parameter-content-mediatype-schema.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_0_8.json => updated-parameter-explode/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore-updated-parameter-name.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore-updated-parameter-style.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_2_3.json => updated-response-header-allow_empty_value/petstore-updated-response-header-allow_empty_value.json} (100%) rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_2_2.json => updated-response-header-allow_empty_value/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_2_0.json => updated-response-header-content-mediatype-schema/petstore-updated-response-header-content-mediatype-schema.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json rename openapi-comparator-tests/src/test/resources/petstore/{basic-petstore-v_1_2_1.json => updated-response-header-explode/petstore.json} (100%) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore-updated-response-header-style.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore.json diff --git a/build.sbt b/build.sbt index 5578452..aea9e74 100644 --- a/build.sbt +++ b/build.sbt @@ -74,7 +74,8 @@ lazy val allProjectAggregates: Seq[ProjectReference] = openapiCirceYaml.projectRefs ++ asyncapiModel.projectRefs ++ asyncapiCirce.projectRefs ++ - asyncapiCirceYaml.projectRefs + asyncapiCirceYaml.projectRefs ++ + openapiComparatorTests.projectRefs lazy val projectAggregates: Seq[ProjectReference] = if (sys.env.isDefinedAt("STTP_NATIVE")) { println("[info] STTP_NATIVE defined, including native in the aggregate projects") diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json b/openapi-comparator-tests/src/test/resources/petstore/added-operation/petstore-added-operation.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_3.json rename to openapi-comparator-tests/src/test/resources/petstore/added-operation/petstore-added-operation.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json b/openapi-comparator-tests/src/test/resources/petstore/added-operation/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_2.json rename to openapi-comparator-tests/src/test/resources/petstore/added-operation/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_6.json rename to openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_5.json rename to openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore-added-parameter.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_4.json rename to openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore-added-parameter.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore.json new file mode 100644 index 0000000..d840ecd --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter/petstore.json @@ -0,0 +1,133 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.3" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-path/petstore-added-path.json b/openapi-comparator-tests/src/test/resources/petstore/added-path/petstore-added-path.json new file mode 100644 index 0000000..b202960 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-path/petstore-added-path.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json b/openapi-comparator-tests/src/test/resources/petstore/added-path/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_1.json rename to openapi-comparator-tests/src/test/resources/petstore/added-path/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_2.json rename to openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_1.json rename to openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore-added-requestbody.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore-added-requestbody.json new file mode 100644 index 0000000..11989e6 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore-added-requestbody.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore.json new file mode 100644 index 0000000..da8239b --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody/petstore.json @@ -0,0 +1,184 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_5.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_4.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_9.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_8.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header/petstore-added-response-header.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_7.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-header/petstore-added-response-header.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_6.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response-header/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response/petstore-added-response.json b/openapi-comparator-tests/src/test/resources/petstore/added-response/petstore-added-response.json new file mode 100644 index 0000000..cff46ba --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response/petstore-added-response.json @@ -0,0 +1,178 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json b/openapi-comparator-tests/src/test/resources/petstore/added-response/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_3.json rename to openapi-comparator-tests/src/test/resources/petstore/added-response/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json new file mode 100644 index 0000000..99a41a2 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Example Pet Store API", + "summary": "Manages pet store operations.", + "description": "This is a sample API for managing a pet store.", + "termsOfService": "https://example.com/terms-of-service", + "contact": { + "name": "API Support Team", + "url": "https://www.example.com/help", + "email": "support@example.com" + }, + "license": { + "name": "Apache License 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore.json new file mode 100644 index 0000000..11989e6 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json b/openapi-comparator-tests/src/test/resources/petstore/identical/petstore-identical.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_4.json rename to openapi-comparator-tests/src/test/resources/petstore/identical/petstore-identical.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/identical/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/identical/petstore.json new file mode 100644 index 0000000..d9e3872 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/identical/petstore.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_1_0.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_empty_value/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_9.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_empty_value/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json new file mode 100644 index 0000000..ed87cde --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json @@ -0,0 +1,152 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.1" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore.json new file mode 100644 index 0000000..ec035d4 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-allow_reserved/petstore.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.0" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore-updated-parameter-content-mediatype-schema.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_7.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore-updated-parameter-content-mediatype-schema.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..fc268bb --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-schema/petstore.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.6" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json new file mode 100644 index 0000000..e616bc2 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json @@ -0,0 +1,150 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.9" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_0_8.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-parameter-explode/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore-updated-parameter-name.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore-updated-parameter-name.json new file mode 100644 index 0000000..4848c8f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore-updated-parameter-name.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{Id}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore.json new file mode 100644 index 0000000..d9e3872 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-name/petstore.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json new file mode 100644 index 0000000..b89c469 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.5" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "sold" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore.json new file mode 100644 index 0000000..ab2b215 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-schema/petstore.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.4" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "schema": { + "type": "string", + "enum": [ + "available", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore-updated-parameter-style.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore-updated-parameter-style.json new file mode 100644 index 0000000..df50c9f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore-updated-parameter-style.json @@ -0,0 +1,149 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.8" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore.json new file mode 100644 index 0000000..319759b --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-style/petstore.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.7" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json new file mode 100644 index 0000000..a7eb4e6 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..9749b19 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-schema/petstore.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json new file mode 100644 index 0000000..f9fb574 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json @@ -0,0 +1,183 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..4cd017f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-content-mediatype-schema/petstore.json @@ -0,0 +1,183 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_empty_value/petstore-updated-response-header-allow_empty_value.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_3.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_empty_value/petstore-updated-response-header-allow_empty_value.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_empty_value/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_2.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_empty_value/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json new file mode 100644 index 0000000..d9e3872 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json @@ -0,0 +1,199 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore.json new file mode 100644 index 0000000..76dfd15 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-allow_reserved/petstore.json @@ -0,0 +1,198 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "allowEmptyValue": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore-updated-response-header-content-mediatype-schema.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_0.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore-updated-response-header-content-mediatype-schema.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..4a90228 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-content-mediatype-schema/petstore.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json new file mode 100644 index 0000000..1269aca --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json @@ -0,0 +1,197 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "explode": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore.json similarity index 100% rename from openapi-comparator-tests/src/test/resources/petstore/basic-petstore-v_1_2_1.json rename to openapi-comparator-tests/src/test/resources/petstore/updated-response-header-explode/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json new file mode 100644 index 0000000..0d458b4 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore.json new file mode 100644 index 0000000..0ff614e --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-schema/petstore.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "schema": { + "type": "integer" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore-updated-response-header-style.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore-updated-response-header-style.json new file mode 100644 index 0000000..b6e3332 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore-updated-response-header-style.json @@ -0,0 +1,196 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "style": "form", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore.json new file mode 100644 index 0000000..e571041 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-response-header-style/petstore.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index c6f5429..911467c 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -1,5 +1,6 @@ package sttp.apispec.openapi.validation +import io.circe import org.scalatest.funsuite.AnyFunSuite import sttp.apispec.SchemaType import sttp.apispec.openapi.{OpenAPI, ResponsesCodeKey} @@ -10,57 +11,81 @@ import sttp.apispec.validation.TypeMismatch class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { override val basedir = "openapi-comparator-tests" + def readOpenAPI(path: String): Either[circe.Error, OpenAPI] = readJson(path).flatMap(_.as[OpenAPI]): @unchecked + private def compare(clientOpenapi: OpenAPI, serverOpenapi: OpenAPI): List[OpenAPICompatibilityIssue] = + new OpenAPIComparator(clientOpenapi, serverOpenapi) + .compare() + test("identical") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked + val Right(clientOpenapi) = readOpenAPI("/petstore/identical/petstore-identical.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/identical/petstore.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("no errors when metadata is updated") { + val Right(clientOpenapi) = readOpenAPI("/petstore/changed-metadata/petstore-changed-metadata.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/changed-metadata/petstore.json") - assert(openAPIComparator.compare().isEmpty) + assert(compare(clientOpenapi, serverOpenapi).isEmpty) } - test("missing path") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_2.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_1.json").flatMap(_.as[OpenAPI]): @unchecked - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing path when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-path/petstore-added-path.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-path/petstore.json") val expected = List(MissingPath("/pets/{petId}")) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing operation") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_3.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_2.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional path") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-path/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-path/petstore-added-path.json") - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + + test("server missing operation when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-operation/petstore.json") val operationIssue = MissingOperation("post") val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing parameter") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_4.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_3.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional operation") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-operation/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing parameter when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter/petstore.json") val parameterIssue = MissingParameter("status") val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible parameter schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_5.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_4.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional parameter") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter schema is incompatible with client schema") { + val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.Array), List(SchemaType.String)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -69,14 +94,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("parameter content is missing media type") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_6.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_5.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing parameter content media-type when client has an extra one") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") val mediaTypeIssue = MissingMediaType("application/json") val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) @@ -85,14 +109,22 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible media type parameter content schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_7.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_6.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional parameter content media-type") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") + val Right(serverOpenapi) = + readOpenAPI("/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter content media-type schema is incompatible with client schema") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-parameter-content-mediatype-schema/petstore-updated-parameter-content-mediatype-schema.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-content-mediatype-schema/petstore.json") val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Array)))) val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaMismatch)) @@ -102,14 +134,12 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible parameter style") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_8.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_7.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter style is incompatible with client parameter style") { + val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore.json") val missMatchIssue = MissMatch("style") val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) @@ -117,14 +147,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible parameter explode") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_0_9.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_8.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter explode is incompatible with client parameter explode") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-explode/petstore.json") val missMatchIssue = MissMatch("explode") val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) @@ -132,14 +161,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible parameter allowEmptyValue") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_0.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_0_9.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter allowEmptyValue is incompatible with client parameter allowEmptyValue") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore.json") val missMatchIssue = MissMatch("allowEmptyValue") val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) @@ -147,14 +175,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible parameter allowReserved") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_0.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server parameter allowReserved is incompatible with client parameter allowReserved") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore.json") val missMatchIssue = MissMatch("allowReserved") val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) @@ -162,28 +189,32 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } -// test("missing request body") { -// val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked -// val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked -// -// val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) -// -// val requestBodyIssue = MissingRequestBody() -// val operationIssue = IncompatibleOperation("get", List(requestBodyIssue)) -// val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) -// val expected = List(pathIssue) -// -// assert(openAPIComparator.compare() == expected) -// } + test("server missing request-body when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore.json") - test("missing request body content media type") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_1.json").flatMap(_.as[OpenAPI]): @unchecked + val requestBodyIssue = MissingRequestBody() + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("no errors when server has an additional request-body") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + + test("server missing request-body content media-type when client has an extra one") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") val mediaTypeIssue = MissingMediaType("application/xml") val requestBodyContentIssue = IncompatibleContent(List(mediaTypeIssue)) @@ -192,14 +223,22 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible request body content media type schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_3.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_2.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional request-body content media-type") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") + val Right(serverOpenapi) = + readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server request-body content media-type schema is incompatible with client schema") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-requestbody-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -210,28 +249,32 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing response") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_4.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_3.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing response when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response/petstore-added-response.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response/petstore.json") val responsesIssue = MissingResponse(ResponsesCodeKey(500)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing response content media type") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_5.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_4.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional response") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response/petstore-added-response.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing response content media-type when client has an extra one") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") val mediaTypeIssues = MissingMediaType("application/xml") val responseContentIssues = IncompatibleContent(List(mediaTypeIssues)) @@ -240,14 +283,22 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible response content media type schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_6.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_5.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional response content media-type") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") + val Right(serverOpenapi) = + readOpenAPI("/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json") - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + + test("server response content media-type schema is incompatible with client schema") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -258,14 +309,12 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing header") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_7.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_6.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing response header when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header/petstore.json") val headerIssue = MissingHeader("X-Rate-Limit") val responsesIssue = IncompatibleResponse(List(headerIssue)) @@ -273,14 +322,20 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_8.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_7.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional response header") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header/petstore.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header schema is incompatible with client schema") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -290,14 +345,14 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("missing header content media type") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_1_9.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_8.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server missing response header content media-type when client has an extra one") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") val mediaTypeIssues = MissingMediaType("application/json") val contentIssues = IncompatibleContent(List(mediaTypeIssues)) @@ -307,14 +362,23 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header content media type schema") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_0.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_1_9.json").flatMap(_.as[OpenAPI]): @unchecked + test("no errors when server has an additional response header content media-type") { + val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") + val Right(serverOpenapi) = readOpenAPI( + "/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header content media-type schema is incompatible with client schema") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-response-header-content-mediatype-schema/petstore-updated-response-header-content-mediatype-schema.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -326,14 +390,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header style") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_1.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_0.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header style is incompatible with client response header style") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-response-header-style/petstore-updated-response-header-style.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-style/petstore.json") val missMatchIssue = MissMatch("style") val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) @@ -342,14 +405,13 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header explode") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_2.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_1.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header explode is incompatible with client response header explode") { + val Right(clientOpenapi) = + readOpenAPI("/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-explode/petstore.json") val missMatchIssue = MissMatch("explode") val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) @@ -358,14 +420,14 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header allowEmptyValue") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_3.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_2.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header allowEmptyValue is incompatible with client response header allowEmptyValue") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-response-header-allow_empty_value/petstore-updated-response-header-allow_empty_value.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_empty_value/petstore.json") val missMatchIssue = MissMatch("allowEmptyValue") val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) @@ -374,14 +436,14 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) } - test("incompatible header allowReserved") { - val Right(clientOpenapi) = readJson("/petstore/basic-petstore-v_1_2_4.json").flatMap(_.as[OpenAPI]): @unchecked - val Right(serverOpenapi) = readJson("/petstore/basic-petstore-v_1_2_3.json").flatMap(_.as[OpenAPI]): @unchecked - - val openAPIComparator = new OpenAPIComparator(clientOpenapi, serverOpenapi) + test("server response header allowReserved is incompatible with client response header allowReserved") { + val Right(clientOpenapi) = readOpenAPI( + "/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json" + ) + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_reserved/petstore.json") val missMatchIssue = MissMatch("allowReserved") val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) @@ -390,6 +452,15 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) val expected = List(pathIssue) - assert(openAPIComparator.compare() == expected) + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("server parameter name is incompatible with client parameter name") { + val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-name/petstore-updated-parameter-name.json") + val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-name/petstore.json") + + val expected = List(MissingPath("/pets/{Id}")) + + assert(compare(clientOpenapi, serverOpenapi) == expected) } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index 33ff4a1..677f322 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -10,13 +10,37 @@ class OpenAPIComparator( clientOpenAPI: OpenAPI, serverOpenAPI: OpenAPI ) { - private val httpMethods = List("get", "post", "patch", "delete", "options", "trace", "head", "put") - private var serverSchemas: Map[String, Schema] = Map.empty[String, Schema] - private var clientSchemas: Map[String, Schema] = Map.empty[String, Schema] - def compare(): List[OpenAPICompatibilityIssue] = { - initSchemas() + private val httpMethods = List( + ("get", (_: PathItem).get), + ("post", (_: PathItem).post), + ("patch", (_: PathItem).patch), + ("delete", (_: PathItem).delete), + ("options", (_: PathItem).options), + ("trace", (_: PathItem).trace), + ("head", (_: PathItem).head), + ("put", (_: PathItem).put) + ) + + private val clientSchemas: Map[String, Schema] = clientOpenAPI.components match { + case Some(components) => + components.schemas.flatMap { + case (key, schema: Schema) => Some(key, schema) + case _ => None + } + case _ => Map.empty[String, Schema] + } + private val serverSchemas: Map[String, Schema] = serverOpenAPI.components match { + case Some(components) => + components.schemas.flatMap { + case (key, schema: Schema) => Some(key, schema) + case _ => None + } + case _ => Map.empty[String, Schema] + } + + def compare(): List[OpenAPICompatibilityIssue] = { clientOpenAPI.paths.pathItems.toList.flatMap { case (pathName, clientPathItem) => val serverPathItem = serverOpenAPI.paths.pathItems.get(pathName) @@ -34,34 +58,14 @@ class OpenAPIComparator( } } - private def initSchemas(): Unit = { - clientSchemas = clientOpenAPI.components match { - case Some(components) => - components.schemas.flatMap { - case (key, schema: Schema) => Some(key, schema) - case _ => None - } - case _ => Map.empty[String, Schema] - } - - serverSchemas = serverOpenAPI.components match { - case Some(components) => - components.schemas.flatMap { - case (key, schema: Schema) => Some(key, schema) - case _ => None - } - case _ => Map.empty[String, Schema] - } - } - private def checkPath( pathName: String, clientPathItem: PathItem, serverPathItem: PathItem ): Option[IncompatiblePath] = { - val issues = httpMethods.flatMap { httpMethod => - val clientOperation = getOperation(clientPathItem, httpMethod) - val serverOperation = getOperation(serverPathItem, httpMethod) + val issues = httpMethods.flatMap { case (httpMethod, getOperation) => + val clientOperation = getOperation(clientPathItem) + val serverOperation = getOperation(serverPathItem) (clientOperation, serverOperation) match { case (Some(_), None) => Some(MissingOperation(httpMethod)) @@ -74,17 +78,6 @@ class OpenAPIComparator( else Some(IncompatiblePath(pathName, issues)) } - private def getOperation(pathItem: PathItem, httpMethod: String): Option[Operation] = httpMethod match { - case "get" => pathItem.get - case "patch" => pathItem.patch - case "delete" => pathItem.delete - case "options" => pathItem.options - case "trace" => pathItem.trace - case "head" => pathItem.head - case "post" => pathItem.post - case "put" => pathItem.put - case _ => None - } private def checkOperation( httpMethod: String, From 6956ffc62365fa4054c52d4d9f54197663978bb8 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Sun, 8 Dec 2024 13:36:15 +0100 Subject: [PATCH 06/16] Fix issues --- .../validation/OpenAPIComparatorTest.scala | 32 ++++++++-------- .../validation/OpenAPIComparator.scala | 24 ++++++------ .../OpenAPICompatibilityIssue.scala | 37 +++++++++++++------ 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 911467c..1dd7708 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -141,8 +141,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore.json") - val missMatchIssue = MissMatch("style") - val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val styleIssue = IncompatibleStyle() + val parameterIssue = IncompatibleParameter("status", List(styleIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) @@ -155,8 +155,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-explode/petstore.json") - val missMatchIssue = MissMatch("explode") - val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val explodeIssue = IncompatibleExplode() + val parameterIssue = IncompatibleParameter("status", List(explodeIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) @@ -169,8 +169,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore.json") - val missMatchIssue = MissMatch("allowEmptyValue") - val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val allowEmptyValueIssue = IncompatibleAllowEmptyValue() + val parameterIssue = IncompatibleParameter("status", List(allowEmptyValueIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) @@ -183,8 +183,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore.json") - val missMatchIssue = MissMatch("allowReserved") - val parameterIssue = IncompatibleParameter("status", List(missMatchIssue)) + val allowReservedIssue = IncompatibleAllowReserved() + val parameterIssue = IncompatibleParameter("status", List(allowReservedIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) val expected = List(pathIssue) @@ -398,8 +398,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-response-header-style/petstore-updated-response-header-style.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-style/petstore.json") - val missMatchIssue = MissMatch("style") - val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val styleIssue = IncompatibleStyle() + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(styleIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) @@ -413,8 +413,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-explode/petstore.json") - val missMatchIssue = MissMatch("explode") - val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val explodeIssue = IncompatibleExplode() + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(explodeIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) @@ -429,8 +429,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { ) val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_empty_value/petstore.json") - val missMatchIssue = MissMatch("allowEmptyValue") - val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val allowEmptyValueIssue = IncompatibleAllowEmptyValue() + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowEmptyValueIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) @@ -445,8 +445,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { ) val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_reserved/petstore.json") - val missMatchIssue = MissMatch("allowReserved") - val headerIssue = IncompatibleHeader("X-Rate-Limit", List(missMatchIssue)) + val allowReservedIssue = IncompatibleAllowReserved() + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowReservedIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index 677f322..f107216 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -25,7 +25,7 @@ class OpenAPIComparator( private val clientSchemas: Map[String, Schema] = clientOpenAPI.components match { case Some(components) => components.schemas.flatMap { - case (key, schema: Schema) => Some(key, schema) + case (key, schema: Schema) => Some((key, schema)) case _ => None } case _ => Map.empty[String, Schema] @@ -34,7 +34,7 @@ class OpenAPIComparator( private val serverSchemas: Map[String, Schema] = serverOpenAPI.components match { case Some(components) => components.schemas.flatMap { - case (key, schema: Schema) => Some(key, schema) + case (key, schema: Schema) => Some((key, schema)) case _ => None } case _ => Map.empty[String, Schema] @@ -139,10 +139,10 @@ class OpenAPIComparator( val issues = checkSchema(clientParameter.schema, serverParameter.schema).toList ++ checkContent(clientParameter.content, serverParameter.content).toList ++ - (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ - (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ - (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ - (if (!isCompatibleAllowReserved) Some(MissMatch("allowReserved")) else None).toList + (if (!isCompatibleStyle) Some(IncompatibleStyle()) else None).toList ++ + (if (!isCompatibleExplode) Some(IncompatibleExplode()) else None).toList ++ + (if (!isCompatibleAllowEmptyValue) Some(IncompatibleAllowEmptyValue()) else None).toList ++ + (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved()) else None).toList if (issues.isEmpty) None @@ -234,7 +234,7 @@ class OpenAPIComparator( if (clientHeader.required.getOrElse(false) && !serverHeader.required.getOrElse(false)) { Some(IncompatibleRequiredHeader(clientHeaderName)) } else { - checkHeader(clientHeaderName, clientHeader, serverHeader) + checkResponseHeader(clientHeaderName, clientHeader, serverHeader) } case None => Some(MissingHeader(clientHeaderName)) case _ => None @@ -249,7 +249,7 @@ class OpenAPIComparator( None } - private def checkHeader( + private def checkResponseHeader( headerName: String, clientHeader: Header, serverHeader: Header @@ -264,10 +264,10 @@ class OpenAPIComparator( val issues = schemaIssues.toList ++ contentIssue.toList ++ - (if (!isCompatibleStyle) Some(MissMatch("style")) else None).toList ++ - (if (!isCompatibleExplode) Some(MissMatch("explode")) else None).toList ++ - (if (!isCompatibleAllowEmptyValue) Some(MissMatch("allowEmptyValue")) else None).toList ++ - (if (!isCompatibleAllowReserved) Some(MissMatch("allowReserved")) else None).toList + (if (!isCompatibleStyle) Some(IncompatibleStyle()) else None).toList ++ + (if (!isCompatibleExplode) Some(IncompatibleExplode()) else None).toList ++ + (if (!isCompatibleAllowEmptyValue) Some(IncompatibleAllowEmptyValue()) else None).toList ++ + (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved()) else None).toList if (issues.nonEmpty) Some(IncompatibleHeader(headerName, issues)) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index dbc4b94..324a4fe 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -13,7 +13,7 @@ sealed abstract class OpenAPICompatibilityIssue { .mkString("\n") } -sealed abstract class SubOpenAPICompatibilityIssue extends OpenAPICompatibilityIssue { +sealed abstract class OpenAPICompatibilitySubIssues extends OpenAPICompatibilityIssue { def subIssues: List[OpenAPICompatibilityIssue] } @@ -25,7 +25,7 @@ case class MissingPath(pathName: String) extends OpenAPICompatibilityIssue { case class IncompatiblePath( pathName: String, subIssues: List[OpenAPICompatibilityIssue] -) extends SubOpenAPICompatibilityIssue { +) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible path $pathName:\n${issuesRepr(subIssues)}" } @@ -38,7 +38,7 @@ case class MissingOperation(httpMethod: String) extends OpenAPICompatibilityIssu case class IncompatibleOperation( httpMethod: String, subIssues: List[OpenAPICompatibilityIssue] -) extends SubOpenAPICompatibilityIssue { +) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible operation $httpMethod:\n${issuesRepr(subIssues)}" } @@ -60,7 +60,7 @@ case class IncompatibleRequiredParameter( case class IncompatibleParameter( name: String, subIssues: List[OpenAPICompatibilityIssue] -) extends SubOpenAPICompatibilityIssue { +) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible parameter $name:\n${issuesRepr(subIssues)}" } @@ -74,7 +74,7 @@ case class IncompatibleSchema( case class IncompatibleContent( subIssues: List[OpenAPICompatibilityIssue] -) extends SubOpenAPICompatibilityIssue { +) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible content:\n${issuesRepr(subIssues)}" } @@ -85,14 +85,29 @@ case class MissingMediaType(mediaType: String) extends OpenAPICompatibilityIssue } case class IncompatibleMediaType(mediaType: String, subIssues: List[OpenAPICompatibilityIssue]) - extends SubOpenAPICompatibilityIssue { + extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible media type $mediaType:\n${issuesRepr(subIssues)}" } -case class MissMatch(name: String) extends OpenAPICompatibilityIssue { +case class IncompatibleStyle() extends OpenAPICompatibilityIssue { def description: String = - s"miss match $name" + s"incompatible style" +} + +case class IncompatibleExplode() extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible explode" +} + +case class IncompatibleAllowEmptyValue() extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible allowEmptyValue" +} + +case class IncompatibleAllowReserved() extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible allowReserved" } case class MissingRequestBody() extends OpenAPICompatibilityIssue { @@ -105,7 +120,7 @@ case class IncompatibleRequiredRequestBody() extends OpenAPICompatibilityIssue { s"request body is required by the client but optional on the server" } -case class IncompatibleRequestBody(subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { +case class IncompatibleRequestBody(subIssues: List[OpenAPICompatibilityIssue]) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible request body:\n${issuesRepr(subIssues)}" } @@ -115,7 +130,7 @@ case class MissingResponse(responsesKey: ResponsesKey) extends OpenAPICompatibil s"missing response for $responsesKey" } -case class IncompatibleResponse(subIssues: List[OpenAPICompatibilityIssue]) extends SubOpenAPICompatibilityIssue { +case class IncompatibleResponse(subIssues: List[OpenAPICompatibilityIssue]) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible response:\n${issuesRepr(subIssues)}" } @@ -131,7 +146,7 @@ case class IncompatibleRequiredHeader(headerName: String) extends OpenAPICompati } case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompatibilityIssue]) - extends SubOpenAPICompatibilityIssue { + extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible header $headerName:\n${issuesRepr(subIssues)}" } From f542720d76a2f66b8c4b270ed34e15aacb161475 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Mon, 9 Dec 2024 15:24:55 +0100 Subject: [PATCH 07/16] Enhance context for issues related to style, explode, allowEmptyValue, and allowReserved values --- .../validation/OpenAPIComparatorTest.scala | 17 ++++++------ .../validation/OpenAPIComparator.scala | 27 +++++++++++++------ .../OpenAPICompatibilityIssue.scala | 18 ++++++------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 1dd7708..6482fe7 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -3,6 +3,7 @@ package sttp.apispec.openapi.validation import io.circe import org.scalatest.funsuite.AnyFunSuite import sttp.apispec.SchemaType +import sttp.apispec.openapi.ParameterStyle import sttp.apispec.openapi.{OpenAPI, ResponsesCodeKey} import sttp.apispec.openapi.circe.openAPIDecoder import sttp.apispec.test.ResourcePlatform @@ -141,7 +142,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore.json") - val styleIssue = IncompatibleStyle() + val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) val parameterIssue = IncompatibleParameter("status", List(styleIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) @@ -155,7 +156,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-explode/petstore.json") - val explodeIssue = IncompatibleExplode() + val explodeIssue = IncompatibleExplode(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(explodeIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) @@ -169,7 +170,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore.json") - val allowEmptyValueIssue = IncompatibleAllowEmptyValue() + val allowEmptyValueIssue = IncompatibleAllowEmptyValue(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(allowEmptyValueIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) @@ -183,7 +184,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore.json") - val allowReservedIssue = IncompatibleAllowReserved() + val allowReservedIssue = IncompatibleAllowReserved(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(allowReservedIssue)) val operationIssue = IncompatibleOperation("get", List(parameterIssue)) val pathIssue = IncompatiblePath("/pets", List(operationIssue)) @@ -398,7 +399,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-response-header-style/petstore-updated-response-header-style.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-style/petstore.json") - val styleIssue = IncompatibleStyle() + val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(styleIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) @@ -413,7 +414,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { readOpenAPI("/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json") val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-explode/petstore.json") - val explodeIssue = IncompatibleExplode() + val explodeIssue = IncompatibleExplode(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(explodeIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) @@ -429,7 +430,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { ) val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_empty_value/petstore.json") - val allowEmptyValueIssue = IncompatibleAllowEmptyValue() + val allowEmptyValueIssue = IncompatibleAllowEmptyValue(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowEmptyValueIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) @@ -445,7 +446,7 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { ) val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_reserved/petstore.json") - val allowReservedIssue = IncompatibleAllowReserved() + val allowReservedIssue = IncompatibleAllowReserved(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowReservedIssue)) val responsesIssue = IncompatibleResponse(List(headerIssue)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index f107216..797feb7 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -139,10 +139,16 @@ class OpenAPIComparator( val issues = checkSchema(clientParameter.schema, serverParameter.schema).toList ++ checkContent(clientParameter.content, serverParameter.content).toList ++ - (if (!isCompatibleStyle) Some(IncompatibleStyle()) else None).toList ++ - (if (!isCompatibleExplode) Some(IncompatibleExplode()) else None).toList ++ - (if (!isCompatibleAllowEmptyValue) Some(IncompatibleAllowEmptyValue()) else None).toList ++ - (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved()) else None).toList + (if (!isCompatibleStyle) Some(IncompatibleStyle(clientParameter.style, serverParameter.style)) + else None).toList ++ + (if (!isCompatibleExplode) Some(IncompatibleExplode(clientParameter.explode, serverParameter.explode)) + else None).toList ++ + (if (!isCompatibleAllowEmptyValue) + Some(IncompatibleAllowEmptyValue(clientParameter.allowEmptyValue, serverParameter.allowEmptyValue)) + else None).toList ++ + (if (!isCompatibleAllowReserved) + Some(IncompatibleAllowReserved(clientParameter.allowReserved, serverParameter.allowReserved)) + else None).toList if (issues.isEmpty) None @@ -264,10 +270,15 @@ class OpenAPIComparator( val issues = schemaIssues.toList ++ contentIssue.toList ++ - (if (!isCompatibleStyle) Some(IncompatibleStyle()) else None).toList ++ - (if (!isCompatibleExplode) Some(IncompatibleExplode()) else None).toList ++ - (if (!isCompatibleAllowEmptyValue) Some(IncompatibleAllowEmptyValue()) else None).toList ++ - (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved()) else None).toList + (if (!isCompatibleStyle) Some(IncompatibleStyle(clientHeader.style, serverHeader.style)) else None).toList ++ + (if (!isCompatibleExplode) Some(IncompatibleExplode(clientHeader.explode, serverHeader.explode)) + else None).toList ++ + (if (!isCompatibleAllowEmptyValue) + Some(IncompatibleAllowEmptyValue(clientHeader.allowEmptyValue, serverHeader.allowEmptyValue)) + else None).toList ++ + (if (!isCompatibleAllowReserved) + Some(IncompatibleAllowReserved(clientHeader.allowReserved, serverHeader.allowReserved)) + else None).toList if (issues.nonEmpty) Some(IncompatibleHeader(headerName, issues)) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 324a4fe..3fc39f2 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -1,6 +1,6 @@ package sttp.apispec.openapi.validation -import sttp.apispec.openapi.ResponsesKey +import sttp.apispec.openapi.{ParameterStyle, ResponsesKey} import sttp.apispec.validation.SchemaCompatibilityIssue sealed abstract class OpenAPICompatibilityIssue { @@ -90,24 +90,24 @@ case class IncompatibleMediaType(mediaType: String, subIssues: List[OpenAPICompa s"incompatible media type $mediaType:\n${issuesRepr(subIssues)}" } -case class IncompatibleStyle() extends OpenAPICompatibilityIssue { +case class IncompatibleStyle(clientValue: Option[ParameterStyle], serverValue: Option[ParameterStyle]) extends OpenAPICompatibilityIssue { def description: String = - s"incompatible style" + s"incompatible style value: client=$clientValue, server=$serverValue" } -case class IncompatibleExplode() extends OpenAPICompatibilityIssue { +case class IncompatibleExplode(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { def description: String = - s"incompatible explode" + s"incompatible explode value: client=$clientValue, server=$serverValue" } -case class IncompatibleAllowEmptyValue() extends OpenAPICompatibilityIssue { +case class IncompatibleAllowEmptyValue(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { def description: String = - s"incompatible allowEmptyValue" + s"incompatible allowEmptyValue value: client=$clientValue, server=$serverValue" } -case class IncompatibleAllowReserved() extends OpenAPICompatibilityIssue { +case class IncompatibleAllowReserved(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { def description: String = - s"incompatible allowReserved" + s"incompatible allowReserved value: client=$clientValue, server=$serverValue" } case class MissingRequestBody() extends OpenAPICompatibilityIssue { From 6cb34719a9e3d57db5308a9951bd81904f9f6e5b Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Mon, 9 Dec 2024 18:54:09 +0100 Subject: [PATCH 08/16] minor improvements --- .../petstore-changed-metadata.json | 22 +-- .../validation/OpenAPIComparatorTest.scala | 158 +++++++++--------- .../validation/OpenAPIComparator.scala | 10 +- 3 files changed, 98 insertions(+), 92 deletions(-) diff --git a/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json index 99a41a2..aa2cfd6 100644 --- a/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json +++ b/openapi-comparator-tests/src/test/resources/petstore/changed-metadata/petstore-changed-metadata.json @@ -25,12 +25,12 @@ "/pets": { "get": { "operationId": "getPets", - "description": "Gets all pets", + "description": "Get pets", "parameters": [ { "name": "status", "in": "query", - "description": "Filter pets by status", + "description": "Filter pets by status value", "required": false, "style": "form", "explode": true, @@ -47,7 +47,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "get pets successfully", "content": { "application/json": { "schema": { @@ -63,7 +63,7 @@ }, "post": { "operationId": "addPet", - "description": "Add a new pet", + "description": "Add new pet", "requestBody": { "required": true, "content": { @@ -81,7 +81,7 @@ }, "responses": { "201": { - "description": "Pet created", + "description": "A new pet is created", "content": { "application/json": { "schema": { @@ -96,7 +96,7 @@ "/pets/{petId}": { "get": { "operationId": "getPetById", - "description": "Get a pet by its ID", + "description": "Get pet by ID", "parameters": [ { "name": "petId", @@ -106,12 +106,12 @@ "type": "integer", "format": "int32" }, - "description": "The ID of the pet to retrieve" + "description": "The ID of the pet" } ], "responses": { "200": { - "description": "Successful response", + "description": "Success", "content": { "application/json": { "schema": { @@ -126,7 +126,7 @@ }, "headers": { "X-Rate-Limit": { - "description": "The number of requests allowed per hour", + "description": "The hourly limit for requests", "style": "form", "explode": true, "allowEmptyValue": true, @@ -142,7 +142,7 @@ } }, "404": { - "description": "Pet not found" + "description": "not found" }, "500": { "description": "Internal Error" @@ -165,7 +165,7 @@ }, "breed": { "type": "string", - "description": "The breed of the pet" + "description": "The pet's breed" } } }, diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 6482fe7..66be131 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -1,6 +1,5 @@ package sttp.apispec.openapi.validation -import io.circe import org.scalatest.funsuite.AnyFunSuite import sttp.apispec.SchemaType import sttp.apispec.openapi.ParameterStyle @@ -12,28 +11,31 @@ import sttp.apispec.validation.TypeMismatch class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { override val basedir = "openapi-comparator-tests" - def readOpenAPI(path: String): Either[circe.Error, OpenAPI] = readJson(path).flatMap(_.as[OpenAPI]): @unchecked + def readOpenAPI(path: String): OpenAPI = readJson(path).flatMap(_.as[OpenAPI]) match { + case Right(openapi) => openapi + case Left(error) => throw new RuntimeException(s"Failed to parse OpenAPI from $path: $error") + } + private def compare(clientOpenapi: OpenAPI, serverOpenapi: OpenAPI): List[OpenAPICompatibilityIssue] = - new OpenAPIComparator(clientOpenapi, serverOpenapi) - .compare() + OpenAPIComparator(clientOpenapi, serverOpenapi).compare() test("identical") { - val Right(clientOpenapi) = readOpenAPI("/petstore/identical/petstore-identical.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/identical/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/identical/petstore-identical.json") + val serverOpenapi = readOpenAPI("/petstore/identical/petstore.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("no errors when metadata is updated") { - val Right(clientOpenapi) = readOpenAPI("/petstore/changed-metadata/petstore-changed-metadata.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/changed-metadata/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/changed-metadata/petstore-changed-metadata.json") + val serverOpenapi = readOpenAPI("/petstore/changed-metadata/petstore.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server missing path when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-path/petstore-added-path.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-path/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-path/petstore-added-path.json") + val serverOpenapi = readOpenAPI("/petstore/added-path/petstore.json") val expected = List(MissingPath("/pets/{petId}")) @@ -41,15 +43,15 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional path") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-path/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-path/petstore-added-path.json") + val clientOpenapi = readOpenAPI("/petstore/added-path/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-path/petstore-added-path.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server missing operation when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-operation/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") + val serverOpenapi = readOpenAPI("/petstore/added-operation/petstore.json") val operationIssue = MissingOperation("post") val pathIssue = IncompatiblePath("/pets", List(operationIssue)) @@ -59,15 +61,15 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional operation") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-operation/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") + val clientOpenapi = readOpenAPI("/petstore/added-operation/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-operation/petstore-added-operation.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server missing parameter when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter/petstore.json") val parameterIssue = MissingParameter("status") val operationIssue = IncompatibleOperation("get", List(parameterIssue)) @@ -78,15 +80,15 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional parameter") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") + val clientOpenapi = readOpenAPI("/petstore/added-parameter/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter/petstore-added-parameter.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server parameter schema is incompatible with client schema") { - val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-schema/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-schema/petstore-updated-parameter-schema.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.Array), List(SchemaType.String)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -99,9 +101,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server missing parameter content media-type when client has an extra one") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") val mediaTypeIssue = MissingMediaType("application/json") val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) @@ -114,18 +116,18 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional parameter content media-type") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") - val Right(serverOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server parameter content media-type schema is incompatible with client schema") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-parameter-content-mediatype-schema/petstore-updated-parameter-content-mediatype-schema.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-content-mediatype-schema/petstore.json") val schemaMismatch = IncompatibleSchema(List(TypeMismatch(List(SchemaType.String), List(SchemaType.Array)))) val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaMismatch)) @@ -139,8 +141,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server parameter style is incompatible with client parameter style") { - val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-style/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore.json") val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) val parameterIssue = IncompatibleParameter("status", List(styleIssue)) @@ -152,9 +154,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server parameter explode is incompatible with client parameter explode") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-explode/petstore-updated-parameter-explode.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-explode/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-explode/petstore.json") val explodeIssue = IncompatibleExplode(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(explodeIssue)) @@ -166,9 +168,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server parameter allowEmptyValue is incompatible with client parameter allowEmptyValue") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore-updated-parameter-allow_empty_value.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-allow_empty_value/petstore.json") val allowEmptyValueIssue = IncompatibleAllowEmptyValue(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(allowEmptyValueIssue)) @@ -180,9 +182,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server parameter allowReserved is incompatible with client parameter allowReserved") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore-updated-parameter-allow_reserved.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-allow_reserved/petstore.json") val allowReservedIssue = IncompatibleAllowReserved(Some(true), None) val parameterIssue = IncompatibleParameter("status", List(allowReservedIssue)) @@ -194,8 +196,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server missing request-body when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") + val serverOpenapi = readOpenAPI("/petstore/added-requestbody/petstore.json") val requestBodyIssue = MissingRequestBody() val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) @@ -206,16 +208,16 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional request-body") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") + val clientOpenapi = readOpenAPI("/petstore/added-requestbody/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-requestbody/petstore-added-requestbody.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server missing request-body content media-type when client has an extra one") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") val mediaTypeIssue = MissingMediaType("application/xml") val requestBodyContentIssue = IncompatibleContent(List(mediaTypeIssue)) @@ -228,18 +230,18 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional request-body content media-type") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") - val Right(serverOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype/petstore-added-requestbody-content-mediatype.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server request-body content media-type schema is incompatible with client schema") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-requestbody-content-mediatype-schema/petstore-updated-requestbody-content-mediatype-schema.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-requestbody-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-requestbody-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -254,8 +256,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server missing response when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response/petstore-added-response.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-response/petstore-added-response.json") + val serverOpenapi = readOpenAPI("/petstore/added-response/petstore.json") val responsesIssue = MissingResponse(ResponsesCodeKey(500)) val operationIssue = IncompatibleOperation("get", List(responsesIssue)) @@ -266,16 +268,16 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional response") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response/petstore-added-response.json") + val clientOpenapi = readOpenAPI("/petstore/added-response/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response/petstore-added-response.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server missing response content media-type when client has an extra one") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") val mediaTypeIssues = MissingMediaType("application/xml") val responseContentIssues = IncompatibleContent(List(mediaTypeIssues)) @@ -288,18 +290,18 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional response content media-type") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") - val Right(serverOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/added-response-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-content-mediatype/petstore-added-response-content-mediatype.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server response content media-type schema is incompatible with client schema") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-response-content-mediatype-schema/petstore-updated-response-content-mediatype-schema.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Object)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -314,8 +316,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server missing response header when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-header/petstore.json") val headerIssue = MissingHeader("X-Rate-Limit") val responsesIssue = IncompatibleResponse(List(headerIssue)) @@ -327,16 +329,16 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional response header") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header/petstore.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") + val clientOpenapi = readOpenAPI("/petstore/added-response-header/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") assert(compare(clientOpenapi, serverOpenapi).isEmpty) } test("server response header schema is incompatible with client schema") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-response-header-schema/petstore-updated-response-header-schema.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.String), List(SchemaType.Integer)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -350,10 +352,10 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server missing response header content media-type when client has an extra one") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") val mediaTypeIssues = MissingMediaType("application/json") val contentIssues = IncompatibleContent(List(mediaTypeIssues)) @@ -367,8 +369,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("no errors when server has an additional response header content media-type") { - val Right(clientOpenapi) = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") - val Right(serverOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI("/petstore/added-response-header-content-mediatype/petstore.json") + val serverOpenapi = readOpenAPI( "/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json" ) @@ -376,10 +378,10 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server response header content media-type schema is incompatible with client schema") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-response-header-content-mediatype-schema/petstore-updated-response-header-content-mediatype-schema.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-content-mediatype-schema/petstore.json") val schemaTypeMismatch = TypeMismatch(List(SchemaType.Integer), List(SchemaType.String)) val schemaIssue = IncompatibleSchema(List(schemaTypeMismatch)) @@ -395,9 +397,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server response header style is incompatible with client response header style") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-response-header-style/petstore-updated-response-header-style.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-style/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-style/petstore.json") val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(styleIssue)) @@ -410,9 +412,9 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server response header explode is incompatible with client response header explode") { - val Right(clientOpenapi) = + val clientOpenapi = readOpenAPI("/petstore/updated-response-header-explode/petstore-updated-response-header-explode.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-explode/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-explode/petstore.json") val explodeIssue = IncompatibleExplode(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(explodeIssue)) @@ -425,10 +427,10 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server response header allowEmptyValue is incompatible with client response header allowEmptyValue") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-response-header-allow_empty_value/petstore-updated-response-header-allow_empty_value.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_empty_value/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-allow_empty_value/petstore.json") val allowEmptyValueIssue = IncompatibleAllowEmptyValue(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowEmptyValueIssue)) @@ -441,10 +443,10 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server response header allowReserved is incompatible with client response header allowReserved") { - val Right(clientOpenapi) = readOpenAPI( + val clientOpenapi = readOpenAPI( "/petstore/updated-response-header-allow_reserved/petstore-updated-response-header-allow_reserved.json" ) - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-response-header-allow_reserved/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/updated-response-header-allow_reserved/petstore.json") val allowReservedIssue = IncompatibleAllowReserved(Some(true), None) val headerIssue = IncompatibleHeader("X-Rate-Limit", List(allowReservedIssue)) @@ -457,8 +459,8 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { } test("server parameter name is incompatible with client parameter name") { - val Right(clientOpenapi) = readOpenAPI("/petstore/updated-parameter-name/petstore-updated-parameter-name.json") - val Right(serverOpenapi) = readOpenAPI("/petstore/updated-parameter-name/petstore.json") + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-name/petstore-updated-parameter-name.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-name/petstore.json") val expected = List(MissingPath("/pets/{Id}")) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index 797feb7..4280ea6 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -2,11 +2,16 @@ package sttp.apispec.openapi.validation import sttp.apispec.{Schema, SchemaLike} import sttp.apispec.openapi.{Header, MediaType, OpenAPI, Operation, Parameter, PathItem, RequestBody, Response} -import sttp.apispec.validation.SchemaComparator +import sttp.apispec.validation.{SchemaComparator, SchemaResolver} import scala.collection.immutable.ListMap -class OpenAPIComparator( +object OpenAPIComparator { + def apply(clientOpenAPI: OpenAPI, serverOpenAPI: OpenAPI): OpenAPIComparator = + new OpenAPIComparator(clientOpenAPI, serverOpenAPI) +} + +class OpenAPIComparator private ( clientOpenAPI: OpenAPI, serverOpenAPI: OpenAPI ) { @@ -201,7 +206,6 @@ class OpenAPIComparator( else None case _ => None - } } From 897c6f67208148b0104716bb45b06f855b876b56 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Mon, 9 Dec 2024 19:08:40 +0100 Subject: [PATCH 09/16] Add MissingSchema incompability issue --- ...ed-parameter-content-mediatype-schema.json | 148 +++++++++++++ .../petstore.json | 144 +++++++++++++ .../petstore.json | 12 +- .../petstore-added-parameter-schema.json | 148 +++++++++++++ .../added-parameter-schema/petstore.json | 141 +++++++++++++ ...-requestbody-content-mediatype-schema.json | 166 +++++++++++++++ .../petstore.json | 162 +++++++++++++++ ...ded-response-content-mediatype-schema.json | 178 ++++++++++++++++ .../petstore.json | 174 ++++++++++++++++ ...ponse-header-content-mediatype-schema.json | 195 ++++++++++++++++++ .../petstore.json | 191 +++++++++++++++++ ...ded-response-header-content-mediatype.json | 5 + .../petstore.json | 8 +- ...petstore-added-response-header-schema.json | 191 +++++++++++++++++ .../petstore.json | 188 +++++++++++++++++ .../validation/OpenAPIComparatorTest.scala | 158 ++++++++++++++ .../validation/OpenAPIComparator.scala | 5 +- .../OpenAPICompatibilityIssue.scala | 5 + 18 files changed, 2204 insertions(+), 15 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore-added-parameter-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore-added-response-header-schema.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json new file mode 100644 index 0000000..319759b --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.7" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..e8f17ca --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype-schema/petstore.json @@ -0,0 +1,144 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.6" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": {} + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json index b89c469..a45541f 100644 --- a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-content-mediatype/petstore.json @@ -31,17 +31,7 @@ "name": "status", "in": "query", "description": "Filter pets by status", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "available", - "sold" - ] - } - } + "required": false } ], "responses": { diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore-added-parameter-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore-added-parameter-schema.json new file mode 100644 index 0000000..8c11826 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore-added-parameter-schema.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.4" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "schema": { + "type": "string", + "enum": [ + "available", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore.json new file mode 100644 index 0000000..235d7de --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-parameter-schema/petstore.json @@ -0,0 +1,141 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.4" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json new file mode 100644 index 0000000..d4cbd5d --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json @@ -0,0 +1,166 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..1fb418e --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-schema/petstore.json @@ -0,0 +1,162 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": {} + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json new file mode 100644 index 0000000..cff46ba --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json @@ -0,0 +1,178 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..373c358 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-content-mediatype-schema/petstore.json @@ -0,0 +1,174 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json new file mode 100644 index 0000000..e571041 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore.json new file mode 100644 index 0000000..2ae996f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype-schema/petstore.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "content": { + "application/json": {} + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json index 4a90228..38eaf60 100644 --- a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json @@ -128,6 +128,11 @@ "X-Rate-Limit": { "description": "The number of requests allowed per hour", "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, "application/json": { "schema": { "type": "string" diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json index 0d458b4..342ae8e 100644 --- a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-content-mediatype/petstore.json @@ -127,8 +127,12 @@ "headers": { "X-Rate-Limit": { "description": "The number of requests allowed per hour", - "schema": { - "type": "string" + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } } } } diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore-added-response-header-schema.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore-added-response-header-schema.json new file mode 100644 index 0000000..0ff614e --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore-added-response-header-schema.json @@ -0,0 +1,191 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "schema": { + "type": "integer" + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore.json new file mode 100644 index 0000000..7a62193 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-response-header-schema/petstore.json @@ -0,0 +1,188 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour" + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 66be131..21d22ec 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -100,6 +100,28 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing parameter schema when client has one") { + val clientOpenapi = + readOpenAPI("/petstore/added-parameter-schema/petstore-added-parameter-schema.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter-schema/petstore.json") + + val schemaIssue = MissingSchema() + val parameterIssue = IncompatibleParameter("status", List(schemaIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a parameter schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-parameter-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-parameter-schema/petstore-added-parameter-schema.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server missing parameter content media-type when client has an extra one") { val clientOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype/petstore-added-parameter-content-mediatype.json") @@ -140,6 +162,34 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing parameter content media-type schema when client has one") { + val clientOpenapi = + readOpenAPI( + "/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json" + ) + val serverOpenapi = readOpenAPI("/petstore/added-parameter-content-mediatype-schema/petstore.json") + + val schemaIssue = MissingSchema() + val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaIssue)) + val parameterContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val parameterIssue = IncompatibleParameter("status", List(parameterContentIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a parameter content media-type schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-parameter-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI( + "/petstore/added-parameter-content-mediatype-schema/petstore-added-parameter-content-mediatype-schema.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server parameter style is incompatible with client parameter style") { val clientOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") val serverOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore.json") @@ -255,6 +305,34 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing request-body content media-type schema when client has one") { + val clientOpenapi = + readOpenAPI( + "/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json" + ) + val serverOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype-schema/petstore.json") + + val schemaIssue = MissingSchema() + val mediaTypeIssue = IncompatibleMediaType("application/json", List(schemaIssue)) + val requestBodyContentIssue = IncompatibleContent(List(mediaTypeIssue)) + val requestBodyIssue = IncompatibleRequestBody(List(requestBodyContentIssue)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a request-body content media-type schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-requestbody-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI( + "/petstore/added-requestbody-content-mediatype-schema/petstore-added-requestbody-content-mediatype-schema.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server missing response when client has an extra one") { val clientOpenapi = readOpenAPI("/petstore/added-response/petstore-added-response.json") val serverOpenapi = readOpenAPI("/petstore/added-response/petstore.json") @@ -315,6 +393,34 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing response content media-type schema when client has one") { + val clientOpenapi = + readOpenAPI( + "/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json" + ) + val serverOpenapi = readOpenAPI("/petstore/added-response-content-mediatype-schema/petstore.json") + + val schemaIssue = MissingSchema() + val mediaTypeIssues = IncompatibleMediaType("application/json", List(schemaIssue)) + val responseContentIssues = IncompatibleContent(List(mediaTypeIssues)) + val responsesIssue = IncompatibleResponse(List(responseContentIssues)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a response content media-type schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-response-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI( + "/petstore/added-response-content-mediatype-schema/petstore-added-response-content-mediatype-schema.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server missing response header when client has an extra one") { val clientOpenapi = readOpenAPI("/petstore/added-response-header/petstore-added-response-header.json") val serverOpenapi = readOpenAPI("/petstore/added-response-header/petstore.json") @@ -351,6 +457,29 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing response header schema when client has one") { + val clientOpenapi = + readOpenAPI("/petstore/added-response-header-schema/petstore-added-response-header-schema.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-header-schema/petstore.json") + + val schemaIssue = MissingSchema() + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(schemaIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a response header schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-response-header-schema/petstore.json") + val serverOpenapi = readOpenAPI("/petstore/added-response-header-schema/petstore-added-response-header-schema.json") + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server missing response header content media-type when client has an extra one") { val clientOpenapi = readOpenAPI( "/petstore/added-response-header-content-mediatype/petstore-added-response-header-content-mediatype.json" @@ -396,6 +525,35 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + test("server missing response header content media-type schema when client has one") { + val clientOpenapi = + readOpenAPI( + "/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json" + ) + val serverOpenapi = readOpenAPI("/petstore/added-response-header-content-mediatype-schema/petstore.json") + + val schemaIssue = MissingSchema() + val mediaTypeIssues = IncompatibleMediaType("application/json", List(schemaIssue)) + val contentIssues = IncompatibleContent(List(mediaTypeIssues)) + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(contentIssues)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a response header content media-type schema but client does not") { + val clientOpenapi = + readOpenAPI("/petstore/added-response-header-content-mediatype-schema/petstore.json") + val serverOpenapi = readOpenAPI( + "/petstore/added-response-header-content-mediatype-schema/petstore-added-response-header-content-mediatype-schema.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + test("server response header style is incompatible with client response header style") { val clientOpenapi = readOpenAPI("/petstore/updated-response-header-style/petstore-updated-response-header-style.json") diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index 4280ea6..a7e9418 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -197,14 +197,15 @@ class OpenAPIComparator private ( clientSchema: Option[SchemaLike], serverSchema: Option[SchemaLike] ): Option[OpenAPICompatibilityIssue] = { - (serverSchema, clientSchema) match { - case (Some(serverSchema), Some(clientSchema)) => + (clientSchema, serverSchema) match { + case (Some(clientSchema), Some(serverSchema)) => val schemaComparator = new SchemaComparator(clientSchemas, serverSchemas) val schemaIssues = schemaComparator.compare(clientSchema, serverSchema) if (schemaIssues.nonEmpty) Some(IncompatibleSchema(schemaIssues)) else None + case (Some(_), None) => Some(MissingSchema()) case _ => None } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 3fc39f2..57954d0 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -150,3 +150,8 @@ case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompati def description: String = s"incompatible header $headerName:\n${issuesRepr(subIssues)}" } + +case class MissingSchema() extends OpenAPICompatibilityIssue { + def description: String = + s"missing schema" +} \ No newline at end of file From ad6cae14ac78bb1486ebba2bb626c77b5e9c28a9 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Mon, 9 Dec 2024 22:28:28 +0100 Subject: [PATCH 10/16] Add documentation --- .../validation/OpenAPIComparator.scala | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index a7e9418..ce6c941 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -11,6 +11,21 @@ object OpenAPIComparator { new OpenAPIComparator(clientOpenAPI, serverOpenAPI) } +/** + * A utility for comparing two OpenAPI specifications to validate their compatibility. + * + * The `OpenAPIComparator` class compares the client's OpenAPI specification with the server's + * specification to detect and highlight compatibility issues. It evaluates various components + * including paths, operations, parameters, request bodies, responses, headers, schemas, content, + * and media types. + * + * Note: This comparator does not compare meta-data, such as the info object, server lists, or + * descriptions in properties. + * + * @param clientOpenAPI the OpenAPI specification provided by the client. + * @param serverOpenAPI the OpenAPI specification provided by the server. + */ + class OpenAPIComparator private ( clientOpenAPI: OpenAPI, serverOpenAPI: OpenAPI @@ -45,6 +60,16 @@ class OpenAPIComparator private ( case _ => Map.empty[String, Schema] } + /** + * Compares the client and server OpenAPI specifications for compatibility. + * + * This method compares the paths in both specifications to identify compatibility issues. + * It detects incompatibilities such as missing paths, parameter mismatches, schema inconsistencies, + * and other discrepancies. It organizes the issues into a tree-like structure, with main issues and their sub-issues. + * + * @return a list of `OpenAPICompatibilityIssue` instances detailing the identified issues. + */ + def compare(): List[OpenAPICompatibilityIssue] = { clientOpenAPI.paths.pathItems.toList.flatMap { case (pathName, clientPathItem) => From dc58c1bed6cf142b12b55eea130d89a25921e144 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Tue, 10 Dec 2024 16:05:09 +0100 Subject: [PATCH 11/16] added IncompatibleRequiredValue with tests --- .../petstore-required-parameter.json | 96 +++++++++ .../petstore/required-parameter/petstore.json | 96 +++++++++ .../petstore-required-request-body.json | 175 ++++++++++++++++ .../required-request-body/petstore.json | 175 ++++++++++++++++ .../petstore-required-response-header.json | 196 ++++++++++++++++++ .../required-response-header/petstore.json | 196 ++++++++++++++++++ .../validation/OpenAPIComparatorTest.scala | 40 ++++ .../validation/OpenAPIComparator.scala | 95 ++++----- .../OpenAPICompatibilityIssue.scala | 31 ++- 9 files changed, 1035 insertions(+), 65 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore-required-parameter.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore-required-request-body.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore-required-response-header.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore-required-parameter.json b/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore-required-parameter.json new file mode 100644 index 0000000..80b4e6a --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore-required-parameter.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore.json new file mode 100644 index 0000000..59ea55a --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-parameter/petstore.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore-required-request-body.json b/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore-required-request-body.json new file mode 100644 index 0000000..9749b19 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore-required-request-body.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore.json new file mode 100644 index 0000000..a8bc71c --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-request-body/petstore.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore-required-response-header.json b/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore-required-response-header.json new file mode 100644 index 0000000..795fe1a --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore-required-response-header.json @@ -0,0 +1,196 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore.json new file mode 100644 index 0000000..059ae5d --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/required-response-header/petstore.json @@ -0,0 +1,196 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.1.2" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "style": "form", + "explode": true, + "allowEmptyValue": true, + "allowReserved": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "headers": { + "X-Rate-Limit": { + "description": "The number of requests allowed per hour", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "404": { + "description": "Pet not found" + }, + "500": { + "description": "Internal Error" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "breed": { + "type": "string", + "description": "The breed of the pet" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "UpdatedPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 21d22ec..45ebb02 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -624,4 +624,44 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + + test("server parameter required value is incompatible with client parameter required value") { + val clientOpenapi = readOpenAPI("/petstore/required-parameter/petstore-required-parameter.json") + val serverOpenapi = readOpenAPI("/petstore/required-parameter/petstore.json") + + val requiredValueIssue = IncompatibleRequiredValue(Some(true), Some(false)) + val parameterIssue = IncompatibleParameter("petId", List(requiredValueIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("server response header required value is incompatible with client response header required value") { + val clientOpenapi = readOpenAPI("/petstore/required-response-header/petstore-required-response-header.json") + val serverOpenapi = readOpenAPI("/petstore/required-response-header/petstore.json") + + val requiredValueIssue = IncompatibleRequiredValue(Some(true), Some(false)) + val headerIssue = IncompatibleHeader("X-Rate-Limit", List(requiredValueIssue)) + val responsesIssue = IncompatibleResponse(List(headerIssue)) + val operationIssue = IncompatibleOperation("get", List(responsesIssue)) + val pathIssue = IncompatiblePath("/pets/{petId}", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("server request-body required value is incompatible with client request-body required value") { + val clientOpenapi = readOpenAPI("/petstore/required-request-body/petstore-required-request-body.json") + val serverOpenapi = readOpenAPI("/petstore/required-request-body/petstore.json") + + val requiredValueIssue = IncompatibleRequiredValue(Some(true), Some(false)) + val requestBodyIssue = IncompatibleRequestBody(List(requiredValueIssue)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index ce6c941..a13a532 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -11,20 +11,20 @@ object OpenAPIComparator { new OpenAPIComparator(clientOpenAPI, serverOpenAPI) } -/** - * A utility for comparing two OpenAPI specifications to validate their compatibility. - * - * The `OpenAPIComparator` class compares the client's OpenAPI specification with the server's - * specification to detect and highlight compatibility issues. It evaluates various components - * including paths, operations, parameters, request bodies, responses, headers, schemas, content, - * and media types. - * - * Note: This comparator does not compare meta-data, such as the info object, server lists, or - * descriptions in properties. - * - * @param clientOpenAPI the OpenAPI specification provided by the client. - * @param serverOpenAPI the OpenAPI specification provided by the server. - */ +/** A utility for comparing two OpenAPI specifications to validate their compatibility. + * + * The `OpenAPIComparator` class compares the client's OpenAPI specification with the server's specification to detect + * and highlight compatibility issues. It evaluates various components including paths, operations, parameters, request + * bodies, responses, headers, schemas, content, and media types. + * + * Note: This comparator does not compare meta-data, such as the info object, server lists, or descriptions in + * properties. + * + * @param clientOpenAPI + * the OpenAPI specification provided by the client. + * @param serverOpenAPI + * the OpenAPI specification provided by the server. + */ class OpenAPIComparator private ( clientOpenAPI: OpenAPI, @@ -60,15 +60,15 @@ class OpenAPIComparator private ( case _ => Map.empty[String, Schema] } - /** - * Compares the client and server OpenAPI specifications for compatibility. - * - * This method compares the paths in both specifications to identify compatibility issues. - * It detects incompatibilities such as missing paths, parameter mismatches, schema inconsistencies, - * and other discrepancies. It organizes the issues into a tree-like structure, with main issues and their sub-issues. - * - * @return a list of `OpenAPICompatibilityIssue` instances detailing the identified issues. - */ + /** Compares the client and server OpenAPI specifications for compatibility. + * + * This method compares the paths in both specifications to identify compatibility issues. It detects + * incompatibilities such as missing paths, parameter mismatches, schema inconsistencies, and other discrepancies. It + * organizes the issues into a tree-like structure, with main issues and their sub-issues. + * + * @return + * a list of `OpenAPICompatibilityIssue` instances detailing the identified issues. + */ def compare(): List[OpenAPICompatibilityIssue] = { clientOpenAPI.paths.pathItems.toList.flatMap { @@ -120,23 +120,14 @@ class OpenAPIComparator private ( val parametersIssue = clientParameters.flatMap { clientParameter => val serverParameter = serverParameters.find(_.name == clientParameter.name) serverParameter match { - case None => Some(MissingParameter(clientParameter.name)) - case Some(serverParameter) => - if (clientParameter.required.getOrElse(false) && !serverParameter.required.getOrElse(false)) { - Some(IncompatibleRequiredParameter(clientParameter.name)) - } else { - checkParameter(clientParameter, serverParameter) - } + case None => Some(MissingParameter(clientParameter.name)) + case Some(serverParameter) => checkParameter(clientParameter, serverParameter) } } val requestBodyIssue = (clientOperation.requestBody, serverOperation.requestBody) match { case (Some(Right(clientRequestBody)), Some(Right(serverRequestBody))) => - if (clientRequestBody.required.getOrElse(false) && !serverRequestBody.required.getOrElse(false)) { - Some(IncompatibleRequiredRequestBody()) - } else { - checkRequestBody(clientRequestBody, serverRequestBody) - } + checkRequestBody(clientRequestBody, serverRequestBody) case (Some(Right(_)), None) => Some(MissingRequestBody()) case _ => None } @@ -165,6 +156,7 @@ class OpenAPIComparator private ( val isCompatibleExplode = serverParameter.explode == clientParameter.explode val isCompatibleAllowEmptyValue = serverParameter.allowEmptyValue == clientParameter.allowEmptyValue val isCompatibleAllowReserved = serverParameter.allowReserved == clientParameter.allowReserved + val isCompatibleRequiredValue = serverParameter.required == clientParameter.required val issues = checkSchema(clientParameter.schema, serverParameter.schema).toList ++ @@ -178,7 +170,10 @@ class OpenAPIComparator private ( else None).toList ++ (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved(clientParameter.allowReserved, serverParameter.allowReserved)) - else None).toList + else None).toList ++ + (if (!isCompatibleRequiredValue) + Some(IncompatibleRequiredValue(clientParameter.required, serverParameter.required)) + else None).toList if (issues.isEmpty) None @@ -231,7 +226,7 @@ class OpenAPIComparator private ( else None case (Some(_), None) => Some(MissingSchema()) - case _ => None + case _ => None } } @@ -253,9 +248,16 @@ class OpenAPIComparator private ( clientRequestBody: RequestBody, serverRequestBody: RequestBody ): Option[IncompatibleRequestBody] = { + val isCompatibleRequiredValue = serverRequestBody.required == clientRequestBody.required val contentIssues = checkContent(clientRequestBody.content, serverRequestBody.content).toList - if (contentIssues.nonEmpty) - Some(IncompatibleRequestBody(contentIssues)) + + val issues = contentIssues ++ + (if (!isCompatibleRequiredValue) + Some(IncompatibleRequiredValue(clientRequestBody.required, serverRequestBody.required)) + else None).toList + + if (issues.nonEmpty) + Some(IncompatibleRequestBody(issues)) else None } @@ -266,14 +268,9 @@ class OpenAPIComparator private ( case (clientHeaderName, Right(clientHeader)) => val serverHeader = serverResponse.headers.get(clientHeaderName) serverHeader match { - case Some(Right(serverHeader)) => - if (clientHeader.required.getOrElse(false) && !serverHeader.required.getOrElse(false)) { - Some(IncompatibleRequiredHeader(clientHeaderName)) - } else { - checkResponseHeader(clientHeaderName, clientHeader, serverHeader) - } - case None => Some(MissingHeader(clientHeaderName)) - case _ => None + case Some(Right(serverHeader)) => checkResponseHeader(clientHeaderName, clientHeader, serverHeader) + case None => Some(MissingHeader(clientHeaderName)) + case _ => None } case _ => None } @@ -296,6 +293,7 @@ class OpenAPIComparator private ( val isCompatibleExplode = serverHeader.explode == clientHeader.explode val isCompatibleAllowEmptyValue = serverHeader.allowEmptyValue == clientHeader.allowEmptyValue val isCompatibleAllowReserved = serverHeader.allowReserved == clientHeader.allowReserved + val isCompatibleRequiredValue = serverHeader.required == clientHeader.required val issues = schemaIssues.toList ++ @@ -308,6 +306,9 @@ class OpenAPIComparator private ( else None).toList ++ (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved(clientHeader.allowReserved, serverHeader.allowReserved)) + else None).toList ++ + (if (!isCompatibleRequiredValue) + Some(IncompatibleRequiredValue(clientHeader.required, serverHeader.required)) else None).toList if (issues.nonEmpty) diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 57954d0..6fdfebc 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -50,11 +50,12 @@ case class MissingParameter( s"missing parameter $name" } -case class IncompatibleRequiredParameter( - name: String +case class IncompatibleRequiredValue( + clientValue: Option[Boolean], + serverValue: Option[Boolean] ) extends OpenAPICompatibilityIssue { def description: String = - s"parameter '$name' is required by the client but optional on the server" + s"required value mismatch: client has $clientValue, but server has $serverValue" } case class IncompatibleParameter( @@ -90,22 +91,26 @@ case class IncompatibleMediaType(mediaType: String, subIssues: List[OpenAPICompa s"incompatible media type $mediaType:\n${issuesRepr(subIssues)}" } -case class IncompatibleStyle(clientValue: Option[ParameterStyle], serverValue: Option[ParameterStyle]) extends OpenAPICompatibilityIssue { +case class IncompatibleStyle(clientValue: Option[ParameterStyle], serverValue: Option[ParameterStyle]) + extends OpenAPICompatibilityIssue { def description: String = s"incompatible style value: client=$clientValue, server=$serverValue" } -case class IncompatibleExplode(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { +case class IncompatibleExplode(clientValue: Option[Boolean], serverValue: Option[Boolean]) + extends OpenAPICompatibilityIssue { def description: String = s"incompatible explode value: client=$clientValue, server=$serverValue" } -case class IncompatibleAllowEmptyValue(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { +case class IncompatibleAllowEmptyValue(clientValue: Option[Boolean], serverValue: Option[Boolean]) + extends OpenAPICompatibilityIssue { def description: String = s"incompatible allowEmptyValue value: client=$clientValue, server=$serverValue" } -case class IncompatibleAllowReserved(clientValue: Option[Boolean], serverValue: Option[Boolean]) extends OpenAPICompatibilityIssue { +case class IncompatibleAllowReserved(clientValue: Option[Boolean], serverValue: Option[Boolean]) + extends OpenAPICompatibilityIssue { def description: String = s"incompatible allowReserved value: client=$clientValue, server=$serverValue" } @@ -115,11 +120,6 @@ case class MissingRequestBody() extends OpenAPICompatibilityIssue { s"missing request body" } -case class IncompatibleRequiredRequestBody() extends OpenAPICompatibilityIssue { - def description: String = - s"request body is required by the client but optional on the server" -} - case class IncompatibleRequestBody(subIssues: List[OpenAPICompatibilityIssue]) extends OpenAPICompatibilitySubIssues { def description: String = s"incompatible request body:\n${issuesRepr(subIssues)}" @@ -140,11 +140,6 @@ case class MissingHeader(headerName: String) extends OpenAPICompatibilityIssue { s"missing header $headerName" } -case class IncompatibleRequiredHeader(headerName: String) extends OpenAPICompatibilityIssue { - def description: String = - s"header '$headerName' is required by the client but optional on the server" -} - case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompatibilityIssue]) extends OpenAPICompatibilitySubIssues { def description: String = @@ -154,4 +149,4 @@ case class IncompatibleHeader(headerName: String, subIssues: List[OpenAPICompati case class MissingSchema() extends OpenAPICompatibilityIssue { def description: String = s"missing schema" -} \ No newline at end of file +} From fd10f354db35ac81f9626d064fbf528b78fb7dd9 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Tue, 10 Dec 2024 16:57:06 +0100 Subject: [PATCH 12/16] Add Encoding Compability Issues --- ...equestbody-content-mediatype-encoding.json | 64 ++++++++ .../petstore.json | 60 +++++++ ...-parameter-content-mediatype-encoding.json | 151 ++++++++++++++++++ .../petstore.json | 151 ++++++++++++++++++ ...equestbody-content-mediatype-encoding.json | 75 +++++++++ .../petstore.json | 64 ++++++++ .../validation/OpenAPIComparatorTest.scala | 58 +++++++ .../validation/OpenAPIComparator.scala | 60 ++++++- .../OpenAPICompatibilityIssue.scala | 17 ++ 9 files changed, 697 insertions(+), 3 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore-updated-parameter-content-mediatype-encoding.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore-updated-requestbody-content-mediatype-encoding.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json new file mode 100644 index 0000000..7056232 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json @@ -0,0 +1,64 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "photo": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "photo": { + "contentType": "image/jpeg" + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore.json new file mode 100644 index 0000000..9ac582f --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-requestbody-content-mediatype-encoding/petstore.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "photo": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": {} + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore-updated-parameter-content-mediatype-encoding.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore-updated-parameter-content-mediatype-encoding.json new file mode 100644 index 0000000..c5c3cb0 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore-updated-parameter-content-mediatype-encoding.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.6" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore.json new file mode 100644 index 0000000..fc268bb --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-parameter-content-mediatype-encoding/petstore.json @@ -0,0 +1,151 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Sample Pet Store App", + "summary": "A pet store manager.", + "description": "This is a sample server for a pet store.", + "termsOfService": "https://example.com/terms/", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.6" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "operationId": "getPets", + "description": "Gets all pets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter pets by status", + "required": false, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "post": { + "operationId": "addPet", + "description": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewPet" + } + } + } + }, + "responses": { + "201": { + "description": "Pet created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "operationId": "getPetById", + "description": "Get a pet by its ID", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + }, + "description": "The ID of the pet to retrieve" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "404": { + "description": "Pet not found" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "NewPet": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + } +} \ No newline at end of file diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore-updated-requestbody-content-mediatype-encoding.json b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore-updated-requestbody-content-mediatype-encoding.json new file mode 100644 index 0000000..2584757 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore-updated-requestbody-content-mediatype-encoding.json @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "photo": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "photo": { + "contentType": "image/jpeg", + "style": "form", + "explode": true, + "allowReserved": false, + "headers": { + "X-Custom-Header": { + "description": "Custom header for encoding", + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore.json new file mode 100644 index 0000000..7056232 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-requestbody-content-mediatype-encoding/petstore.json @@ -0,0 +1,64 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "photo": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "photo": { + "contentType": "image/jpeg" + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 45ebb02..10cf9cb 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -664,4 +664,62 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + + test("server missing request-body content media-type encoding when client has one") { + val clientOpenapi = readOpenAPI( + "/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json" + ) + val serverOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype-encoding/petstore.json") + + val encodingIssue = MissingEncoding("photo") + val mediaTypeIssues = IncompatibleMediaType("multipart/form-data", List(encodingIssue)) + val contentIssues = IncompatibleContent(List(mediaTypeIssues)) + val requestBodyIssue = IncompatibleRequestBody(List(contentIssues)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } + + test("no errors when server has a request-body content media-type encoding but client does not") { + val clientOpenapi = readOpenAPI("/petstore/added-requestbody-content-mediatype-encoding/petstore.json") + val serverOpenapi = readOpenAPI( + "/petstore/added-requestbody-content-mediatype-encoding/petstore-added-requestbody-content-mediatype-encoding.json" + ) + + assert(compare(clientOpenapi, serverOpenapi).isEmpty) + } + + test( + "server request-body content media-type encoding is incompatible with client request-body content media-type encoding" + ) { + val clientOpenapi = + readOpenAPI( + "/petstore/updated-requestbody-content-mediatype-encoding/petstore-updated-requestbody-content-mediatype-encoding.json" + ) + val serverOpenapi = readOpenAPI("/petstore/updated-requestbody-content-mediatype-encoding/petstore.json") + + val missingHeaderIssue = MissingHeader("X-Custom-Header") + val allowReservedIssue = IncompatibleAllowReserved(Some(false), None) + val explodeIssue = IncompatibleExplode(Some(true), None) + val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) + val encodingIssue = IncompatibleEncoding( + "photo", + List( + missingHeaderIssue, + styleIssue, + explodeIssue, + allowReservedIssue + ) + ) + val mediaTypeIssue = IncompatibleMediaType("multipart/form-data", List(encodingIssue)) + val contentIssue = IncompatibleContent(List(mediaTypeIssue)) + val requestBodyIssue = IncompatibleRequestBody(List(contentIssue)) + val operationIssue = IncompatibleOperation("post", List(requestBodyIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index a13a532..c3ce10b 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -1,7 +1,17 @@ package sttp.apispec.openapi.validation import sttp.apispec.{Schema, SchemaLike} -import sttp.apispec.openapi.{Header, MediaType, OpenAPI, Operation, Parameter, PathItem, RequestBody, Response} +import sttp.apispec.openapi.{ + Encoding, + Header, + MediaType, + OpenAPI, + Operation, + Parameter, + PathItem, + RequestBody, + Response +} import sttp.apispec.validation.{SchemaComparator, SchemaResolver} import scala.collection.immutable.ListMap @@ -200,17 +210,61 @@ class OpenAPIComparator private ( Some(IncompatibleContent(issues.toList)) } + private def checkEncoding( + encodingName: String, + clientEncoding: Encoding, + serverEncoding: Encoding + ): Option[IncompatibleEncoding] = { + val isCompatibleStyle = serverEncoding.style == clientEncoding.style + val isCompatibleExplode = serverEncoding.explode == clientEncoding.explode + val isCompatibleAllowReserved = serverEncoding.allowReserved == clientEncoding.allowReserved + val isCompatibleContentType = serverEncoding.contentType == clientEncoding.contentType + val headerIssues = clientEncoding.headers.flatMap { + case (clientHeaderName, Right(clientHeader)) => + val serverHeader = serverEncoding.headers.get(clientHeaderName) + serverHeader match { + case Some(Right(serverHeader)) => checkResponseHeader(clientHeaderName, clientHeader, serverHeader) + case None => Some(MissingHeader(clientHeaderName)) + case _ => None + } + case _ => None + } + + val issues = headerIssues ++ + (if (!isCompatibleStyle) Some(IncompatibleStyle(clientEncoding.style, serverEncoding.style)) else None).toList ++ + (if (!isCompatibleContentType) + Some(IncompatibleContentType(clientEncoding.contentType, serverEncoding.contentType)) + else None).toList ++ + (if (!isCompatibleExplode) Some(IncompatibleExplode(clientEncoding.explode, serverEncoding.explode)) + else None).toList ++ + (if (!isCompatibleAllowReserved) + Some(IncompatibleAllowReserved(clientEncoding.allowReserved, serverEncoding.allowReserved)) + else None).toList + + if (issues.nonEmpty) + Some(IncompatibleEncoding(encodingName, issues.toList)) + else + None + } + private def checkMediaType( mediaType: String, clientMediaTypeDescription: MediaType, serverMediaTypeDescription: MediaType ): Option[IncompatibleMediaType] = { - val issues = checkSchema(clientMediaTypeDescription.schema, serverMediaTypeDescription.schema) + val encodingIssues = clientMediaTypeDescription.encoding.flatMap { case (clientEncodingName, clientEncoding) => + val serverEncoding = serverMediaTypeDescription.encoding.get(clientEncodingName) + serverEncoding match { + case None => Some(MissingEncoding(clientEncodingName)) + case Some(serverEncoding) => checkEncoding(clientEncodingName, clientEncoding, serverEncoding) + } + } + + val issues = checkSchema(clientMediaTypeDescription.schema, serverMediaTypeDescription.schema) ++ encodingIssues if (issues.nonEmpty) Some(IncompatibleMediaType(mediaType, issues.toList)) else None - // TODO: encoding? } private def checkSchema( diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 6fdfebc..8017e2e 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -150,3 +150,20 @@ case class MissingSchema() extends OpenAPICompatibilityIssue { def description: String = s"missing schema" } + +case class MissingEncoding(encodingName: String) extends OpenAPICompatibilityIssue { + def description: String = + s"missing encoding $encodingName" +} + +case class IncompatibleEncoding(encodingName: String, subIssues: List[OpenAPICompatibilityIssue]) + extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible encoding $encodingName:\n${issuesRepr(subIssues)}" +} + +case class IncompatibleContentType(clientValue: Option[String], serverValue: Option[String]) + extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible contentType: client=$clientValue, server=$serverValue" +} \ No newline at end of file From cd63ab97e91c4b0fd978f52ad9ead8396560dddb Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Tue, 10 Dec 2024 19:10:47 +0100 Subject: [PATCH 13/16] Add Security Requirements Compatibility Issues --- .../petstore-updated-operation-security.json | 65 +++++++++++++++++++ .../updated-operation-security/petstore.json | 64 ++++++++++++++++++ .../validation/OpenAPIComparatorTest.scala | 15 +++++ .../validation/OpenAPIComparator.scala | 21 ++---- .../OpenAPICompatibilityIssue.scala | 10 ++- 5 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore-updated-operation-security.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore-updated-operation-security.json b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore-updated-operation-security.json new file mode 100644 index 0000000..857f276 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore-updated-operation-security.json @@ -0,0 +1,65 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "get": { + "summary": "Get a list of pets", + "security": [ + { + "OAuth2": [ + "read:pets", + "write:pets" + ] + } + ], + "responses": { + "200": { + "description": "A list of pets.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "read:pets": "Read pets information", + "write:pets": "Add or modify pets" + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore.json new file mode 100644 index 0000000..eaec9c2 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-security/petstore.json @@ -0,0 +1,64 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "get": { + "summary": "Get a list of pets", + "security": [ + { + "OAuth2": [ + "read:pets" + ] + } + ], + "responses": { + "200": { + "description": "A list of pets.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "read:pets": "Read pets information", + "write:pets": "Add or modify pets" + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 10cf9cb..5102184 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -8,6 +8,8 @@ import sttp.apispec.openapi.circe.openAPIDecoder import sttp.apispec.test.ResourcePlatform import sttp.apispec.validation.TypeMismatch +import scala.collection.immutable.ListMap + class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { override val basedir = "openapi-comparator-tests" @@ -722,4 +724,17 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + + test("server operation security is incompatible with client operation security") { + val clientOpenapi = readOpenAPI("/petstore/updated-operation-security/petstore-updated-operation-security.json") + val serverOpenapi = readOpenAPI("/petstore/updated-operation-security/petstore.json") + + val securityRequirementIssue = + IncompatibleSecurityRequirement(ListMap("OAuth2" -> Vector("read:pets", "write:pets"))) + val operationIssue = IncompatibleOperation("get", List(securityRequirementIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + val expected = List(pathIssue) + + assert(compare(clientOpenapi, serverOpenapi) == expected) + } } diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index c3ce10b..0cf39d1 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -1,17 +1,7 @@ package sttp.apispec.openapi.validation -import sttp.apispec.{Schema, SchemaLike} -import sttp.apispec.openapi.{ - Encoding, - Header, - MediaType, - OpenAPI, - Operation, - Parameter, - PathItem, - RequestBody, - Response -} +import sttp.apispec.{Schema, SchemaLike, SecurityRequirement} +import sttp.apispec.openapi.{Encoding, Header, MediaType, OpenAPI, Operation, Parameter, PathItem, RequestBody, Response} import sttp.apispec.validation.{SchemaComparator, SchemaResolver} import scala.collection.immutable.ListMap @@ -153,8 +143,11 @@ class OpenAPIComparator private ( case _ => None } - // TODO: callbacks, security? - val issues = parametersIssue ++ requestBodyIssue ++ responsesIssues + // TODO: callbacks + val incompatibleSecurityRequirements = clientOperation.security.filterNot(serverOperation.security.contains) + val securityIssues = incompatibleSecurityRequirements.map(IncompatibleSecurityRequirement(_)) + + val issues = parametersIssue ++ requestBodyIssue ++ responsesIssues ++ securityIssues if (issues.isEmpty) None else diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala index 8017e2e..12f625d 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPICompatibilityIssue.scala @@ -1,5 +1,6 @@ package sttp.apispec.openapi.validation +import sttp.apispec.SecurityRequirement import sttp.apispec.openapi.{ParameterStyle, ResponsesKey} import sttp.apispec.validation.SchemaCompatibilityIssue @@ -163,7 +164,12 @@ case class IncompatibleEncoding(encodingName: String, subIssues: List[OpenAPICom } case class IncompatibleContentType(clientValue: Option[String], serverValue: Option[String]) - extends OpenAPICompatibilityIssue { + extends OpenAPICompatibilityIssue { def description: String = s"incompatible contentType: client=$clientValue, server=$serverValue" -} \ No newline at end of file +} + +case class IncompatibleSecurityRequirement(securityRequirement: SecurityRequirement) extends OpenAPICompatibilityIssue { + def description: String = + s"incompatible security requirement $securityRequirement" +} From 8c01a866b3e127d29f6e46117f41d35de2e8db72 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Tue, 10 Dec 2024 19:33:13 +0100 Subject: [PATCH 14/16] Fix compile issue --- .../petstore-added-operation-callback.json | 73 ++++++++++++ .../added-operation-callback/petstore.json | 41 +++++++ .../petstore-updated-operation-callback.json | 106 ++++++++++++++++++ .../updated-operation-callback/petstore.json | 90 +++++++++++++++ .../validation/OpenAPIComparator.scala | 22 +++- 5 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore-added-operation-callback.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore-updated-operation-callback.json create mode 100644 openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore.json diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore-added-operation-callback.json b/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore-added-operation-callback.json new file mode 100644 index 0000000..b997e73 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore-added-operation-callback.json @@ -0,0 +1,73 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully." + } + }, + "callbacks": { + "onPetStatusChange": { + "{$request.body#/callbackUrl}": { + "post": { + "summary": "Notify about pet status change", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Callback successfully processed." + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore.json new file mode 100644 index 0000000..6255aed --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/added-operation-callback/petstore.json @@ -0,0 +1,41 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully." + } + }, + "callbacks": {} + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore-updated-operation-callback.json b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore-updated-operation-callback.json new file mode 100644 index 0000000..60a6f64 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore-updated-operation-callback.json @@ -0,0 +1,106 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully." + } + }, + "callbacks": { + "onPetStatusChange": { + "{$request.body#/callbackUrl}": { + "post": { + "summary": "Notify about pet status change", + "description": "This callback is triggered whenever the status of a pet changes.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The new status of the pet.", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "The time when the status change occurred." + }, + "petId": { + "type": "integer", + "description": "The unique ID of the pet whose status changed." + } + }, + "required": [ + "status", + "timestamp", + "petId" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Callback successfully processed." + }, + "400": { + "description": "Invalid payload provided.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Details about the error." + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore.json b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore.json new file mode 100644 index 0000000..2a1d226 --- /dev/null +++ b/openapi-comparator-tests/src/test/resources/petstore/updated-operation-callback/petstore.json @@ -0,0 +1,90 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Simple Pet Store API", + "version": "1.0.0" + }, + "paths": { + "/pets": { + "post": { + "summary": "Add a new pet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Pet created successfully." + } + }, + "callbacks": { + "onPetStatusChange": { + "{$request.body#/callbackUrl}": { + "post": { + "summary": "Notify about pet status change", + "description": "This callback is triggered whenever the status of a pet changes.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The new status of the pet.", + "enum": [ + "available", + "pending", + "sold" + ] + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "The time when the status change occurred." + }, + "petId": { + "type": "integer", + "description": "The unique ID of the pet whose status changed." + } + }, + "required": [ + "status", + "timestamp", + "petId" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Callback successfully processed." + } + } + } + } + } + } + } + } + } +} diff --git a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala index 0cf39d1..e30e7ee 100644 --- a/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala +++ b/openapi-model/src/main/scala/sttp/apispec/openapi/validation/OpenAPIComparator.scala @@ -1,8 +1,18 @@ package sttp.apispec.openapi.validation -import sttp.apispec.{Schema, SchemaLike, SecurityRequirement} -import sttp.apispec.openapi.{Encoding, Header, MediaType, OpenAPI, Operation, Parameter, PathItem, RequestBody, Response} -import sttp.apispec.validation.{SchemaComparator, SchemaResolver} +import sttp.apispec.{Schema, SchemaLike} +import sttp.apispec.openapi.{ + Encoding, + Header, + MediaType, + OpenAPI, + Operation, + Parameter, + PathItem, + RequestBody, + Response +} +import sttp.apispec.validation.SchemaComparator import scala.collection.immutable.ListMap @@ -174,9 +184,9 @@ class OpenAPIComparator private ( (if (!isCompatibleAllowReserved) Some(IncompatibleAllowReserved(clientParameter.allowReserved, serverParameter.allowReserved)) else None).toList ++ - (if (!isCompatibleRequiredValue) - Some(IncompatibleRequiredValue(clientParameter.required, serverParameter.required)) - else None).toList + (if (!isCompatibleRequiredValue) + Some(IncompatibleRequiredValue(clientParameter.required, serverParameter.required)) + else None).toList if (issues.isEmpty) None From 83431f58d71192aed55053e7d33d50d90ba518c8 Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 16 Dec 2024 10:27:49 +0100 Subject: [PATCH 15/16] Try increasing memory --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47fc81..c02bfc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 env: STTP_NATIVE: 1 - JAVA_OPTS: -Xmx4G -Xss4M # larger stack needed due to Circe generating large trees: https://github.com/lampepfl/dotty/issues/18669 + JAVA_OPTS: -Xmx6G -Xss4M # larger stack needed due to Circe generating large trees: https://github.com/lampepfl/dotty/issues/18669 steps: - name: Checkout uses: actions/checkout@v2 From 1c41d740b4becb92b89e36d7d311fbc11a004f46 Mon Sep 17 00:00:00 2001 From: abdelfetah18 Date: Mon, 16 Dec 2024 14:24:06 +0100 Subject: [PATCH 16/16] Add tests for issue description rendering --- .../validation/OpenAPIComparatorTest.scala | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala index 5102184..445831e 100644 --- a/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala +++ b/openapi-comparator-tests/src/test/scala/sttp/apispec/openapi/validation/OpenAPIComparatorTest.scala @@ -737,4 +737,36 @@ class OpenAPIComparatorTest extends AnyFunSuite with ResourcePlatform { assert(compare(clientOpenapi, serverOpenapi) == expected) } + + test("format single issue description correctly") { + val clientOpenapi = readOpenAPI("/petstore/added-path/petstore-added-path.json") + val serverOpenapi = readOpenAPI("/petstore/added-path/petstore.json") + + val expected = List(MissingPath("/pets/{petId}")) + val expectedDescription = "List(missing path: /pets/{petId})" + + val result = compare(clientOpenapi, serverOpenapi) + + assert(result == expected) + assert(result.toString() == expectedDescription) + } + + test("render list of issues with proper indentation") { + val clientOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore-updated-parameter-style.json") + val serverOpenapi = readOpenAPI("/petstore/updated-parameter-style/petstore.json") + + val styleIssue = IncompatibleStyle(Some(ParameterStyle.Form), None) + val parameterIssue = IncompatibleParameter("status", List(styleIssue)) + val operationIssue = IncompatibleOperation("get", List(parameterIssue)) + val pathIssue = IncompatiblePath("/pets", List(operationIssue)) + + val expected = List(pathIssue) + val expectedDescription = + "List(incompatible path /pets:\n- incompatible operation get:\n - incompatible parameter status:\n - incompatible style value: client=Some(Form), server=None)" + + val result = compare(clientOpenapi, serverOpenapi) + + assert(result == expected) + assert(result.toString() == expectedDescription) + } }