Skip to content

Commit 376a620

Browse files
committed
fix(openapi): handle responses for status codes not in specification.
1 parent e50768a commit 376a620

File tree

4 files changed

+206
-40
lines changed

4 files changed

+206
-40
lines changed

mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/OpenApiPluginImpl.kt

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -298,52 +298,47 @@ class OpenApiPluginImpl @Inject constructor(
298298
return@build completedUnitFuture()
299299
}
300300

301-
val context = mutableMapOf<String, Any>()
302-
context["operation"] = operation
303-
304301
val resourceConfig = httpExchange.get<BasicResourceConfig>(ResourceUtil.RESOURCE_CONFIG_KEY)
305302

306303
val defaultBehaviourHandler: DefaultBehaviourHandler = { responseBehaviour: ResponseBehaviour ->
307304
// set status code regardless of response strategy
308-
val response = httpExchange.response.setStatusCode(responseBehaviour.statusCode)
309-
310-
findApiResponse(operation, responseBehaviour.statusCode)?.let { specResponse ->
311-
if (!responseBehaviour.responseHeaders.containsKey(HttpUtil.CONTENT_TYPE)) {
312-
setContentTypeFromSpec(httpExchange, responseBehaviour, specResponse)
313-
}
314-
315-
// build a response from the specification
316-
val exampleSender =
317-
ResponseSender { httpExchange: HttpExchange, responseBehaviour: ResponseBehaviour ->
318-
exampleService.serveExample(
319-
imposterConfig,
320-
pluginConfig,
321-
httpExchange,
322-
responseBehaviour,
323-
specResponse,
324-
spec
325-
)
305+
httpExchange.response.setStatusCode(responseBehaviour.statusCode)
306+
307+
// build a response from the specification
308+
val exampleSender = ResponseSender { httpExchange: HttpExchange, _: ResponseBehaviour ->
309+
findApiResponse(operation, responseBehaviour.statusCode)?.let { specResponse ->
310+
LOGGER.trace("Using output response: {}", specResponse)
311+
312+
if (!responseBehaviour.responseHeaders.containsKey(HttpUtil.CONTENT_TYPE)) {
313+
setContentTypeFromSpec(httpExchange, responseBehaviour, specResponse)
326314
}
327315

328-
// attempt to serve an example from the specification, falling back if not present
329-
return@let responseService.sendResponse(
330-
pluginConfig,
331-
resourceConfig,
332-
httpExchange,
333-
responseBehaviour,
334-
exampleSender,
335-
this::fallback
336-
)
337-
} ?: run {
338-
LOGGER.warn(
339-
"No response found in specification for {} and status code {}",
340-
describeRequestShort(httpExchange),
341-
responseBehaviour.statusCode
342-
)
343-
makeFuture { response.end() }
316+
exampleService.serveExample(
317+
imposterConfig,
318+
pluginConfig,
319+
httpExchange,
320+
responseBehaviour,
321+
specResponse,
322+
spec
323+
)
324+
} ?: return@ResponseSender false
344325
}
326+
327+
// attempt to serve an example from the specification, falling back if not present
328+
responseService.sendResponse(
329+
pluginConfig,
330+
resourceConfig,
331+
httpExchange,
332+
responseBehaviour,
333+
exampleSender,
334+
this::fallback
335+
)
345336
}
346337

338+
val context = mutableMapOf(
339+
"operation" to operation,
340+
)
341+
347342
responseRoutingService.route(
348343
pluginConfig,
349344
resourceConfig,
@@ -421,10 +416,12 @@ class OpenApiPluginImpl @Inject constructor(
421416
*/
422417
private fun fallback(httpExchange: HttpExchange, responseBehaviour: ResponseBehaviour): Boolean {
423418
LOGGER.warn(
424-
"No example match found and no response file set for mock response for {} with status code {}" +
425-
" - sending empty response", describeRequestShort(httpExchange), responseBehaviour.statusCode
419+
"No example match found in OpenAPI specification for {} with status code {} and no response file or content set - sending empty response",
420+
describeRequestShort(httpExchange),
421+
responseBehaviour.statusCode,
426422
)
423+
// should this be a 400, as it's not in the spec?
427424
httpExchange.response.end()
428425
return true
429426
}
430-
}
427+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2024.
3+
*
4+
* This file is part of Imposter.
5+
*
6+
* "Commons Clause" License Condition v1.0
7+
*
8+
* The Software is provided to you by the Licensor under the License, as
9+
* defined below, subject to the following condition.
10+
*
11+
* Without limiting other conditions in the License, the grant of rights
12+
* under the License will not include, and the License does not grant to
13+
* you, the right to Sell the Software.
14+
*
15+
* For purposes of the foregoing, "Sell" means practicing any or all of
16+
* the rights granted to you under the License to provide to third parties,
17+
* for a fee or other consideration (including without limitation fees for
18+
* hosting or consulting/support services related to the Software), a
19+
* product or service whose value derives, entirely or substantially, from
20+
* the functionality of the Software. Any license notice or attribution
21+
* required by the License must also include this Commons Clause License
22+
* Condition notice.
23+
*
24+
* Software: Imposter
25+
*
26+
* License: GNU Lesser General Public License version 3
27+
*
28+
* Licensor: Peter Cornish
29+
*
30+
* Imposter is free software: you can redistribute it and/or modify
31+
* it under the terms of the GNU Lesser General Public License as published by
32+
* the Free Software Foundation, either version 3 of the License, or
33+
* (at your option) any later version.
34+
*
35+
* Imposter is distributed in the hope that it will be useful,
36+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
37+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38+
* GNU Lesser General Public License for more details.
39+
*
40+
* You should have received a copy of the GNU Lesser General Public License
41+
* along with Imposter. If not, see <https://www.gnu.org/licenses/>.
42+
*/
43+
package io.gatehill.imposter.plugin.openapi
44+
45+
import io.gatehill.imposter.server.BaseVerticleTest
46+
import io.gatehill.imposter.util.HttpUtil
47+
import io.restassured.RestAssured
48+
import io.restassured.http.ContentType
49+
import io.vertx.ext.unit.TestContext
50+
import org.hamcrest.Matchers.equalTo
51+
import org.junit.Before
52+
import org.junit.Test
53+
54+
/**
55+
* Tests for mock responses when the status code is not defined within the OpenAPI spec.
56+
*
57+
* @author Pete Cornish
58+
*/
59+
class UndefinedResourceTest : BaseVerticleTest() {
60+
override val pluginClass = OpenApiPluginImpl::class.java
61+
62+
@Before
63+
@Throws(Exception::class)
64+
override fun setUp(testContext: TestContext) {
65+
super.setUp(testContext)
66+
RestAssured.baseURI = "http://$host:$listenPort"
67+
}
68+
69+
override val testConfigDirs = listOf(
70+
"/openapi3/undefined-resource"
71+
)
72+
73+
/**
74+
* Should trigger warning and fallback handler.
75+
*
76+
* @param testContext
77+
*/
78+
@Test
79+
fun `should return fallback response`(testContext: TestContext) {
80+
RestAssured.given()
81+
.log().ifValidationFails()
82+
.accept(ContentType.JSON)
83+
.`when`().get("/cats")
84+
.then()
85+
.log().ifValidationFails()
86+
.statusCode(HttpUtil.HTTP_CREATED)
87+
}
88+
89+
/**
90+
* Should return custom response.
91+
*
92+
* @param testContext
93+
*/
94+
@Test
95+
fun `should return custom response`(testContext: TestContext) {
96+
RestAssured.given()
97+
.log().ifValidationFails()
98+
.accept(ContentType.JSON)
99+
.`when`().get("/dogs")
100+
.then()
101+
.log().ifValidationFails()
102+
.statusCode(HttpUtil.HTTP_ACCEPTED)
103+
.body(equalTo("Custom response"))
104+
}
105+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
plugin: openapi
3+
specFile: spec.yaml
4+
5+
resources:
6+
- method: get
7+
path: /cats
8+
response:
9+
statusCode: 201
10+
11+
- method: get
12+
path: /dogs
13+
response:
14+
statusCode: 202
15+
content: "Custom response"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Swagger Petstore
5+
license:
6+
name: MIT
7+
paths:
8+
/cats:
9+
get:
10+
summary: List all cats
11+
operationId: listCats
12+
responses:
13+
"200":
14+
description: cats response
15+
content:
16+
application/json:
17+
schema:
18+
$ref: "#/components/schemas/PetResponse"
19+
20+
/dogs:
21+
get:
22+
summary: List all dogs
23+
operationId: listDogs
24+
responses:
25+
"200":
26+
description: dogs response
27+
content:
28+
application/json:
29+
schema:
30+
$ref: "#/components/schemas/PetResponse"
31+
32+
components:
33+
schemas:
34+
PetResponse:
35+
type: object
36+
required:
37+
- code
38+
- message
39+
properties:
40+
code:
41+
type: integer
42+
format: int32
43+
message:
44+
type: string
45+
example:
46+
{
47+
"code": 1,
48+
"message": "Pet response"
49+
}

0 commit comments

Comments
 (0)