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