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