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