Fix POST request with insert parameter 96/109196/14
authorSamuel Schneider <samuel.schneider@pantheon.tech>
Thu, 7 Dec 2023 11:53:59 +0000 (12:53 +0100)
committerRobert Varga <nite@hq.sk>
Thu, 28 Mar 2024 06:58:40 +0000 (06:58 +0000)
Fix the issue when Fix POST request with insert parameter
were incorrectly trying to add data to parent of the parent
element. POST requests inherently operate on parent resources
in the path.

Add tests for this behavior.

JIRA: NETCONF-1178
Change-Id: I4bf3218321cf3348b1ecef2f841899c3f7dbf1d6
Signed-off-by: Samuel Schneider <samuel.schneider@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/jaxrs/RestconfDataPostTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractJukeboxTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategyTest.java

index 83fed38b24c32c17efac6f6653fcd85605af70ad..8b88f9cd92db0dd25028d3f73be7ad3c3483251a 100644 (file)
@@ -504,9 +504,8 @@ public abstract class RestconfStrategy {
             final NormalizedNode data, final @Nullable Insert insert) {
         final ListenableFuture<? extends CommitInfo> future;
         if (insert != null) {
-            final var parentPath = path.coerceParent();
-            checkListAndOrderedType(parentPath);
-            future = insertAndCommitPost(path, data, insert, parentPath);
+            checkListAndOrderedType(path);
+            future = insertAndCommitPost(path, data, insert);
         } else {
             future = createAndCommit(prepareWriteExecution(), path, data);
         }
@@ -530,41 +529,40 @@ public abstract class RestconfStrategy {
     }
 
     private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
-            final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parent) {
-        final var grandParent = parent.coerceParent();
+            final NormalizedNode data, final @NonNull Insert insert) {
         final var tx = prepareWriteExecution();
 
         return switch (insert.insert()) {
             case FIRST -> {
-                final var readData = tx.readList(grandParent);
+                final var readData = tx.readList(path);
                 if (readData == null || readData.isEmpty()) {
                     tx.replace(path, data);
                 } else {
-                    checkItemDoesNotExists(exists(path), path);
-                    tx.remove(grandParent);
+                    checkListDataDoesNotExist(path, data);
+                    tx.remove(path);
                     tx.replace(path, data);
-                    tx.replace(grandParent, readData);
+                    tx.replace(path, readData);
                 }
                 yield tx.commit();
             }
             case LAST -> createAndCommit(tx, path, data);
             case BEFORE -> {
-                final var readData = tx.readList(grandParent);
+                final var readData = tx.readList(path);
                 if (readData == null || readData.isEmpty()) {
                     tx.replace(path, data);
                 } else {
-                    checkItemDoesNotExists(exists(path), path);
-                    insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, true);
+                    checkListDataDoesNotExist(path, data);
+                    insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, true);
                 }
                 yield tx.commit();
             }
             case AFTER -> {
-                final var readData = tx.readList(grandParent);
+                final var readData = tx.readList(path);
                 if (readData == null || readData.isEmpty()) {
                     tx.replace(path, data);
                 } else {
-                    checkItemDoesNotExists(exists(path), path);
-                    insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, false);
+                    checkListDataDoesNotExist(path, data);
+                    insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, false);
                 }
                 yield tx.commit();
             }
@@ -722,8 +720,8 @@ public abstract class RestconfStrategy {
 
     private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
             final NormalizedNode data, final PathArgument pointArg, final NormalizedNodeContainer<?> readList,
-            final YangInstanceIdentifier grandParentPath, final boolean before) {
-        tx.remove(grandParentPath);
+            final boolean before) {
+        tx.remove(path);
 
         int lastItemPosition = 0;
         for (var nodeChild : readList.body()) {
@@ -737,13 +735,11 @@ public abstract class RestconfStrategy {
         }
 
         int lastInsertedPosition = 0;
-        final var emptySubtree = fromInstanceId(modelContext(), grandParentPath);
-        tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
         for (var nodeChild : readList.body()) {
             if (lastInsertedPosition == lastItemPosition) {
                 tx.replace(path, data);
             }
-            tx.replace(grandParentPath.node(nodeChild.name()), nodeChild);
+            tx.replace(path.node(nodeChild.name()), nodeChild);
             lastInsertedPosition++;
         }
 
@@ -768,6 +764,23 @@ public abstract class RestconfStrategy {
         return tx.commit();
     }
 
+    /**
+     * Check if child items do NOT already exists in List at specified {@code path}.
+     *
+     * @param data Data to be checked
+     * @param path Path to be checked
+     * @throws RestconfDocumentedException if data already exists.
+     */
+    private void checkListDataDoesNotExist(final YangInstanceIdentifier path, final NormalizedNode data) {
+        if (data instanceof NormalizedNodeContainer<?> dataNode) {
+            for (final var node : dataNode.body()) {
+                checkItemDoesNotExists(exists(path.node(node.name())), path.node(node.name()));
+            }
+        } else {
+            throw new RestconfDocumentedException("Unexpected node type: " + data.getClass().getName());
+        }
+    }
+
     /**
      * Check if items do NOT already exists at specified {@code path}.
      *
index 205fd4af0c29d420612cba23f9e4f0b826ada2eb..bb8f4a42dc6ff8d9f0be35f336ec1fb730c88e7b 100644 (file)
@@ -9,13 +9,20 @@ package org.opendaylight.restconf.nb.jaxrs;
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
 
 import java.net.URI;
+import java.util.List;
+import java.util.Optional;
 import javax.ws.rs.container.AsyncResponse;
 import javax.ws.rs.core.MultivaluedHashMap;
 import javax.ws.rs.core.UriBuilder;
@@ -26,6 +33,7 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
@@ -33,13 +41,19 @@ import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 
 @ExtendWith(MockitoExtension.class)
 class RestconfDataPostTest extends AbstractRestconfTest {
+    private static final String BASE_URI = "http://localhost:8181/rests/";
+    private static final String INSERT = "insert";
+    private static final String POINT = "point";
     @Mock
     private DOMDataTreeReadWriteTransaction tx;
     @Mock
+    private DOMDataTreeReadTransaction readTx;
+    @Mock
     private AsyncResponse asyncResponse;
 
     @BeforeEach
@@ -54,7 +68,7 @@ class RestconfDataPostTest extends AbstractRestconfTest {
         doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID,
             ImmutableNodes.newContainerBuilder().withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME)).build());
-        doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
 
         assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox"),
             assertResponse(201, ar -> restconf.postDataJSON(stringInputStream("""
@@ -70,7 +84,7 @@ class RestconfDataPostTest extends AbstractRestconfTest {
         final var node = PLAYLIST_IID.node(BAND_ENTRY.name());
         doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, node);
         doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, node, BAND_ENTRY);
-        doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
 
         assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=name%20of%20band"),
             assertResponse(201, ar -> restconf.postDataJSON(JUKEBOX_API_PATH, stringInputStream("""
@@ -123,4 +137,125 @@ class RestconfDataPostTest extends AbstractRestconfTest {
             .build();
         assertEquals(expectedPath, actualPath);
     }
+
+    @Test
+    public void testPostDataWithInsertLast() {
+        // Mocking the query parameters to include 'insert=last'
+        final var queryParams = new MultivaluedHashMap<String, String>();
+        queryParams.put(INSERT, List.of("last"));
+        doReturn(queryParams).when(uriInfo).getQueryParameters();
+
+        doReturn(immediateFalseFluentFuture()).when(tx)
+            .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+        doNothing().when(tx).put(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class),
+            any(NormalizedNode.class));
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+        assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+            assertResponse(201, ar -> restconf.postDataJSON(
+                apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+                    {
+                      "example-jukebox:song" : [
+                        {
+                           "index": "3"
+                        }
+                      ]
+                    }"""), uriInfo, ar)).getLocation());
+        verify(tx, times(1)).put(any(), any(), any());
+    }
+
+    @Test
+    public void testPostDataWithInsertFirst() {
+        // Mocking the query parameters to include 'insert=first'
+        final var queryParams = new MultivaluedHashMap<String, String>();
+        queryParams.put(INSERT, List.of("first"));
+        doReturn(queryParams).when(uriInfo).getQueryParameters();
+        doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+        doReturn(immediateFalseFluentFuture()).when(readTx)
+            .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+        // Mocking existed playlist with two songs in DS
+        doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+            .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+        doNothing().when(tx).put(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class),
+                any(NormalizedNode.class));
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+        assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+            assertResponse(201, ar -> restconf.postDataJSON(
+                apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+                    {
+                      "example-jukebox:song" : [
+                        {
+                           "index": "3"
+                        }
+                      ]
+                    }"""), uriInfo, ar)).getLocation());
+        verify(tx, times(3)).put(any(), any(), any());
+    }
+
+    @Test
+    public void testPostDataWithInsertBefore() {
+        // Mocking the query parameters to include 'insert=before' and 'point=example-jukebox:jukebox/playlist=0/song=2'
+        final var queryParams = new MultivaluedHashMap<String, String>();
+        queryParams.put(INSERT, List.of("before"));
+        queryParams.put(POINT, List.of("example-jukebox:jukebox/playlist=0/song=2"));
+        doReturn(queryParams).when(uriInfo).getQueryParameters();
+        doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+        doReturn(immediateFalseFluentFuture()).when(readTx)
+            .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+        // Mocking existed playlist with two songs in DS
+        doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+            .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+        assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+            assertResponse(201, ar -> restconf.postDataJSON(
+                apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+                    {
+                      "example-jukebox:song" : [
+                        {
+                           "index": "3",
+                           "id" = "C"
+                        }
+                      ]
+                    }"""), uriInfo, ar)).getLocation());
+
+        verify(tx, times(3)).put(any(), any(), any());
+    }
+
+    @Test
+    public void testPostDataWithInsertAfter() {
+        // Mocking the query parameters to include 'insert=after' and 'point=example-jukebox:jukebox/playlist=0/song=1'
+        final var queryParams = new MultivaluedHashMap<String, String>();
+        queryParams.put(INSERT, List.of("after"));
+        queryParams.put(POINT, List.of("example-jukebox:jukebox/playlist=0/song=1"));
+        doReturn(queryParams).when(uriInfo).getQueryParameters();
+        doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+        doReturn(immediateFalseFluentFuture()).when(readTx)
+            .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+        doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+            .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+        doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+        assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+            assertResponse(201, ar -> restconf.postDataJSON(
+                apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+                    {
+                      "example-jukebox:song" : [
+                        {
+                           "index": "3",
+                           "id" = "C"
+                        }
+                      ]
+                    }"""), uriInfo, ar)).getLocation());
+
+        verify(tx, times(3)).put(any(), any(), any());
+    }
 }
index 8a7907fa74aa2ad0f477f6ad18332c7062b9cf3d..d0e592ab209752944e16ac72f4675a0605e3cbe3 100644 (file)
@@ -14,6 +14,7 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.yangtools.yang.common.Decimal64;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.Uint32;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
@@ -76,26 +77,19 @@ public abstract class AbstractJukeboxTest {
         .build();
     protected static final MapEntryNode SONG1 = ImmutableNodes.newMapEntryBuilder()
         .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
-            "1"))
+            Uint32.ONE))
         .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
         .build();
     protected static final MapEntryNode SONG2 = ImmutableNodes.newMapEntryBuilder()
         .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
-            "2"))
+            Uint32.TWO))
         .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
         .build();
     protected static final SystemMapNode PLAYLIST_WITH_SONGS = ImmutableNodes.newSystemMapBuilder()
-        .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
-        .withChild(ImmutableNodes.newMapEntryBuilder()
-            .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME,
-                "0"))
-                .withChild(ImmutableNodes.newUserMapBuilder()
-                    .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
-                    .withChild(SONG1)
-                    .withChild(SONG2)
-                    .build())
-                .build())
-            .build();
+        .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
+            .withChild(SONG1)
+            .withChild(SONG2)
+        .build();
 
     protected static final @NonNull EffectiveModelContext JUKEBOX_SCHEMA =
         YangParserTestUtils.parseYangResourceDirectory("/jukebox");
index cc6951227c53c02380890d1553067ce5d8313c99..880ee29ef89d6fe3cafdf62607189d5f7e838a1d 100644 (file)
@@ -42,11 +42,9 @@ import org.opendaylight.restconf.server.api.JsonResourceBody;
 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)
@@ -216,21 +214,7 @@ public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyT
         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));
+        doReturn(PLAYLIST_WITH_SONGS).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>();
@@ -267,21 +251,7 @@ public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyT
         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));
+        doReturn(PLAYLIST_WITH_SONGS).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>();
@@ -289,11 +259,16 @@ public final class NetconfRestconfStrategyTest extends AbstractRestconfStrategyT
         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"),
+        spyStrategy.dataPOST(ApiPath.parse("example-jukebox:jukebox/playlist=0"),
             new JsonDataPostBody(stringInputStream("""
-            {
-              "id" = "C"
-            }""")), queryParams);
+                {
+                  "example-jukebox:song" : [
+                    {
+                       "index": "3",
+                       "id" = "C"
+                    }
+                  ]
+                }""")), queryParams);
 
         // Counting how many times we insert items in list
         verify(spyTx, times(3)).replace(any(), any());