Fix PUT/POST requests for lists 27/109127/13
authorOleksandr Zharov <oleksandr.zharov@pantheon.tech>
Thu, 30 Nov 2023 11:07:00 +0000 (12:07 +0100)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Tue, 13 Feb 2024 09:59:41 +0000 (09:59 +0000)
Added condition into PUT/POST requests with insert=after option
that covers insert after last element of the list.

JIRA: NETCONF-1180
Change-Id: Ib55818741c82c907648b259c2bd362b9b4348e52
Signed-off-by: Oleksandr Zharov <oleksandr.zharov@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategyTest.java

index e253886bb23d48a557f809b694c3ec112854c1c0..eeeec703b07922d322fba2cd186c26ab25dbe092 100644 (file)
@@ -456,6 +456,13 @@ public abstract class RestconfStrategy {
             tx.replace(childPath, nodeChild);
             lastInsertedPosition++;
         }
+
+        // In case we are inserting after last element
+        if (!before) {
+            if (lastInsertedPosition == lastItemPosition) {
+                tx.replace(path, data);
+            }
+        }
     }
 
     private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
@@ -739,6 +746,13 @@ public abstract class RestconfStrategy {
             tx.replace(grandParentPath.node(nodeChild.name()), nodeChild);
             lastInsertedPosition++;
         }
+
+        // In case we are inserting after last element
+        if (!before) {
+            if (lastInsertedPosition == lastItemPosition) {
+                tx.replace(path, data);
+            }
+        }
     }
 
     private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
index 973fe9e525d2b0236c2514a0a5387f9071490461..4b45f707cce07169bc1769b8311bdc7ca57d94af 100644 (file)
@@ -13,11 +13,15 @@ import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFailedFluentFuture;
 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
 
 import com.google.common.util.concurrent.Futures;
+import java.text.ParseException;
+import java.util.HashMap;
 import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
@@ -29,15 +33,20 @@ import org.opendaylight.mdsal.common.api.TransactionCommitFailedException;
 import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult;
 import org.opendaylight.netconf.api.NetconfDocumentedException;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
+import org.opendaylight.restconf.nb.rfc8040.databind.JsonDataPostBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.opendaylight.yangtools.yang.common.Uint32;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.w3c.dom.DOMException;
 
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
@@ -193,6 +202,103 @@ public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyT
             Optional.empty());
     }
 
+    /**
+     * Test for put with insert=after option for last element of the list. Here we're trying to insert new element of
+     * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
+     * this we check how many times replace transaction was called to know how many items was inserted.
+     * @throws ParseException if ApiPath string cannot be parsed
+     */
+    @Test
+    public void testPutDataWithInsertAfterLast() throws ParseException {
+        // Spy of jukeboxStrategy will be used later to count how many items was inserted
+        final var spyStrategy = spy(jukeboxStrategy());
+        final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
+        doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
+        doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
+
+        // For this test we are using
+        final var songsList = ImmutableNodes.newUserMapBuilder()
+            .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SONG_QNAME))
+            .withChild(ImmutableNodes.newMapEntryBuilder()
+                .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
+                    Uint32.valueOf(1)))
+                .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
+                .build())
+            .withChild(ImmutableNodes.newMapEntryBuilder()
+                .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
+                    Uint32.valueOf(2)))
+                .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
+                .build())
+            .build();
+        doReturn(songsList).when(spyTx).readList(any(YangInstanceIdentifier.class));
+
+        // Creating query params to insert new item after last existing item in list
+        final var queryParams = new HashMap<String, String>();
+        queryParams.put("insert", "after");
+        queryParams.put("point", "example-jukebox:jukebox/playlist=0/song=2");
+
+        // Inserting new song at 3rd position (aka as last element)
+        spyStrategy.dataPUT(ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
+            new JsonResourceBody(stringInputStream("""
+                {
+                  "example-jukebox:song" : [
+                    {
+                       "index": "3",
+                       "id" = "C"
+                    }
+                  ]
+                }""")), queryParams);
+
+        // Counting how many times we insert items in list
+        verify(spyTx, times(3)).replace(any(), any());
+    }
+
+    /**
+     * Test for post with insert=after option for last element of the list. Here we're trying to insert new element of
+     * the ordered list after last existing element. Test uses list with two items and add third one at the end. After
+     * this we check how many times replace transaction was called to know how many items was inserted.
+     * @throws ParseException if ApiPath string cannot be parsed
+     */
+    @Test
+    public void testPostDataWithInsertAfterLast() throws ParseException {
+        // Spy of jukeboxStrategy will be used later to count how many items was inserted
+        final var spyStrategy = spy(jukeboxStrategy());
+        final var spyTx = spy(jukeboxStrategy().prepareWriteExecution());
+        doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
+        doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
+
+        // For this test we are using
+        final var songsList = ImmutableNodes.newUserMapBuilder()
+            .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SONG_QNAME))
+                .withChild(ImmutableNodes.newMapEntryBuilder()
+                    .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME,
+                        SONG_INDEX_QNAME, Uint32.valueOf(1)))
+                    .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
+                    .build())
+                .withChild(ImmutableNodes.newMapEntryBuilder()
+                    .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME,
+                        SONG_INDEX_QNAME, Uint32.valueOf(2)))
+                    .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
+                    .build())
+            .build();
+        doReturn(songsList).when(spyTx).readList(any(YangInstanceIdentifier.class));
+
+        // Creating query params to insert new item after last existing item in list
+        final var queryParams = new HashMap<String, String>();
+        queryParams.put("insert", "after");
+        queryParams.put("point", "example-jukebox:jukebox/playlist=0/song=2");
+
+        // Inserting new song at 3rd position (aka as last element)
+        spyStrategy.dataPOST(ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
+            new JsonDataPostBody(stringInputStream("""
+            {
+              "id" = "C"
+            }""")), queryParams);
+
+        // Counting how many times we insert items in list
+        verify(spyTx, times(3)).replace(any(), any());
+    }
+
     @Test
     public void testPutReplaceListData() {
         doReturn(immediateFluentFuture(Optional.of(mock(ContainerNode.class)))).when(netconfService)