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.netconf.sal.rest.doc.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;
16 import com.fasterxml.jackson.databind.JsonNode;
17 import com.fasterxml.jackson.databind.node.ObjectNode;
18 import com.google.common.collect.ImmutableList;
19 import java.util.ArrayList;
20 import java.util.List;
22 import java.util.stream.Collectors;
23 import java.util.stream.StreamSupport;
24 import org.junit.Test;
25 import org.opendaylight.netconf.sal.rest.doc.swagger.OpenApiObject;
26 import org.opendaylight.netconf.sal.rest.doc.swagger.SwaggerObject;
28 public final class ApiDocGeneratorRFC8040Test extends AbstractApiDocTest {
29 private static final String NAME = "toaster2";
30 private static final String MY_YANG = "my-yang";
31 private static final String MY_YANG_REVISION = "2022-10-06";
32 private static final String REVISION_DATE = "2009-11-20";
33 private static final String NAME_2 = "toaster";
34 private static final String REVISION_DATE_2 = "2009-11-20";
35 private static final String PATH_PARAMS_TEST_MODULE = "path-params-test";
36 private static final String MANDATORY_TEST = "mandatory-test";
37 private static final String CONFIG_ROOT_CONTAINER = "mandatory-test_config_root-container";
38 private static final String CONFIG_ROOT_CONTAINER_POST = "mandatory-test_config_root-container_post";
39 private static final String ROOT_CONTAINER = "mandatory-test_root-container";
40 private static final String CONFIG_MANDATORY_CONTAINER = "mandatory-test_root-container_config_mandatory-container";
41 private static final String CONFIG_MANDATORY_CONTAINER_POST
42 = "mandatory-test_root-container_config_mandatory-container_post";
43 private static final String MANDATORY_CONTAINER = "mandatory-test_root-container_mandatory-container";
44 private static final String CONFIG_MANDATORY_LIST = "mandatory-test_root-container_config_mandatory-list";
45 private static final String CONFIG_MANDATORY_LIST_POST = "mandatory-test_root-container_config_mandatory-list_post";
46 private static final String MANDATORY_LIST = "mandatory-test_root-container_mandatory-list";
47 private static final String MANDATORY_TEST_MODULE = "mandatory-test_module";
48 private static final String CHOICE_TEST_MODULE = "choice-test";
49 private static final String PROPERTIES = "properties";
50 private static final String CONTAINER = "container";
51 private static final String LIST = "list";
53 private final ApiDocGeneratorRFC8040 generator = new ApiDocGeneratorRFC8040(SCHEMA_SERVICE);
56 * Test that paths are generated according to the model.
59 public void testPaths() {
60 final SwaggerObject doc = (SwaggerObject) generator.getApiDeclaration(NAME, REVISION_DATE, URI_INFO,
61 ApiDocServiceImpl.OAversion.V2_0);
63 assertEquals(List.of("/rests/data",
64 "/rests/data/toaster2:toaster",
65 "/rests/data/toaster2:toaster/toasterSlot={slotId}",
66 "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo",
67 "/rests/data/toaster2:lst",
68 "/rests/data/toaster2:lst/cont1",
69 "/rests/data/toaster2:lst/cont1/cont11",
70 "/rests/data/toaster2:lst/cont1/lst11",
71 "/rests/data/toaster2:lst/lst1={key1},{key2}",
72 "/rests/operations/toaster2:make-toast",
73 "/rests/operations/toaster2:cancel-toast",
74 "/rests/operations/toaster2:restock-toaster"),
75 ImmutableList.copyOf(doc.getPaths().fieldNames()));
79 * Test that generated configuration paths allow to use operations: get, put, delete and post.
82 public void testConfigPaths() {
83 final List<String> configPaths = List.of("/rests/data/toaster2:lst",
84 "/rests/data/toaster2:lst/cont1",
85 "/rests/data/toaster2:lst/cont1/cont11",
86 "/rests/data/toaster2:lst/cont1/lst11",
87 "/rests/data/toaster2:lst/lst1={key1},{key2}");
89 final SwaggerObject doc = (SwaggerObject) generator.getApiDeclaration(NAME, REVISION_DATE, URI_INFO,
90 ApiDocServiceImpl.OAversion.V2_0);
92 for (final String path : configPaths) {
93 final JsonNode node = doc.getPaths().get(path);
94 assertFalse(node.path("get").isMissingNode());
95 assertFalse(node.path("put").isMissingNode());
96 assertFalse(node.path("delete").isMissingNode());
97 assertFalse(node.path("post").isMissingNode());
102 * Test that generated document contains the following definitions.
105 public void testDefinitions() {
106 final SwaggerObject doc = (SwaggerObject) generator.getApiDeclaration(NAME, REVISION_DATE, URI_INFO,
107 ApiDocServiceImpl.OAversion.V2_0);
109 final ObjectNode definitions = doc.getDefinitions();
110 assertNotNull(definitions);
112 final JsonNode configLstTop = definitions.get("toaster2_config_lst_TOP");
113 assertNotNull(configLstTop);
114 DocGenTestHelper.containsReferences(configLstTop, "toaster2:lst", "#/definitions/toaster2_config_lst");
116 final JsonNode configLst = definitions.get("toaster2_config_lst");
117 assertNotNull(configLst);
118 DocGenTestHelper.containsReferences(configLst, "lst1", "#/definitions/toaster2_lst_config_lst1");
119 DocGenTestHelper.containsReferences(configLst, "cont1", "#/definitions/toaster2_lst_config_cont1");
121 final JsonNode configLst1Top = definitions.get("toaster2_lst_config_lst1_TOP");
122 assertNotNull(configLst1Top);
123 DocGenTestHelper.containsReferences(configLst1Top, "toaster2:lst1", "#/definitions/toaster2_lst_config_lst1");
125 final JsonNode configLst1 = definitions.get("toaster2_lst_config_lst1");
126 assertNotNull(configLst1);
128 final JsonNode configCont1Top = definitions.get("toaster2_lst_config_cont1_TOP");
129 assertNotNull(configCont1Top);
130 DocGenTestHelper.containsReferences(configCont1Top, "toaster2:cont1",
131 "#/definitions/toaster2_lst_config_cont1");
133 final JsonNode configCont1 = definitions.get("toaster2_lst_config_cont1");
134 assertNotNull(configCont1);
135 DocGenTestHelper.containsReferences(configCont1, "cont11", "#/definitions/toaster2_lst_cont1_config_cont11");
136 DocGenTestHelper.containsReferences(configCont1, "lst11", "#/definitions/toaster2_lst_cont1_config_lst11");
138 final JsonNode configCont11Top = definitions.get("toaster2_lst_cont1_config_cont11_TOP");
139 assertNotNull(configCont11Top);
140 DocGenTestHelper.containsReferences(configCont11Top, "toaster2:cont11",
141 "#/definitions/toaster2_lst_cont1_config_cont11");
143 final JsonNode configCont11 = definitions.get("toaster2_lst_cont1_config_cont11");
144 assertNotNull(configCont11);
146 final JsonNode configLst11Top = definitions.get("toaster2_lst_cont1_config_lst11_TOP");
147 assertNotNull(configLst11Top);
148 DocGenTestHelper.containsReferences(configLst11Top, "toaster2:lst11",
149 "#/definitions/toaster2_lst_cont1_config_lst11");
151 final JsonNode configLst11 = definitions.get("toaster2_lst_cont1_config_lst11");
152 assertNotNull(configLst11);
156 * Test that generated document contains RPC definition for "make-toast" with correct input.
159 public void testRPC() {
160 final SwaggerObject doc = (SwaggerObject) generator.getApiDeclaration(NAME_2, REVISION_DATE_2, URI_INFO,
161 ApiDocServiceImpl.OAversion.V2_0);
164 final ObjectNode definitions = doc.getDefinitions();
165 final JsonNode inputTop = definitions.get("toaster_make-toast_input_TOP");
166 assertNotNull(inputTop);
167 final String testString = "{\"toaster:input\":{\"$ref\":\"#/definitions/toaster_make-toast_input\"}}";
168 assertEquals(testString, inputTop.get("properties").toString());
169 final JsonNode input = definitions.get("toaster_make-toast_input");
170 final JsonNode properties = input.get("properties");
171 assertTrue(properties.has("toasterDoneness"));
172 assertTrue(properties.has("toasterToastType"));
176 public void testMandatory() {
177 final var doc = (OpenApiObject) generator.getApiDeclaration(MANDATORY_TEST, null, URI_INFO,
178 ApiDocServiceImpl.OAversion.V3_0);
180 final var definitions = doc.getComponents().getSchemas();
181 final var containersWithRequired = new ArrayList<String>();
183 final var reqRootContainerElements = Set.of("mandatory-root-leaf", "mandatory-container",
184 "mandatory-first-choice", "mandatory-list");
185 verifyRequiredField(definitions.get(CONFIG_ROOT_CONTAINER), reqRootContainerElements);
186 containersWithRequired.add(CONFIG_ROOT_CONTAINER);
187 verifyRequiredField(definitions.get(ROOT_CONTAINER), reqRootContainerElements);
188 containersWithRequired.add(ROOT_CONTAINER);
189 verifyRequiredField(definitions.get(CONFIG_ROOT_CONTAINER_POST), reqRootContainerElements);
190 containersWithRequired.add(CONFIG_ROOT_CONTAINER_POST);
192 final var reqMandatoryContainerElements = Set.of("mandatory-leaf", "leaf-list-with-min-elements");
193 verifyRequiredField(definitions.get(CONFIG_MANDATORY_CONTAINER), reqMandatoryContainerElements);
194 containersWithRequired.add(CONFIG_MANDATORY_CONTAINER);
195 verifyRequiredField(definitions.get(MANDATORY_CONTAINER), reqMandatoryContainerElements);
196 containersWithRequired.add(MANDATORY_CONTAINER);
197 verifyRequiredField(definitions.get(CONFIG_MANDATORY_CONTAINER_POST), reqMandatoryContainerElements);
198 containersWithRequired.add(CONFIG_MANDATORY_CONTAINER_POST);
200 final var reqMandatoryListElements = Set.of("mandatory-list-field");
201 verifyRequiredField(definitions.get(CONFIG_MANDATORY_LIST), reqMandatoryListElements);
202 containersWithRequired.add(CONFIG_MANDATORY_LIST);
203 verifyRequiredField(definitions.get(MANDATORY_LIST), reqMandatoryListElements);
204 containersWithRequired.add(MANDATORY_LIST);
205 verifyRequiredField(definitions.get(CONFIG_MANDATORY_LIST_POST), reqMandatoryListElements);
206 containersWithRequired.add(CONFIG_MANDATORY_LIST_POST);
208 final var testModuleMandatoryArray = Set.of("root-container", "root-mandatory-list");
209 verifyRequiredField(definitions.get(MANDATORY_TEST_MODULE), testModuleMandatoryArray);
210 containersWithRequired.add(MANDATORY_TEST_MODULE);
212 verifyThatPropertyDoesNotHaveRequired(containersWithRequired, definitions);
216 * Test that request parameters are correctly numbered.
219 * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
222 public void testParametersNumbering() {
223 final var doc = (OpenApiObject) generator.getApiDeclaration(PATH_PARAMS_TEST_MODULE, null, URI_INFO,
224 ApiDocServiceImpl.OAversion.V3_0);
226 var pathToList1 = "/rests/data/path-params-test:cont/list1={name}";
227 assertTrue(doc.getPaths().has(pathToList1));
228 assertEquals(List.of("name"), getPathParameters(doc.getPaths(), pathToList1));
230 var pathToList2 = "/rests/data/path-params-test:cont/list1={name}/list2={name1}";
231 assertTrue(doc.getPaths().has(pathToList2));
232 assertEquals(List.of("name", "name1"), getPathParameters(doc.getPaths(), pathToList2));
234 var pathToList3 = "/rests/data/path-params-test:cont/list3={name}";
235 assertTrue(doc.getPaths().has(pathToList3));
236 assertEquals(List.of("name"), getPathParameters(doc.getPaths(), pathToList3));
238 var pathToList4 = "/rests/data/path-params-test:cont/list1={name}/list4={name1}";
239 assertTrue(doc.getPaths().has(pathToList4));
240 assertEquals(List.of("name", "name1"), getPathParameters(doc.getPaths(), pathToList4));
242 var pathToList5 = "/rests/data/path-params-test:cont/list1={name}/cont2";
243 assertTrue(doc.getPaths().has(pathToList4));
244 assertEquals(List.of("name"), getPathParameters(doc.getPaths(), pathToList5));
247 private static void verifyThatPropertyDoesNotHaveRequired(final List<String> expected,
248 final ObjectNode definitions) {
249 final var fields = definitions.fields();
250 while (fields.hasNext()) {
251 final var next = fields.next();
252 final var nodeName = next.getKey();
253 final var jsonNode = next.getValue();
254 if (expected.contains(nodeName)) {
257 assertNull("Json node " + nodeName + " should not have 'required' field in body",
258 jsonNode.get("required"));
262 private static void verifyRequiredField(final JsonNode rootContainer, final Set<String> expected) {
263 assertNotNull(rootContainer);
264 final var required = rootContainer.get("required");
265 assertNotNull(required);
266 assertTrue(required.isArray());
267 final var actualContainerArray = StreamSupport.stream(required.spliterator(), false)
268 .map(JsonNode::textValue)
269 .collect(Collectors.toSet());
270 assertEquals(expected, actualContainerArray);
274 * Test that request for actions is correct and has parameters.
277 public void testActionPathsParams() {
278 final var doc = (OpenApiObject) generator.getApiDeclaration("action-types", null, URI_INFO,
279 ApiDocServiceImpl.OAversion.V3_0);
281 final var pathWithParameters = "/rests/operations/action-types:list={name}/list-action";
282 assertTrue(doc.getPaths().has(pathWithParameters));
283 assertEquals(List.of("name"), getPathParameters(doc.getPaths(), pathWithParameters));
285 final var pathWithoutParameters = "/rests/operations/action-types:multi-container/inner-container/action";
286 assertTrue(doc.getPaths().has(pathWithoutParameters));
287 assertEquals(List.of(), getPathParameters(doc.getPaths(), pathWithoutParameters));
291 public void testChoice() {
292 final var doc = (SwaggerObject) generator.getApiDeclaration(CHOICE_TEST_MODULE, null, URI_INFO,
293 ApiDocServiceImpl.OAversion.V2_0);
296 final var definitions = doc.getDefinitions();
297 JsonNode firstContainer = definitions.get("choice-test_first-container");
298 assertEquals("default-value",
299 firstContainer.get(PROPERTIES).get("leaf-default").get("default").asText());
300 assertFalse(firstContainer.get(PROPERTIES).has("leaf-non-default"));
302 JsonNode secondContainer = definitions.get("choice-test_second-container");
303 assertTrue(secondContainer.get(PROPERTIES).has("leaf-first-case"));
304 assertFalse(secondContainer.get(PROPERTIES).has("leaf-second-case"));
308 public void testSimpleOpenApiObjects() {
309 final var doc = (OpenApiObject) generator.getApiDeclaration(MY_YANG, MY_YANG_REVISION, URI_INFO,
310 ApiDocServiceImpl.OAversion.V3_0);
311 assertEquals(List.of("/rests/data", "/rests/data/my-yang:data"),
312 ImmutableList.copyOf(doc.getPaths().fieldNames()));
314 final var JsonNodeMyYangData = doc.getPaths().get("/rests/data/my-yang:data");
315 verifyRequestRef(JsonNodeMyYangData.path("post"),
316 "#/components/schemas/my-yang_config_data_post",
317 "#/components/schemas/my-yang_config_data_post_xml");
318 verifyRequestRef(JsonNodeMyYangData.path("put"), "#/components/schemas/my-yang_config_data_TOP",
319 "#/components/schemas/my-yang_config_data");
320 verifyRequestRef(JsonNodeMyYangData.path("get"), "#/components/schemas/my-yang_data_TOP",
321 "#/components/schemas/my-yang_data");
323 // Test `components/schemas` objects
324 final var definitions = doc.getComponents().getSchemas();
325 assertEquals(7, definitions.size());
326 assertTrue(definitions.has("my-yang_config_data"));
327 assertTrue(definitions.has("my-yang_config_data_post"));
328 assertTrue(definitions.has("my-yang_config_data_post_xml"));
329 assertTrue(definitions.has("my-yang_config_data_TOP"));
330 assertTrue(definitions.has("my-yang_data"));
331 assertTrue(definitions.has("my-yang_data_TOP"));
332 assertTrue(definitions.has("my-yang_module"));
336 public void testToaster2OpenApiObjects() {
337 final var doc = (OpenApiObject) generator.getApiDeclaration(NAME, REVISION_DATE, URI_INFO,
338 ApiDocServiceImpl.OAversion.V3_0);
339 final var jsonNodeToaster = doc.getPaths().get("/rests/data/toaster2:toaster");
340 verifyPostRequestRef(jsonNodeToaster.path("post"),
341 "#/components/schemas/toaster2_toaster_config_toasterSlot_post",
342 "#/components/schemas/toaster2_toaster_config_toasterSlot_post_xml", LIST);
343 verifyRequestRef(jsonNodeToaster.path("put"), "#/components/schemas/toaster2_config_toaster_TOP",
344 "#/components/schemas/toaster2_config_toaster");
345 verifyRequestRef(jsonNodeToaster.path("get"), "#/components/schemas/toaster2_toaster_TOP",
346 "#/components/schemas/toaster2_toaster");
348 final var jsonNodeToasterSlot = doc.getPaths().get("/rests/data/toaster2:toaster/toasterSlot={slotId}");
349 verifyPostRequestRef(jsonNodeToasterSlot.path("post"),
350 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post",
351 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post_xml", CONTAINER);
352 verifyRequestRef(jsonNodeToasterSlot.path("put"),
353 "#/components/schemas/toaster2_toaster_config_toasterSlot_TOP",
354 "#/components/schemas/toaster2_toaster_config_toasterSlot");
355 verifyRequestRef(jsonNodeToasterSlot.path("get"), "#/components/schemas/toaster2_toaster_toasterSlot_TOP",
356 "#/components/schemas/toaster2_toaster_toasterSlot");
358 final var jsonNodeSlotInfo = doc.getPaths().get(
359 "/rests/data/toaster2:toaster/toasterSlot={slotId}/toaster-augmented:slotInfo");
360 verifyRequestRef(jsonNodeSlotInfo.path("post"),
361 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post",
362 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post_xml");
363 verifyRequestRef(jsonNodeSlotInfo.path("put"),
364 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_TOP",
365 "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo");
366 verifyRequestRef(jsonNodeSlotInfo.path("get"), "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo_TOP",
367 "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo");
369 final var jsonNodeLst = doc.getPaths().get("/rests/data/toaster2:lst");
370 verifyPostRequestRef(jsonNodeLst.path("post"), "#/components/schemas/toaster2_lst_config_cont1_post",
371 "#/components/schemas/toaster2_lst_config_cont1_post_xml", CONTAINER);
372 verifyRequestRef(jsonNodeLst.path("put"), "#/components/schemas/toaster2_config_lst_TOP",
373 "#/components/schemas/toaster2_config_lst");
374 verifyRequestRef(jsonNodeLst.path("get"), "#/components/schemas/toaster2_lst_TOP",
375 "#/components/schemas/toaster2_lst");
377 final var jsonNodeLst1 = doc.getPaths().get("/rests/data/toaster2:lst/lst1={key1},{key2}");
378 verifyRequestRef(jsonNodeLst1.path("post"), "#/components/schemas/toaster2_lst_config_lst1_post",
379 "#/components/schemas/toaster2_lst_config_lst1_post_xml");
380 verifyRequestRef(jsonNodeLst1.path("put"), "#/components/schemas/toaster2_lst_config_lst1_TOP",
381 "#/components/schemas/toaster2_lst_config_lst1");
382 verifyRequestRef(jsonNodeLst1.path("get"), "#/components/schemas/toaster2_lst_lst1_TOP",
383 "#/components/schemas/toaster2_lst_lst1");
385 final var jsonNodeMakeToast = doc.getPaths().get("/rests/operations/toaster2:make-toast");
386 assertTrue(jsonNodeMakeToast.path("get").isMissingNode());
387 verifyRequestRef(jsonNodeMakeToast.path("post"), "#/components/schemas/toaster2_make-toast_input_TOP",
388 "#/components/schemas/toaster2_make-toast_input");
390 final var jsonNodeCancelToast = doc.getPaths().get("/rests/operations/toaster2:cancel-toast");
391 assertTrue(jsonNodeCancelToast.path("get").isMissingNode());
392 // Test RPC with empty input
393 final var postContent = jsonNodeCancelToast.path("post").get("requestBody").get("content");
394 final var jsonSchema = postContent.get("application/json").get("schema");
395 assertNull(jsonSchema.get("$ref"));
396 assertEquals(2, jsonSchema.size());
397 final var xmlSchema = postContent.get("application/xml").get("schema");
398 assertNull(xmlSchema.get("$ref"));
399 assertEquals(2, xmlSchema.size());
400 // Test `components/schemas` objects
401 final var definitions = doc.getComponents().getSchemas();
402 assertEquals(60, definitions.size());
406 * Test JSON and XML references for request operation.
408 private static void verifyRequestRef(final JsonNode path, final String expectedJsonRef,
409 final String expectedXmlRef) {
410 final JsonNode postContent;
411 if (path.get("requestBody") != null) {
412 postContent = path.get("requestBody").get("content");
414 postContent = path.get("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 verifyPostRequestRef(final JsonNode path, final String expectedJsonRef,
426 final String expectedXmlRef, String nodeType) {
427 final JsonNode postContent;
428 if (path.get("requestBody") != null) {
429 postContent = path.get("requestBody").get("content");
431 postContent = path.get("responses").get("200").get("content");
433 assertNotNull(postContent);
434 final String postJsonRef;
435 if (nodeType.equals(CONTAINER)) {
436 postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
437 .path("$ref").textValue();
439 postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
440 .path("items").path("$ref").textValue();
442 assertEquals(expectedJsonRef, postJsonRef);
443 final var postXmlRef = postContent.get("application/xml").get("schema").get("$ref");
444 assertNotNull(postXmlRef);
445 assertEquals(expectedXmlRef, postXmlRef.textValue());