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.getPathGetParameters;
18 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathPostParameters;
19 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.BASIC_AUTH_NAME;
20 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.COMPONENTS_PREFIX;
22 import java.util.ArrayList;
23 import java.util.HashSet;
24 import java.util.List;
27 import javax.ws.rs.core.UriInfo;
28 import org.junit.BeforeClass;
29 import org.junit.Test;
30 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
31 import org.opendaylight.restconf.openapi.DocGenTestHelper;
32 import org.opendaylight.restconf.openapi.model.MediaTypeObject;
33 import org.opendaylight.restconf.openapi.model.OpenApiObject;
34 import org.opendaylight.restconf.openapi.model.Operation;
35 import org.opendaylight.restconf.openapi.model.Path;
36 import org.opendaylight.restconf.openapi.model.Property;
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;
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";
52 private static DOMSchemaService schemaService;
53 private static UriInfo uriInfo;
55 private final OpenApiGeneratorRFC8040 generator = new OpenApiGeneratorRFC8040(schemaService);
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");
66 * Test that paths are generated according to the model.
69 public void testPaths() {
70 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
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());
88 * Test that generated configuration paths allow to use operations: get, put, patch, delete and post.
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");
100 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
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());
110 for (final String path : configPathsForPost) {
111 final Path node = doc.paths().get(path);
112 assertNotNull(node.post());
117 * Test that generated document contains the following schemas.
120 public void testSchemas() {
121 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
123 final Map<String, Schema> schemas = doc.components().schemas();
124 assertNotNull(schemas);
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");
131 final Schema configLst1 = schemas.get("toaster2_lst_lst1");
132 assertNotNull(configLst1);
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");
139 final Schema configCont11 = schemas.get("toaster2_lst_cont1_cont11");
140 assertNotNull(configCont11);
142 final Schema configLst11 = schemas.get("toaster2_lst_cont1_lst11");
143 assertNotNull(configLst11);
147 * Test that reference to schema in each path is valid (all referenced schemas exist).
150 public void testSchemasExistenceSingleModule() {
151 final var document = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
152 assertNotNull(document);
153 final var referencedSchemas = new HashSet<String>();
154 for (final var path : document.paths().values()) {
155 referencedSchemas.addAll(extractSchemaRefFromPath(path));
157 final var schemaNames = document.components().schemas().keySet();
158 for (final var ref : referencedSchemas) {
159 assertTrue("Referenced schema " + ref + " does not exist", schemaNames.contains(ref));
164 * Test that generated document contains RPC schemas for "make-toast" with correct input.
167 public void testRPC() {
168 final OpenApiObject doc = generator.getApiDeclaration("toaster", "2009-11-20", uriInfo);
171 final Map<String, Schema> schemas = doc.components().schemas();
172 final Schema input = schemas.get("toaster_make-toast_input");
173 final Map<String, Property> properties = input.properties();
174 assertTrue(properties.containsKey("toasterDoneness"));
175 assertTrue(properties.containsKey("toasterToastType"));
179 public void testChoice() {
180 final var doc = generator.getApiDeclaration("choice-test", null, uriInfo);
183 final var schemas = doc.components().schemas();
184 final var firstContainer = schemas.get("choice-test_first-container");
185 assertEquals("default-value", firstContainer.properties().get("leaf-default").defaultValue().toString());
186 assertFalse(firstContainer.properties().containsKey("leaf-non-default"));
188 final var secondContainer = schemas.get("choice-test_second-container");
189 assertTrue(secondContainer.properties().containsKey("leaf-first-case"));
190 assertFalse(secondContainer.properties().containsKey("leaf-second-case"));
194 public void testMandatory() {
195 final var doc = generator.getApiDeclaration(MANDATORY_TEST, null, uriInfo);
197 final var schemas = doc.components().schemas();
198 final var containersWithRequired = new ArrayList<String>();
200 final var reqRootContainerElements = List.of("mandatory-root-leaf", "mandatory-container",
201 "mandatory-first-choice", "mandatory-list");
202 verifyRequiredField(schemas.get(CONFIG_ROOT_CONTAINER), reqRootContainerElements);
203 containersWithRequired.add(CONFIG_ROOT_CONTAINER);
205 final var reqMandatoryContainerElements = List.of("mandatory-leaf", "leaf-list-with-min-elements");
206 verifyRequiredField(schemas.get(CONFIG_MANDATORY_CONTAINER), reqMandatoryContainerElements);
207 containersWithRequired.add(CONFIG_MANDATORY_CONTAINER);
209 final var reqMandatoryListElements = List.of("mandatory-list-field");
210 verifyRequiredField(schemas.get(CONFIG_MANDATORY_LIST), reqMandatoryListElements);
211 containersWithRequired.add(CONFIG_MANDATORY_LIST);
213 final var testModuleMandatoryArray = List.of("root-container", "root-mandatory-list");
214 verifyRequiredField(schemas.get(MANDATORY_TEST_MODULE), testModuleMandatoryArray);
215 containersWithRequired.add(MANDATORY_TEST_MODULE);
217 verifyThatOthersNodeDoesNotHaveRequiredField(containersWithRequired, schemas);
221 * Test that checks for correct amount of parameters in requests.
224 public void testRecursiveParameters() {
225 final var configPaths = Map.of("/rests/data/recursive:container-root", 0,
226 "/rests/data/recursive:container-root/root-list={name}", 1,
227 "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}", 2,
228 "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}/super-nested-list={name2}", 3);
230 final var doc = generator.getApiDeclaration("recursive", "2023-05-22", uriInfo);
233 final var paths = doc.paths();
234 assertEquals(5, paths.size());
236 for (final var expectedPath : configPaths.entrySet()) {
237 assertTrue(paths.containsKey(expectedPath.getKey()));
238 final int expectedSize = expectedPath.getValue();
240 final var path = paths.get(expectedPath.getKey());
242 final var get = path.get();
244 assertEquals(expectedSize + 1, get.parameters().size());
246 final var put = path.put();
248 assertEquals(expectedSize, put.parameters().size());
250 final var delete = path.delete();
251 assertNotNull(delete);
252 assertEquals(expectedSize, delete.parameters().size());
254 final var patch = path.patch();
255 assertNotNull(patch);
256 assertEquals(expectedSize, patch.parameters().size());
259 // we do not generate POST for lists
260 final var path = paths.get("/rests/data/recursive:container-root");
261 final var post = path.post();
262 final int expectedSize = configPaths.get("/rests/data/recursive:container-root");
263 assertEquals(expectedSize, post.parameters().size());
267 * Test that request parameters are correctly numbered.
270 * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
273 public void testParametersNumbering() {
274 final var doc = generator.getApiDeclaration("path-params-test", null, uriInfo);
276 var pathToList1 = "/rests/data/path-params-test:cont/list1={name}";
277 assertTrue(doc.paths().containsKey(pathToList1));
278 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList1));
280 var pathToList2 = "/rests/data/path-params-test:cont/list1={name}/list2={name1}";
281 assertTrue(doc.paths().containsKey(pathToList2));
282 assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList2));
284 var pathToList3 = "/rests/data/path-params-test:cont/list3={name}";
285 assertTrue(doc.paths().containsKey(pathToList3));
286 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList3));
288 var pathToList4 = "/rests/data/path-params-test:cont/list1={name}/list4={name1}";
289 assertTrue(doc.paths().containsKey(pathToList4));
290 assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList4));
292 var pathToList5 = "/rests/data/path-params-test:cont/list1={name}/cont2";
293 assertTrue(doc.paths().containsKey(pathToList4));
294 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList5));
298 * Test that request for actions is correct and has parameters.
301 public void testActionPathsParams() {
302 final var doc = generator.getApiDeclaration("action-types", null, uriInfo);
304 final var pathWithParameters = "/rests/operations/action-types:list={name}/list-action";
305 assertTrue(doc.paths().containsKey(pathWithParameters));
306 assertEquals(List.of("name"), getPathPostParameters(doc.paths(), pathWithParameters));
308 final var pathWithoutParameters = "/rests/operations/action-types:multi-container/inner-container/action";
309 assertTrue(doc.paths().containsKey(pathWithoutParameters));
310 assertEquals(List.of(), getPathPostParameters(doc.paths(), pathWithoutParameters));
314 public void testSimpleOpenApiObjects() {
315 final var doc = generator.getApiDeclaration("my-yang", "2022-10-06", uriInfo);
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 verifyPostDataRequestRef(JsonNodeMyYangData.post(), "#/components/schemas/my-yang_data",
320 "#/components/schemas/my-yang_data");
321 verifyRequestRef(JsonNodeMyYangData.put(), "#/components/schemas/my-yang_data", CONTAINER);
322 verifyRequestRef(JsonNodeMyYangData.get(), "#/components/schemas/my-yang_data", CONTAINER);
324 // Test `components/schemas` objects
325 final var definitions = doc.components().schemas();
326 assertEquals(2, definitions.size());
327 assertTrue(definitions.containsKey("my-yang_data"));
328 assertTrue(definitions.containsKey("my-yang_module"));
332 public void testToaster2OpenApiObjects() {
333 final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
335 final var jsonNodeToaster = doc.paths().get("/rests/data/toaster2:toaster");
336 verifyRequestRef(jsonNodeToaster.post(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
337 verifyRequestRef(jsonNodeToaster.put(), "#/components/schemas/toaster2_toaster", CONTAINER);
338 verifyRequestRef(jsonNodeToaster.get(), "#/components/schemas/toaster2_toaster", CONTAINER);
340 final var jsonNodeToasterSlot = doc.paths().get("/rests/data/toaster2:toaster/toasterSlot={slotId}");
341 verifyRequestRef(jsonNodeToasterSlot.put(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
342 verifyRequestRef(jsonNodeToasterSlot.get(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
344 final var jsonNodeSlotInfo = doc.paths().get(
345 "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo");
346 verifyPostDataRequestRef(jsonNodeSlotInfo.post(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
347 "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo");
348 verifyRequestRef(jsonNodeSlotInfo.put(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
350 verifyRequestRef(jsonNodeSlotInfo.get(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
353 final var jsonNodeLst = doc.paths().get("/rests/data/toaster2:lst={lf1}");
354 verifyRequestRef(jsonNodeLst.put(), "#/components/schemas/toaster2_lst", LIST);
355 verifyRequestRef(jsonNodeLst.get(), "#/components/schemas/toaster2_lst", LIST);
357 final var jsonNodeLst1 = doc.paths().get("/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}");
358 verifyRequestRef(jsonNodeLst1.put(), "#/components/schemas/toaster2_lst_lst1", LIST);
359 verifyRequestRef(jsonNodeLst1.get(), "#/components/schemas/toaster2_lst_lst1", LIST);
361 final var jsonNodeMakeToast = doc.paths().get("/rests/operations/toaster2:make-toast");
362 assertNull(jsonNodeMakeToast.get());
363 verifyRequestRef(jsonNodeMakeToast.post(), "#/components/schemas/toaster2_make-toast_input", CONTAINER);
365 final var jsonNodeCancelToast = doc.paths().get("/rests/operations/toaster2:cancel-toast");
366 assertNull(jsonNodeCancelToast.get());
367 // Test RPC with empty input
368 final var postContent = jsonNodeCancelToast.post().requestBody().content();
369 final var jsonSchema = postContent.get("application/json").schema();
370 assertNull(jsonSchema.ref());
371 final var xmlSchema = postContent.get("application/xml").schema();
372 assertNull(xmlSchema.ref());
374 // Test `components/schemas` objects
375 final var definitions = doc.components().schemas();
376 assertEquals(18, definitions.size());
380 * Test that checks if securitySchemes and security elements are present.
383 public void testAuthenticationFeature() {
384 final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
386 assertEquals("[{basicAuth=[]}]", doc.security().toString());
387 assertEquals("Http[type=http, scheme=basic, description=null, bearerFormat=null]",
388 doc.components().securitySchemes().get(BASIC_AUTH_NAME).toString());
390 // take list of all defined security scheme objects => all names of registered SecuritySchemeObjects
391 final var securitySchemesObjectNames = doc.components().securitySchemes().keySet();
392 assertTrue("No Security Schemes Object is defined", securitySchemesObjectNames.size() > 0);
394 // collect all referenced security scheme objects
395 final var referencedSecurityObjects = new HashSet<String>();
396 doc.security().forEach(map -> referencedSecurityObjects.addAll(map.keySet()));
398 // verify, that each reference references name of registered Security Scheme Object
399 for (final var secObjRef : referencedSecurityObjects) {
400 assertTrue(securitySchemesObjectNames.contains(secObjRef));
405 * Test that checks if namespace for rpc is present.
408 public void testRpcNamespace() {
409 final var doc = generator.getApiDeclaration("toaster", "2009-11-20", uriInfo);
410 assertNotNull("Failed to find Datastore API", doc);
411 final var paths = doc.paths();
412 final var path = paths.get("/rests/operations/toaster:cancel-toast");
414 final var content = path.post().requestBody().content().get("application/xml");
415 assertNotNull(content);
416 final var schema = content.schema();
417 assertNotNull(schema);
418 final var xml = schema.xml();
420 final var namespace = xml.namespace();
421 assertNotNull(namespace);
422 assertEquals("http://netconfcentral.org/ns/toaster", namespace);
426 * Test that checks if namespace for actions is present.
429 public void testActionsNamespace() {
430 final var doc = generator.getApiDeclaration("action-types", null, uriInfo);
431 assertNotNull("Failed to find Datastore API", doc);
432 final var paths = doc.paths();
433 final var path = paths.get("/rests/operations/action-types:multi-container/inner-container/action");
435 final var content = path.post().requestBody().content().get("application/xml");
436 assertNotNull(content);
437 final var schema = content.schema();
438 assertNotNull(schema);
439 final var xml = schema.xml();
441 final var namespace = xml.namespace();
442 assertNotNull(namespace);
443 assertEquals("urn:ietf:params:xml:ns:yang:test:action:types", namespace);
447 * Test JSON and XML references for request operation.
449 private static void verifyPostDataRequestRef(final Operation operation, final String expectedJsonRef,
450 final String expectedXmlRef) {
451 final Map<String, MediaTypeObject> postContent;
452 if (operation.requestBody() != null) {
453 postContent = operation.requestBody().content();
455 postContent = operation.responses().get("200").content();
457 assertNotNull(postContent);
458 final var postJsonRef = postContent.get("application/json").schema().ref();
459 assertNotNull(postJsonRef);
460 assertEquals(expectedJsonRef, postJsonRef);
461 final var postXmlRef = postContent.get("application/xml").schema().ref();
462 assertNotNull(postXmlRef);
463 assertEquals(expectedXmlRef, postXmlRef);
466 private static void verifyRequestRef(final Operation operation, final String expectedRef, final String nodeType) {
467 final Map<String, MediaTypeObject> postContent;
468 if (operation.requestBody() != null) {
469 postContent = operation.requestBody().content();
471 postContent = operation.responses().get("200").content();
473 assertNotNull(postContent);
474 final String postJsonRef;
475 if (nodeType.equals(CONTAINER)) {
476 postJsonRef = postContent.get("application/json").schema().properties().values().iterator().next().ref();
478 postJsonRef = postContent.get("application/json").schema().properties().values().iterator().next().items()
481 assertNotNull(postJsonRef);
482 assertEquals(expectedRef, postJsonRef);
483 final var postXmlRef = postContent.get("application/xml").schema().ref();
484 assertNotNull(postXmlRef);
485 assertEquals(expectedRef, postXmlRef);
488 private static void verifyThatOthersNodeDoesNotHaveRequiredField(final List<String> expected,
489 final Map<String, Schema> schemas) {
490 for (final var schema : schemas.entrySet()) {
491 if (expected.contains(schema.getKey())) {
494 assertNull("Json node " + schema.getKey() + " should not have 'required' field in body",
495 schema.getValue().required());
499 private static void verifyRequiredField(final Schema rootContainer, final List<String> expected) {
500 assertNotNull(rootContainer);
501 final var required = rootContainer.required();
502 assertNotNull(required);
503 assertEquals(expected, required);
506 private static Set<String> extractSchemaRefFromPath(final Path path) {
510 final var references = new HashSet<String>();
511 final var get = path.get();
513 references.addAll(schemaRefFromContent(get.responses().get("200").content()));
515 final var post = path.post();
517 references.addAll(schemaRefFromContent(post.requestBody().content()));
519 final var put = path.put();
521 references.addAll(schemaRefFromContent(put.requestBody().content()));
523 final var patch = path.patch();
525 references.addAll(schemaRefFromContent(patch.requestBody().content()));
531 * The schema node does not have 1 specific structure and the "$ref" child is not always the first child after
532 * schema. Possible schema structures include:
534 * <li>schema/$ref/{reference}</li>
535 * <li>schema/properties/{nodeName}/$ref/{reference}</li>
536 * <li>schema/properties/{nodeName}/items/$ref/{reference}</li>
538 * @param content the element identified with key "content"
539 * @return the set of referenced schemas
541 private static Set<String> schemaRefFromContent(final Map<String, MediaTypeObject> content) {
542 final HashSet<String> refs = new HashSet<>();
543 content.values().forEach(mediaType -> {
544 final var schema = mediaType.schema();
545 final var props = mediaType.schema().properties();
548 // either there is no node with the key "properties", try to find immediate child of schema
550 } else if (props.values().iterator().next().items() == null) {
551 // or the "properties" is defined and under that we didn't find the "items" node
552 // try to get "$ref" as immediate child under properties
553 ref = props.values().iterator().next().ref();
555 // or the "items" node is defined, in which case we try to get the "$ref" from this node
556 ref = props.values().iterator().next().items().ref();
560 refs.add(ref.replaceFirst(COMPONENTS_PREFIX, ""));