diff --git a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt index 63ec5ae8f..e237431a1 100644 --- a/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt +++ b/core/src/main/kotlin/in/specmatic/conversions/OpenApiSpecification.kt @@ -11,6 +11,7 @@ import `in`.specmatic.core.value.Value import `in`.specmatic.core.wsdl.parser.message.MULTIPLE_ATTRIBUTE_VALUE import `in`.specmatic.core.wsdl.parser.message.OCCURS_ATTRIBUTE_NAME import `in`.specmatic.core.wsdl.parser.message.OPTIONAL_ATTRIBUTE_VALUE +import io.cucumber.messages.internal.com.fasterxml.jackson.databind.ObjectMapper import io.cucumber.messages.types.Step import io.ktor.util.reflect.* import io.swagger.v3.oas.models.OpenAPI @@ -53,14 +54,15 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI } fun fromYAML(yamlContent: String, filePath: String): OpenApiSpecification { - val parseResult: SwaggerParseResult = OpenAPIV3Parser().readContents(yamlContent, null, resolveExternalReferences(), filePath) + val parseResult: SwaggerParseResult = + OpenAPIV3Parser().readContents(yamlContent, null, resolveExternalReferences(), filePath) val openApi: OpenAPI? = parseResult.openAPI - if(openApi == null) { + if (openApi == null) { logger.debug("Failed to parse OpenAPI from file $filePath\n\n$yamlContent") parseResult.messages.filterNotNull().let { - if(it.isNotEmpty()) { + if (it.isNotEmpty()) { val parserMessages = parseResult.messages.joinToString(System.lineSeparator()) logger.log("Error parsing file $filePath") logger.log(parserMessages.prependIndent(" ")) @@ -228,12 +230,21 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI val requestBodyExample: Map = requestBodyExample(operation, exampleName) - val requestExamples = parameterExamples.plus(requestBodyExample) + val requestExamples = parameterExamples.plus(requestBodyExample).map { (key, value) -> + if (value.toString().contains("externalValue")) "${key}_filename" to value + else key to value + }.toMap() when { requestExamples.isNotEmpty() -> Row( - requestExamples.keys.toList(), - requestExamples.values.toList().map { value: Any? -> value?.toString() ?: "" }) + requestExamples.keys.toList().map { keyName: String -> keyName }, + requestExamples.values.toList().map { value: Any? -> value?.toString() ?: "" } + .map { valueString: String -> + if (valueString.contains("externalValue")) { + ObjectMapper().readValue(valueString, Map::class.java).values.first() + .toString() + } else valueString + }) else -> Row() } } @@ -244,7 +255,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI val scenarioName = scenarioName(operation, response, httpRequestPattern, null) - val scenarioDetails = object: ScenarioDetailsForResult { + val scenarioDetails = object : ScenarioDetailsForResult { override val ignoreFailure: Boolean get() = false override val name: String @@ -276,7 +287,12 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI httpRequestPattern = httpRequestPattern, httpResponsePattern = httpResponsePattern, ignoreFailure = ignoreFailure, - examples = if(specmaticExampleRows.isNotEmpty()) listOf(Examples(specmaticExampleRows.first().columnNames, specmaticExampleRows)) else emptyList() + examples = if (specmaticExampleRows.isNotEmpty()) listOf( + Examples( + specmaticExampleRows.first().columnNames, + specmaticExampleRows + ) + ) else emptyList() ) } }.flatten() @@ -292,8 +308,11 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI operation.requestBody?.content?.values?.firstOrNull()?.examples?.get(exampleName)?.value val requestBodyExample: Map = if (requestExampleValue != null) { - if(operation.requestBody?.content?.entries?.first()?.key == "application/x-www-form-urlencoded" || operation.requestBody?.content?.entries?.first()?.key == "multipart/form-data") { - val jsonExample = attempt("Could not parse example $exampleName for operation \"${operation.summary}\"") { parsedJSON(requestExampleValue.toString()) as JSONObjectValue } + if (operation.requestBody?.content?.entries?.first()?.key == "application/x-www-form-urlencoded" || operation.requestBody?.content?.entries?.first()?.key == "multipart/form-data") { + val jsonExample = + attempt("Could not parse example $exampleName for operation \"${operation.summary}\"") { + parsedJSON(requestExampleValue.toString()) as JSONObjectValue + } jsonExample.jsonObject.map { (key, value) -> key to value.toString() }.toMap() @@ -338,7 +357,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI status: String, headersMap: Map ): List> { - if(response.content == null) { + if (response.content == null) { val responsePattern = HttpResponsePattern( headersPattern = HttpHeadersPattern(headersMap), status = status.toIntOrNull() ?: DEFAULT_RESPONSE_CODE @@ -350,7 +369,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI return response.content.map { (contentType, mediaType) -> val responsePattern = HttpResponsePattern( headersPattern = HttpHeadersPattern(headersMap), - status = if(status == "default") 1000 else status.toInt(), + status = if (status == "default") 1000 else status.toInt(), body = when (contentType) { "application/xml" -> toXMLPattern(mediaType) else -> toSpecmaticPattern(mediaType) @@ -365,9 +384,10 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI path: String, httpMethod: String, operation: Operation ): List { - val contractSecuritySchemes: Map = openApi.components?.securitySchemes?.mapValues { (_, scheme) -> - toSecurityScheme(scheme) - } ?: emptyMap() + val contractSecuritySchemes: Map = + openApi.components?.securitySchemes?.mapValues { (_, scheme) -> + toSecurityScheme(scheme) + } ?: emptyMap() val parameters = operation.parameters @@ -383,7 +403,10 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI val urlMatcher = toURLMatcherWithOptionalQueryParams(path, securityQueryParams) val headersPattern = HttpHeadersPattern(headersMap) val requestPattern = HttpRequestPattern( - urlMatcher = urlMatcher, method = httpMethod, headersPattern = headersPattern, securitySchemes = operationSecuritySchemes + urlMatcher = urlMatcher, + method = httpMethod, + headersPattern = headersPattern, + securitySchemes = operationSecuritySchemes ) return when (operation.requestBody) { @@ -406,15 +429,28 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI resolveReferenceToSchema(mediaType.schema.`$ref`).second } - val parts: List = partSchemas.properties.map { (partName, partSchema) -> - val partContentType = mediaType.encoding?.get(partName)?.contentType - val partNameWithPresence = if(partSchemas.required?.contains(partName) == true) - partName - else - "$partName?" - - MultiPartContentPattern(partNameWithPresence, toSpecmaticPattern(partSchema, emptyList()), partContentType) - } + val parts: List = + partSchemas.properties.map { (partName, partSchema) -> + val partContentType = mediaType.encoding?.get(partName)?.contentType + val partNameWithPresence = if (partSchemas.required?.contains(partName) == true) + partName + else + "$partName?" + + if (partSchema is BinarySchema) { + MultiPartFilePattern( + partNameWithPresence, + toSpecmaticPattern(partSchema, emptyList()), + partContentType + ) + } else { + MultiPartContentPattern( + partNameWithPresence, + toSpecmaticPattern(partSchema, emptyList()), + partContentType + ) + } + } requestPattern.copy(multiPartFormDataPattern = parts) } @@ -449,14 +485,14 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI } private fun toSecurityScheme(securityScheme: SecurityScheme): OpenAPISecurityScheme { - if(securityScheme.scheme == BEARER_SECURITY_SCHEME) + if (securityScheme.scheme == BEARER_SECURITY_SCHEME) return BearerSecurityScheme() - if(securityScheme.type == SecurityScheme.Type.APIKEY) { - if(securityScheme.`in` == SecurityScheme.In.HEADER) + if (securityScheme.type == SecurityScheme.Type.APIKEY) { + if (securityScheme.`in` == SecurityScheme.In.HEADER) return APIKeyInHeaderSecurityScheme(securityScheme.name) - if(securityScheme.`in` == SecurityScheme.In.QUERY) + if (securityScheme.`in` == SecurityScheme.In.QUERY) return APIKeyInQueryParamSecurityScheme(securityScheme.name) } @@ -503,6 +539,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI null -> NumberPattern() else -> toEnum(schema, patternName) { enumValue -> NumberValue(enumValue.toString().toInt()) } } + is BinarySchema -> BinaryPattern() is NumberSchema -> NumberPattern() is UUIDSchema -> StringPattern() is DateTimeSchema -> DateTimePattern @@ -548,8 +585,7 @@ class OpenApiSpecification(private val openApiFile: String, val openApi: OpenAPI else -> { if (schema.nullable == true && schema.additionalProperties == null && schema.`$ref` == null) { NullPattern - } - else if (schema.additionalProperties != null) { + } else if (schema.additionalProperties != null) { toDictionaryPattern(schema, typeStack, patternName) } else { when (schema.`$ref`) { diff --git a/core/src/main/kotlin/in/specmatic/core/pattern/BinaryPattern.kt b/core/src/main/kotlin/in/specmatic/core/pattern/BinaryPattern.kt new file mode 100644 index 000000000..b54993b95 --- /dev/null +++ b/core/src/main/kotlin/in/specmatic/core/pattern/BinaryPattern.kt @@ -0,0 +1,49 @@ +package `in`.specmatic.core.pattern + +import `in`.specmatic.core.Resolver +import `in`.specmatic.core.Result +import `in`.specmatic.core.mismatchResult +import `in`.specmatic.core.value.BinaryValue +import `in`.specmatic.core.value.JSONArrayValue +import `in`.specmatic.core.value.StringValue +import `in`.specmatic.core.value.Value +import org.apache.commons.lang3.RandomUtils + +data class BinaryPattern( + override val typeAlias: String? = null, +) : Pattern, ScalarType { + + override fun matches(sampleData: Value?, resolver: Resolver): Result { + return when (sampleData) { + is StringValue -> return Result.Success() + else -> mismatchResult("string", sampleData, resolver.mismatchMessages) + } + } + + override fun encompasses( + otherPattern: Pattern, + thisResolver: Resolver, + otherResolver: Resolver, + typeStack: TypeStack + ): Result { + return encompasses(this, otherPattern, thisResolver, otherResolver, typeStack) + } + + override fun listOf(valueList: List, resolver: Resolver): Value { + return JSONArrayValue(valueList) + } + + override fun generate(resolver: Resolver): Value = BinaryValue(RandomUtils.nextBytes(20)) + + override fun newBasedOn(row: Row, resolver: Resolver): List = listOf(this) + override fun newBasedOn(resolver: Resolver): List = listOf(this) + override fun negativeBasedOn(row: Row, resolver: Resolver): List { + return listOf(NullPattern, NumberPattern(), BooleanPattern) + } + + override fun parse(value: String, resolver: Resolver): Value = StringValue(value) + override val typeName: String = "string" + + override val pattern: Any = "(string)" + override fun toString(): String = pattern.toString() +} \ No newline at end of file diff --git a/core/src/main/kotlin/in/specmatic/core/value/BinaryValue.kt b/core/src/main/kotlin/in/specmatic/core/value/BinaryValue.kt new file mode 100644 index 000000000..2da4e9856 --- /dev/null +++ b/core/src/main/kotlin/in/specmatic/core/value/BinaryValue.kt @@ -0,0 +1,61 @@ +package `in`.specmatic.core.value + +import `in`.specmatic.core.ExampleDeclarations +import `in`.specmatic.core.Result +import `in`.specmatic.core.pattern.ExactValuePattern +import `in`.specmatic.core.pattern.Pattern +import `in`.specmatic.core.pattern.StringPattern +import io.ktor.http.* +import io.ktor.util.* +import org.w3c.dom.Document +import org.w3c.dom.Node + +data class BinaryValue(val byteArray: ByteArray = ByteArray(0)) : Value, ScalarValue, XMLValue { + override val httpContentType = "application/octet-stream" + + override fun valueErrorSnippet(): String = displayableValue() + + @OptIn(InternalAPI::class) + override fun displayableValue(): String = toStringLiteral().quote() + override fun toStringLiteral() = byteArray.toString() + override fun displayableType(): String = "binary" + override fun exactMatchElseType(): Pattern = ExactValuePattern(this) + + override fun build(document: Document): Node = document.createTextNode(byteArray.toString()) + + override fun matchFailure(): Result.Failure = + Result.Failure("Unexpected child value found: $byteArray") + + override fun addSchema(schema: XMLNode): XMLValue = this + + override fun listOf(valueList: List): Value { + return JSONArrayValue(valueList) + } + + override fun type(): Pattern = StringPattern() + + override fun typeDeclarationWithKey( + key: String, + types: Map, + exampleDeclarations: ExampleDeclarations + ): Pair = + primitiveTypeDeclarationWithKey(key, types, exampleDeclarations, displayableType(), byteArray.toString()) + + override fun typeDeclarationWithoutKey( + exampleKey: String, + types: Map, + exampleDeclarations: ExampleDeclarations + ): Pair = + primitiveTypeDeclarationWithoutKey( + exampleKey, + types, + exampleDeclarations, + displayableType(), + byteArray.toString() + ) + + override val nativeValue: ByteArray + get() = byteArray + + override fun toString() = byteArray.toString() +} diff --git a/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt b/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt index 3e83dcf62..35fe21195 100644 --- a/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt +++ b/core/src/test/kotlin/in/specmatic/conversions/OpenApiKtTest.kt @@ -2,6 +2,7 @@ package `in`.specmatic.conversions import com.fasterxml.jackson.annotation.JsonProperty import `in`.specmatic.core.* +import `in`.specmatic.core.HttpRequest import `in`.specmatic.core.log.Verbose import `in`.specmatic.core.log.logger import `in`.specmatic.core.pattern.ContractException @@ -12,7 +13,6 @@ import `in`.specmatic.core.value.Value import `in`.specmatic.stub.HttpStub import `in`.specmatic.stub.createStubFromContracts import `in`.specmatic.test.TestExecutor -import org.apache.http.HttpHeaders.AUTHORIZATION import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Ignore @@ -22,10 +22,11 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import org.springframework.core.ParameterizedTypeReference -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod +import org.springframework.http.* import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap @@ -35,6 +36,8 @@ import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper import java.io.File import java.net.URI import java.util.function.Consumer +import java.util.stream.Stream + internal class OpenApiKtTest { companion object { @@ -56,6 +59,14 @@ Scenario: zero should return not found fun setup() { logger = Verbose() } + + @JvmStatic + fun multiPartFileUploadSpecs(): Stream { + return Stream.of( + Arguments.of("openapi/helloMultipart.yaml", ".*"), + Arguments.of("openapi/helloMultipartWithExamples.yaml", "input.txt"), + ) + } } @Test @@ -303,8 +314,7 @@ Background: } } ) - } - finally { + } finally { System.clearProperty(Flags.negativeTestingFlag) } @@ -762,7 +772,7 @@ Feature: Authenticated ) val contractTests = contract.generateContractTestScenarios(emptyList()) - val result = executeTest(contractTests.single(), object: TestExecutor { + val result = executeTest(contractTests.single(), object : TestExecutor { override fun execute(request: HttpRequest): HttpResponse { assertThat(request.headers).containsEntry("X-API-KEY", "abc123") return HttpResponse.OK("success") @@ -777,6 +787,99 @@ Feature: Authenticated assertThat(result).isInstanceOf(Result.Success::class.java) } + @ParameterizedTest + @MethodSource("multiPartFileUploadSpecs") + fun `should generate test with multipart file upload`(openApiFile: String, fileName: String) { + val contract: Feature = parseGherkinStringToFeature( + """ +Feature: multipart file upload + + Background: + Given openapi $openApiFile + """.trimIndent(), sourceSpecPath + ) + + val contractTests = contract.generateContractTestScenarios(emptyList()) + val result = executeTest(contractTests.single(), object : TestExecutor { + override fun execute(request: HttpRequest): HttpResponse { + val multipartFileValues = request.multiPartFormData.filterIsInstance() + assertThat(multipartFileValues.size).isEqualTo(1) + assertThat(multipartFileValues.first().name).isEqualTo("fileName") + assertThat(multipartFileValues.first().filename).matches(fileName) + return HttpResponse.OK("success") + } + + override fun setServerState(serverState: Map) { + + } + + }) + + assertThat(result).isInstanceOf(Result.Success::class.java) + } + + @Test + fun `should generate stub that accepts file upload data`() { + val feature = parseGherkinStringToFeature( + """ +Feature: Hello world + +Background: + Given openapi openapi/helloMultipart.yaml + """.trimIndent(), sourceSpecPath + ) + + HttpStub(feature).use { + val restTemplate = RestTemplate() + val body: MultiValueMap = LinkedMultiValueMap() + body.add("orderId", 1) + body.add("userId", 2) + val filePair: MultiValueMap = LinkedMultiValueMap() + val contentDisposition = ContentDisposition + .builder("form-data") + .name("fileName") + .filename("input.txt") + .build() + filePair.add(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) + val fileEntity = HttpEntity("test".toByteArray(), filePair) + body.add("fileName", fileEntity) + val headers = HttpHeaders() + headers.contentType = MediaType.MULTIPART_FORM_DATA + val requestEntity = HttpEntity(body, headers) + val response: ResponseEntity = restTemplate + .postForEntity(URI.create("http://localhost:9000/hello"), requestEntity, String::class.java) + assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + } + } + + @Test + fun `should generate stub that that returns error when multipart content is not a file`() { + val feature = parseGherkinStringToFeature( + """ +Feature: Hello world + +Background: + Given openapi openapi/helloMultipart.yaml + """.trimIndent(), sourceSpecPath + ) + + HttpStub(feature).use { + val restTemplate = RestTemplate() + val body: MultiValueMap = LinkedMultiValueMap() + body.add("orderId", 1) + body.add("userId", 2) + body.add("fileName", "not a file") + val headers = HttpHeaders() + headers.contentType = MediaType.MULTIPART_FORM_DATA + val requestEntity = HttpEntity(body, headers) + val httpClientErrorException = assertThrows { + restTemplate + .postForEntity(URI.create("http://localhost:9000/hello"), requestEntity, String::class.java) + } + assertThat(httpClientErrorException.message).contains("The contract expected a file, but got content instead.") + } + } + @Test fun `should generate test with bearer auth security scheme value from row`() { val contract: Feature = parseGherkinStringToFeature( @@ -797,7 +900,7 @@ Feature: Authenticated ) val contractTests = contract.generateContractTestScenarios(emptyList()) - val result = executeTest(contractTests.single(), object: TestExecutor { + val result = executeTest(contractTests.single(), object : TestExecutor { override fun execute(request: HttpRequest): HttpResponse { assertThat(request.headers).containsEntry("Authorization", "Bearer abc123") return HttpResponse.OK("success") @@ -832,7 +935,7 @@ Feature: Authenticated ) val contractTests = contract.generateContractTestScenarios(emptyList()) - val result = executeTest(contractTests.single(), object: TestExecutor { + val result = executeTest(contractTests.single(), object : TestExecutor { override fun execute(request: HttpRequest): HttpResponse { assertThat(request.queryParams).containsEntry("apiKey", "abc123") return HttpResponse.OK("success") @@ -1492,7 +1595,10 @@ Scenario: zero should return not found val feature = parseGherkinStringToFeature(openAPISpec, sourceSpecPath) - val result = feature.matches(HttpRequest("GET", "/hello/10"), HttpResponse(500, body = parsedJSONObject("""{"data": "information"}"""))) + val result = feature.matches( + HttpRequest("GET", "/hello/10"), + HttpResponse(500, body = parsedJSONObject("""{"data": "information"}""")) + ) assertThat(result).isTrue } @@ -1514,7 +1620,7 @@ Scenario: zero should return not found val results: Results = feature.copy(generativeTestingEnabled = true).executeTests(object : TestExecutor { override fun execute(request: HttpRequest): HttpResponse { val jsonBody = request.body as JSONObjectValue - if(jsonBody.jsonObject.get("id")?.toStringLiteral()?.toIntOrNull() != null) + if (jsonBody.jsonObject.get("id")?.toStringLiteral()?.toIntOrNull() != null) return HttpResponse(200, body = StringValue("it worked")) return HttpResponse(400, body = parsedJSONObject("""{"data": "information"}""")) @@ -1549,7 +1655,7 @@ Scenario: zero should return not found val results: Results = feature.copy(generativeTestingEnabled = true).executeTests(object : TestExecutor { override fun execute(request: HttpRequest): HttpResponse { val jsonBody = request.body as JSONObjectValue - if(jsonBody.jsonObject.get("id")?.toStringLiteral()?.toIntOrNull() != null) + if (jsonBody.jsonObject.get("id")?.toStringLiteral()?.toIntOrNull() != null) return HttpResponse(200, body = StringValue("it worked")) return HttpResponse(400, body = parsedJSONObject("""{"error_in_400": "message"}""")) diff --git a/core/src/test/resources/openapi/helloMultipart.yaml b/core/src/test/resources/openapi/helloMultipart.yaml new file mode 100644 index 000000000..71601ae86 --- /dev/null +++ b/core/src/test/resources/openapi/helloMultipart.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing +paths: + /hello: + post: + summary: hello world + description: Optional extended description in CommonMark or HTML. + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - "orderId" + - "userId" + - "fileName" + properties: + orderId: + type: integer + userId: + type: integer + fileName: + type: string + format: binary + responses: + '200': + description: Says hello + content: + application/json: + schema: + type: string \ No newline at end of file diff --git a/core/src/test/resources/openapi/helloMultipartWithExamples.yaml b/core/src/test/resources/openapi/helloMultipartWithExamples.yaml new file mode 100644 index 000000000..e0ba2f312 --- /dev/null +++ b/core/src/test/resources/openapi/helloMultipartWithExamples.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing +paths: + /hello: + post: + summary: hello world + description: Optional extended description in CommonMark or HTML. + requestBody: + content: + multipart/form-data: + schema: + type: object + required: + - "orderId" + - "userId" + - "fileName" + properties: + orderId: + type: integer + userId: + type: integer + fileName: + type: string + format: binary + examples: + 200_OKAY: + value: + orderId: 1 + userId: 2 + fileName: + externalValue: "input.txt" + responses: + '200': + description: Says hello + content: + application/json: + schema: + type: string + examples: + 200_OKAY: + value: {string} \ No newline at end of file