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.NodeIdentifier;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
55 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
56 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
57 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
58 import org.w3c.dom.DOMException;
60 @RunWith(MockitoJUnitRunner.StrictStubs.class)
61 public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyTest {
63 private NetconfDataTreeService netconfService;
66 public void before() {
67 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
68 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
69 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
70 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).lock();
71 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
72 .delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
73 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),
78 RestconfStrategy newStrategy(final DatabindContext databind) {
79 return new NetconfRestconfStrategy(databind, netconfService, null, null, null, null);
83 RestconfStrategy testDeleteDataStrategy() {
84 return jukeboxStrategy();
88 RestconfStrategy testNegativeDeleteDataStrategy() {
89 doReturn(Futures.immediateFailedFuture(new TransactionCommitFailedException(
90 "Commit of transaction " + this + " failed", new NetconfDocumentedException("id",
91 ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR)))).when(netconfService).commit();
92 return jukeboxStrategy();
96 public void testDeleteFullList() {
97 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
98 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "playlist"))
99 .node(SONG_QNAME).build();
100 final var songListWildcardPath = songListPath.node(NodeIdentifierWithPredicates.of(SONG_QNAME));
101 final var song1Path = songListPath.node(SONG1.name());
102 final var song2Path = songListPath.node(SONG2.name());
103 final var songListData = ImmutableNodes.newUserMapBuilder()
104 .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
105 .withChild(SONG1).withChild(SONG2).build();
106 final var songKeyFields = List.of(YangInstanceIdentifier.of(SONG_INDEX_QNAME));
108 // data fetched using key field names to minimize amount of data returned
109 doReturn(immediateFluentFuture(Optional.of(songListData))).when(netconfService)
110 .getConfig(songListWildcardPath, songKeyFields);
111 // list elements expected to be deleted one by one
112 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
113 .delete(LogicalDatastoreType.CONFIGURATION, song1Path);
114 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
115 .delete(LogicalDatastoreType.CONFIGURATION, song2Path);
117 jukeboxStrategy().delete(new SettableRestconfFuture<>(), null, songListPath);
118 verify(netconfService).getConfig(songListWildcardPath, songKeyFields);
119 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song1Path);
120 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song2Path);
121 verify(netconfService, never()).delete(LogicalDatastoreType.CONFIGURATION, songListPath);
125 RestconfStrategy testPostContainerDataStrategy() {
126 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
127 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
128 .create(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
129 return jukeboxStrategy();
133 RestconfStrategy testPostListDataStrategy(final MapEntryNode entryNode, final YangInstanceIdentifier node) {
134 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
135 // FIXME: exact match
136 .merge(any(), any(), any(), any());
137 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
138 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).create(
139 LogicalDatastoreType.CONFIGURATION, node, entryNode, Optional.empty());
140 return jukeboxStrategy();
144 RestconfStrategy testPostDataFailStrategy(final DOMException domException) {
145 doReturn(immediateFailedFluentFuture(domException)).when(netconfService)
146 // FIXME: exact match
147 .create(any(), any(), any(), any());
148 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
149 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
150 return jukeboxStrategy();
154 RestconfStrategy testPatchContainerDataStrategy() {
155 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),any(),
157 return jukeboxStrategy();
161 RestconfStrategy testPatchLeafDataStrategy() {
162 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
163 .merge(any(), any(), any(), any());
164 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
165 return jukeboxStrategy();
169 RestconfStrategy testPatchListDataStrategy() {
170 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
171 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
172 .merge(any(), any(),any(),any());
173 return jukeboxStrategy();
177 public void testPutCreateContainerData() {
178 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
179 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
180 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
181 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
183 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
184 verify(netconfService).lock();
185 verify(netconfService).getConfig(JUKEBOX_IID);
186 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
191 public void testPutReplaceContainerData() {
192 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
193 .getConfig(JUKEBOX_IID);
194 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
195 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
196 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
198 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
199 verify(netconfService).getConfig(JUKEBOX_IID);
200 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
205 public void testPutCreateLeafData() {
206 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(GAP_IID);
207 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
208 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
209 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
211 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
212 verify(netconfService).getConfig(GAP_IID);
213 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
217 public void testPutReplaceLeafData() {
218 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
220 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
221 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
222 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
224 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
225 verify(netconfService).getConfig(GAP_IID);
226 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
230 public void testPutCreateListData() {
231 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
232 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
233 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
234 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
236 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
237 verify(netconfService).getConfig(JUKEBOX_IID);
238 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
243 * Test for put with insert=after option for last element of the list. Here we're trying to insert new element of
244 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
245 * this we check how many times replace transaction was called to know how many items was inserted.
246 * @throws ParseException if ApiPath string cannot be parsed
249 public void testPutDataWithInsertAfterLast() throws ParseException {
250 // Spy of jukeboxStrategy will be used later to count how many items was inserted
251 final var spyStrategy = spy(jukeboxStrategy());
252 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
253 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
254 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
256 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
257 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
258 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
260 // Inserting new song at 3rd position (aka as last element)
261 spyStrategy.dataPUT(ServerRequest.of(QueryParameters.of(
262 // insert new item after last existing item in list
263 InsertParam.AFTER, PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")),
264 PrettyPrintParam.TRUE),
265 ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
266 new JsonResourceBody(stringInputStream("""
268 "example-jukebox:song" : [
276 // Counting how many times we insert items in list
277 verify(spyTx, times(3)).replace(any(), any());
281 * Test for post with insert=after option for last element of the list. Here we're trying to insert new element of
282 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
283 * this we check how many times replace transaction was called to know how many items was inserted.
284 * @throws ParseException if ApiPath string cannot be parsed
287 public void testPostDataWithInsertAfterLast() throws ParseException {
288 // Spy of jukeboxStrategy will be used later to count how many items was inserted
289 final var spyStrategy = spy(jukeboxStrategy());
290 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
291 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
292 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
294 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
295 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
296 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
298 // Inserting new song at 3rd position (aka as last element)
299 spyStrategy.dataPOST(ServerRequest.of(QueryParameters.of(InsertParam.AFTER,
300 PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")), PrettyPrintParam.FALSE),
301 ApiPath.parse("example-jukebox:jukebox/playlist=0"),
302 // insert new item after last existing item in list
303 new JsonDataPostBody(stringInputStream("""
305 "example-jukebox:song" : [
313 // Counting how many times we insert items in list
314 verify(spyTx, times(3)).replace(any(), any());
318 public void testPutReplaceListData() {
319 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
320 .getConfig(JUKEBOX_IID);
321 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
322 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
323 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
325 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
326 verify(netconfService).getConfig(JUKEBOX_IID);
327 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
332 RestconfStrategy testPatchDataReplaceMergeAndRemoveStrategy() {
333 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
334 .remove(LogicalDatastoreType.CONFIGURATION, ARTIST_IID);
335 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
336 // FIXME: exact match
337 .replace(any(), any(), any(), any());
338 return jukeboxStrategy();
342 RestconfStrategy testPatchDataCreateAndDeleteStrategy() {
343 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
344 .create(LogicalDatastoreType.CONFIGURATION, PLAYER_IID, EMPTY_JUKEBOX, Optional.empty());
345 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
346 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
347 return jukeboxStrategy();
351 RestconfStrategy testPatchMergePutContainerStrategy() {
352 return jukeboxStrategy();
356 RestconfStrategy deleteNonexistentDataTestStrategy() {
357 doReturn(Futures.immediateFailedFuture(
358 new TransactionCommitFailedException("Commit of transaction " + this + " failed",
359 new NetconfDocumentedException("id", ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR))))
360 .when(netconfService).commit();
361 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
362 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
363 return jukeboxStrategy();
367 void assertTestDeleteNonexistentData(final PatchStatusContext status, final PatchStatusEntity edit) {
368 assertNull(edit.getEditErrors());
369 final var globalErrors = status.globalErrors();
370 assertNotNull(globalErrors);
371 assertEquals(1, globalErrors.size());
372 final var globalError = globalErrors.get(0);
373 assertEquals("Data does not exist", globalError.getErrorMessage());
374 assertEquals(ErrorType.PROTOCOL, globalError.getErrorType());
375 assertEquals(ErrorTag.DATA_MISSING, globalError.getErrorTag());
379 RestconfStrategy readDataConfigTestStrategy() {
380 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
381 return mockStrategy();
385 RestconfStrategy readAllHavingOnlyConfigTestStrategy() {
386 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
387 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).get(PATH);
388 return mockStrategy();
392 RestconfStrategy readAllHavingOnlyNonConfigTestStrategy() {
393 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
394 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
395 return mockStrategy();
399 RestconfStrategy readDataNonConfigTestStrategy() {
400 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
401 return mockStrategy();
405 RestconfStrategy readContainerDataAllTestStrategy() {
406 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
407 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
408 return mockStrategy();
412 RestconfStrategy readContainerDataConfigNoValueOfContentTestStrategy() {
413 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
414 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
415 return mockStrategy();
419 RestconfStrategy readListDataAllTestStrategy() {
420 doReturn(immediateFluentFuture(Optional.of(LIST_DATA))).when(netconfService).get(PATH_3);
421 doReturn(immediateFluentFuture(Optional.of(LIST_DATA_2))).when(netconfService).getConfig(PATH_3);
422 return mockStrategy();
426 RestconfStrategy readOrderedListDataAllTestStrategy() {
427 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_1))).when(netconfService).get(PATH_3);
428 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_2))).when(netconfService).getConfig(PATH_3);
429 return mockStrategy();
433 RestconfStrategy readUnkeyedListDataAllTestStrategy() {
434 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_1))).when(netconfService).get(PATH_3);
435 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_2))).when(netconfService).getConfig(PATH_3);
436 return mockStrategy();
440 RestconfStrategy readLeafListDataAllTestStrategy() {
441 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_1))).when(netconfService)
442 .get(LEAF_SET_NODE_PATH);
443 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_2))).when(netconfService)
444 .getConfig(LEAF_SET_NODE_PATH);
445 return mockStrategy();
449 RestconfStrategy readOrderedLeafListDataAllTestStrategy() {
450 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_1))).when(netconfService)
451 .get(LEAF_SET_NODE_PATH);
452 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_2))).when(netconfService)
453 .getConfig(LEAF_SET_NODE_PATH);
454 return mockStrategy();
458 RestconfStrategy readDataWrongPathOrNoContentTestStrategy() {
459 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
460 return mockStrategy();