2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.openapi.impl;
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;
19 import com.fasterxml.jackson.databind.JsonNode;
20 import java.util.List;
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;
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";
40 private static EffectiveModelContext context;
41 private static DOMSchemaService schemaService;
43 private final OpenApiGeneratorRFC8040 generator = new OpenApiGeneratorRFC8040(schemaService);
46 public static void beforeClass() {
47 schemaService = mock(DOMSchemaService.class);
48 context = YangParserTestUtils.parseYangResourceDirectory("/yang");
49 when(schemaService.getGlobalContext()).thenReturn(context);
53 * Test that paths are generated according to the model.
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);
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());
76 * Test that generated configuration paths allow to use operations: get, put, patch, delete and post.
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}");
86 final var module = context.findModule(TOASTER_2, Revision.of(REVISION_DATE)).orElseThrow();
87 final OpenApiObject doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
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());
100 * Test that generated document contains the following schemas.
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);
107 final Map<String, Schema> schemas = doc.components().schemas();
108 assertNotNull(schemas);
110 final Schema configLstTop = schemas.get("toaster2_config_lst_TOP");
111 assertNotNull(configLstTop);
112 DocGenTestHelper.containsReferences(configLstTop, "lst", "#/components/schemas/toaster2_config_lst");
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");
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");
123 final Schema configLst1 = schemas.get("toaster2_lst_config_lst1");
124 assertNotNull(configLst1);
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");
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");
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");
142 final Schema configCont11 = schemas.get("toaster2_lst_cont1_config_cont11");
143 assertNotNull(configCont11);
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");
150 final Schema configLst11 = schemas.get("toaster2_lst_cont1_config_lst11");
151 assertNotNull(configLst11);
155 * Test that generated document contains RPC schemas for "make-toast" with correct input.
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);
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"));
175 public void testChoice() {
176 final var module = context.findModule("choice-test").orElseThrow();
177 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
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"));
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"));
192 public void testMandatory() {
193 final var module = context.findModule(MANDATORY_TEST).orElseThrow();
194 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
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);
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);
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"]
217 * Test that checks for correct amount of parameters in requests.
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);
226 final var module = context.findModule("recursive", Revision.of("2023-05-22")).orElseThrow();
227 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
230 final var paths = doc.paths();
231 assertEquals(5, paths.size());
233 for (final var expectedPath : configPaths.entrySet()) {
234 assertTrue(paths.containsKey(expectedPath.getKey()));
235 final int expectedSize = expectedPath.getValue();
237 final var path = paths.get(expectedPath.getKey());
239 final var get = path.get();
241 assertEquals(expectedSize + 1, get.parameters().size());
243 final var put = path.put();
245 assertEquals(expectedSize, put.parameters().size());
247 final var delete = path.delete();
248 assertNotNull(delete);
249 assertEquals(expectedSize, delete.parameters().size());
251 final var post = path.post();
253 assertEquals(expectedSize, post.parameters().size());
255 final var patch = path.patch();
256 assertNotNull(patch);
257 assertEquals(expectedSize, patch.parameters().size());
262 * Test that request parameters are correctly numbered.
265 * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
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);
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));
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));
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));
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));
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));
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);
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");
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"));
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);
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");
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");
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");
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");
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");
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");
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());
381 // Test `components/schemas` objects
382 final var definitions = doc.components().schemas();
383 assertEquals(44, definitions.size());
387 * Test JSON and XML references for request operation.
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");
395 postContent = operation.responses().get("200").get("content");
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());
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());