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.net.*;
24  import java.util.*;
25  
26  import org.apache.juneau.*;
27  import org.apache.juneau.collections.*;
28  import org.junit.jupiter.api.*;
29  
30  /**
31   * Testcase for {@link Items}.
32   */
33  class Items_Test extends TestBase {
34  
35  	@Nested class A_basicTests extends TestBase {
36  
37  		private static final BeanTester<Items> TESTER =
38  			testBean(
39  				bean()
40  					.setCollectionFormat("a")
41  					.setDefault("b")
42  					.setEnum(list("c1", "c2"))
43  					.setExclusiveMaximum(true)
44  					.setExclusiveMinimum(true)
45  					.setFormat("d")
46  					.setItems(bean().setType("e"))
47  					.setMaxItems(1)
48  					.setMaxLength(2)
49  					.setMaximum(3)
50  					.setMinItems(4)
51  					.setMinLength(5)
52  					.setMinimum(6)
53  					.setMultipleOf(7)
54  					.setPattern("f")
55  					.setRef("g")
56  					.setType("h")
57  					.setUniqueItems(true)
58  			)
59  			.props("collectionFormat,default,enum,exclusiveMaximum,exclusiveMinimum,format,items{type},maximum,maxItems,maxLength,minimum,minItems,minLength,multipleOf,pattern,ref,type,uniqueItems")
60  			.vals("a,b,[c1,c2],true,true,d,{e},3,1,2,6,4,5,7,f,g,h,true")
61  			.json("{'$ref':'g',collectionFormat:'a','default':'b','enum':['c1','c2'],exclusiveMaximum:true,exclusiveMinimum:true,format:'d',items:{type:'e'},maxItems:1,maxLength:2,maximum:3,minItems:4,minLength:5,minimum:6,multipleOf:7,pattern:'f',type:'h',uniqueItems:true}")
62  			.string("{'$ref':'g','collectionFormat':'a','default':'b','enum':['c1','c2'],'exclusiveMaximum':true,'exclusiveMinimum':true,'format':'d','items':{'type':'e'},'maxItems':1,'maxLength':2,'maximum':3,'minItems':4,'minLength':5,'minimum':6,'multipleOf':7,'pattern':'f','type':'h','uniqueItems':true}".replace('\'','"'))
63  		;
64  
65  		@Test void a01_gettersAndSetters() {
66  			TESTER.assertGettersAndSetters();
67  		}
68  
69  		@Test void a02_copy() {
70  			TESTER.assertCopy();
71  		}
72  
73  		@Test void a03_toJson() {
74  			TESTER.assertToJson();
75  		}
76  
77  		@Test void a04_fromJson() {
78  			TESTER.assertFromJson();
79  		}
80  
81  		@Test void a05_roundTrip() {
82  			TESTER.assertRoundTrip();
83  		}
84  
85  		@Test void a06_toString() {
86  			TESTER.assertToString();
87  		}
88  
89  		@Test void a07_keySet() {
90  			assertList(TESTER.bean().keySet(), "$ref", "collectionFormat", "default", "enum", "exclusiveMaximum", "exclusiveMinimum", "format", "items", "maxItems", "maxLength", "maximum", "minItems", "minLength", "minimum", "multipleOf", "pattern", "type", "uniqueItems");
91  		}
92  
93  		@Test void a08_nullParameters() {
94  			var x = bean();
95  			assertThrows(IllegalArgumentException.class, () -> x.get(null, String.class));
96  			assertThrows(IllegalArgumentException.class, () -> x.set(null, "value"));
97  		}
98  
99  		@Test void a08b_getSetRef() {
100 			// Test get/set with "$ref" property to cover switch branches
101 			var x = bean();
102 			x.set("$ref", "#/components/schemas/MyItem");
103 			assertEquals("#/components/schemas/MyItem", x.get("$ref", String.class));
104 			assertEquals("#/components/schemas/MyItem", x.getRef());
105 		}
106 
107 		@Test void a09_addMethods() {
108 			assertBean(
109 				bean()
110 					.addEnum("a1", "a2"),
111 				"enum",
112 				"[a1,a2]"
113 			);
114 		}
115 
116 		@Test void a10_asMap() {
117 			assertBean(
118 				bean()
119 					.setType("a")
120 					.set("x1", "x1a")
121 					.asMap(),
122 				"type,x1",
123 				"a,x1a"
124 			);
125 		}
126 
127 		@Test void a11_extraKeys() {
128 			var x = bean().set("x1", "x1a").set("x2", "x2a");
129 			assertList(x.extraKeys(), "x1", "x2");
130 			assertEmpty(bean().extraKeys());
131 		}
132 
133 		@Test void a12_getItemsProperty() {
134 			var x = bean().setItems(bean().setType("a"));
135 			assertBean(x.get("items", Items.class), "type", "a");
136 		}
137 
138 		@Test void a13_strictMode() {
139 			assertThrows(RuntimeException.class, () -> bean().strict().set("foo", "bar"));
140 			assertDoesNotThrow(() -> bean().set("foo", "bar"));
141 
142 			assertFalse(bean().isStrict());
143 			assertTrue(bean().strict().isStrict());
144 			assertFalse(bean().strict(false).isStrict());
145 
146 			var x = bean().strict();
147 			var y = bean();
148 			assertThrowsWithMessage(IllegalArgumentException.class, "Invalid value passed in to setType(String).  Value='invalid', valid values=['string','number','integer','boolean','array']", () -> x.setType("invalid"));
149 			assertDoesNotThrow(() -> x.setType("string"));
150 			assertDoesNotThrow(() -> x.setType("number"));
151 			assertDoesNotThrow(() -> x.setType("integer"));
152 			assertDoesNotThrow(() -> x.setType("boolean"));
153 			assertDoesNotThrow(() -> x.setType("array"));
154 			assertDoesNotThrow(() -> y.setType("invalid"));
155 
156 			assertThrowsWithMessage(RuntimeException.class, "Invalid value passed in to setCollectionFormat(String).  Value='invalid', valid values=[csv, ssv, tsv, pipes, multi]", () -> x.setCollectionFormat("invalid"));
157 			assertDoesNotThrow(() -> x.setCollectionFormat("csv"));
158 			assertDoesNotThrow(() -> x.setCollectionFormat("ssv"));
159 			assertDoesNotThrow(() -> x.setCollectionFormat("tsv"));
160 			assertDoesNotThrow(() -> x.setCollectionFormat("pipes"));
161 			assertDoesNotThrow(() -> x.setCollectionFormat("multi"));
162 			assertDoesNotThrow(() -> y.setCollectionFormat("invalid"));
163 		}
164 	}
165 
166 	@Nested class B_emptyTests extends TestBase {
167 
168 		private static final BeanTester<Items> TESTER =
169 			testBean(bean())
170 			.props("type,format,items,collectionFormat,default,maximum,exclusiveMaximum,minimum,exclusiveMinimum,maxLength,minLength,pattern,maxItems,minItems,uniqueItems,enum,multipleOf,ref")
171 			.vals("<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>")
172 			.json("{}")
173 			.string("{}")
174 		;
175 
176 		@Test void b01_gettersAndSetters() {
177 			TESTER.assertGettersAndSetters();
178 		}
179 
180 		@Test void b02_copy() {
181 			TESTER.assertCopy();
182 		}
183 
184 		@Test void b03_toJson() {
185 			TESTER.assertToJson();
186 		}
187 
188 		@Test void b04_fromJson() {
189 			TESTER.assertFromJson();
190 		}
191 
192 		@Test void b05_roundTrip() {
193 			TESTER.assertRoundTrip();
194 		}
195 
196 		@Test void b06_toString() {
197 			TESTER.assertToString();
198 		}
199 
200 		@Test void b07_keySet() {
201 			assertEmpty(TESTER.bean().keySet());
202 		}
203 	}
204 
205 	@Nested class C_extraProperties extends TestBase {
206 		private static final BeanTester<Items> TESTER =
207 			testBean(
208 				bean()
209 					.set("additionalItems", schemaInfo("a"))
210 					.set("allOf", list(schemaInfo("b1"), schemaInfo("b2")))
211 					.set("collectionFormat", "c")
212 					.set("default", "d")
213 					.set("discriminator", "e")
214 					.set("enum", list("f1", "f2"))
215 					.set("example", "g")
216 					.set("exclusiveMaximum", true)
217 					.set("exclusiveMinimum", true)
218 					.set("externalDocs", externalDocumentation().setUrl(URI.create("h")))
219 					.set("format", "i")
220 					.set("items", bean().setType("j"))
221 					.set("maxItems", 1)
222 					.set("maxLength", 2)
223 					.set("maxProperties", 3)
224 					.set("maximum", 4)
225 					.set("minItems", 5)
226 					.set("minLength", 6)
227 					.set("minProperties", 7)
228 					.set("minimum", 8)
229 					.set("multipleOf", 9)
230 					.set("pattern", "k")
231 					.set("properties", map("l1", schemaInfo("l2")))
232 					.set("readOnly", true)
233 					.set("required", list("m1", "m2"))
234 					.set("title", "n")
235 					.set("type", "o")
236 					.set("uniqueItems", true)
237 					.set("xml", xml().setName("p"))
238 					.set("x1", "x1a")
239 					.set("x2", null)
240 			)
241 			.props("additionalItems{type},allOf{#{type}},collectionFormat,default,discriminator,enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,pattern,properties{l1{type}},readOnly,required{#{toString}},title,type,uniqueItems,xml{name},x1,x2")
242 			.vals("{a},{[{b1},{b2}]},c,d,e,{[{f1},{f2}]},g,true,true,{h},i,{j},1,2,3,4,5,6,7,8,9,k,{{l2}},true,{[{m1},{m2}]},n,o,true,{p},x1a,<null>")
243 			.json("{additionalItems:{type:'a'},allOf:[{type:'b1'},{type:'b2'}],collectionFormat:'c','default':'d',discriminator:'e','enum':['f1','f2'],example:'g',exclusiveMaximum:true,exclusiveMinimum:true,externalDocs:{url:'h'},format:'i',items:{type:'j'},maxItems:1,maxLength:2,maxProperties:3,maximum:4,minItems:5,minLength:6,minProperties:7,minimum:8,multipleOf:9,pattern:'k',properties:{l1:{type:'l2'}},readOnly:true,required:['m1','m2'],title:'n',type:'o',uniqueItems:true,x1:'x1a',xml:{name:'p'}}")
244 			.string("{'additionalItems':{'type':'a'},'allOf':[{'type':'b1'},{'type':'b2'}],'collectionFormat':'c','default':'d','discriminator':'e','enum':['f1','f2'],'example':'g','exclusiveMaximum':true,'exclusiveMinimum':true,'externalDocs':{'url':'h'},'format':'i','items':{'type':'j'},'maxItems':1,'maxLength':2,'maxProperties':3,'maximum':4,'minItems':5,'minLength':6,'minProperties':7,'minimum':8,'multipleOf':9,'pattern':'k','properties':{'l1':{'type':'l2'}},'readOnly':true,'required':['m1','m2'],'title':'n','type':'o','uniqueItems':true,'x1':'x1a','xml':{'name':'p'}}".replace('\'', '"'))
245 		;
246 
247 		@Test void c01_gettersAndSetters() {
248 			TESTER.assertGettersAndSetters();
249 		}
250 
251 		@Test void c02_copy() {
252 			TESTER.assertCopy();
253 		}
254 
255 		@Test void c03_toJson() {
256 			TESTER.assertToJson();
257 		}
258 
259 		@Test void c04_fromJson() {
260 			TESTER.assertFromJson();
261 		}
262 
263 		@Test void c05_roundTrip() {
264 			TESTER.assertRoundTrip();
265 		}
266 
267 		@Test void c06_toString() {
268 			TESTER.assertToString();
269 		}
270 
271 		@Test void c07_keySet() {
272 			assertList(TESTER.bean().keySet(), "additionalItems", "allOf", "collectionFormat", "default", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "x1", "x2", "xml");
273 		}
274 
275 		@Test void c08_get() {
276 			assertMapped(
277 				TESTER.bean(), (obj,prop) -> obj.get(prop, Object.class),
278 				"additionalItems{type},allOf{#{type}},collectionFormat,default,discriminator,enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,pattern,properties{l1{type}},readOnly,required{#{toString}},title,type,uniqueItems,xml{name},x1,x2",
279 				"{a},{[{b1},{b2}]},c,d,e,{[{f1},{f2}]},g,true,true,{h},i,{j},1,2,3,4,5,6,7,8,9,k,{{l2}},true,{[{m1},{m2}]},n,o,true,{p},x1a,<null>"
280 			);
281 		}
282 
283 		@Test void c09_getTypes() {
284 			assertMapped(
285 				TESTER.bean(), (obj,prop) -> simpleClassNameOf(obj.get(prop, Object.class)),
286 				"additionalItems,allOf,collectionFormat,default,discriminator,enum,example,exclusiveMaximum,exclusiveMinimum,externalDocs,format,items,maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,pattern,properties,readOnly,required,title,type,uniqueItems,xml,x1,x2",
287 				"SchemaInfo,ArrayList,String,String,String,ArrayList,String,Boolean,Boolean,ExternalDocumentation,String,Items,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,String,LinkedHashMap,Boolean,ArrayList,String,String,Boolean,Xml,String,<null>"
288 			);
289 		}
290 
291 		@Test void c10_nullPropertyValue() {
292 			assertThrows(IllegalArgumentException.class, ()->bean().get(null));
293 			assertThrows(IllegalArgumentException.class, ()->bean().get(null, String.class));
294 			assertThrows(IllegalArgumentException.class, ()->bean().set(null, "a"));
295 		}
296 	}
297 
298 	@Nested class D_refs extends TestBase {
299 
300 		@Test void d01_resolveRefs_basic() {
301 			var openApi = openApi()
302 				.setComponents(components().setSchemas(Map.of(
303 					"MyItem", schemaInfo().setType("string")
304 				)));
305 			assertBean(
306 				items().setRef("#/components/schemas/MyItem").resolveRefs(openApi, new ArrayDeque<>(), 10),
307 				"type",
308 				"string"
309 			);
310 		}
311 
312 		@Test void d02_resolveRefs_nestedItems() {
313 			var openApi = openApi()
314 				.setComponents(components().setSchemas(Map.of(
315 					"MyItem", schemaInfo().setType("string"),
316 					"MyArray", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/MyItem"))
317 				)));
318 
319 			assertBean(
320 				items().setRef("#/components/schemas/MyArray").resolveRefs(openApi, new ArrayDeque<>(), 10),
321 				"type,items{type}",
322 				"array,{string}"
323 			);
324 		}
325 
326 		@Test void d03_resolveRefs_maxDepth() {
327 			var openApi = openApi()
328 				.setComponents(components().setSchemas(Map.of(
329 					"MyItem", schemaInfo().setType("string"),
330 					"MyArray", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/MyItem"))
331 				)));
332 			assertBean(
333 				items().setRef("#/components/schemas/MyArray").resolveRefs(openApi, new ArrayDeque<>(), 1),
334 				"type,items{ref}",
335 				"array,{#/components/schemas/MyItem}"
336 			);
337 		}
338 
339 		@Test void d04_resolveRefsWithRef() {
340 			var openApi = openApi()
341 				.setComponents(components().setSchemas(Map.of(
342 					"MyItem", schemaInfo().setType("string")
343 				)));
344 
345 			assertBean(
346 				items().setRef("#/components/schemas/MyItem").resolveRefs(openApi, new ArrayDeque<>(), 10),
347 				"type",
348 				"string"
349 			);
350 
351 			var refStack = new ArrayDeque<String>();
352 			refStack.add("#/components/schemas/MyItem");
353 
354 			// With ref stack contains.
355 			assertBean(
356 				items().setRef("#/components/schemas/MyItem").resolveRefs(openApi, refStack, 10),
357 				"ref",
358 				"#/components/schemas/MyItem"
359 			);
360 
361 			// With max depth.
362 			assertBean(
363 				items().setRef("#/components/schemas/MyItem").resolveRefs(openApi, new ArrayDeque<>(), 0),
364 				"ref",
365 				"#/components/schemas/MyItem"
366 			);
367 
368 			// With properties.
369 			assertBean(
370 				items()
371 					.set("properties", JsonMap.of("prop1", JsonMap.of("$ref", "#/components/schemas/MyItem")))
372 					.resolveRefs(openApi, new ArrayDeque<>(), 10),
373 				"properties{prop1{type}}",
374 				"{{string}}"
375 			);
376 
377 			// With items.
378 			assertBean(
379 				items().setItems(items().setRef("#/components/schemas/MyItem")).resolveRefs(openApi, new ArrayDeque<>(), 10),
380 				"items{type}",
381 				"{string}"
382 			);
383 
384 			// Examle null.
385 			assertBean(
386 				items().set("example", "test").resolveRefs(openApi, new ArrayDeque<>(), 10),
387 				"example",
388 				"<null>"
389 			);
390 
391 			// Without ref.
392 			assertBean(
393 				items().setType("string").resolveRefs(openApi, new ArrayDeque<>(), 10),
394 				"type,example",
395 				"string,<null>"
396 			);
397 
398 			// With null items.
399 			assertBean(
400 				items().setType("string").resolveRefs(openApi, new ArrayDeque<>(), 10), // items is null
401 				"type,items,example",
402 				"string,<null>,<null>"
403 			);
404 
405 			// With null properties.
406 			assertBean(
407 				items().setType("string").resolveRefs(openApi, new ArrayDeque<>(), 10), // no properties set
408 				"type,example",
409 				"string,<null>"
410 			);
411 		}
412 
413 		@Test void d04_resolveRefs_noRefNoItems() {
414 			// Test resolveRefs when both ref and items are null (covers the missing branch)
415 			var openApi = openApi()
416 				.setComponents(components().setSchemas(map("MyItem", schemaInfo().setType("string"))));
417 
418 			var items = bean()
419 				.setType("string")
420 				.setFormat("text");
421 
422 			var result = items.resolveRefs(openApi, new ArrayDeque<>(), 10);
423 
424 			// Should return the same object unchanged
425 			assertSame(items, result);
426 			assertEquals("string", result.getType());
427 			assertEquals("text", result.getFormat());
428 		}
429 
430 		@Test void d05_resolveRefs_circularReference() {
431 			// Test circular reference detection in extra attributes
432 			var openApi = openApi()
433 				.setComponents(components().setSchemas(map(
434 					"Item1", schemaInfo().setRef("#/components/schemas/Item2"),
435 					"Item2", schemaInfo().setRef("#/components/schemas/Item1")
436 				)));
437 
438 			var refStack = new ArrayDeque<String>();
439 			refStack.add("#/components/schemas/Item1");
440 			
441 			var item = items()
442 				.setType("object")
443 				.set("properties", JsonMap.of("prop1", JsonMap.of("$ref", "#/components/schemas/Item1")));
444 			var result = item.resolveRefs(openApi, refStack, 10);
445 
446 			// Should return object with unresolved circular ref in properties
447 			assertSame(item, result);
448 		}
449 
450 		@Test void d06_resolveRefs_maxDepthDirect() {
451 			// Test max depth directly
452 			var openApi = openApi()
453 				.setComponents(components().setSchemas(map("MyItem", schemaInfo().setType("string"))));
454 
455 			var refStack = new ArrayDeque<String>();
456 			refStack.add("dummy1");
457 			refStack.add("dummy2");
458 			refStack.add("dummy3");
459 			
460 			var item = items()
461 				.setType("object")
462 				.set("properties", JsonMap.of("prop1", JsonMap.of("$ref", "#/components/schemas/MyItem")));
463 			var result = item.resolveRefs(openApi, refStack, 3);
464 
465 			// Should return object with unresolved ref due to max depth
466 			assertSame(item, result);
467 		}
468 
469 		@Test void d07_resolveRefs_withJsonList() {
470 			// Test resolveRefs with JsonList to cover the instanceof JsonList branch
471 			var openApi = openApi()
472 				.setComponents(components().setSchemas(map("MyItem", schemaInfo().setType("string"))));
473 
474 			var item = items()
475 				.setType("object")
476 				.set("someList", JsonList.of(JsonMap.of("$ref", "#/components/schemas/MyItem")));
477 			var result = item.resolveRefs(openApi, new ArrayDeque<>(), 10);
478 
479 			// JsonList should have its refs resolved
480 			assertSame(item, result);
481 		}
482 
483 		@Test void d08_resolveRefs_withPrimitiveValue() {
484 			// Test resolveRefs with primitive values (not JsonMap or JsonList) to cover the false branch
485 			var openApi = openApi()
486 				.setComponents(components().setSchemas(map("MyItem", schemaInfo().setType("string"))));
487 
488 			var item = items()
489 				.setType("object")
490 				.set("someString", "plain string value")
491 				.set("someNumber", 42)
492 				.set("someBoolean", true);
493 			var result = item.resolveRefs(openApi, new ArrayDeque<>(), 10);
494 
495 			// Primitive values should be unchanged
496 			assertSame(item, result);
497 			assertEquals("plain string value", item.get("someString", String.class));
498 			assertEquals(42, item.get("someNumber", Integer.class));
499 			assertEquals(true, item.get("someBoolean", Boolean.class));
500 		}
501 	}
502 
503 	//---------------------------------------------------------------------------------------------
504 	// Helper methods
505 	//---------------------------------------------------------------------------------------------
506 
507 	private static Items bean() {
508 		return items();
509 	}
510 }