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.ArrayList;
21 import java.util.List;
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;
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";
50 private static EffectiveModelContext context;
51 private static DOMSchemaService schemaService;
53 private final OpenApiGeneratorRFC8040 generator = new OpenApiGeneratorRFC8040(schemaService);
56 public static void beforeClass() {
57 schemaService = mock(DOMSchemaService.class);
58 context = YangParserTestUtils.parseYangResourceDirectory("/yang");
59 when(schemaService.getGlobalContext()).thenReturn(context);
63 * Test that paths are generated according to the model.
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);
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());
86 * Test that generated configuration paths allow to use operations: get, put, patch, delete and post.
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}");
96 final var module = context.findModule(TOASTER_2, Revision.of(REVISION_DATE)).orElseThrow();
97 final OpenApiObject doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
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());
110 * Test that generated document contains the following schemas.
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);
117 final Map<String, Schema> schemas = doc.components().schemas();
118 assertNotNull(schemas);
120 final Schema configLstTop = schemas.get("toaster2_config_lst_TOP");
121 assertNotNull(configLstTop);
122 DocGenTestHelper.containsReferences(configLstTop, "lst", "#/components/schemas/toaster2_config_lst");
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");
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");
133 final Schema configLst1 = schemas.get("toaster2_lst_config_lst1");
134 assertNotNull(configLst1);
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");
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");
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");
152 final Schema configCont11 = schemas.get("toaster2_lst_cont1_config_cont11");
153 assertNotNull(configCont11);
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");
160 final Schema configLst11 = schemas.get("toaster2_lst_cont1_config_lst11");
161 assertNotNull(configLst11);
165 * Test that generated document contains RPC schemas for "make-toast" with correct input.
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);
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"));
185 public void testChoice() {
186 final var module = context.findModule("choice-test").orElseThrow();
187 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
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"));
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"));
202 public void testMandatory() {
203 final var module = context.findModule(MANDATORY_TEST).orElseThrow();
204 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
206 final var schemas = doc.components().schemas();
207 final var containersWithRequired = new ArrayList<String>();
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);
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);
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);
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);
232 verifyThatOthersNodeDoesNotHaveRequiredField(containersWithRequired, schemas);
236 * Test that checks for correct amount of parameters in requests.
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);
245 final var module = context.findModule("recursive", Revision.of("2023-05-22")).orElseThrow();
246 final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
249 final var paths = doc.paths();
250 assertEquals(5, paths.size());
252 for (final var expectedPath : configPaths.entrySet()) {
253 assertTrue(paths.containsKey(expectedPath.getKey()));
254 final int expectedSize = expectedPath.getValue();
256 final var path = paths.get(expectedPath.getKey());
258 final var get = path.get();
260 assertEquals(expectedSize + 1, get.parameters().size());
262 final var put = path.put();
264 assertEquals(expectedSize, put.parameters().size());
266 final var delete = path.delete();
267 assertNotNull(delete);
268 assertEquals(expectedSize, delete.parameters().size());
270 final var post = path.post();
272 assertEquals(expectedSize, post.parameters().size());
274 final var patch = path.patch();
275 assertNotNull(patch);
276 assertEquals(expectedSize, patch.parameters().size());
281 * Test that request parameters are correctly numbered.
284 * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
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);
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));
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));
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));
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));
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));
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);
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");
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"));
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);
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");
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");
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");
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");
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");
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");
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());
400 // Test `components/schemas` objects
401 final var definitions = doc.components().schemas();
402 assertEquals(44, definitions.size());
406 * Test JSON and XML references for request operation.
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");
414 postContent = operation.responses().get("200").get("content");
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());
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);
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()) {
445 assertNull("Json node " + nodeName + " should not have 'required' field in body",
446 jsonNode.get("required"));
447 verifyRecursivelyThatPropertyDoesNotHaveRequired(expected, jsonNode);
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);