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