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