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.junit.jupiter.api.*;
30  
31  /**
32   * Testcase for {@link SchemaInfo}.
33   */
34  class SchemaInfo_Test extends TestBase {
35  
36  	@Nested class A_basicTests extends TestBase {
37  
38  		private static final BeanTester<SchemaInfo> TESTER =
39  			testBean(
40  				bean()
41  					.setDefault("a")
42  					.setDeprecated(true)
43  					.setDescription("b")
44  					.setEnum(l("c1", "c2"))
45  					.setExample("d")
46  					.setExclusiveMaximum(true)
47  					.setExclusiveMinimum(true)
48  					.setFormat("e")
49  					.setItems(items("f"))
50  					.setMaxItems(1)
51  					.setMaxLength(2)
52  					.setMaxProperties(3)
53  					.setMaximum(4)
54  					.setMinItems(5)
55  					.setMinLength(6)
56  					.setMinProperties(7)
57  					.setMinimum(8)
58  					.setMultipleOf(9)
59  					.setNullable(true)
60  					.setPattern("g")
61  					.setReadOnly(true)
62  					.setRef("h")
63  					.setRequired(l("i"))
64  					.setTitle("j")
65  					.setType("k")
66  					.setUniqueItems(true)
67  					.setWriteOnly(true)
68  			)
69  			.props("default,deprecated,description,enum,example,exclusiveMaximum,exclusiveMinimum,format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,nullable,pattern,readOnly,ref,required,title,type,uniqueItems,writeOnly")
70  			.vals("a,true,b,[c1,c2],d,true,true,e,{f},1,2,3,4,5,6,7,8,9,true,g,true,h,[i],j,k,true,true")
71  			.json("{'$ref':'h','default':'a',deprecated:true,description:'b','enum':['c1','c2'],example:'d',exclusiveMaximum:true,exclusiveMinimum:true,format:'e',items:{type:'f'},maxItems:1,maxLength:2,maxProperties:3,maximum:4,minItems:5,minLength:6,minProperties:7,minimum:8,multipleOf:9,nullable:true,pattern:'g',readOnly:true,required:['i'],title:'j',type:'k',uniqueItems:true,writeOnly:true}")
72  			.string("{'$ref':'h','default':'a','deprecated':true,'description':'b','enum':['c1','c2'],'example':'d','exclusiveMaximum':true,'exclusiveMinimum':true,'format':'e','items':{'type':'f'},'maxItems':1,'maxLength':2,'maxProperties':3,'maximum':4,'minItems':5,'minLength':6,'minProperties':7,'minimum':8,'multipleOf':9,'nullable':true,'pattern':'g','readOnly':true,'required':['i'],'title':'j','type':'k','uniqueItems':true,'writeOnly':true}".replace('\'','"'))
73  		;
74  
75  		@Test void a01_gettersAndSetters() {
76  			TESTER.assertGettersAndSetters();
77  		}
78  
79  		@Test void a02_copy() {
80  			TESTER.assertCopy();
81  		}
82  
83  		@Test void a03_toJson() {
84  			TESTER.assertToJson();
85  		}
86  
87  		@Test void a04_fromJson() {
88  			TESTER.assertFromJson();
89  		}
90  
91  		@Test void a05_roundTrip() {
92  			TESTER.assertRoundTrip();
93  		}
94  
95  		@Test void a06_toString() {
96  			TESTER.assertToString();
97  		}
98  
99  		@Test void a07_keySet() {
100 			assertList(TESTER.bean().keySet(), "$ref", "default", "deprecated", "description", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "nullable", "pattern", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly");
101 		}
102 
103 		@Test void a08_nullParameters() {
104 			var x = bean();
105 			assertThrows(IllegalArgumentException.class, () -> x.get(null, String.class));
106 			assertThrows(IllegalArgumentException.class, () -> x.set(null, "value"));
107 		}
108 
109 		@Test void a08b_getSetRef() {
110 			// Test get/set with "$ref" property to cover switch branches
111 			var x = bean();
112 			x.set("$ref", "#/components/schemas/MySchema");
113 			assertEquals("#/components/schemas/MySchema", x.get("$ref", String.class));
114 			assertEquals("#/components/schemas/MySchema", x.getRef());
115 		}
116 
117 		@Test void a09_addMethods() {
118 			assertBean(
119 				bean()
120 					.addEnum("a1", "a2")
121 					.addRequired("b1", "b2")
122 					.addAllOf(schemaInfo("c1"), schemaInfo("c2"))
123 					.addAnyOf(schemaInfo("d1"), schemaInfo("d2"))
124 					.addOneOf(schemaInfo("e1"), schemaInfo("e2")),
125 				"enum,required,allOf{#{type}},anyOf{#{type}},oneOf{#{type}}",
126 				"[a1,a2],[b1,b2],{[{c1},{c2}]},{[{d1},{d2}]},{[{e1},{e2}]}"
127 			);
128 		}
129 
130 		@Test void a10_asMap() {
131 			assertBean(
132 				bean()
133 					.setType("a")
134 					.set("x1", "x1a")
135 					.asMap(),
136 				"type,x1",
137 				"a,x1a"
138 			);
139 		}
140 
141 		@Test void a11_extraKeys() {
142 			var x = bean().set("x1", "x1a").set("x2", "x2a");
143 			assertList(x.extraKeys(), "x1", "x2");
144 			assertEmpty(bean().extraKeys());
145 		}
146 
147 		@Test void a12_strictMode() {
148 			assertThrows(RuntimeException.class, () -> bean().strict().set("foo", "bar"));
149 			assertDoesNotThrow(() -> bean().set("foo", "bar"));
150 
151 			assertFalse(bean().isStrict());
152 			assertTrue(bean().strict().isStrict());
153 			assertFalse(bean().strict(false).isStrict());
154 		}
155 
156 		@Test void a13_collectionSetters() {
157 			var x = bean()
158 				.setEnum(l("a1", "a2"))
159 				.setRequired(l("b1", "b2"));
160 
161 			assertBean(x,
162 				"enum,required",
163 				"[a1,a2],[b1,b2]"
164 			);
165 		}
166 	}
167 
168 	@Nested class B_emptyTests extends TestBase {
169 
170 		private static final BeanTester<SchemaInfo> TESTER =
171 			testBean(bean())
172 			.props("type,format,title,description,default,multipleOf,maximum,exclusiveMaximum,minimum,exclusiveMinimum,maxLength,minLength,pattern,maxItems,minItems,uniqueItems,maxProperties,minProperties,required,enum,items,example,nullable,readOnly,writeOnly,deprecated,ref")
173 			.vals("<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<null>,<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<SchemaInfo> TESTER =
209 			testBean(
210 				bean()
211 					.set("additionalItems", schemaInfo("a"))
212 					.set("additionalProperties", schemaInfo("b"))
213 					.set("allOf", l(schemaInfo("c1"), schemaInfo("c2")))
214 					.set("anyOf", l(schemaInfo("d1"), schemaInfo("d2")))
215 					.set("default", "e")
216 					.set("description", "f")
217 					.set("discriminator", discriminator("g"))
218 					.set("enum", l("h1", "h2"))
219 					.set("example", "i")
220 					.set("exclusiveMaximum", true)
221 					.set("exclusiveMinimum", true)
222 					.set("externalDocs", externalDocumentation().setUrl(URI.create("j")))
223 					.set("format", "k")
224 					.set("items", schemaInfo("l"))
225 					.set("maxItems", 1)
226 					.set("maxLength", 2)
227 					.set("maxProperties", 3)
228 					.set("maximum", 4)
229 					.set("minItems", 5)
230 					.set("minLength", 6)
231 					.set("minProperties", 7)
232 					.set("minimum", 8)
233 					.set("multipleOf", 9)
234 					.set("not", schemaInfo("m"))
235 					.set("nullable", true)
236 					.set("oneOf", l(schemaInfo("n1"), schemaInfo("n2")))
237 					.set("pattern", "o")
238 					.set("properties", m("p1", schemaInfo("p2")))
239 					.set("readOnly", true)
240 					.set("required", l("q1", "q2"))
241 					.set("title", "r")
242 					.set("type", "s")
243 					.set("uniqueItems", true)
244 					.set("writeOnly", true)
245 					.set("xml", xml().setName("t"))
246 					.set("x1", "x1a")
247 					.set("x2", null)
248 			)
249 			.props("additionalItems{type},additionalProperties{type},allOf{#{type}},anyOf{#{type}},default,description,discriminator{propertyName},enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not{type},nullable,oneOf{#{type}},pattern,properties{p1{type}},readOnly,required{#{toString}},title,type,uniqueItems,writeOnly,xml{name},x1,x2")
250 			.vals("{a},{b},{[{c1},{c2}]},{[{d1},{d2}]},e,f,{g},{[{h1},{h2}]},i,true,true,{j},k,{l},1,2,3,4,5,6,7,8,9,{m},true,{[{n1},{n2}]},o,{{p2}},true,{[{q1},{q2}]},r,s,true,true,{t},x1a,<null>")
251 			.json("{additionalItems:{type:'a'},additionalProperties:{type:'b'},allOf:[{type:'c1'},{type:'c2'}],anyOf:[{type:'d1'},{type:'d2'}],'default':'e',description:'f',discriminator:{propertyName:'g'},'enum':['h1','h2'],example:'i',exclusiveMaximum:true,exclusiveMinimum:true,externalDocs:{url:'j'},format:'k',items:{type:'l'},maxItems:1,maxLength:2,maxProperties:3,maximum:4,minItems:5,minLength:6,minProperties:7,minimum:8,multipleOf:9,not:{type:'m'},nullable:true,oneOf:[{type:'n1'},{type:'n2'}],pattern:'o',properties:{p1:{type:'p2'}},readOnly:true,required:['q1','q2'],title:'r',type:'s',uniqueItems:true,writeOnly:true,x1:'x1a',xml:{name:'t'}}")
252 			.string("{'additionalItems':{'type':'a'},'additionalProperties':{'type':'b'},'allOf':[{'type':'c1'},{'type':'c2'}],'anyOf':[{'type':'d1'},{'type':'d2'}],'default':'e','description':'f','discriminator':{'propertyName':'g'},'enum':['h1','h2'],'example':'i','exclusiveMaximum':true,'exclusiveMinimum':true,'externalDocs':{'url':'j'},'format':'k','items':{'type':'l'},'maxItems':1,'maxLength':2,'maxProperties':3,'maximum':4,'minItems':5,'minLength':6,'minProperties':7,'minimum':8,'multipleOf':9,'not':{'type':'m'},'nullable':true,'oneOf':[{'type':'n1'},{'type':'n2'}],'pattern':'o','properties':{'p1':{'type':'p2'}},'readOnly':true,'required':['q1','q2'],'title':'r','type':'s','uniqueItems':true,'writeOnly':true,'x1':'x1a','xml':{'name':'t'}}".replace('\'', '"'))
253 		;
254 
255 		@Test void c01_gettersAndSetters() {
256 			TESTER.assertGettersAndSetters();
257 		}
258 
259 		@Test void c02_copy() {
260 			TESTER.assertCopy();
261 		}
262 
263 		@Test void c03_toJson() {
264 			TESTER.assertToJson();
265 		}
266 
267 		@Test void c04_fromJson() {
268 			TESTER.assertFromJson();
269 		}
270 
271 		@Test void c05_roundTrip() {
272 			TESTER.assertRoundTrip();
273 		}
274 
275 		@Test void c06_toString() {
276 			TESTER.assertToString();
277 		}
278 
279 		@Test void c07_keySet() {
280 			assertList(TESTER.bean().keySet(), "additionalProperties", "allOf", "anyOf", "default", "description", "discriminator", "enum", "example", "exclusiveMaximum", "exclusiveMinimum", "externalDocs", "format", "items", "maxItems", "maxLength", "maxProperties", "maximum", "minItems", "minLength", "minProperties", "minimum", "multipleOf", "not", "nullable", "oneOf", "pattern", "properties", "readOnly", "required", "title", "type", "uniqueItems", "writeOnly", "xml", "additionalItems", "x1", "x2");
281 		}
282 
283 		@Test void c08_get() {
284 			assertMapped(
285 				TESTER.bean(), (obj,prop) -> obj.get(prop, Object.class),
286 				"additionalItems{type},additionalProperties{type},allOf{#{type}},anyOf{#{type}},default,description,discriminator{propertyName},enum{#{toString}},example,exclusiveMaximum,exclusiveMinimum,externalDocs{url},format,items{type},maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not{type},nullable,oneOf{#{type}},pattern,properties{p1{type}},readOnly,required{#{toString}},title,type,uniqueItems,writeOnly,xml{name},x1,x2",
287 				"{a},{b},{[{c1},{c2}]},{[{d1},{d2}]},e,f,{g},{[{h1},{h2}]},i,true,true,{j},k,{l},1,2,3,4,5,6,7,8,9,{m},true,{[{n1},{n2}]},o,{{p2}},true,{[{q1},{q2}]},r,s,true,true,{t},x1a,<null>"
288 			);
289 		}
290 
291 		@Test void c09_getTypes() {
292 			assertMapped(
293 				TESTER.bean(), (obj,prop) -> cns(obj.get(prop, Object.class)),
294 				"additionalItems,additionalProperties,allOf,anyOf,default,description,discriminator,enum,example,exclusiveMaximum,exclusiveMinimum,externalDocs,format,items,maxItems,maxLength,maxProperties,maximum,minItems,minLength,minProperties,minimum,multipleOf,not,nullable,oneOf,pattern,properties,readOnly,required,title,type,uniqueItems,writeOnly,xml,x1,x2",
295 				"SchemaInfo,SchemaInfo,ArrayList,ArrayList,String,String,Discriminator,ArrayList,String,Boolean,Boolean,ExternalDocumentation,String,Items,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,Integer,SchemaInfo,Boolean,ArrayList,String,LinkedHashMap,Boolean,ArrayList,String,String,Boolean,Boolean,Xml,String,<null>"
296 			);
297 		}
298 
299 		@Test void c10_nullPropertyValue() {
300 			assertThrows(IllegalArgumentException.class, ()->bean().get(null));
301 			assertThrows(IllegalArgumentException.class, ()->bean().get(null, String.class));
302 			assertThrows(IllegalArgumentException.class, ()->bean().set(null, "a"));
303 		}
304 	}
305 
306 	@Nested class D_refs extends TestBase {
307 
308 		@Test void d01_resolveRefs_basic() {
309 			var openApi = openApi()
310 				.setComponents(components().setSchemas(m(
311 					"Pet", schemaInfo().setType("object").setTitle("Pet")
312 				)));
313 
314 			assertBean(
315 				schemaInfo().setRef("#/components/schemas/Pet").resolveRefs(openApi, new ArrayDeque<>(), 10),
316 				"type,title",
317 				"object,Pet"
318 			);
319 		}
320 
321 		@Test void d02_resolveRefs_nested() {
322 			var openApi = openApi()
323 				.setComponents(components().setSchemas(m(
324 					"Pet", schemaInfo().setType("object").setTitle("Pet"),
325 					"Pets", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/Pet"))
326 				)));
327 
328 			assertBean(
329 				schemaInfo().setRef("#/components/schemas/Pets").resolveRefs(openApi, new ArrayDeque<>(), 10),
330 				"type,items{type,title}",
331 				"array,{object,Pet}"
332 			);
333 		}
334 
335 		@Test void d03_resolveRefs_maxDepth() {
336 			var openApi = openApi()
337 				.setComponents(components().setSchemas(m(
338 					"Pet", schemaInfo().setType("object").setTitle("Pet"),
339 					"Pets", schemaInfo().setType("array").setItems(items().setRef("#/components/schemas/Pet"))
340 				)));
341 
342 			assertBean(
343 				schemaInfo().setRef("#/components/schemas/Pets").resolveRefs(openApi, new ArrayDeque<>(), 1),
344 				"type,items{ref}",
345 				"array,{#/components/schemas/Pet}"
346 			);
347 		}
348 
349 		@Test void d04_resolveRefs_circular() {
350 			var openApi = openApi()
351 				.setComponents(components().setSchemas(m(
352 					"A", schemaInfo().setType("object").setTitle("A").setProperties(m("b", schemaInfo().setRef("#/components/schemas/B"))),
353 					"B", schemaInfo().setType("object").setTitle("B").setProperties(m("a", schemaInfo().setRef("#/components/schemas/A")))
354 				)));
355 
356 			assertBean(
357 				schemaInfo().setRef("#/components/schemas/A").resolveRefs(openApi, new ArrayDeque<>(), 10),
358 				"type,title,properties{b{type,title,properties{a{ref}}}}",
359 				"object,A,{{object,B,{{#/components/schemas/A}}}}"
360 			);
361 		}
362 
363 		@Test void d05_resolveRefs() {
364 			// Test resolveRefs when both ref and allOf are null (covers the missing branch)
365 			var openApi = openApi()
366 				.setComponents(components().setSchemas(map("MySchema", schemaInfo().setType("object").setTitle("My Schema"))));
367 
368 			var schema = bean()
369 				.setType("string")
370 				.setDescription("Test schema");
371 
372 			var result = schema.resolveRefs(openApi, new ArrayDeque<>(), 10);
373 
374 			// Should return the same object unchanged
375 			assertSame(schema, result);
376 			assertEquals("string", result.getType());
377 			assertEquals("Test schema", result.getDescription());
378 		}
379 
380 		@Test void d06_resolveRefs_maxDepthDirect() {
381 			// Test max depth directly (covers the refStack.size() >= maxDepth branch)
382 			var openApi = openApi()
383 				.setComponents(components().setSchemas(map("MySchema", schemaInfo().setType("string"))));
384 
385 			var refStack = new ArrayDeque<String>();
386 			refStack.add("dummy1");
387 			refStack.add("dummy2");
388 			refStack.add("dummy3");
389 
390 			var schema = schemaInfo().setRef("#/components/schemas/MySchema");
391 			var result = schema.resolveRefs(openApi, refStack, 3);
392 
393 			// Should return the original object without resolving
394 			assertSame(schema, result);
395 			assertEquals("#/components/schemas/MySchema", result.getRef());
396 		}
397 
398 		@Test void d07_resolveRefs_additionalProperties() {
399 			// Test resolveRefs with additionalProperties to cover that branch
400 			var openApi = openApi()
401 				.setComponents(components().setSchemas(map(
402 					"MyAdditional", schemaInfo().setType("string").setDescription("Additional property schema")
403 				)));
404 
405 			var schema = schemaInfo()
406 				.setType("object")
407 				.setAdditionalProperties(schemaInfo().setRef("#/components/schemas/MyAdditional"));
408 			var result = schema.resolveRefs(openApi, new ArrayDeque<>(), 10);
409 
410 			// additionalProperties should have its ref resolved
411 			assertSame(schema, result);
412 			assertNotNull(result.getAdditionalProperties());
413 			assertEquals("string", result.getAdditionalProperties().getType());
414 			assertNull(result.getAdditionalProperties().getRef());
415 		}
416 	}
417 
418 	//---------------------------------------------------------------------------------------------
419 	// Helper methods
420 	//---------------------------------------------------------------------------------------------
421 
422 	private static SchemaInfo bean() {
423 		return schemaInfo();
424 	}
425 }