Skip to content

Commit

Permalink
fix(openapi): handle responses for status codes not in specification.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Nov 5, 2024
1 parent e50768a commit 376a620
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -298,52 +298,47 @@ class OpenApiPluginImpl @Inject constructor(
return@build completedUnitFuture()
}

val context = mutableMapOf<String, Any>()
context["operation"] = operation

val resourceConfig = httpExchange.get<BasicResourceConfig>(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,
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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"))
}
}
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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"
}

0 comments on commit 376a620

Please sign in to comment.