View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.bean.openapi3;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.apache.juneau.bean.openapi3.OpenApiBuilder.*;
21  import static org.junit.jupiter.api.Assertions.*;
22  
23  import java.util.*;
24  
25  import org.apache.juneau.*;
26  import org.junit.jupiter.api.*;
27  
28  /**
29   * Testcase for {@link HeaderInfo}.
30   */
31  class HeaderInfo_Test extends TestBase {
32  
33  	@Nested class A_basicTests extends TestBase {
34  
35  		private static final BeanTester<HeaderInfo> TESTER =
36  			testBean(
37  				bean()
38  					.setAllowEmptyValue(true)
39  					.setAllowReserved(true)
40  					.setDeprecated(true)
41  					.setDescription("a")
42  					.setExample("b")
43  					.setExamples(map("c1",example().setDescription("c2")))
44  					.setExplode(true)
45  					.setRef("c")
46  					.setRequired(true)
47  					.setSchema(schemaInfo().setType("d"))
48  			)
49  			.props("allowEmptyValue,allowReserved,deprecated,description,example,examples{c1{description}},explode,ref,required,schema{type}")
50  			.vals("true,true,true,a,b,{{c2}},true,c,true,{d}")
51  			.json("{'$ref':'c',allowEmptyValue:true,allowReserved:true,deprecated:true,description:'a',examples:{c1:{description:'c2'}},explode:true,required:true,schema:{type:'d'},'x-example':'b'}")
52  			.string("{'$ref':'c','allowEmptyValue':true,'allowReserved':true,'deprecated':true,'description':'a','examples':{'c1':{'description':'c2'}},'explode':true,'required':true,'schema':{'type':'d'},'x-example':'b'}".replace('\'','"'))
53  		;
54  
55  		@Test void a01_gettersAndSetters() {
56  			TESTER.assertGettersAndSetters();
57  		}
58  
59  		@Test void a02_copy() {
60  			TESTER.assertCopy();
61  		}
62  
63  		@Test void a03_toJson() {
64  			TESTER.assertToJson();
65  		}
66  
67  		@Test void a04_fromJson() {
68  			TESTER.assertFromJson();
69  		}
70  
71  		@Test void a05_roundTrip() {
72  			TESTER.assertRoundTrip();
73  		}
74  
75  		@Test void a06_toString() {
76  			TESTER.assertToString();
77  		}
78  
79  		@Test void a07_keySet() {
80  			assertList(TESTER.bean().keySet(), "$ref", "allowEmptyValue", "allowReserved", "deprecated", "description", "examples", "explode", "required", "schema", "x-example");
81  		}
82  
83  		@Test void a08_nullParameters() {
84  			var x = bean();
85  			assertThrows(IllegalArgumentException.class, () -> x.get(null, String.class));
86  			assertThrows(IllegalArgumentException.class, () -> x.set(null, "value"));
87  			assertThrows(IllegalArgumentException.class, () -> x.addExample(null, example()));
88  			assertThrows(IllegalArgumentException.class, () -> x.addExample("test", null));
89  		}
90  
91  		@Test void a08b_getSetRef() {
92  			// Test get/set with "$ref" property to cover switch branches
93  			var x = bean();
94  			x.set("$ref", "#/components/headers/MyHeader");
95  			assertEquals("#/components/headers/MyHeader", x.get("$ref", String.class));
96  			assertEquals("#/components/headers/MyHeader", x.getRef());
97  		}
98  
99  		@Test void a09_addMethods() {
100 			assertBean(
101 				bean()
102 					.addExample("a1", example().setSummary("a2")),
103 				"examples{a1{summary}}",
104 				"{{a2}}"
105 			);
106 		}
107 
108 		@Test void a10_asMap() {
109 			assertBean(
110 				bean()
111 					.setDescription("a")
112 					.set("x1", "x1a")
113 					.asMap(),
114 				"description,x1",
115 				"a,x1a"
116 			);
117 		}
118 
119 		@Test void a11_extraKeys() {
120 			var x = bean().set("x1", "x1a").set("x2", "x2a");
121 			assertList(x.extraKeys(), "x1", "x2");
122 			assertEmpty(bean().extraKeys());
123 		}
124 
125 		@Test void a12_strictMode() {
126 			assertThrows(RuntimeException.class, () -> bean().strict().set("foo", "bar"));
127 			assertDoesNotThrow(() -> bean().set("foo", "bar"));
128 
129 			assertFalse(bean().isStrict());
130 			assertTrue(bean().strict().isStrict());
131 			assertFalse(bean().strict(false).isStrict());
132 		}
133 	}
134 
135 	@Nested class B_emptyTests extends TestBase {
136 
137 		private static final BeanTester<HeaderInfo> TESTER =
138 			testBean(bean())
139 			.props("description,required,explode,deprecated,allowEmptyValue,allowReserved,ref,schema,example,examples")
140 			.vals("<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>")
141 			.json("{}")
142 			.string("{}")
143 		;
144 
145 		@Test void b01_gettersAndSetters() {
146 			TESTER.assertGettersAndSetters();
147 		}
148 
149 		@Test void b02_copy() {
150 			TESTER.assertCopy();
151 		}
152 
153 		@Test void b03_toJson() {
154 			TESTER.assertToJson();
155 		}
156 
157 		@Test void b04_fromJson() {
158 			TESTER.assertFromJson();
159 		}
160 
161 		@Test void b05_roundTrip() {
162 			TESTER.assertRoundTrip();
163 		}
164 
165 		@Test void b06_toString() {
166 			TESTER.assertToString();
167 		}
168 
169 		@Test void b07_keySet() {
170 			assertEmpty(TESTER.bean().keySet());
171 		}
172 	}
173 
174 	@Nested class C_extraProperties extends TestBase {
175 		private static final BeanTester<HeaderInfo> TESTER =
176 			testBean(
177 				bean()
178 					.set("allowEmptyValue", true)
179 					.set("allowReserved", true)
180 					.set("deprecated", true)
181 					.set("description", "a")
182 					.set("x-example", "b")
183 					.set("examples", map("c1", example().setSummary("c2")))
184 					.set("explode", true)
185 					.set("required", true)
186 					.set("schema", schemaInfo("d"))
187 					.set("style", "e")
188 					.set("x1", "x1a")
189 					.set("x2", null)
190 			)
191 			.props("allowEmptyValue,allowReserved,deprecated,description,example,examples{c1{summary}},explode,required,schema{type},style,x1,x2")
192 			.vals("true,true,true,a,b,{{c2}},true,true,{d},e,x1a,<null>")
193 			.json("{allowEmptyValue:true,allowReserved:true,deprecated:true,description:'a',examples:{c1:{summary:'c2'}},explode:true,required:true,schema:{type:'d'},style:'e','x-example':'b',x1:'x1a'}")
194 			.string("{'allowEmptyValue':true,'allowReserved':true,'deprecated':true,'description':'a','examples':{'c1':{'summary':'c2'}},'explode':true,'required':true,'schema':{'type':'d'},'style':'e','x-example':'b','x1':'x1a'}".replace('\'', '"'))
195 		;
196 
197 		@Test void c01_gettersAndSetters() {
198 			TESTER.assertGettersAndSetters();
199 		}
200 
201 		@Test void c02_copy() {
202 			TESTER.assertCopy();
203 		}
204 
205 		@Test void c03_toJson() {
206 			TESTER.assertToJson();
207 		}
208 
209 		@Test void c04_fromJson() {
210 			TESTER.assertFromJson();
211 		}
212 
213 		@Test void c05_roundTrip() {
214 			TESTER.assertRoundTrip();
215 		}
216 
217 		@Test void c06_toString() {
218 			TESTER.assertToString();
219 		}
220 
221 		@Test void c07_keySet() {
222 			assertList(TESTER.bean().keySet(), "allowEmptyValue", "allowReserved", "deprecated", "description", "examples", "explode", "required", "schema", "style", "x-example", "x1", "x2");
223 		}
224 
225 		@Test void c08_get() {
226 			assertMapped(
227 				TESTER.bean(), (obj,prop) -> obj.get(prop, Object.class),
228 				"allowEmptyValue,allowReserved,deprecated,description,x-example,examples{c1{summary}},explode,required,schema{type},style,x1,x2",
229 				"true,true,true,a,b,{{c2}},true,true,{d},e,x1a,<null>"
230 			);
231 		}
232 
233 		@Test void c09_getTypes() {
234 			assertMapped(
235 				TESTER.bean(), (obj,prop) -> simpleClassNameOf(obj.get(prop, Object.class)),
236 				"allowEmptyValue,allowReserved,deprecated,description,x-example,examples,explode,required,schema,style,x1,x2",
237 				"Boolean,Boolean,Boolean,String,String,LinkedHashMap,Boolean,Boolean,SchemaInfo,String,String,<null>"
238 			);
239 		}
240 
241 		@Test void c10_nullPropertyValue() {
242 			assertThrows(IllegalArgumentException.class, ()->bean().get(null));
243 			assertThrows(IllegalArgumentException.class, ()->bean().get(null, String.class));
244 			assertThrows(IllegalArgumentException.class, ()->bean().set(null, "a"));
245 		}
246 	}
247 
248 	@Nested class D_refs extends TestBase {
249 
250 		@Test void d01_resolveRefs_basic() {
251 			var openApi = openApi()
252 				.setComponents(components().setSchemas(Map.of(
253 					"MyHeader", schemaInfo().setType("string").setDescription("My Header")
254 				)));
255 
256 			assertBean(
257 				headerInfo().setRef("#/components/schemas/MyHeader").resolveRefs(openApi, new ArrayDeque<>(), 10),
258 				"type,description",
259 				"string,My Header"
260 			);
261 
262 			// Max depth
263 			assertBean(
264 				headerInfo().setRef("#/components/schemas/MyHeader").resolveRefs(openApi, new ArrayDeque<>(), 0),
265 				"ref",
266 				"#/components/schemas/MyHeader"
267 			);
268 		}
269 
270 		@Test void d04_resolveRefs_noRef() {
271 			// Test resolveRefs when ref is null (covers the missing branch)
272 			var openApi = openApi()
273 				.setComponents(components().setSchemas(map("MyHeader", schemaInfo().setType("string"))));
274 
275 			var header = bean()
276 				.setDescription("Test header")
277 				.setRequired(true);
278 
279 			var result = header.resolveRefs(openApi, new ArrayDeque<>(), 10);
280 
281 			// Should return the same object unchanged
282 			assertSame(header, result);
283 			assertEquals("Test header", result.getDescription());
284 			assertTrue(result.getRequired());
285 		}
286 
287 		@Test void d05_resolveRefs_circularReference() {
288 			// Test circular reference detection (covers the refStack.contains(ref) branch)
289 			var openApi = openApi()
290 				.setComponents(components().setSchemas(map(
291 					"Header1", schemaInfo().setRef("#/components/schemas/Header2"),
292 					"Header2", schemaInfo().setRef("#/components/schemas/Header1")
293 				)));
294 
295 			var refStack = new ArrayDeque<String>();
296 			refStack.add("#/components/schemas/Header1"); // Pre-populate to simulate circular reference
297 			
298 			var header = headerInfo().setRef("#/components/schemas/Header1");
299 			var result = header.resolveRefs(openApi, refStack, 10);
300 
301 			// Should return the original object without resolving
302 			assertSame(header, result);
303 			assertEquals("#/components/schemas/Header1", result.getRef());
304 		}
305 
306 		@Test void d06_resolveRefs_maxDepthDirect() {
307 			// Test max depth directly (covers the refStack.size() >= maxDepth branch)
308 			var openApi = openApi()
309 				.setComponents(components().setSchemas(map("MyHeader", schemaInfo().setType("string"))));
310 
311 			// Create a refStack at max depth
312 			var refStack = new ArrayDeque<String>();
313 			refStack.add("dummy1");
314 			refStack.add("dummy2");
315 			refStack.add("dummy3");
316 			
317 			var header = headerInfo().setRef("#/components/schemas/MyHeader");
318 			var result = header.resolveRefs(openApi, refStack, 3); // maxDepth = 3, refStack.size() = 3
319 
320 			// Should return the original object without resolving
321 			assertSame(header, result);
322 			assertEquals("#/components/schemas/MyHeader", result.getRef());
323 		}
324 	}
325 
326 	//---------------------------------------------------------------------------------------------
327 	// Helper methods
328 	//---------------------------------------------------------------------------------------------
329 
330 	private static HeaderInfo bean() {
331 		return headerInfo();
332 	}
333 }