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.ErrorMessage;
39 import org.opendaylight.restconf.api.QueryParameters;
40 import org.opendaylight.restconf.api.query.InsertParam;
41 import org.opendaylight.restconf.api.query.PointParam;
42 import org.opendaylight.restconf.api.query.PrettyPrintParam;
43 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
44 import org.opendaylight.restconf.server.api.DatabindContext;
45 import org.opendaylight.restconf.server.api.JsonDataPostBody;
46 import org.opendaylight.restconf.server.api.JsonResourceBody;
47 import org.opendaylight.restconf.server.api.PatchStatusContext;
48 import org.opendaylight.restconf.server.api.PatchStatusEntity;
49 import org.opendaylight.restconf.server.api.ServerRequest;
50 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
51 import org.opendaylight.yangtools.yang.common.ErrorTag;
52 import org.opendaylight.yangtools.yang.common.ErrorType;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
56 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
57 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
58 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
59 import org.w3c.dom.DOMException;
61 @RunWith(MockitoJUnitRunner.StrictStubs.class)
62 public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyTest {
64 private NetconfDataTreeService netconfService;
67 public void before() {
68 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
69 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
70 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
71 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).lock();
72 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
73 .delete(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
74 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),
79 RestconfStrategy newStrategy(final DatabindContext databind) {
80 return new NetconfRestconfStrategy(databind, netconfService, null, null, null, null);
84 RestconfStrategy testDeleteDataStrategy() {
85 return jukeboxStrategy();
89 RestconfStrategy testNegativeDeleteDataStrategy() {
90 doReturn(Futures.immediateFailedFuture(new TransactionCommitFailedException(
91 "Commit of transaction " + this + " failed", new NetconfDocumentedException("id",
92 ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR)))).when(netconfService).commit();
93 return jukeboxStrategy();
97 public void testDeleteFullList() {
98 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
99 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "playlist"))
100 .node(SONG_QNAME).build();
101 final var songListWildcardPath = songListPath.node(NodeIdentifierWithPredicates.of(SONG_QNAME));
102 final var song1Path = songListPath.node(SONG1.name());
103 final var song2Path = songListPath.node(SONG2.name());
104 final var songListData = ImmutableNodes.newUserMapBuilder()
105 .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
106 .withChild(SONG1).withChild(SONG2).build();
107 final var songKeyFields = List.of(YangInstanceIdentifier.of(SONG_INDEX_QNAME));
109 // data fetched using key field names to minimize amount of data returned
110 doReturn(immediateFluentFuture(Optional.of(songListData))).when(netconfService)
111 .getConfig(songListWildcardPath, songKeyFields);
112 // list elements expected to be deleted one by one
113 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
114 .delete(LogicalDatastoreType.CONFIGURATION, song1Path);
115 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
116 .delete(LogicalDatastoreType.CONFIGURATION, song2Path);
118 jukeboxStrategy().delete(new SettableRestconfFuture<>(), null, songListPath);
119 verify(netconfService).getConfig(songListWildcardPath, songKeyFields);
120 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song1Path);
121 verify(netconfService).delete(LogicalDatastoreType.CONFIGURATION, song2Path);
122 verify(netconfService, never()).delete(LogicalDatastoreType.CONFIGURATION, songListPath);
126 RestconfStrategy testPostContainerDataStrategy() {
127 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
128 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
129 .create(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
130 return jukeboxStrategy();
134 RestconfStrategy testPostListDataStrategy(final MapEntryNode entryNode, final YangInstanceIdentifier node) {
135 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
136 // FIXME: exact match
137 .merge(any(), any(), any(), any());
138 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
139 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).create(
140 LogicalDatastoreType.CONFIGURATION, node, entryNode, Optional.empty());
141 return jukeboxStrategy();
145 RestconfStrategy testPostDataFailStrategy(final DOMException domException) {
146 doReturn(immediateFailedFluentFuture(domException)).when(netconfService)
147 // FIXME: exact match
148 .create(any(), any(), any(), any());
149 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).discardChanges();
150 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).unlock();
151 return jukeboxStrategy();
155 RestconfStrategy testPatchContainerDataStrategy() {
156 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).merge(any(), any(),any(),
158 return jukeboxStrategy();
162 RestconfStrategy testPatchLeafDataStrategy() {
163 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
164 .merge(any(), any(), any(), any());
165 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
166 return jukeboxStrategy();
170 RestconfStrategy testPatchListDataStrategy() {
171 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
172 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
173 .merge(any(), any(),any(),any());
174 return jukeboxStrategy();
178 public void testPutCreateContainerData() {
179 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
180 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
181 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
182 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
184 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
185 verify(netconfService).lock();
186 verify(netconfService).getConfig(JUKEBOX_IID);
187 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
192 public void testPutReplaceContainerData() {
193 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
194 .getConfig(JUKEBOX_IID);
195 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
196 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
197 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX, Optional.empty());
199 jukeboxStrategy().putData(JUKEBOX_IID, EMPTY_JUKEBOX, null);
200 verify(netconfService).getConfig(JUKEBOX_IID);
201 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX,
206 public void testPutCreateLeafData() {
207 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(GAP_IID);
208 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
209 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
210 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
212 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
213 verify(netconfService).getConfig(GAP_IID);
214 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
218 public void testPutReplaceLeafData() {
219 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
221 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
222 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
223 .replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
225 jukeboxStrategy().putData(GAP_IID, GAP_LEAF, null);
226 verify(netconfService).getConfig(GAP_IID);
227 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, GAP_IID, GAP_LEAF, Optional.empty());
231 public void testPutCreateListData() {
232 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(JUKEBOX_IID);
233 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
234 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
235 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
237 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
238 verify(netconfService).getConfig(JUKEBOX_IID);
239 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
244 * Test for put with insert=after option for last element of the list. Here we're trying to insert new element of
245 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
246 * this we check how many times replace transaction was called to know how many items was inserted.
247 * @throws ParseException if ApiPath string cannot be parsed
250 public void testPutDataWithInsertAfterLast() throws ParseException {
251 // Spy of jukeboxStrategy will be used later to count how many items was inserted
252 final var spyStrategy = spy(jukeboxStrategy());
253 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
254 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
255 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
257 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
258 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
259 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
261 // Inserting new song at 3rd position (aka as last element)
262 spyStrategy.dataPUT(ServerRequest.of(QueryParameters.of(
263 // insert new item after last existing item in list
264 InsertParam.AFTER, PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")),
265 PrettyPrintParam.TRUE),
266 ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
267 new JsonResourceBody(stringInputStream("""
269 "example-jukebox:song" : [
277 // Counting how many times we insert items in list
278 verify(spyTx, times(3)).replace(any(), any());
282 * Test for post with insert=after option for last element of the list. Here we're trying to insert new element of
283 * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
284 * this we check how many times replace transaction was called to know how many items was inserted.
285 * @throws ParseException if ApiPath string cannot be parsed
288 public void testPostDataWithInsertAfterLast() throws ParseException {
289 // Spy of jukeboxStrategy will be used later to count how many items was inserted
290 final var spyStrategy = spy(jukeboxStrategy());
291 final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
292 doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
293 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
295 final var songListPath = YangInstanceIdentifier.builder().node(JUKEBOX_QNAME).node(PLAYLIST_QNAME)
296 .node(NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME, "0")).node(SONG_QNAME).build();
297 doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(spyTx).read(songListPath);
299 // Inserting new song at 3rd position (aka as last element)
300 spyStrategy.dataPOST(ServerRequest.of(QueryParameters.of(InsertParam.AFTER,
301 PointParam.forUriValue("example-jukebox:jukebox/playlist=0/song=2")), PrettyPrintParam.FALSE),
302 ApiPath.parse("example-jukebox:jukebox/playlist=0"),
303 // insert new item after last existing item in list
304 new JsonDataPostBody(stringInputStream("""
306 "example-jukebox:song" : [
314 // Counting how many times we insert items in list
315 verify(spyTx, times(3)).replace(any(), any());
319 public void testPutReplaceListData() {
320 doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)
321 .getConfig(JUKEBOX_IID);
322 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService).commit();
323 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
324 .replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS, Optional.empty());
326 jukeboxStrategy().putData(JUKEBOX_IID, JUKEBOX_WITH_BANDS, null);
327 verify(netconfService).getConfig(JUKEBOX_IID);
328 verify(netconfService).replace(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, JUKEBOX_WITH_BANDS,
333 RestconfStrategy testPatchDataReplaceMergeAndRemoveStrategy() {
334 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
335 .remove(LogicalDatastoreType.CONFIGURATION, ARTIST_IID);
336 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
337 // FIXME: exact match
338 .replace(any(), any(), any(), any());
339 return jukeboxStrategy();
343 RestconfStrategy testPatchDataCreateAndDeleteStrategy() {
344 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
345 .create(LogicalDatastoreType.CONFIGURATION, PLAYER_IID, EMPTY_JUKEBOX, Optional.empty());
346 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
347 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
348 return jukeboxStrategy();
352 RestconfStrategy testPatchMergePutContainerStrategy() {
353 return jukeboxStrategy();
357 RestconfStrategy deleteNonexistentDataTestStrategy() {
358 doReturn(Futures.immediateFailedFuture(
359 new TransactionCommitFailedException("Commit of transaction " + this + " failed",
360 new NetconfDocumentedException("id", ErrorType.RPC, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR))))
361 .when(netconfService).commit();
362 doReturn(Futures.immediateFuture(new DefaultDOMRpcResult())).when(netconfService)
363 .delete(LogicalDatastoreType.CONFIGURATION, CREATE_AND_DELETE_TARGET);
364 return jukeboxStrategy();
368 void assertTestDeleteNonexistentData(final PatchStatusContext status, final PatchStatusEntity edit) {
369 assertNull(edit.getEditErrors());
370 final var globalErrors = status.globalErrors();
371 assertNotNull(globalErrors);
372 assertEquals(1, globalErrors.size());
373 final var globalError = globalErrors.get(0);
374 assertEquals(new ErrorMessage("Data does not exist"), globalError.message());
375 assertEquals(ErrorType.PROTOCOL, globalError.type());
376 assertEquals(ErrorTag.DATA_MISSING, globalError.tag());
380 RestconfStrategy readDataConfigTestStrategy() {
381 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
382 return mockStrategy();
386 RestconfStrategy readAllHavingOnlyConfigTestStrategy() {
387 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
388 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).get(PATH);
389 return mockStrategy();
393 RestconfStrategy readAllHavingOnlyNonConfigTestStrategy() {
394 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
395 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
396 return mockStrategy();
400 RestconfStrategy readDataNonConfigTestStrategy() {
401 doReturn(immediateFluentFuture(Optional.of(DATA_2))).when(netconfService).get(PATH_2);
402 return mockStrategy();
406 RestconfStrategy readContainerDataAllTestStrategy() {
407 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
408 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
409 return mockStrategy();
413 RestconfStrategy readContainerDataConfigNoValueOfContentTestStrategy() {
414 doReturn(immediateFluentFuture(Optional.of(DATA_3))).when(netconfService).getConfig(PATH);
415 doReturn(immediateFluentFuture(Optional.of(DATA_4))).when(netconfService).get(PATH);
416 return mockStrategy();
420 RestconfStrategy readListDataAllTestStrategy() {
421 doReturn(immediateFluentFuture(Optional.of(LIST_DATA))).when(netconfService).get(PATH_3);
422 doReturn(immediateFluentFuture(Optional.of(LIST_DATA_2))).when(netconfService).getConfig(PATH_3);
423 return mockStrategy();
427 RestconfStrategy readOrderedListDataAllTestStrategy() {
428 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_1))).when(netconfService).get(PATH_3);
429 doReturn(immediateFluentFuture(Optional.of(ORDERED_MAP_NODE_2))).when(netconfService).getConfig(PATH_3);
430 return mockStrategy();
434 RestconfStrategy readUnkeyedListDataAllTestStrategy() {
435 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_1))).when(netconfService).get(PATH_3);
436 doReturn(immediateFluentFuture(Optional.of(UNKEYED_LIST_NODE_2))).when(netconfService).getConfig(PATH_3);
437 return mockStrategy();
441 RestconfStrategy readLeafListDataAllTestStrategy() {
442 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_1))).when(netconfService)
443 .get(LEAF_SET_NODE_PATH);
444 doReturn(immediateFluentFuture(Optional.of(LEAF_SET_NODE_2))).when(netconfService)
445 .getConfig(LEAF_SET_NODE_PATH);
446 return mockStrategy();
450 RestconfStrategy readOrderedLeafListDataAllTestStrategy() {
451 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_1))).when(netconfService)
452 .get(LEAF_SET_NODE_PATH);
453 doReturn(immediateFluentFuture(Optional.of(ORDERED_LEAF_SET_NODE_2))).when(netconfService)
454 .getConfig(LEAF_SET_NODE_PATH);
455 return mockStrategy();
459 RestconfStrategy readDataWrongPathOrNoContentTestStrategy() {
460 doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(PATH_2);
461 return mockStrategy();