Re-implement logic for security and securitySchemes
[netconf.git] / restconf / restconf-openapi / src / test / java / org / opendaylight / restconf / openapi / mountpoints / MountPointOpenApiTest.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.openapi.mountpoints;
9
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertNotNull;
12 import static org.junit.Assert.assertTrue;
13 import static org.mockito.Mockito.mock;
14 import static org.mockito.Mockito.when;
15 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathGetParameters;
16 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathPostParameters;
17 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.BASIC_AUTH_NAME;
18
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.TreeSet;
25 import java.util.stream.Collectors;
26 import javax.ws.rs.core.UriInfo;
27 import org.glassfish.jersey.internal.util.collection.ImmutableMultivaluedMap;
28 import org.junit.Before;
29 import org.junit.BeforeClass;
30 import org.junit.Test;
31 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
32 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
33 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
34 import org.opendaylight.restconf.openapi.DocGenTestHelper;
35 import org.opendaylight.restconf.openapi.impl.MountPointOpenApiGeneratorRFC8040;
36 import org.opendaylight.restconf.openapi.model.OpenApiObject;
37 import org.opendaylight.restconf.openapi.model.Operation;
38 import org.opendaylight.restconf.openapi.model.Path;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
42 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
43
44 public final class MountPointOpenApiTest {
45     private static final String TOASTER = "toaster";
46     private static final String TOASTER_REVISION = "2009-11-20";
47     private static final Long DEVICE_ID = 1L;
48     private static final String DEVICE_NAME = "123";
49     private static final String TOASTER_NODE_PATH = "/rests/data/nodes/node=123/yang-ext:mount/toaster:toaster";
50     private static final String TOASTER_NODE_GET_SUMMARY = "GET - " + DEVICE_NAME + " - toaster - toaster";
51     private static final String GET_ALL = "http://localhost:8181/openapi/api/v3/mounts/" + DEVICE_ID;
52     private static final String GET_TOASTER = "http://localhost:8181/openapi/api/v3/mounts/%s/%s(%s)".formatted(
53         DEVICE_ID, TOASTER, TOASTER_REVISION);
54     private static final String HTTP_URL = "http://localhost/path";
55     private static final YangInstanceIdentifier INSTANCE_ID = YangInstanceIdentifier.builder()
56             .node(QName.create("", "nodes"))
57             .node(QName.create("", "node"))
58             .nodeWithKey(QName.create("", "node"), QName.create("", "id"), DEVICE_NAME).build();
59     private static final String INSTANCE_URL = "/nodes/node=123/";
60
61     private static EffectiveModelContext context;
62     private static DOMSchemaService schemaService;
63
64     private MountPointOpenApi openApi;
65     private UriInfo uriDeviceAll;
66     private UriInfo uriDeviceToaster;
67
68     @BeforeClass
69     public static void beforeClass() {
70         schemaService = mock(DOMSchemaService.class);
71         context = YangParserTestUtils.parseYangResourceDirectory("/yang");
72         when(schemaService.getGlobalContext()).thenReturn(context);
73     }
74
75     @Before
76     public void before() throws Exception {
77         // We are sharing the global schema service and the mount schema service
78         // in our test.
79         // OK for testing - real thing would have separate instances.
80         final DOMMountPoint mountPoint = mock(DOMMountPoint.class);
81         when(mountPoint.getService(DOMSchemaService.class)).thenReturn(Optional.of(schemaService));
82
83         final DOMMountPointService service = mock(DOMMountPointService.class);
84         when(service.getMountPoint(INSTANCE_ID)).thenReturn(Optional.of(mountPoint));
85
86         openApi = new MountPointOpenApiGeneratorRFC8040(schemaService, service).getMountPointOpenApi();
87
88         uriDeviceAll = DocGenTestHelper.createMockUriInfo(GET_ALL);
89         when(uriDeviceAll.getQueryParameters()).thenReturn(ImmutableMultivaluedMap.empty());
90         uriDeviceToaster = DocGenTestHelper.createMockUriInfo(GET_TOASTER);
91         when(uriDeviceToaster.getQueryParameters()).thenReturn(ImmutableMultivaluedMap.empty());
92     }
93
94     @Test()
95     public void getInstanceIdentifiers() {
96         assertEquals(0, openApi.getInstanceIdentifiers().size());
97         openApi.onMountPointCreated(INSTANCE_ID); // add this ID into the list of mount points
98         assertEquals(1, openApi.getInstanceIdentifiers().size());
99         assertEquals((Long) 1L, openApi.getInstanceIdentifiers().entrySet().iterator().next()
100                 .getValue());
101         assertEquals(INSTANCE_URL, openApi.getInstanceIdentifiers().entrySet().iterator().next()
102                 .getKey());
103         openApi.onMountPointRemoved(INSTANCE_ID); // remove ID from list of mount points
104         assertEquals(0, openApi.getInstanceIdentifiers().size());
105     }
106
107     @Test
108     public void testGetDataStoreApi() throws Exception {
109         final UriInfo mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
110         openApi.onMountPointCreated(INSTANCE_ID); // add this ID into the list of mount points
111
112         final OpenApiObject mountPointApi = openApi.getMountPointApi(mockInfo, 1L, "Datastores", "-");
113         assertNotNull("Failed to find Datastore API", mountPointApi);
114
115         final Map<String, Path> paths = mountPointApi.paths();
116         assertNotNull(paths);
117
118         assertEquals("Unexpected api list size", 2, paths.size());
119
120         final Set<String> actualUrls = new TreeSet<>();
121
122         for (final Map.Entry<String, Path> path : paths.entrySet()) {
123             actualUrls.add(path.getKey());
124             final Operation getOperation = path.getValue().get();
125             assertNotNull("Unexpected operation method on " + path, getOperation);
126             assertNotNull("Expected non-null desc on " + path, getOperation.description());
127         }
128
129         assertEquals(Set.of("/rests/data" + INSTANCE_URL + "yang-ext:mount",
130             "/rests/operations" + INSTANCE_URL + "yang-ext:mount"), actualUrls);
131     }
132
133     /**
134      * Test that creates mount point api with all models from yang folder and checks operation paths for these models.
135      */
136     @Test
137     public void testGetMountPointApi() throws Exception {
138         final UriInfo mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
139         openApi.onMountPointCreated(INSTANCE_ID);
140
141         final OpenApiObject mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
142         assertNotNull("Failed to find Datastore API", mountPointApi);
143
144         final Map<String, Path> paths = mountPointApi.paths();
145         assertNotNull(paths);
146
147         assertEquals("Unexpected api list size", 78, paths.size());
148
149         final List<Operation> getOperations = new ArrayList<>();
150         final List<Operation> postOperations = new ArrayList<>();
151         final List<Operation> putOperations = new ArrayList<>();
152         final List<Operation> patchOperations = new ArrayList<>();
153         final List<Operation> deleteOperations = new ArrayList<>();
154
155         for (final Map.Entry<String, Path> path : paths.entrySet()) {
156             Optional.ofNullable(path.getValue().get()).ifPresent(getOperations::add);
157             Optional.ofNullable(path.getValue().post()).ifPresent(postOperations::add);
158             Optional.ofNullable(path.getValue().put()).ifPresent(putOperations::add);
159             Optional.ofNullable(path.getValue().patch()).ifPresent(patchOperations::add);
160             Optional.ofNullable(path.getValue().delete()).ifPresent(deleteOperations::add);
161         }
162
163         assertEquals("Unexpected GET paths size", 70, getOperations.size());
164         assertEquals("Unexpected POST paths size", 21, postOperations.size());
165         assertEquals("Unexpected PUT paths size", 68, putOperations.size());
166         assertEquals("Unexpected PATCH paths size", 68, patchOperations.size());
167         assertEquals("Unexpected DELETE paths size", 68, deleteOperations.size());
168     }
169
170     /**
171      * Test that checks for correct amount of parameters in requests.
172      */
173     @Test
174     @SuppressWarnings("checkstyle:lineLength")
175     public void testMountPointRecursiveParameters() throws Exception {
176         final var configPaths = Map.of("/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root", 0,
177             "/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root/root-list={name}", 1,
178             "/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root/root-list={name}/nested-list={name1}", 2,
179             "/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root/root-list={name}/nested-list={name1}/super-nested-list={name2}", 3);
180
181         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
182         openApi.onMountPointCreated(INSTANCE_ID);
183
184         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, "recursive", "2023-05-22");
185         assertNotNull("Failed to find Datastore API", mountPointApi);
186
187         final var paths = mountPointApi.paths();
188         assertEquals(5, paths.size());
189
190         for (final var expectedPath : configPaths.entrySet()) {
191             assertTrue(paths.containsKey(expectedPath.getKey()));
192             final int expectedSize = expectedPath.getValue();
193
194             final var path = paths.get(expectedPath.getKey());
195
196             final var get = path.get();
197             assertNotNull(get);
198             assertEquals(expectedSize + 1, get.parameters().size());
199
200             final var put = path.put();
201             assertNotNull(put);
202             assertEquals(expectedSize, put.parameters().size());
203
204             final var delete = path.delete();
205             assertNotNull(delete);
206             assertEquals(expectedSize, delete.parameters().size());
207
208             final var patch = path.patch();
209             assertNotNull(patch);
210             assertEquals(expectedSize, patch.parameters().size());
211         }
212
213         // POST request exists only for containers
214         final var post = paths.get("/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root").post();
215         assertNotNull(post);
216         final int expectedSize = configPaths.get("/rests/data/nodes/node=123/yang-ext:mount/recursive:container-root");
217         assertEquals(expectedSize, post.parameters().size());
218     }
219
220     /**
221      * Test that request parameters are correctly numbered.
222      *
223      * <p>
224      * It means we should have name and name1, etc. when we have the same parameter in path multiple times.
225      */
226     @Test
227     public void testParametersNumberingForMountPointApi() throws Exception {
228         final UriInfo mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
229         openApi.onMountPointCreated(INSTANCE_ID);
230
231         final OpenApiObject mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
232         assertNotNull("Failed to find Datastore API", mountPointApi);
233
234         var pathToList1 = "/rests/data/nodes/node=123/yang-ext:mount/path-params-test:cont/list1={name}";
235         assertTrue(mountPointApi.paths().containsKey(pathToList1));
236         assertEquals(List.of("name"), getPathGetParameters(mountPointApi.paths(), pathToList1));
237
238         var pathToList2 = "/rests/data/nodes/node=123/yang-ext:mount/path-params-test:cont/list1={name}/list2={name1}";
239         assertTrue(mountPointApi.paths().containsKey(pathToList2));
240         assertEquals(List.of("name", "name1"), getPathGetParameters(mountPointApi.paths(), pathToList2));
241
242         var pathToList3 = "/rests/data/nodes/node=123/yang-ext:mount/path-params-test:cont/list3={name}";
243         assertTrue(mountPointApi.paths().containsKey(pathToList3));
244         assertEquals(List.of("name"), getPathGetParameters(mountPointApi.paths(), pathToList3));
245
246         var pathToList4 = "/rests/data/nodes/node=123/yang-ext:mount/path-params-test:cont/list1={name}/list4={name1}";
247         assertTrue(mountPointApi.paths().containsKey(pathToList4));
248         assertEquals(List.of("name", "name1"), getPathGetParameters(mountPointApi.paths(), pathToList4));
249
250         var pathToList5 = "/rests/data/nodes/node=123/yang-ext:mount/path-params-test:cont/list1={name}/cont2";
251         assertTrue(mountPointApi.paths().containsKey(pathToList5));
252         assertEquals(List.of("name"), getPathGetParameters(mountPointApi.paths(), pathToList5));
253     }
254
255     /**
256      * Test that request parameters are correctly typed.
257      */
258     @Test
259     public void testParametersTypesForMountPointApi() throws Exception {
260         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
261         openApi.onMountPointCreated(INSTANCE_ID);
262         final var doc = openApi.getMountPointApi(mockInfo, 1L, null);
263         final var pathToContainer = "/rests/data/nodes/node=123/yang-ext:mount/typed-params:typed/";
264         final var integerTypes = List.of("uint64", "uint32", "uint16", "uint8", "int64", "int32", "int16", "int8");
265         for (final var type: integerTypes) {
266             final var typeKey = type + "-key";
267             final var path = pathToContainer + type + "={" + typeKey + "}";
268             assertTrue(doc.paths().containsKey(path));
269             assertEquals("integer", doc.paths().get(path).get().parameters().get(0).schema().type());
270         }
271     }
272
273     /**
274      * Test that request for actions is correct and has parameters.
275      */
276     @Test
277     public void testActionPathsParamsForMountPointApi() throws Exception {
278         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
279         openApi.onMountPointCreated(INSTANCE_ID);
280
281         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
282         assertNotNull("Failed to find Datastore API", mountPointApi);
283
284         final var pathWithParameters =
285             "/rests/operations/nodes/node=123/yang-ext:mount/action-types:list={name}/list-action";
286         assertTrue(mountPointApi.paths().containsKey(pathWithParameters));
287         assertEquals(List.of("name"), getPathPostParameters(mountPointApi.paths(), pathWithParameters));
288
289         final var pathWithoutParameters =
290             "/rests/operations/nodes/node=123/yang-ext:mount/action-types:multi-container/inner-container/action";
291         assertTrue(mountPointApi.paths().containsKey(pathWithoutParameters));
292         assertEquals(List.of(), getPathPostParameters(mountPointApi.paths(), pathWithoutParameters));
293     }
294
295     /**
296      * Test that checks if securitySchemes and security elements are present.
297      */
298     @Test
299     public void testAuthenticationFeature() throws Exception {
300         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
301         openApi.onMountPointCreated(INSTANCE_ID);
302         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
303
304         assertEquals("[{basicAuth=[]}]", mountPointApi.security().toString());
305         assertEquals("Http[type=http, scheme=basic, description=null, bearerFormat=null]",
306             mountPointApi.components().securitySchemes().get(BASIC_AUTH_NAME).toString());
307     }
308
309     @Test
310     public void testSummaryForAllModules() {
311         openApi.onMountPointCreated(INSTANCE_ID);
312         // get OpenApiObject for the device (all modules)
313         final OpenApiObject openApiAll = openApi.getMountPointApi(uriDeviceAll, DEVICE_ID, null);
314         final String getToasterSummary = openApiAll.paths()
315             .get(TOASTER_NODE_PATH)
316             .get()
317             .summary();
318         assertEquals(TOASTER_NODE_GET_SUMMARY, getToasterSummary);
319     }
320
321     @Test
322     public void testSummaryForSingleModule() {
323         openApi.onMountPointCreated(INSTANCE_ID);
324         // get OpenApiObject for a specific module (toaster) associated with the mounted device
325         final OpenApiObject openApiToaster = openApi.getMountPointApi(uriDeviceToaster, DEVICE_ID, TOASTER,
326             TOASTER_REVISION);
327         final String getToasterSummary = openApiToaster.paths()
328             .get(TOASTER_NODE_PATH)
329             .get()
330             .summary();
331         assertEquals(TOASTER_NODE_GET_SUMMARY, getToasterSummary);
332     }
333
334     @Test
335     public void testPathsForSpecificModuleOfMounted() {
336         openApi.onMountPointCreated(INSTANCE_ID);
337
338         // get OpenApiObject for the device (all modules)
339         final OpenApiObject openApiAll = openApi.getMountPointApi(uriDeviceAll, DEVICE_ID, null);
340         // get OpenApiObject for a specific module (toaster(2009-11-20))
341         final OpenApiObject openApiToaster = openApi.getMountPointApi(uriDeviceToaster, DEVICE_ID, TOASTER,
342             TOASTER_REVISION);
343
344         /*
345             filter paths from openapi for all modules down to only those that are present in openapi for toaster.
346             The Path object for the path, that in this case ends with "yang-ext:mount" is constructed in a different way
347             when requesting OpenApiObject for a single module compared to requesting it for all modules.
348             We do not want to include it in this particular comparison, so filter it out
349          */
350         final Set<Map.Entry<String, Path>> toasterPathsFromAll = openApiAll.paths().entrySet()
351             .stream()
352             .filter(e -> openApiToaster.paths().containsKey(e.getKey()) && !e.getKey().endsWith("yang-ext:mount"))
353             .collect(Collectors.toSet());
354
355         final Set<Map.Entry<String, Path>> toasterPathsFromToaster = openApiToaster.paths().entrySet()
356             .stream()
357             .filter(e -> !e.getKey().endsWith("yang-ext:mount"))
358             .collect(Collectors.toSet());
359
360         // verify that the filtered set (from openapi for all modules) is the same as the set from openapi for toaster
361         assertEquals(toasterPathsFromToaster, toasterPathsFromAll);
362     }
363
364     /**
365      * Test that checks if namespace for rpc is present.
366      */
367     @Test
368     public void testRpcNamespace() throws Exception {
369         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
370         openApi.onMountPointCreated(INSTANCE_ID);
371
372         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
373         assertNotNull("Failed to find Datastore API", mountPointApi);
374         final var paths = mountPointApi.paths();
375         final var path = paths.get("/rests/operations/nodes/node=123/yang-ext:mount/toaster:cancel-toast");
376         assertNotNull(path);
377         final var content = path.post().requestBody().content().get("application/xml");
378         assertNotNull(content);
379         final var schema = content.schema();
380         assertNotNull(schema);
381         final var xml = schema.xml();
382         assertNotNull(xml);
383         final var namespace = xml.namespace();
384         assertNotNull(namespace);
385         assertEquals("http://netconfcentral.org/ns/toaster", namespace);
386     }
387
388     /**
389      * Test that checks if namespace for actions is present.
390      */
391     @Test
392     public void testActionsNamespace() throws Exception {
393         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
394         openApi.onMountPointCreated(INSTANCE_ID);
395         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
396         assertNotNull("Failed to find Datastore API", mountPointApi);
397         final var paths = mountPointApi.paths();
398         final var path = paths.get(
399             "/rests/operations/nodes/node=123/yang-ext:mount/action-types:multi-container/inner-container/action");
400         assertNotNull(path);
401         final var content = path.post().requestBody().content().get("application/xml");
402         assertNotNull(content);
403         final var schema = content.schema();
404         assertNotNull(schema);
405         final var xml = schema.xml();
406         assertNotNull(xml);
407         final var namespace = xml.namespace();
408         assertNotNull(namespace);
409         assertEquals("urn:ietf:params:xml:ns:yang:test:action:types", namespace);
410     }
411
412     /**
413      * Test that number of elements in payload is correct.
414      */
415     @SuppressWarnings("unchecked")
416     @Test
417     public void testLeafListWithMinElementsPayloadOnMountPoint() throws Exception {
418         final var mockInfo = DocGenTestHelper.createMockUriInfo(HTTP_URL);
419         openApi.onMountPointCreated(INSTANCE_ID);
420         final var mountPointApi = openApi.getMountPointApi(mockInfo, 1L, null);
421         assertNotNull(mountPointApi);
422         final var paths = mountPointApi.paths();
423         final var path =
424             paths.get("/rests/data/nodes/node=123/yang-ext:mount/mandatory-test:root-container/mandatory-container");
425         assertNotNull(path);
426         final var requestBody = path.put().requestBody().content();
427         assertNotNull(requestBody);
428         final var jsonRef = requestBody.get("application/json").schema().properties()
429             .get("mandatory-test:mandatory-container").ref();
430         assertNotNull(jsonRef);
431         final var xmlRef = requestBody.get("application/xml").schema().ref();
432         assertNotNull(xmlRef);
433         final var schema =
434             mountPointApi.components().schemas().get("mandatory-test_root-container_mandatory-container");
435         assertNotNull(schema);
436         final var minItems = schema.properties().get("leaf-list-with-min-elements").minItems();
437         assertNotNull(minItems);
438         final var listOfExamples = ((List<String>) schema.properties().get("leaf-list-with-min-elements").example());
439         assertNotNull(listOfExamples);
440         assertEquals(jsonRef, xmlRef);
441         assertEquals(listOfExamples.size(), minItems.intValue());
442     }
443 }