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