diff --git a/mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/OpenApiPluginImpl.kt b/mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/OpenApiPluginImpl.kt index cf265add9..cd36e5b94 100644 --- a/mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/OpenApiPluginImpl.kt +++ b/mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/OpenApiPluginImpl.kt @@ -298,52 +298,47 @@ class OpenApiPluginImpl @Inject constructor( return@build completedUnitFuture() } - val context = mutableMapOf() - context["operation"] = operation - val resourceConfig = httpExchange.get(ResourceUtil.RESOURCE_CONFIG_KEY) val defaultBehaviourHandler: DefaultBehaviourHandler = { responseBehaviour: ResponseBehaviour -> // set status code regardless of response strategy - val response = httpExchange.response.setStatusCode(responseBehaviour.statusCode) - - findApiResponse(operation, responseBehaviour.statusCode)?.let { specResponse -> - if (!responseBehaviour.responseHeaders.containsKey(HttpUtil.CONTENT_TYPE)) { - setContentTypeFromSpec(httpExchange, responseBehaviour, specResponse) - } - - // build a response from the specification - val exampleSender = - ResponseSender { httpExchange: HttpExchange, responseBehaviour: ResponseBehaviour -> - exampleService.serveExample( - imposterConfig, - pluginConfig, - httpExchange, - responseBehaviour, - specResponse, - spec - ) + httpExchange.response.setStatusCode(responseBehaviour.statusCode) + + // build a response from the specification + val exampleSender = ResponseSender { httpExchange: HttpExchange, _: ResponseBehaviour -> + findApiResponse(operation, responseBehaviour.statusCode)?.let { specResponse -> + LOGGER.trace("Using output response: {}", specResponse) + + if (!responseBehaviour.responseHeaders.containsKey(HttpUtil.CONTENT_TYPE)) { + setContentTypeFromSpec(httpExchange, responseBehaviour, specResponse) } - // attempt to serve an example from the specification, falling back if not present - return@let responseService.sendResponse( - pluginConfig, - resourceConfig, - httpExchange, - responseBehaviour, - exampleSender, - this::fallback - ) - } ?: run { - LOGGER.warn( - "No response found in specification for {} and status code {}", - describeRequestShort(httpExchange), - responseBehaviour.statusCode - ) - makeFuture { response.end() } + exampleService.serveExample( + imposterConfig, + pluginConfig, + httpExchange, + responseBehaviour, + specResponse, + spec + ) + } ?: return@ResponseSender false } + + // attempt to serve an example from the specification, falling back if not present + responseService.sendResponse( + pluginConfig, + resourceConfig, + httpExchange, + responseBehaviour, + exampleSender, + this::fallback + ) } + val context = mutableMapOf( + "operation" to operation, + ) + responseRoutingService.route( pluginConfig, resourceConfig, @@ -421,10 +416,12 @@ class OpenApiPluginImpl @Inject constructor( */ private fun fallback(httpExchange: HttpExchange, responseBehaviour: ResponseBehaviour): Boolean { LOGGER.warn( - "No example match found and no response file set for mock response for {} with status code {}" + - " - sending empty response", describeRequestShort(httpExchange), responseBehaviour.statusCode + "No example match found in OpenAPI specification for {} with status code {} and no response file or content set - sending empty response", + describeRequestShort(httpExchange), + responseBehaviour.statusCode, ) + // should this be a 400, as it's not in the spec? httpExchange.response.end() return true } -} \ No newline at end of file +} diff --git a/mock/openapi/src/test/java/io/gatehill/imposter/plugin/openapi/UndefinedResourceTest.kt b/mock/openapi/src/test/java/io/gatehill/imposter/plugin/openapi/UndefinedResourceTest.kt new file mode 100644 index 000000000..5e6471991 --- /dev/null +++ b/mock/openapi/src/test/java/io/gatehill/imposter/plugin/openapi/UndefinedResourceTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024. + * + * This file is part of Imposter. + * + * "Commons Clause" License Condition v1.0 + * + * The Software is provided to you by the Licensor under the License, as + * defined below, subject to the following condition. + * + * Without limiting other conditions in the License, the grant of rights + * under the License will not include, and the License does not grant to + * you, the right to Sell the Software. + * + * For purposes of the foregoing, "Sell" means practicing any or all of + * the rights granted to you under the License to provide to third parties, + * for a fee or other consideration (including without limitation fees for + * hosting or consulting/support services related to the Software), a + * product or service whose value derives, entirely or substantially, from + * the functionality of the Software. Any license notice or attribution + * required by the License must also include this Commons Clause License + * Condition notice. + * + * Software: Imposter + * + * License: GNU Lesser General Public License version 3 + * + * Licensor: Peter Cornish + * + * Imposter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Imposter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Imposter. If not, see . + */ +package io.gatehill.imposter.plugin.openapi + +import io.gatehill.imposter.server.BaseVerticleTest +import io.gatehill.imposter.util.HttpUtil +import io.restassured.RestAssured +import io.restassured.http.ContentType +import io.vertx.ext.unit.TestContext +import org.hamcrest.Matchers.equalTo +import org.junit.Before +import org.junit.Test + +/** + * Tests for mock responses when the status code is not defined within the OpenAPI spec. + * + * @author Pete Cornish + */ +class UndefinedResourceTest : BaseVerticleTest() { + override val pluginClass = OpenApiPluginImpl::class.java + + @Before + @Throws(Exception::class) + override fun setUp(testContext: TestContext) { + super.setUp(testContext) + RestAssured.baseURI = "http://$host:$listenPort" + } + + override val testConfigDirs = listOf( + "/openapi3/undefined-resource" + ) + + /** + * Should trigger warning and fallback handler. + * + * @param testContext + */ + @Test + fun `should return fallback response`(testContext: TestContext) { + RestAssured.given() + .log().ifValidationFails() + .accept(ContentType.JSON) + .`when`().get("/cats") + .then() + .log().ifValidationFails() + .statusCode(HttpUtil.HTTP_CREATED) + } + + /** + * Should return custom response. + * + * @param testContext + */ + @Test + fun `should return custom response`(testContext: TestContext) { + RestAssured.given() + .log().ifValidationFails() + .accept(ContentType.JSON) + .`when`().get("/dogs") + .then() + .log().ifValidationFails() + .statusCode(HttpUtil.HTTP_ACCEPTED) + .body(equalTo("Custom response")) + } +} diff --git a/mock/openapi/src/test/resources/openapi3/undefined-resource/mock-config.yaml b/mock/openapi/src/test/resources/openapi3/undefined-resource/mock-config.yaml new file mode 100644 index 000000000..b96d5bd58 --- /dev/null +++ b/mock/openapi/src/test/resources/openapi3/undefined-resource/mock-config.yaml @@ -0,0 +1,15 @@ +--- +plugin: openapi +specFile: spec.yaml + +resources: + - method: get + path: /cats + response: + statusCode: 201 + + - method: get + path: /dogs + response: + statusCode: 202 + content: "Custom response" diff --git a/mock/openapi/src/test/resources/openapi3/undefined-resource/spec.yaml b/mock/openapi/src/test/resources/openapi3/undefined-resource/spec.yaml new file mode 100644 index 000000000..093b3434b --- /dev/null +++ b/mock/openapi/src/test/resources/openapi3/undefined-resource/spec.yaml @@ -0,0 +1,49 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +paths: + /cats: + get: + summary: List all cats + operationId: listCats + responses: + "200": + description: cats response + content: + application/json: + schema: + $ref: "#/components/schemas/PetResponse" + + /dogs: + get: + summary: List all dogs + operationId: listDogs + responses: + "200": + description: dogs response + content: + application/json: + schema: + $ref: "#/components/schemas/PetResponse" + +components: + schemas: + PetResponse: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + example: + { + "code": 1, + "message": "Pet response" + }