2 * Copyright (c) 2023 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.nb.rfc8040.rests.transactions;
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertNotNull;
12 import static org.junit.Assert.assertNull;
13 import static org.mockito.ArgumentMatchers.any;
14 import static org.mockito.Mockito.doReturn;
15 import static org.mockito.Mockito.mock;
16 import static org.mockito.Mockito.never;
17 import static org.mockito.Mockito.spy;
18 import static org.mockito.Mockito.times;
19 import static org.mockito.Mockito.verify;
20 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFailedFluentFuture;
21 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
23 import com.google.common.util.concurrent.Futures;
24 import java.text.ParseException;
25 import java.util.List;
26 import java.util.Optional;
27 import org.junit.Before;
28 import org.junit.Test;
29 import org.junit.runner.RunWith;
30 import org.mockito.Mock;
31 import org.mockito.junit.MockitoJUnitRunner;
32 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
33 import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
34 import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult;
35 import org.opendaylight.netconf.api.NetconfDocumentedException;
36 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
37 import org.opendaylight.restconf.api.ApiPath;
38 import org.opendaylight.restconf.api.QueryParameters;
39 import org.opendaylight.restconf.api.query.InsertParam;
40 import org.opendaylight.restconf.api.query.PointParam;
41 import org.opendaylight.restconf.api.query.PrettyPrintParam;
42 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
43 import org.opendaylight.restconf.server.api.DatabindContext;
44 import org.opendaylight.restconf.server.api.JsonDataPostBody;
45 import org.opendaylight.restconf.server.api.JsonResourceBody;
46 import org.opendaylight.restconf.server.api.PatchStatusContext;
47 import org.opendaylight.restconf.server.api.PatchStatusEntity;
48 import org.opendaylight.restconf.server.api.ServerRequest;
49 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
50 import org.opendaylight.yangtools.yang.common.ErrorTag;
51 import org.opendaylight.yangtools.yang.common.ErrorType;
52 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
54 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
55 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
56 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
57 import org.w3c.dom.DOMException;
59 @RunWith(MockitoJUnitRunner.StrictStubs.class)
60 public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyTest {
62 private NetconfDataTreeService netconfService;
65 public void before() {
66 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
67 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
68 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
69 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).lock();
70 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
71 .delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
72 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),
77 RestconfStrategy newStrategy(final DatabindContext databind) {
78 return new NetconfRestconfStrategy(databind, netconfService, null, null, null, null);
82 RestconfStrategy testDeleteDataStrategy() {
83 return jukeboxStrategy();
87 RestconfStrategy testNegativeDeleteDataStrategy() {
88 doReturn(Futures.immediateFailedFuture(new TransactionCommitFailedException(
89 "Commit of transaction " + this + " failed", new NetconfDocumentedException("id",
90 ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR)))).when(netconfService).commit();
91 return jukeboxStrategy();
95 public void testDeleteFullList() {
96 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
97 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "playlist"))
98 .node(SONG_QNAME).build();
99 final var songListWildcardPath = songListPath.node(NodeIdentifierWithPredicates.of(SONG_QNAME));
100 final var song1Path = songListPath.node(SONG1.name());
101 final var song2Path = songListPath.node(SONG2.name());
102 final var songListData = Builders.orderedMapBuilder()
103 .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SONG_QNAME))
104 .withChild(SONG1).withChild(SONG2).build();
105 final var songKeyFields = List.of(YangInstanceIdentifier.of(SONG_INDEX_QNAME));
107 // data fetched using key field names to minimize amount of data returned
108 doReturn(immediateFluentFuture(Optional.of(songListData))).when(netconfService)
109 .getConfig(songListWildcardPath, songKeyFields);
110 // list elements expected to be deleted one by one
111 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
112 .delete(LogicalDatastoreType.CONFIGURATION, song1Path);
113 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
114 .delete(LogicalDatastoreType.CONFIGURATION, song2Path);
116 jukeboxStrategy().delete(new SettableRestconfFuture<>(), null, songListPath);
117 verify(netconfService).getConfig(songListWildcardPath, songKeyFields);
118 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song1Path);
119 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song2Path);
120 verify(netconfService, never()).delete(LogicalDatastoreType.CONFIGURATION, songListPath);
124 RestconfStrategy testPostContainerDataStrategy() {
125 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
126 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
127 .create(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
128 return jukeboxStrategy();
132 RestconfStrategy testPostListDataStrategy(final MapEntryNode entryNode, final YangInstanceIdentifier node) {
133 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
134 // FIXME: exact match
135 .merge(any(), any(), any(), any());
136 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
137 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).create(
138 LogicalDatastoreType.CONFIGURATION, node, entryNode, Optional.empty());
139 return jukeboxStrategy();
143 RestconfStrategy testPostDataFailStrategy(final DOMException domException) {
144 doReturn(immediateFailedFluentFuture(domException)).when(netconfService)
145 // FIXME: exact match
146 .create(any(), any(), any(), any());
147 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
148 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
149 return jukeboxStrategy();
153 RestconfStrategy testPatchContainerDataStrategy() {
154 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),any(),
156 return jukeboxStrategy();
160 RestconfStrategy testPatchLeafDataStrategy() {
161 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
162 .merge(any(), any(), any(), any());
163 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
164 return jukeboxStrategy();
168 RestconfStrategy testPatchListDataStrategy() {
169 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
170 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
171 .merge(any(), any(),any(),any());
172 return jukeboxStrategy();
176 public void testPutCreateContainerData() {
177 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
178 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
179 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
180 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
182 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
183 verify(netconfService).lock();
184 verify(netconfService).getConfig(JUKEBOX_IID);
185 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
190 public void testPutReplaceContainerData() {
191 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
192 .getConfig(JUKEBOX_IID);
193 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
194 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
195 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
197 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
198 verify(netconfService).getConfig(JUKEBOX_IID);
199 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
204 public void testPutCreateLeafData() {
205 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(GAP_IID);
206 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
207 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
208 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
210 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
211 verify(netconfService).getConfig(GAP_IID);
212 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
216 public void testPutReplaceLeafData() {
217 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
219 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
220 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
221 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
223 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
224 verify(netconfService).getConfig(GAP_IID);
225 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
229 public void testPutCreateListData() {
230 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
231 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
232 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
233 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
235 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
236 verify(netconfService).getConfig(JUKEBOX_IID);
237 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
242 * Test for put with insert=after option for last element of the list. Here we're trying to insert new element of
243 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
244 * this we check how many times replace transaction was called to know how many items was inserted.
245 * @throws ParseException if ApiPath string cannot be parsed
248 public void testPutDataWithInsertAfterLast() throws ParseException {
249 // Spy of jukeboxStrategy will be used later to count how many items was inserted
250 final var spyStrategy = spy(jukeboxStrategy());
251 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
252 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
253 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
255 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
256 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
257 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
259 // Inserting new song at 3rd position (aka as last element)
260 spyStrategy.dataPUT(ServerRequest.of(QueryParameters.of(
261 // insert new item after last existing item in list
262 InsertParam.AFTER, PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")),
263 PrettyPrintParam.TRUE),
264 ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
265 new JsonResourceBody(stringInputStream("""
267 "example-jukebox:song" : [
275 // Counting how many times we insert items in list
276 verify(spyTx, times(3)).replace(any(), any());
280 * Test for post with insert=after option for last element of the list. Here we're trying to insert new element of
281 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
282 * this we check how many times replace transaction was called to know how many items was inserted.
283 * @throws ParseException if ApiPath string cannot be parsed
286 public void testPostDataWithInsertAfterLast() throws ParseException {
287 // Spy of jukeboxStrategy will be used later to count how many items was inserted
288 final var spyStrategy = spy(jukeboxStrategy());
289 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
290 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
291 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
293 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
294 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
295 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
297 // Inserting new song at 3rd position (aka as last element)
298 spyStrategy.dataPOST(ServerRequest.of(QueryParameters.of(InsertParam.AFTER,
299 PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")), PrettyPrintParam.FALSE),
300 ApiPath.parse("example-jukebox:jukebox/playlist=0"),
301 // insert new item after last existing item in list
302 new JsonDataPostBody(stringInputStream("""
304 "example-jukebox:song" : [
312 // Counting how many times we insert items in list
313 verify(spyTx, times(3)).replace(any(), any());
317 public void testPutReplaceListData() {
318 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
319 .getConfig(JUKEBOX_IID);
320 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
321 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
322 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
324 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
325 verify(netconfService).getConfig(JUKEBOX_IID);
326 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
331 RestconfStrategy testPatchDataReplaceMergeAndRemoveStrategy() {
332 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
333 .remove(LogicalDatastoreType.CONFIGURATION, ARTIST_IID);
334 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
335 // FIXME: exact match
336 .replace(any(), any(), any(), any());
337 return jukeboxStrategy();
341 RestconfStrategy testPatchDataCreateAndDeleteStrategy() {
342 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
343 .create(LogicalDatastoreType.CONFIGURATION, PLAYER_IID, EMPTY_JUKEBOX, Optional.empty());
344 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
345 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
346 return jukeboxStrategy();
350 RestconfStrategy testPatchMergePutContainerStrategy() {
351 return jukeboxStrategy();
355 RestconfStrategy deleteNonexistentDataTestStrategy() {
356 doReturn(Futures.immediateFailedFuture(
357 new TransactionCommitFailedException("Commit of transaction " + this + " failed",
358 new NetconfDocumentedException("id", ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR))))
359 .when(netconfService).commit();
360 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
361 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
362 return jukeboxStrategy();
366 void assertTestDeleteNonexistentData(final PatchStatusContext status, final PatchStatusEntity edit) {
367 assertNull(edit.getEditErrors());
368 final var globalErrors = status.globalErrors();
369 assertNotNull(globalErrors);
370 assertEquals(1, globalErrors.size());
371 final var globalError = globalErrors.get(0);
372 assertEquals("Data does not exist", globalError.getErrorMessage());
373 assertEquals(ErrorType.PROTOCOL, globalError.getErrorType());
374 assertEquals(ErrorTag.DATA_MISSING, globalError.getErrorTag());
378 RestconfStrategy readDataConfigTestStrategy() {
379 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
380 return mockStrategy();
384 RestconfStrategy readAllHavingOnlyConfigTestStrategy() {
385 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
386 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).get(PATH);
387 return mockStrategy();
391 RestconfStrategy readAllHavingOnlyNonConfigTestStrategy() {
392 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
393 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
394 return mockStrategy();
398 RestconfStrategy readDataNonConfigTestStrategy() {
399 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
400 return mockStrategy();
404 RestconfStrategy readContainerDataAllTestStrategy() {
405 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
406 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
407 return mockStrategy();
411 RestconfStrategy readContainerDataConfigNoValueOfContentTestStrategy() {
412 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
413 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
414 return mockStrategy();
418 RestconfStrategy readListDataAllTestStrategy() {
419 doReturn(immediateFluentFuture(Optional.of(LIST_DATA))).when(netconfService).get(PATH_3);
420 doReturn(immediateFluentFuture(Optional.of(LIST_DATA_2))).when(netconfService).getConfig(PATH_3);
421 return mockStrategy();
425 RestconfStrategy readOrderedListDataAllTestStrategy() {
426 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_1))).when(netconfService).get(PATH_3);
427 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_2))).when(netconfService).getConfig(PATH_3);
428 return mockStrategy();
432 RestconfStrategy readUnkeyedListDataAllTestStrategy() {
433 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_1))).when(netconfService).get(PATH_3);
434 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_2))).when(netconfService).getConfig(PATH_3);
435 return mockStrategy();
439 RestconfStrategy readLeafListDataAllTestStrategy() {
440 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_1))).when(netconfService)
441 .get(LEAF_SET_NODE_PATH);
442 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_2))).when(netconfService)
443 .getConfig(LEAF_SET_NODE_PATH);
444 return mockStrategy();
448 RestconfStrategy readOrderedLeafListDataAllTestStrategy() {
449 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_1))).when(netconfService)
450 .get(LEAF_SET_NODE_PATH);
451 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_2))).when(netconfService)
452 .getConfig(LEAF_SET_NODE_PATH);
453 return mockStrategy();
457 RestconfStrategy readDataWrongPathOrNoContentTestStrategy() {
458 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
459 return mockStrategy();