2 * Copyright (c) 2016 Cisco Systems, Inc. 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.nb.rfc8040.rests.services.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.assertThrows;
15 import static org.junit.Assert.assertTrue;
16 import static org.mockito.ArgumentMatchers.any;
17 import static org.mockito.Mockito.doNothing;
18 import static org.mockito.Mockito.doReturn;
19 import static org.mockito.Mockito.mock;
20 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
21 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
22 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
24 import java.io.ByteArrayInputStream;
25 import java.io.InputStream;
27 import java.nio.charset.StandardCharsets;
28 import java.util.Collection;
29 import java.util.List;
30 import java.util.Optional;
32 import javax.ws.rs.container.AsyncResponse;
33 import javax.ws.rs.core.MultivaluedHashMap;
34 import javax.ws.rs.core.MultivaluedMap;
35 import javax.ws.rs.core.Response;
36 import javax.ws.rs.core.UriBuilder;
37 import javax.ws.rs.core.UriInfo;
38 import org.junit.Before;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.mockito.ArgumentCaptor;
42 import org.mockito.Mock;
43 import org.mockito.junit.MockitoJUnitRunner;
44 import org.opendaylight.mdsal.common.api.CommitInfo;
45 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
46 import org.opendaylight.mdsal.dom.api.DOMActionService;
47 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
48 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
49 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
50 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
51 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
52 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
53 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
54 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
55 import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
56 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
57 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
58 import org.opendaylight.restconf.common.patch.PatchContext;
59 import org.opendaylight.restconf.common.patch.PatchEntity;
60 import org.opendaylight.restconf.common.patch.PatchStatusContext;
61 import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
62 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
63 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
64 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
65 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
66 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.NetconfRestconfStrategy;
67 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
68 import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
69 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
70 import org.opendaylight.yangtools.yang.common.ErrorTag;
71 import org.opendaylight.yangtools.yang.common.ErrorType;
72 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
73 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
74 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
75 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
76 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
77 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
78 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
80 @RunWith(MockitoJUnitRunner.StrictStubs.class)
81 public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
82 private static final NodeIdentifier PLAYLIST_NID = new NodeIdentifier(PLAYLIST_QNAME);
83 private static final NodeIdentifier LIBRARY_NID = new NodeIdentifier(LIBRARY_QNAME);
85 // config contains one child the same as in operational and one additional
86 private static final ContainerNode CONFIG_JUKEBOX = Builders.containerBuilder()
87 .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
88 .withChild(CONT_PLAYER)
89 .withChild(Builders.containerBuilder().withNodeIdentifier(LIBRARY_NID).build())
91 // operational contains one child the same as in config and one additional
92 private static final ContainerNode OPER_JUKEBOX = Builders.containerBuilder()
93 .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
94 .withChild(CONT_PLAYER)
95 .withChild(Builders.mapBuilder().withNodeIdentifier(PLAYLIST_NID).build())
98 private RestconfDataServiceImpl dataService;
101 private DOMTransactionChain domTransactionChain;
103 private UriInfo uriInfo;
105 private DOMDataTreeReadWriteTransaction readWrite;
107 private DOMDataTreeReadTransaction read;
109 private DOMDataTreeWriteTransaction write;
111 private DOMMountPointService mountPointService;
113 private DOMMountPoint mountPoint;
115 private DOMDataBroker mountDataBroker;
117 private NetconfDataTreeService netconfService;
119 private DOMActionService actionService;
121 private RestconfStreamsSubscriptionService delegRestconfSubscrService;
123 private MultivaluedMap<String, String> queryParamenters;
125 private AsyncResponse asyncResponse;
128 public void setUp() throws Exception {
129 doReturn(Set.of()).when(queryParamenters).entrySet();
130 doReturn(queryParamenters).when(uriInfo).getQueryParameters();
132 doReturn(CommitInfo.emptyFluentFuture()).when(readWrite).commit();
134 final var dataBroker = mock(DOMDataBroker.class);
135 doReturn(read).when(dataBroker).newReadOnlyTransaction();
136 doReturn(readWrite).when(dataBroker).newReadWriteTransaction();
138 dataService = new RestconfDataServiceImpl(() -> DatabindContext.ofModel(JUKEBOX_SCHEMA), dataBroker,
139 mountPointService, delegRestconfSubscrService, actionService, new StreamsConfiguration(0, 1, 0, false));
140 doReturn(Optional.of(mountPoint)).when(mountPointService)
141 .getMountPoint(any(YangInstanceIdentifier.class));
142 doReturn(Optional.of(FixedDOMSchemaService.of(JUKEBOX_SCHEMA))).when(mountPoint)
143 .getService(DOMSchemaService.class);
144 doReturn(Optional.of(mountDataBroker)).when(mountPoint).getService(DOMDataBroker.class);
145 doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class);
146 doReturn(read).when(mountDataBroker).newReadOnlyTransaction();
147 doReturn(readWrite).when(mountDataBroker).newReadWriteTransaction();
151 public void testReadData() {
152 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
153 doReturn(immediateFluentFuture(Optional.of(EMPTY_JUKEBOX))).when(read)
154 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
155 doReturn(immediateFluentFuture(Optional.empty()))
156 .when(read).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
157 final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
158 assertNotNull(response);
159 assertEquals(200, response.getStatus());
160 assertEquals(EMPTY_JUKEBOX, ((NormalizedNodePayload) response.getEntity()).data());
164 public void testReadRootData() {
165 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
166 doReturn(immediateFluentFuture(Optional.of(wrapNodeByDataRootContainer(CONFIG_JUKEBOX))))
168 .read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
169 doReturn(immediateFluentFuture(Optional.of(wrapNodeByDataRootContainer(OPER_JUKEBOX))))
171 .read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.of());
172 final Response response = dataService.readData(uriInfo);
173 assertNotNull(response);
174 assertEquals(200, response.getStatus());
176 final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
177 assertTrue(data instanceof ContainerNode);
178 final Collection<DataContainerChild> rootNodes = ((ContainerNode) data).body();
179 assertEquals(1, rootNodes.size());
180 final Collection<DataContainerChild> allDataChildren = ((ContainerNode) rootNodes.iterator().next()).body();
181 assertEquals(3, allDataChildren.size());
184 private static ContainerNode wrapNodeByDataRootContainer(final DataContainerChild data) {
185 return Builders.containerBuilder()
186 .withNodeIdentifier(NodeIdentifier.create(SchemaContext.NAME))
192 * Test read data from mount point when both {@link LogicalDatastoreType#CONFIGURATION} and
193 * {@link LogicalDatastoreType#OPERATIONAL} contains the same data and some additional data to be merged.
196 public void testReadDataMountPoint() {
197 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
198 doReturn(immediateFluentFuture(Optional.of(CONFIG_JUKEBOX))).when(read)
199 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
200 doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(read)
201 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
203 final Response response = dataService.readData(
204 "example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", uriInfo);
206 assertNotNull(response);
207 assertEquals(200, response.getStatus());
209 // response must contain all child nodes from config and operational containers merged in one container
210 final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
211 assertTrue(data instanceof ContainerNode);
212 assertEquals(3, ((ContainerNode) data).size());
213 assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
214 assertNotNull(((ContainerNode) data).childByArg(LIBRARY_NID));
215 assertNotNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
219 public void testReadDataNoData() {
220 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
221 doReturn(immediateFluentFuture(Optional.empty()))
222 .when(read).read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
223 doReturn(immediateFluentFuture(Optional.empty()))
224 .when(read).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
226 final var errors = assertThrows(RestconfDocumentedException.class,
227 () -> dataService.readData("example-jukebox:jukebox", uriInfo)).getErrors();
228 assertEquals(1, errors.size());
229 final var error = errors.get(0);
230 assertEquals(ErrorType.PROTOCOL, error.getErrorType());
231 assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
232 assertEquals("Request could not be completed because the relevant data model content does not exist",
233 error.getErrorMessage());
237 * Read data from config datastore according to content parameter.
240 public void testReadDataConfigTest() {
241 final MultivaluedHashMap<String, String> parameters = new MultivaluedHashMap<>();
242 parameters.put("content", List.of("config"));
244 doReturn(parameters).when(uriInfo).getQueryParameters();
245 doReturn(immediateFluentFuture(Optional.of(CONFIG_JUKEBOX))).when(read)
246 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
248 final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
250 assertNotNull(response);
251 assertEquals(200, response.getStatus());
253 // response must contain only config data
254 final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
256 // config data present
257 assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
258 assertNotNull(((ContainerNode) data).childByArg(LIBRARY_NID));
261 assertNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
265 * Read data from operational datastore according to content parameter.
268 public void testReadDataOperationalTest() {
269 final MultivaluedHashMap<String, String> parameters = new MultivaluedHashMap<>();
270 parameters.put("content", List.of("nonconfig"));
272 doReturn(parameters).when(uriInfo).getQueryParameters();
273 doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(read)
274 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
276 final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
278 assertNotNull(response);
279 assertEquals(200, response.getStatus());
281 // response must contain only operational data
282 final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
284 // state data present
285 assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
286 assertNotNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
288 // config data absent
289 assertNull(((ContainerNode) data).childByArg(LIBRARY_NID));
293 public void testPutData() {
294 doReturn(immediateTrueFluentFuture()).when(read)
295 .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
296 doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
297 final var response = dataService.putDataJSON("example-jukebox:jukebox", uriInfo, stringInputStream("""
299 "example-jukebox:jukebox" : {
305 assertNotNull(response);
306 assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
310 public void testPutDataWithMountPoint() {
311 doReturn(immediateTrueFluentFuture()).when(read)
312 .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
313 doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
314 final var response = dataService.putDataXML("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox",
315 uriInfo, stringInputStream("""
316 <jukebox xmlns="http://example.com/ns/example-jukebox">
321 assertNotNull(response);
322 assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
325 private static InputStream stringInputStream(final String str) {
326 return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
330 public void testPostData() {
331 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
332 doReturn(immediateFalseFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
333 doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID,
334 Builders.containerBuilder().withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME)).build());
335 doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
337 final var response = dataService.postDataJSON(new ByteArrayInputStream("""
339 "example-jukebox:jukebox" : {
341 }""".getBytes(StandardCharsets.UTF_8)), uriInfo);
342 assertEquals(201, response.getStatus());
343 assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox"), response.getLocation());
347 public void testPostMapEntryData() {
348 doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
349 final var node = PLAYLIST_IID.node(BAND_ENTRY.name());
350 doReturn(immediateFalseFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, node);
351 doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, node, BAND_ENTRY);
352 doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
354 final var response = dataService.postDataJSON("example-jukebox:jukebox", new ByteArrayInputStream("""
356 "example-jukebox:playlist" : {
357 "name" : "name of band",
358 "description" : "band description"
360 }""".getBytes(StandardCharsets.UTF_8)),
362 assertEquals(201, response.getStatus());
363 assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=name%20of%20band"),
364 response.getLocation());
368 public void testDeleteData() {
369 doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
370 doReturn(immediateTrueFluentFuture())
371 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
372 final var captor = ArgumentCaptor.forClass(Response.class);
373 doReturn(true).when(asyncResponse).resume(captor.capture());
374 dataService.deleteData("example-jukebox:jukebox", asyncResponse);
376 assertEquals(204, captor.getValue().getStatus());
380 public void testDeleteDataNotExisting() {
381 doReturn(immediateFalseFluentFuture())
382 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
383 final var captor = ArgumentCaptor.forClass(RestconfDocumentedException.class);
384 doReturn(true).when(asyncResponse).resume(captor.capture());
385 dataService.deleteData("example-jukebox:jukebox", asyncResponse);
387 final var errors = captor.getValue().getErrors();
388 assertEquals(1, errors.size());
389 final var error = errors.get(0);
390 assertEquals(ErrorType.PROTOCOL, error.getErrorType());
391 assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
395 * Test of deleting data on mount point.
398 public void testDeleteDataMountPoint() {
399 doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
400 doReturn(immediateTrueFluentFuture())
401 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
402 final var captor = ArgumentCaptor.forClass(Response.class);
403 doReturn(true).when(asyncResponse).resume(captor.capture());
404 dataService.deleteData("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", asyncResponse);
406 assertEquals(204, captor.getValue().getStatus());
410 public void testPatchData() {
411 final var patch = new PatchContext("test patch id", List.of(
412 new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
413 new PatchEntity("replace data", Operation.Replace, JUKEBOX_IID, EMPTY_JUKEBOX),
414 new PatchEntity("delete data", Operation.Delete, GAP_IID)));
416 doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
417 doReturn(immediateFalseFluentFuture())
418 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
419 doReturn(immediateTrueFluentFuture())
420 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
421 final var status = dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null);
422 assertTrue(status.ok());
423 assertEquals(3, status.editCollection().size());
424 assertEquals("replace data", status.editCollection().get(1).getEditId());
428 public void testPatchDataMountPoint() throws Exception {
429 final var patch = new PatchContext("test patch id", List.of(
430 new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
431 new PatchEntity("replace data", Operation.Replace, JUKEBOX_IID, EMPTY_JUKEBOX),
432 new PatchEntity("delete data", Operation.Delete, GAP_IID)));
434 doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
435 doReturn(immediateFalseFluentFuture())
436 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
437 doReturn(immediateTrueFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
439 final var status = dataService.yangPatchData(JUKEBOX_SCHEMA, patch, mountPoint);
440 assertTrue(status.ok());
441 assertEquals(3, status.editCollection().size());
442 assertNull(status.globalErrors());
446 public void testPatchDataDeleteNotExist() {
447 final PatchContext patch = new PatchContext("test patch id", List.of(
448 new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
449 new PatchEntity("remove data", Operation.Remove, GAP_IID),
450 new PatchEntity("delete data", Operation.Delete, GAP_IID)));
452 doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
453 doReturn(immediateFalseFluentFuture())
454 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
455 doReturn(immediateFalseFluentFuture())
456 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
457 doReturn(true).when(readWrite).cancel();
459 final PatchStatusContext status = dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null);
461 assertFalse(status.ok());
462 assertEquals(3, status.editCollection().size());
463 assertTrue(status.editCollection().get(0).isOk());
464 assertTrue(status.editCollection().get(1).isOk());
465 assertFalse(status.editCollection().get(2).isOk());
466 assertFalse(status.editCollection().get(2).getEditErrors().isEmpty());
467 final String errorMessage = status.editCollection().get(2).getEditErrors().get(0).getErrorMessage();
468 assertEquals("Data does not exist", errorMessage);
472 public void testGetRestconfStrategy() {
473 RestconfStrategy restconfStrategy = dataService.getRestconfStrategy(mountPoint);
474 assertTrue(restconfStrategy instanceof MdsalRestconfStrategy);
476 doReturn(Optional.of(netconfService)).when(mountPoint).getService(NetconfDataTreeService.class);
477 restconfStrategy = dataService.getRestconfStrategy(mountPoint);
478 assertTrue(restconfStrategy instanceof NetconfRestconfStrategy);