Skip to content

Commit 6b6e115

Browse files
committed
openapi bug test
1 parent e483de4 commit 6b6e115

File tree

1 file changed

+141
-0
lines changed

1 file changed

+141
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.openapi.fromsmithy;
6+
7+
import org.junit.jupiter.api.Test;
8+
import software.amazon.smithy.model.Model;
9+
import software.amazon.smithy.model.node.Node;
10+
import software.amazon.smithy.model.node.ObjectNode;
11+
import software.amazon.smithy.model.shapes.ShapeId;
12+
import software.amazon.smithy.openapi.OpenApiConfig;
13+
14+
import static org.junit.jupiter.api.Assertions.*;
15+
16+
/**
17+
* Tests that service-level errors don't override operation-specific error examples.
18+
* This reproduces the healthcare service issue where ValidationException (service-level)
19+
* overrides EligibilityCheckOutputValidationErrors (operation-level) examples.
20+
*/
21+
public class ServiceLevelErrorOverrideTest {
22+
23+
@Test
24+
public void serviceLevelErrorsShouldNotOverrideOperationSpecificExamples() {
25+
// Create a model that mimics the healthcare service structure
26+
String modelText = "$version: \"2.0\"\n" +
27+
"namespace test.service.error\n" +
28+
"use aws.protocols#restJson1\n" +
29+
"\n" +
30+
"@restJson1\n" +
31+
"service TestService {\n" +
32+
" version: \"1.0.0\",\n" +
33+
" operations: [TestOp]\n" +
34+
" errors: [ServiceError] // Service-level 400 error\n" +
35+
"}\n" +
36+
"\n" +
37+
"@http(method: \"POST\", uri: \"/test\")\n" +
38+
"operation TestOp {\n" +
39+
" input: TestInput,\n" +
40+
" output: TestOutput,\n" +
41+
" errors: [CustomError] // Operation-specific 400 error\n" +
42+
"}\n" +
43+
"\n" +
44+
"structure TestInput { value: String }\n" +
45+
"structure TestOutput { result: String }\n" +
46+
"\n" +
47+
"// Service-level 400 error\n" +
48+
"@error(\"client\")\n" +
49+
"@httpError(400)\n" +
50+
"structure ServiceError {\n" +
51+
" message: String\n" +
52+
"}\n" +
53+
"\n" +
54+
"// Operation-specific 400 error - should have examples generated\n" +
55+
"@error(\"client\")\n" +
56+
"@httpError(400)\n" +
57+
"structure CustomError {\n" +
58+
" message: String,\n" +
59+
" code: String,\n" +
60+
" details: String\n" +
61+
"}\n" +
62+
"\n" +
63+
"apply TestOp @examples([\n" +
64+
" {\n" +
65+
" title: \"Success example\"\n" +
66+
" input: { value: \"good\" }\n" +
67+
" output: { result: \"success\" }\n" +
68+
" },\n" +
69+
" {\n" +
70+
" title: \"Custom error example\"\n" +
71+
" input: { value: \"bad\" }\n" +
72+
" error: {\n" +
73+
" shapeId: CustomError\n" +
74+
" content: {\n" +
75+
" message: \"Custom error occurred\"\n" +
76+
" code: \"CUSTOM_ERROR\"\n" +
77+
" details: \"This should appear in 400 response examples\"\n" +
78+
" }\n" +
79+
" }\n" +
80+
" allowConstraintErrors: true\n" +
81+
" }\n" +
82+
"])";
83+
84+
Model model = Model.assembler()
85+
.addImport(getClass().getResource("/META-INF/smithy/aws.protocols.smithy"))
86+
.addUnparsedModel("test.smithy", modelText)
87+
.assemble()
88+
.unwrap();
89+
90+
// Convert to OpenAPI with oneOf enabled (like healthcare service had)
91+
OpenApiConfig config = new OpenApiConfig();
92+
config.setService(ShapeId.from("test.service.error#TestService"));
93+
config.setOnErrorStatusConflict(OpenApiConfig.ErrorStatusConflictHandlingStrategy.ONE_OF);
94+
95+
Node result = OpenApiConverter.create().config(config).convertToNode(model);
96+
ObjectNode openApi = result.expectObjectNode();
97+
98+
// Print for debugging
99+
System.out.println("Generated OpenAPI:");
100+
System.out.println(Node.prettyPrintJson(result));
101+
102+
// Navigate to the 400 response
103+
ObjectNode paths = openApi.expectMember("paths").expectObjectNode();
104+
ObjectNode testPath = paths.expectMember("/test").expectObjectNode();
105+
ObjectNode postOp = testPath.expectMember("post").expectObjectNode();
106+
ObjectNode responses = postOp.expectMember("responses").expectObjectNode();
107+
108+
// Should have a 400 response
109+
assertTrue(responses.getMember("400").isPresent(), "Expected 400 response for client errors");
110+
111+
ObjectNode response400 = responses.expectMember("400").expectObjectNode();
112+
ObjectNode content = response400.expectMember("content").expectObjectNode();
113+
ObjectNode jsonContent = content.expectMember("application/json").expectObjectNode();
114+
115+
// The bug: When ValidationException (service-level) and CustomError (operation-level)
116+
// both map to 400, the service-level error takes precedence and operation-specific
117+
// examples are lost
118+
assertTrue(jsonContent.getMember("examples").isPresent(),
119+
"BUG: Service-level ValidationException overrides operation-specific CustomError examples. " +
120+
"Expected CustomError examples to appear in 400 response but they are missing.");
121+
122+
ObjectNode examples = jsonContent.expectMember("examples").expectObjectNode();
123+
assertFalse(examples.getMembers().isEmpty(),
124+
"Expected CustomError examples in 400 response content");
125+
126+
// Look for our specific custom error example content
127+
boolean foundCustomErrorExample = examples.getMembers().values().stream()
128+
.filter(Node::isObjectNode)
129+
.map(node -> node.expectObjectNode())
130+
.filter(example -> example.getMember("value").isPresent())
131+
.map(example -> example.expectMember("value").expectObjectNode())
132+
.anyMatch(value ->
133+
value.getMember("code").filter(node -> "CUSTOM_ERROR".equals(node.expectStringNode().getValue())).isPresent() &&
134+
value.getMember("message").filter(node -> "Custom error occurred".equals(node.expectStringNode().getValue())).isPresent()
135+
);
136+
137+
assertTrue(foundCustomErrorExample,
138+
"Expected to find CustomError example with code 'CUSTOM_ERROR' and message 'Custom error occurred' " +
139+
"but service-level ValidationException appears to be overriding it");
140+
}
141+
}

0 commit comments

Comments
 (0)