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;
21 import com.fasterxml.jackson.databind.JsonNode;
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.OpenApiObject;
33 import org.opendaylight.restconf.openapi.model.Operation;
34 import org.opendaylight.restconf.openapi.model.Path;
35 import org.opendaylight.restconf.openapi.model.Property;
36 import org.opendaylight.restconf.openapi.model.Schema;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
40 public final class OpenApiGeneratorRFC8040Test {
41 private static final String TOASTER_2 = "toaster2";
42 private static final String REVISION_DATE = "2009-11-20";
43 private static final String MANDATORY_TEST = "mandatory-test";
44 private static final String CONFIG_ROOT_CONTAINER = "mandatory-test_root-container";
45 private static final String CONFIG_MANDATORY_CONTAINER = "mandatory-test_root-container_mandatory-container";
46 private static final String CONFIG_MANDATORY_LIST = "mandatory-test_root-container_mandatory-list";
47 private static final String MANDATORY_TEST_MODULE = "mandatory-test_module";
48 private static final String CONTAINER = "container";
49 private static final String LIST = "list";
51 private static DOMSchemaService schemaService;
52 private static UriInfo uriInfo;
54 private final OpenApiGeneratorRFC8040 generator = new OpenApiGeneratorRFC8040(schemaService);
57 public static void beforeClass() throws Exception {
58 schemaService = mock(DOMSchemaService.class);
59 final EffectiveModelContext context = YangParserTestUtils.parseYangResourceDirectory("/yang");
60 when(schemaService.getGlobalContext()).thenReturn(context);
61 uriInfo = DocGenTestHelper.createMockUriInfo("http://localhost/path");
65 * Test that paths are generated according to the model.
68 public void testPaths() {
69 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
71 assertEquals(Set.of("/rests/data",
72 "/rests/data/toaster2:toaster",
73 "/rests/data/toaster2:toaster/toasterSlot={slotId}",
74 "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo",
75 "/rests/data/toaster2:lst={lf1}",
76 "/rests/data/toaster2:lst={lf1}/cont1",
77 "/rests/data/toaster2:lst={lf1}/cont1/cont11",
78 "/rests/data/toaster2:lst={lf1}/cont1/lst11={lf111}",
79 "/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}",
80 "/rests/operations/toaster2:make-toast",
81 "/rests/operations/toaster2:cancel-toast",
82 "/rests/operations/toaster2:restock-toaster"),
83 doc.paths().keySet());
87 * Test that generated configuration paths allow to use operations: get, put, patch, delete and post.
90 public void testConfigPaths() {
91 final List<String> configPaths = List.of("/rests/data/toaster2:lst={lf1}",
92 "/rests/data/toaster2:lst={lf1}/cont1",
93 "/rests/data/toaster2:lst={lf1}/cont1/cont11",
94 "/rests/data/toaster2:lst={lf1}/cont1/lst11={lf111}",
95 "/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}");
96 final List<String> configPathsForPost = List.of("/rests/data/toaster2:lst={lf1}/cont1",
97 "/rests/data/toaster2:lst={lf1}/cont1/cont11");
99 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
101 for (final String path : configPaths) {
102 final Path node = doc.paths().get(path);
103 assertNotNull(node.get());
104 assertNotNull(node.put());
105 assertNotNull(node.delete());
106 assertNotNull(node.patch());
109 for (final String path : configPathsForPost) {
110 final Path node = doc.paths().get(path);
111 assertNotNull(node.post());
116 * Test that generated document contains the following schemas.
119 public void testSchemas() {
120 final OpenApiObject doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
122 final Map<String, Schema> schemas = doc.components().schemas();
123 assertNotNull(schemas);
125 final Schema configLst = schemas.get("toaster2_lst");
126 assertNotNull(configLst);
127 DocGenTestHelper.containsReferences(configLst, "lst1", "#/components/schemas/toaster2_lst_lst1");
128 DocGenTestHelper.containsReferences(configLst, "cont1", "#/components/schemas/toaster2_lst_cont1");
130 final Schema configLst1 = schemas.get("toaster2_lst_lst1");
131 assertNotNull(configLst1);
133 final Schema configCont1 = schemas.get("toaster2_lst_cont1");
134 assertNotNull(configCont1);
135 DocGenTestHelper.containsReferences(configCont1, "cont11", "#/components/schemas/toaster2_lst_cont1_cont11");
136 DocGenTestHelper.containsReferences(configCont1, "lst11", "#/components/schemas/toaster2_lst_cont1_lst11");
138 final Schema configCont11 = schemas.get("toaster2_lst_cont1_cont11");
139 assertNotNull(configCont11);
141 final Schema configLst11 = schemas.get("toaster2_lst_cont1_lst11");
142 assertNotNull(configLst11);
146 * Test that generated document contains RPC schemas for "make-toast" with correct input.
149 public void testRPC() {
150 final OpenApiObject doc = generator.getApiDeclaration("toaster", "2009-11-20", uriInfo);
153 final Map<String, Schema> schemas = doc.components().schemas();
154 final Schema input = schemas.get("toaster_make-toast_input");
155 final Map<String, Property> properties = input.properties();
156 assertTrue(properties.containsKey("toasterDoneness"));
157 assertTrue(properties.containsKey("toasterToastType"));
161 public void testChoice() {
162 final var doc = generator.getApiDeclaration("choice-test", null, uriInfo);
165 final var schemas = doc.components().schemas();
166 final var firstContainer = schemas.get("choice-test_first-container");
167 assertEquals("default-value", firstContainer.properties().get("leaf-default").defaultValue().toString());
168 assertFalse(firstContainer.properties().containsKey("leaf-non-default"));
170 final var secondContainer = schemas.get("choice-test_second-container");
171 assertTrue(secondContainer.properties().containsKey("leaf-first-case"));
172 assertFalse(secondContainer.properties().containsKey("leaf-second-case"));
176 public void testMandatory() {
177 final var doc = generator.getApiDeclaration(MANDATORY_TEST, null, uriInfo);
179 final var schemas = doc.components().schemas();
180 final var containersWithRequired = new ArrayList<String>();
182 final var reqRootContainerElements = List.of("mandatory-root-leaf", "mandatory-container",
183 "mandatory-first-choice", "mandatory-list");
184 verifyRequiredField(schemas.get(CONFIG_ROOT_CONTAINER), reqRootContainerElements);
185 containersWithRequired.add(CONFIG_ROOT_CONTAINER);
187 final var reqMandatoryContainerElements = List.of("mandatory-leaf", "leaf-list-with-min-elements");
188 verifyRequiredField(schemas.get(CONFIG_MANDATORY_CONTAINER), reqMandatoryContainerElements);
189 containersWithRequired.add(CONFIG_MANDATORY_CONTAINER);
191 final var reqMandatoryListElements = List.of("mandatory-list-field");
192 verifyRequiredField(schemas.get(CONFIG_MANDATORY_LIST), reqMandatoryListElements);
193 containersWithRequired.add(CONFIG_MANDATORY_LIST);
195 final var testModuleMandatoryArray = List.of("root-container", "root-mandatory-list");
196 verifyRequiredField(schemas.get(MANDATORY_TEST_MODULE), testModuleMandatoryArray);
197 containersWithRequired.add(MANDATORY_TEST_MODULE);
199 verifyThatOthersNodeDoesNotHaveRequiredField(containersWithRequired, schemas);
203 * Test that checks for correct amount of parameters in requests.
206 public void testRecursiveParameters() {
207 final var configPaths = Map.of("/rests/data/recursive:container-root", 0,
208 "/rests/data/recursive:container-root/root-list={name}", 1,
209 "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}", 2,
210 "/rests/data/recursive:container-root/root-list={name}/nested-list={name1}/super-nested-list={name2}", 3);
212 final var doc = generator.getApiDeclaration("recursive", "2023-05-22", uriInfo);
215 final var paths = doc.paths();
216 assertEquals(5, paths.size());
218 for (final var expectedPath : configPaths.entrySet()) {
219 assertTrue(paths.containsKey(expectedPath.getKey()));
220 final int expectedSize = expectedPath.getValue();
222 final var path = paths.get(expectedPath.getKey());
224 final var get = path.get();
226 assertEquals(expectedSize + 1, get.parameters().size());
228 final var put = path.put();
230 assertEquals(expectedSize, put.parameters().size());
232 final var delete = path.delete();
233 assertNotNull(delete);
234 assertEquals(expectedSize, delete.parameters().size());
236 final var patch = path.patch();
237 assertNotNull(patch);
238 assertEquals(expectedSize, patch.parameters().size());
241 // we do not generate POST for lists
242 final var path = paths.get("/rests/data/recursive:container-root");
243 final var post = path.post();
244 final int expectedSize = configPaths.get("/rests/data/recursive:container-root");
245 assertEquals(expectedSize, post.parameters().size());
249 * Test that request parameters are correctly numbered.
252 * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
255 public void testParametersNumbering() {
256 final var doc = generator.getApiDeclaration("path-params-test", null, uriInfo);
258 var pathToList1 = "/rests/data/path-params-test:cont/list1={name}";
259 assertTrue(doc.paths().containsKey(pathToList1));
260 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList1));
262 var pathToList2 = "/rests/data/path-params-test:cont/list1={name}/list2={name1}";
263 assertTrue(doc.paths().containsKey(pathToList2));
264 assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList2));
266 var pathToList3 = "/rests/data/path-params-test:cont/list3={name}";
267 assertTrue(doc.paths().containsKey(pathToList3));
268 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList3));
270 var pathToList4 = "/rests/data/path-params-test:cont/list1={name}/list4={name1}";
271 assertTrue(doc.paths().containsKey(pathToList4));
272 assertEquals(List.of("name", "name1"), getPathGetParameters(doc.paths(), pathToList4));
274 var pathToList5 = "/rests/data/path-params-test:cont/list1={name}/cont2";
275 assertTrue(doc.paths().containsKey(pathToList4));
276 assertEquals(List.of("name"), getPathGetParameters(doc.paths(), pathToList5));
280 * Test that request for actions is correct and has parameters.
283 public void testActionPathsParams() {
284 final var doc = generator.getApiDeclaration("action-types", null, uriInfo);
286 final var pathWithParameters = "/rests/operations/action-types:list={name}/list-action";
287 assertTrue(doc.paths().containsKey(pathWithParameters));
288 assertEquals(List.of("name"), getPathPostParameters(doc.paths(), pathWithParameters));
290 final var pathWithoutParameters = "/rests/operations/action-types:multi-container/inner-container/action";
291 assertTrue(doc.paths().containsKey(pathWithoutParameters));
292 assertEquals(List.of(), getPathPostParameters(doc.paths(), pathWithoutParameters));
296 public void testSimpleOpenApiObjects() {
297 final var doc = generator.getApiDeclaration("my-yang", "2022-10-06", uriInfo);
299 assertEquals(Set.of("/rests/data", "/rests/data/my-yang:data"), doc.paths().keySet());
300 final var JsonNodeMyYangData = doc.paths().get("/rests/data/my-yang:data");
301 verifyPostDataRequestRef(JsonNodeMyYangData.post(), "#/components/schemas/my-yang_data",
302 "#/components/schemas/my-yang_data");
303 verifyRequestRef(JsonNodeMyYangData.put(), "#/components/schemas/my-yang_data", CONTAINER);
304 verifyRequestRef(JsonNodeMyYangData.get(), "#/components/schemas/my-yang_data", CONTAINER);
306 // Test `components/schemas` objects
307 final var definitions = doc.components().schemas();
308 assertEquals(2, definitions.size());
309 assertTrue(definitions.containsKey("my-yang_data"));
310 assertTrue(definitions.containsKey("my-yang_module"));
314 public void testToaster2OpenApiObjects() {
315 final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
317 final var jsonNodeToaster = doc.paths().get("/rests/data/toaster2:toaster");
318 verifyRequestRef(jsonNodeToaster.post(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
319 verifyRequestRef(jsonNodeToaster.put(), "#/components/schemas/toaster2_toaster", CONTAINER);
320 verifyRequestRef(jsonNodeToaster.get(), "#/components/schemas/toaster2_toaster", CONTAINER);
322 final var jsonNodeToasterSlot = doc.paths().get("/rests/data/toaster2:toaster/toasterSlot={slotId}");
323 verifyRequestRef(jsonNodeToasterSlot.put(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
324 verifyRequestRef(jsonNodeToasterSlot.get(), "#/components/schemas/toaster2_toaster_toasterSlot", LIST);
326 final var jsonNodeSlotInfo = doc.paths().get(
327 "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo");
328 verifyPostDataRequestRef(jsonNodeSlotInfo.post(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
329 "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo");
330 verifyRequestRef(jsonNodeSlotInfo.put(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
332 verifyRequestRef(jsonNodeSlotInfo.get(), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo",
335 final var jsonNodeLst = doc.paths().get("/rests/data/toaster2:lst={lf1}");
336 verifyRequestRef(jsonNodeLst.put(), "#/components/schemas/toaster2_lst", LIST);
337 verifyRequestRef(jsonNodeLst.get(), "#/components/schemas/toaster2_lst", LIST);
339 final var jsonNodeLst1 = doc.paths().get("/rests/data/toaster2:lst={lf1}/lst1={key1},{key2}");
340 verifyRequestRef(jsonNodeLst1.put(), "#/components/schemas/toaster2_lst_lst1", LIST);
341 verifyRequestRef(jsonNodeLst1.get(), "#/components/schemas/toaster2_lst_lst1", LIST);
343 final var jsonNodeMakeToast = doc.paths().get("/rests/operations/toaster2:make-toast");
344 assertNull(jsonNodeMakeToast.get());
345 verifyRequestRef(jsonNodeMakeToast.post(), "#/components/schemas/toaster2_make-toast_input", CONTAINER);
347 final var jsonNodeCancelToast = doc.paths().get("/rests/operations/toaster2:cancel-toast");
348 assertNull(jsonNodeCancelToast.get());
349 // Test RPC with empty input
350 final var postContent = jsonNodeCancelToast.post().requestBody().get("content");
351 final var jsonSchema = postContent.get("application/json").get("schema");
352 assertNull(jsonSchema.get("$ref"));
353 assertEquals(2, jsonSchema.size());
354 final var xmlSchema = postContent.get("application/xml").get("schema");
355 assertNull(xmlSchema.get("$ref"));
356 assertEquals(2, xmlSchema.size());
358 // Test `components/schemas` objects
359 final var definitions = doc.components().schemas();
360 assertEquals(18, definitions.size());
364 * Test that checks if securitySchemes and security elements are present.
367 public void testAuthenticationFeature() {
368 final var doc = generator.getApiDeclaration(TOASTER_2, REVISION_DATE, uriInfo);
370 assertEquals("[{basicAuth=[]}]", doc.security().toString());
371 assertEquals("Http[type=http, scheme=basic, description=null, bearerFormat=null]",
372 doc.components().securitySchemes().get(BASIC_AUTH_NAME).toString());
374 // take list of all defined security scheme objects => all names of registered SecuritySchemeObjects
375 final var securitySchemesObjectNames = doc.components().securitySchemes().keySet();
376 assertTrue("No Security Schemes Object is defined", securitySchemesObjectNames.size() > 0);
378 // collect all referenced security scheme objects
379 final var referencedSecurityObjects = new HashSet<String>();
380 doc.security().forEach(map -> referencedSecurityObjects.addAll(map.keySet()));
382 // verify, that each reference references name of registered Security Scheme Object
383 for (final var secObjRef : referencedSecurityObjects) {
384 assertTrue(securitySchemesObjectNames.contains(secObjRef));
389 * Test JSON and XML references for request operation.
391 private static void verifyPostDataRequestRef(final Operation operation, final String expectedJsonRef,
392 final String expectedXmlRef) {
393 final JsonNode postContent;
394 if (operation.requestBody() != null) {
395 postContent = operation.requestBody().get("content");
397 postContent = operation.responses().get("200").get("content");
399 assertNotNull(postContent);
400 final var postJsonRef = postContent.get("application/json").get("schema").get("$ref");
401 assertNotNull(postJsonRef);
402 assertEquals(expectedJsonRef, postJsonRef.textValue());
403 final var postXmlRef = postContent.get("application/xml").get("schema").get("$ref");
404 assertNotNull(postXmlRef);
405 assertEquals(expectedXmlRef, postXmlRef.textValue());
408 private static void verifyRequestRef(final Operation operation, final String expectedRef, final String nodeType) {
409 final JsonNode postContent;
410 if (operation.requestBody() != null) {
411 postContent = operation.requestBody().path("content");
413 postContent = operation.responses().path("200").path("content");
415 assertNotNull(postContent);
416 final String postJsonRef;
417 if (nodeType.equals(CONTAINER)) {
418 postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
419 .path("$ref").textValue();
421 postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
422 .path("items").path("$ref").textValue();
424 assertNotNull(postJsonRef);
425 assertEquals(expectedRef, postJsonRef);
426 final var postXmlRef = postContent.path("application/xml").path("schema").path("$ref");
427 assertNotNull(postXmlRef);
428 assertEquals(expectedRef, postXmlRef.textValue());
431 private static void verifyThatOthersNodeDoesNotHaveRequiredField(final List<String> expected,
432 final Map<String, Schema> schemas) {
433 for (final var schema : schemas.entrySet()) {
434 if (expected.contains(schema.getKey())) {
437 assertNull("Json node " + schema.getKey() + " should not have 'required' field in body",
438 schema.getValue().required());
442 private static void verifyRequiredField(final Schema rootContainer, final List<String> expected) {
443 assertNotNull(rootContainer);
444 final var required = rootContainer.required();
445 assertNotNull(required);
446 assertEquals(expected, required);