Eliminate ObjectNode(s) from Schema class
[netconf.git] / restconf / restconf-openapi / src / test / java / org / opendaylight / restconf / openapi / impl / OpenApiGeneratorRFC8040Test.java
1 /*
2  * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.openapi.impl;
9
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertFalse;
12 import static org.junit.Assert.assertNotNull;
13 import static org.junit.Assert.assertNull;
14 import static org.junit.Assert.assertTrue;
15 import static org.mockito.Mockito.mock;
16 import static org.mockito.Mockito.when;
17 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathGetParameters;
18 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathPostParameters;
19 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.BASIC_AUTH_NAME;
20
21 import com.fasterxml.jackson.databind.JsonNode;
22 import java.util.ArrayList;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import javax.ws.rs.core.UriInfo;
28 import org.junit.BeforeClass;
29 import org.junit.Test;
30 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
31 import org.opendaylight.restconf.openapi.DocGenTestHelper;
32 import org.opendaylight.restconf.openapi.model.OpenApiObject;
33 import org.opendaylight.restconf.openapi.model.Operation;
34 import org.opendaylight.restconf.openapi.model.Path;
35 import org.opendaylight.restconf.openapi.model.Property;
36 import org.opendaylight.restconf.openapi.model.Schema;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
39
40 public final class OpenApiGeneratorRFC8040Test {
41     private static final String TOASTER_2 = "toaster2";
42     private static final String REVISION_DATE = "2009-11-20";
43     private static final String MANDATORY_TEST = "mandatory-test";
44     private static final String CONFIG_ROOT_CONTAINER = "mandatory-test_root-container";
45     private static final String CONFIG_MANDATORY_CONTAINER = "mandatory-test_root-container_mandatory-container";
46     private static final String CONFIG_MANDATORY_LIST = "mandatory-test_root-container_mandatory-list";
47     private static final String MANDATORY_TEST_MODULE = "mandatory-test_module";
48     private static final String CONTAINER = "container";
49     private static final String LIST = "list";
50
51     private static DOMSchemaService schemaService;
52     private static UriInfo uriInfo;
53
54     private final OpenApiGeneratorRFC8040 generator = new OpenApiGeneratorRFC8040(schemaService);
55
56     @BeforeClass
57     public static void beforeClass() throws Exception {
58         schemaService = mock(DOMSchemaService.class);
59         final EffectiveModelContext context = YangParserTestUtils.parseYangResourceDirectory("/yang");
60         when(schemaService.getGlobalContext()).thenReturn(context);
61         uriInfo = DocGenTestHelper.createMockUriInfo("http://localhost/path");
62     }
63
64     /**
65      * Test that paths are generated according to the model.
66      */
67     @Test
68     public void testPaths() {
69         final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
70
71         assertEquals(Set.of("/rests/data",
72             "/rests/data/toaster2:toaster",
73             "/rests/data/toaster2:toaster/toasterSlot={slotId}",
74             "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo",
75             "/rests/data/toaster2:lst={lf1}",
76             "/rests/data/toaster2:lst={lf1}/cont1",
77             "/rests/data/toaster2:lst={lf1}/cont1/cont11",
78             "/rests/data/toaster2:lst={lf1}/cont1/lst11={lf111}",
79             "/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}",
80             "/rests/operations/toaster2:make-toast",
81             "/rests/operations/toaster2:cancel-toast",
82             "/rests/operations/toaster2:restock-toaster"),
83             doc.paths().keySet());
84     }
85
86     /**
87      * Test that generated configuration paths allow to use operations: get, put, patch, delete and post.
88      */
89     @Test
90     public void testConfigPaths() {
91         final List<String> configPaths = List.of("/rests/data/toaster2:lst={lf1}",
92                 "/rests/data/toaster2:lst={lf1}/cont1",
93                 "/rests/data/toaster2:lst={lf1}/cont1/cont11",
94                 "/rests/data/toaster2:lst={lf1}/cont1/lst11={lf111}",
95                 "/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}");
96         final List<String> configPathsForPost = List.of("/rests/data/toaster2:lst={lf1}/cont1",
97                 "/rests/data/toaster2:lst={lf1}/cont1/cont11");
98
99         final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
100
101         for (final String path : configPaths) {
102             final Path node = doc.paths().get(path);
103             assertNotNull(node.get());
104             assertNotNull(node.put());
105             assertNotNull(node.delete());
106             assertNotNull(node.patch());
107         }
108
109         for (final String path : configPathsForPost) {
110             final Path node = doc.paths().get(path);
111             assertNotNull(node.post());
112         }
113     }
114
115     /**
116      * Test that generated document contains the following schemas.
117      */
118     @Test
119     public void testSchemas() {
120         final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
121
122         final Map<String, Schema> schemas = doc.components().schemas();
123         assertNotNull(schemas);
124
125         final Schema configLst = schemas.get("toaster2_lst");
126         assertNotNull(configLst);
127         DocGenTestHelper.containsReferences(configLst, "lst1", "#/components/schemas/toaster2_lst_lst1");
128         DocGenTestHelper.containsReferences(configLst, "cont1", "#/components/schemas/toaster2_lst_cont1");
129
130         final Schema configLst1 = schemas.get("toaster2_lst_lst1");
131         assertNotNull(configLst1);
132
133         final Schema configCont1 = schemas.get("toaster2_lst_cont1");
134         assertNotNull(configCont1);
135         DocGenTestHelper.containsReferences(configCont1, "cont11", "#/components/schemas/toaster2_lst_cont1_cont11");
136         DocGenTestHelper.containsReferences(configCont1, "lst11", "#/components/schemas/toaster2_lst_cont1_lst11");
137
138         final Schema configCont11 = schemas.get("toaster2_lst_cont1_cont11");
139         assertNotNull(configCont11);
140
141         final Schema configLst11 = schemas.get("toaster2_lst_cont1_lst11");
142         assertNotNull(configLst11);
143     }
144
145     /**
146      * Test that generated document contains RPC schemas for "make-toast" with correct input.
147      */
148     @Test
149     public void testRPC() {
150         final OpenApiObject doc = generator.getApiDeclaration("toaster", "2009-11-20", uriInfo);
151         assertNotNull(doc);
152
153         final Map<String, Schema> schemas = doc.components().schemas();
154         final Schema input = schemas.get("toaster_make-toast_input");
155         final Map<String, Property> properties = input.properties();
156         assertTrue(properties.containsKey("toasterDoneness"));
157         assertTrue(properties.containsKey("toasterToastType"));
158     }
159
160     @Test
161     public void testChoice() {
162         final var doc = generator.getApiDeclaration("choice-test", null, uriInfo);
163         assertNotNull(doc);
164
165         final var schemas = doc.components().schemas();
166         final var firstContainer = schemas.get("choice-test_first-container");
167         assertEquals("default-value", firstContainer.properties().get("leaf-default").defaultValue().toString());
168         assertFalse(firstContainer.properties().containsKey("leaf-non-default"));
169
170         final var secondContainer = schemas.get("choice-test_second-container");
171         assertTrue(secondContainer.properties().containsKey("leaf-first-case"));
172         assertFalse(secondContainer.properties().containsKey("leaf-second-case"));
173     }
174
175     @Test
176     public void testMandatory() {
177         final var doc = generator.getApiDeclaration(MANDATORY_TEST, null, uriInfo);
178         assertNotNull(doc);
179         final var schemas = doc.components().schemas();
180         final var containersWithRequired = new ArrayList<String>();
181
182         final var reqRootContainerElements = List.of("mandatory-root-leaf", "mandatory-container",
183             "mandatory-first-choice", "mandatory-list");
184         verifyRequiredField(schemas.get(CONFIG_ROOT_CONTAINER), reqRootContainerElements);
185         containersWithRequired.add(CONFIG_ROOT_CONTAINER);
186
187         final var reqMandatoryContainerElements = List.of("mandatory-leaf", "leaf-list-with-min-elements");
188         verifyRequiredField(schemas.get(CONFIG_MANDATORY_CONTAINER), reqMandatoryContainerElements);
189         containersWithRequired.add(CONFIG_MANDATORY_CONTAINER);
190
191         final var reqMandatoryListElements = List.of("mandatory-list-field");
192         verifyRequiredField(schemas.get(CONFIG_MANDATORY_LIST), reqMandatoryListElements);
193         containersWithRequired.add(CONFIG_MANDATORY_LIST);
194
195         final var testModuleMandatoryArray = List.of("root-container", "root-mandatory-list");
196         verifyRequiredField(schemas.get(MANDATORY_TEST_MODULE), testModuleMandatoryArray);
197         containersWithRequired.add(MANDATORY_TEST_MODULE);
198
199         verifyThatOthersNodeDoesNotHaveRequiredField(containersWithRequired, schemas);
200     }
201
202     /**
203      * Test that checks for correct amount of parameters in requests.
204      */
205     @Test
206     public void testRecursiveParameters() {
207         final var configPaths = Map.of("/rests/data/recursive:container-root", 0,
208             "/rests/data/recursive:container-root/root-list={name}", 1,
209             "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}", 2,
210             "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}/super-nested-list={name2}", 3);
211
212         final var doc = generator.getApiDeclaration("recursive", "2023-05-22", uriInfo);
213         assertNotNull(doc);
214
215         final var paths = doc.paths();
216         assertEquals(5, paths.size());
217
218         for (final var expectedPath : configPaths.entrySet()) {
219             assertTrue(paths.containsKey(expectedPath.getKey()));
220             final int expectedSize = expectedPath.getValue();
221
222             final var path = paths.get(expectedPath.getKey());
223
224             final var get = path.get();
225             assertNotNull(get);
226             assertEquals(expectedSize + 1, get.parameters().size());
227
228             final var put = path.put();
229             assertNotNull(put);
230             assertEquals(expectedSize, put.parameters().size());
231
232             final var delete = path.delete();
233             assertNotNull(delete);
234             assertEquals(expectedSize, delete.parameters().size());
235
236             final var patch = path.patch();
237             assertNotNull(patch);
238             assertEquals(expectedSize, patch.parameters().size());
239         }
240
241         // we do not generate POST for lists
242         final var path = paths.get("/rests/data/recursive:container-root");
243         final var post = path.post();
244         final int expectedSize = configPaths.get("/rests/data/recursive:container-root");
245         assertEquals(expectedSize, post.parameters().size());
246     }
247
248     /**
249      * Test that request parameters are correctly numbered.
250      *
251      * <p>
252      * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
253      */
254     @Test
255     public void testParametersNumbering() {
256         final var doc = generator.getApiDeclaration("path-params-test", null, uriInfo);
257
258         var pathToList1 = "/rests/data/path-params-test:cont/list1={name}";
259         assertTrue(doc.paths().containsKey(pathToList1));
260         assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList1));
261
262         var pathToList2 = "/rests/data/path-params-test:cont/list1={name}/list2={name1}";
263         assertTrue(doc.paths().containsKey(pathToList2));
264         assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList2));
265
266         var pathToList3 = "/rests/data/path-params-test:cont/list3={name}";
267         assertTrue(doc.paths().containsKey(pathToList3));
268         assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList3));
269
270         var pathToList4 = "/rests/data/path-params-test:cont/list1={name}/list4={name1}";
271         assertTrue(doc.paths().containsKey(pathToList4));
272         assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList4));
273
274         var pathToList5 = "/rests/data/path-params-test:cont/list1={name}/cont2";
275         assertTrue(doc.paths().containsKey(pathToList4));
276         assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList5));
277     }
278
279     /**
280      * Test that request for actions is correct and has parameters.
281      */
282     @Test
283     public void testActionPathsParams() {
284         final var doc = generator.getApiDeclaration("action-types", null, uriInfo);
285
286         final var pathWithParameters = "/rests/operations/action-types:list={name}/list-action";
287         assertTrue(doc.paths().containsKey(pathWithParameters));
288         assertEquals(List.of("name"), getPathPostParameters(doc.paths(), pathWithParameters));
289
290         final var pathWithoutParameters = "/rests/operations/action-types:multi-container/inner-container/action";
291         assertTrue(doc.paths().containsKey(pathWithoutParameters));
292         assertEquals(List.of(), getPathPostParameters(doc.paths(), pathWithoutParameters));
293     }
294
295     @Test
296     public void testSimpleOpenApiObjects() {
297         final var doc = generator.getApiDeclaration("my-yang", "2022-10-06", uriInfo);
298
299         assertEquals(Set.of("/rests/data", "/rests/data/my-yang:data"), doc.paths().keySet());
300         final var JsonNodeMyYangData = doc.paths().get("/rests/data/my-yang:data");
301         verifyPostDataRequestRef(JsonNodeMyYangData.post(), "#/components/schemas/my-yang_data",
302             "#/components/schemas/my-yang_data");
303         verifyRequestRef(JsonNodeMyYangData.put(), "#/components/schemas/my-yang_data", CONTAINER);
304         verifyRequestRef(JsonNodeMyYangData.get(), "#/components/schemas/my-yang_data", CONTAINER);
305
306         // Test `components/schemas` objects
307         final var definitions = doc.components().schemas();
308         assertEquals(2, definitions.size());
309         assertTrue(definitions.containsKey("my-yang_data"));
310         assertTrue(definitions.containsKey("my-yang_module"));
311     }
312
313     @Test
314     public void testToaster2OpenApiObjects() {
315         final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
316
317         final var jsonNodeToaster = doc.paths().get("/rests/data/toaster2:toaster");
318         verifyRequestRef(jsonNodeToaster.post(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
319         verifyRequestRef(jsonNodeToaster.put(), "#/components/schemas/toaster2_toaster", CONTAINER);
320         verifyRequestRef(jsonNodeToaster.get(), "#/components/schemas/toaster2_toaster", CONTAINER);
321
322         final var jsonNodeToasterSlot = doc.paths().get("/rests/data/toaster2:toaster/toasterSlot={slotId}");
323         verifyRequestRef(jsonNodeToasterSlot.put(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
324         verifyRequestRef(jsonNodeToasterSlot.get(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
325
326         final var jsonNodeSlotInfo = doc.paths().get(
327             "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo");
328         verifyPostDataRequestRef(jsonNodeSlotInfo.post(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
329             "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo");
330         verifyRequestRef(jsonNodeSlotInfo.put(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
331             CONTAINER);
332         verifyRequestRef(jsonNodeSlotInfo.get(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
333             CONTAINER);
334
335         final var jsonNodeLst = doc.paths().get("/rests/data/toaster2:lst={lf1}");
336         verifyRequestRef(jsonNodeLst.put(), "#/components/schemas/toaster2_lst", LIST);
337         verifyRequestRef(jsonNodeLst.get(), "#/components/schemas/toaster2_lst", LIST);
338
339         final var jsonNodeLst1 = doc.paths().get("/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}");
340         verifyRequestRef(jsonNodeLst1.put(), "#/components/schemas/toaster2_lst_lst1", LIST);
341         verifyRequestRef(jsonNodeLst1.get(), "#/components/schemas/toaster2_lst_lst1", LIST);
342
343         final var jsonNodeMakeToast = doc.paths().get("/rests/operations/toaster2:make-toast");
344         assertNull(jsonNodeMakeToast.get());
345         verifyRequestRef(jsonNodeMakeToast.post(), "#/components/schemas/toaster2_make-toast_input", CONTAINER);
346
347         final var jsonNodeCancelToast = doc.paths().get("/rests/operations/toaster2:cancel-toast");
348         assertNull(jsonNodeCancelToast.get());
349         // Test RPC with empty input
350         final var postContent = jsonNodeCancelToast.post().requestBody().get("content");
351         final var jsonSchema = postContent.get("application/json").get("schema");
352         assertNull(jsonSchema.get("$ref"));
353         assertEquals(2, jsonSchema.size());
354         final var xmlSchema = postContent.get("application/xml").get("schema");
355         assertNull(xmlSchema.get("$ref"));
356         assertEquals(2, xmlSchema.size());
357
358         // Test `components/schemas` objects
359         final var definitions = doc.components().schemas();
360         assertEquals(18, definitions.size());
361     }
362
363     /**
364      * Test that checks if securitySchemes and security elements are present.
365      */
366     @Test
367     public void testAuthenticationFeature() {
368         final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
369
370         assertEquals("[{basicAuth=[]}]", doc.security().toString());
371         assertEquals("Http[type=http, scheme=basic, description=null, bearerFormat=null]",
372             doc.components().securitySchemes().get(BASIC_AUTH_NAME).toString());
373
374         // take list of all defined security scheme objects => all names of registered SecuritySchemeObjects
375         final var securitySchemesObjectNames = doc.components().securitySchemes().keySet();
376         assertTrue("No Security Schemes Object is defined", securitySchemesObjectNames.size() > 0);
377
378         // collect all referenced security scheme objects
379         final var referencedSecurityObjects = new HashSet<String>();
380         doc.security().forEach(map -> referencedSecurityObjects.addAll(map.keySet()));
381
382         // verify, that each reference references name of registered Security Scheme Object
383         for (final var secObjRef : referencedSecurityObjects) {
384             assertTrue(securitySchemesObjectNames.contains(secObjRef));
385         }
386     }
387
388     /**
389      *  Test JSON and XML references for request operation.
390      */
391     private static void verifyPostDataRequestRef(final Operation operation, final String expectedJsonRef,
392             final String expectedXmlRef) {
393         final JsonNode postContent;
394         if (operation.requestBody() != null) {
395             postContent = operation.requestBody().get("content");
396         } else {
397             postContent = operation.responses().get("200").get("content");
398         }
399         assertNotNull(postContent);
400         final var postJsonRef = postContent.get("application/json").get("schema").get("$ref");
401         assertNotNull(postJsonRef);
402         assertEquals(expectedJsonRef, postJsonRef.textValue());
403         final var postXmlRef = postContent.get("application/xml").get("schema").get("$ref");
404         assertNotNull(postXmlRef);
405         assertEquals(expectedXmlRef, postXmlRef.textValue());
406     }
407
408     private static void verifyRequestRef(final Operation operation, final String expectedRef, final String nodeType) {
409         final JsonNode postContent;
410         if (operation.requestBody() != null) {
411             postContent = operation.requestBody().path("content");
412         } else {
413             postContent = operation.responses().path("200").path("content");
414         }
415         assertNotNull(postContent);
416         final String postJsonRef;
417         if (nodeType.equals(CONTAINER)) {
418             postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
419                 .path("$ref").textValue();
420         } else {
421             postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
422                 .path("items").path("$ref").textValue();
423         }
424         assertNotNull(postJsonRef);
425         assertEquals(expectedRef, postJsonRef);
426         final var postXmlRef = postContent.path("application/xml").path("schema").path("$ref");
427         assertNotNull(postXmlRef);
428         assertEquals(expectedRef, postXmlRef.textValue());
429     }
430
431     private static void verifyThatOthersNodeDoesNotHaveRequiredField(final List<String> expected,
432             final Map<String, Schema> schemas) {
433         for (final var schema : schemas.entrySet()) {
434             if (expected.contains(schema.getKey())) {
435                 continue;
436             }
437             assertNull("Json node " + schema.getKey() + " should not have 'required' field in body",
438                 schema.getValue().required());
439         }
440     }
441
442     private static void verifyRequiredField(final Schema rootContainer, final List<String> expected) {
443         assertNotNull(rootContainer);
444         final var required = rootContainer.required();
445         assertNotNull(required);
446         assertEquals(expected, required);
447     }
448 }