Merge "CM SUBSCRIPTION: add new subscription for non existing xpath"
[cps.git] / cps-rest / src / test / groovy / org / onap / cps / rest / controller / DataRestControllerSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2022 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
7  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
8  *  ================================================================================
9  *  Licensed under the Apache License, Version 2.0 (the "License");
10  *  you may not use this file except in compliance with the License.
11  *  You may obtain a copy of the License at
12  *
13  *        http://www.apache.org/licenses/LICENSE-2.0
14  *
15  *  Unless required by applicable law or agreed to in writing, software
16  *  distributed under the License is distributed on an "AS IS" BASIS,
17  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  *  See the License for the specific language governing permissions and
19  *  limitations under the License.
20  *
21  *  SPDX-License-Identifier: Apache-2.0
22  *  ============LICENSE_END=========================================================
23  */
24
25 package org.onap.cps.rest.controller
26
27 import com.fasterxml.jackson.databind.ObjectMapper
28 import groovy.json.JsonSlurper
29 import org.onap.cps.api.CpsDataService
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.model.DataNode
32 import org.onap.cps.spi.model.DataNodeBuilder
33 import org.onap.cps.spi.model.DeltaReportBuilder
34 import org.onap.cps.utils.ContentType
35 import org.onap.cps.utils.DateTimeUtility
36 import org.onap.cps.utils.JsonObjectMapper
37 import org.onap.cps.utils.PrefixResolver
38 import org.spockframework.spring.SpringBean
39 import org.springframework.beans.factory.annotation.Autowired
40 import org.springframework.beans.factory.annotation.Value
41 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
42 import org.springframework.http.HttpStatus
43 import org.springframework.http.MediaType
44 import org.springframework.test.web.servlet.MockMvc
45 import spock.lang.Shared
46 import spock.lang.Specification
47
48 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
49 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
50 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
51 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
52 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
53 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
54 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
55
56 @WebMvcTest(DataRestController)
57 class DataRestControllerSpec extends Specification {
58
59     @SpringBean
60     CpsDataService mockCpsDataService = Mock()
61
62     @SpringBean
63     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
64
65     @SpringBean
66     PrefixResolver prefixResolver = Mock()
67
68     @Autowired
69     MockMvc mvc
70
71     @Value('${rest.api.cps-base-path}')
72     def basePath
73
74     def dataNodeBaseEndpointV1
75     def dataNodeBaseEndpointV2
76     def dataspaceName = 'my_dataspace'
77     def anchorName = 'my_anchor'
78     def noTimestamp = null
79
80     @Shared
81     def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
82
83     @Shared
84     def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
85
86     @Shared
87     def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
88
89     @Shared
90     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
91
92     @Shared
93     static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/parent-1')
94         .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
95
96     @Shared
97     static DataNode dataNodeWithLeavesNoChildren2 = new DataNodeBuilder().withXpath('/parent-2')
98         .withLeaves([leaf: 'value']).build()
99
100     @Shared
101     static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
102         .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
103
104     def setup() {
105         dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
106         dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
107     }
108
109     def 'Create a node: #scenario.'() {
110         given: 'endpoint to create a node'
111             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
112         when: 'post is invoked with datanode endpoint and json'
113             def response =
114                 mvc.perform(
115                     post(endpoint)
116                         .contentType(contentType)
117                         .param('xpath', parentNodeXpath)
118                         .content(requestBody)
119                 ).andReturn().response
120         then: 'a created response is returned'
121             response.status == HttpStatus.CREATED.value()
122         then: 'the java API was called with the correct parameters'
123             1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
124         where: 'following xpath parameters are are used'
125             scenario                                   | parentNodeXpath | contentType                | expectedContentType | requestBody     | expectedData
126             'JSON content: no xpath parameter'         | ''              | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
127             'JSON content: xpath parameter point root' | '/'             | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
128             'XML content: no xpath parameter'          | ''              | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
129             'XML content: xpath parameter point root'  | '/'             | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
130     }
131
132     def 'Create a node with observed-timestamp'() {
133         given: 'endpoint to create a node'
134             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
135         when: 'post is invoked with datanode endpoint and json'
136             def response =
137                 mvc.perform(
138                     post(endpoint)
139                         .contentType(contentType)
140                         .param('xpath', '')
141                         .param('observed-timestamp', observedTimestamp)
142                         .content(content)
143                 ).andReturn().response
144         then: 'a created response is returned'
145             response.status == expectedHttpStatus.value()
146         then: 'the java API was called with the correct parameters'
147             expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
148                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
149         where:
150             scenario                          | observedTimestamp              | contentType                | content         || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
151             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
152             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
153             'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
154     }
155
156     def 'Create a child node #scenario'() {
157         given: 'endpoint to create a node'
158             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
159         and: 'parent node xpath'
160             def parentNodeXpath = 'some xpath'
161         when: 'post is invoked with datanode endpoint and json'
162             def postRequestBuilder = post(endpoint)
163                 .contentType(contentType)
164                 .param('xpath', parentNodeXpath)
165                 .content(requestBody)
166             if (observedTimestamp != null)
167                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
168             def response =
169                 mvc.perform(postRequestBuilder).andReturn().response
170         then: 'a created response is returned'
171             response.status == HttpStatus.CREATED.value()
172         then: 'the java API was called with the correct parameters'
173             1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
174                 DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
175         where:
176             scenario                          | observedTimestamp              | contentType                | requestBody     | expectedData     | expectedContentType
177             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
178             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
179             'without observed-timestamp JSON' | null                           | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
180             'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
181     }
182
183     def 'save list elements under root node #scenario.'() {
184         given: 'root node xpath '
185             def rootNodeXpath = '/'
186         when: 'list-node endpoint is invoked with post (create) operation'
187             def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
188                 .contentType(MediaType.APPLICATION_JSON)
189                 .param('xpath', rootNodeXpath )
190                 .content(requestBodyJson)
191             if (observedTimestamp != null)
192                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
193             def response = mvc.perform(postRequestBuilder).andReturn().response
194         then: 'a created response is returned'
195             response.status == expectedHttpStatus.value()
196         then: 'the java API was called with the correct parameters'
197             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, rootNodeXpath, expectedJsonData,
198                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
199         where:
200             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
201             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
202             'without observed-timestamp'      | null                           || 1                | HttpStatus.CREATED
203             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
204     }
205
206     def 'Save list elements #scenario.'() {
207         given: 'parent node xpath '
208             def parentNodeXpath = 'parent node xpath'
209         when: 'list-node endpoint is invoked with post (create) operation'
210             def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
211                 .contentType(MediaType.APPLICATION_JSON)
212                 .param('xpath', parentNodeXpath)
213                 .content(requestBodyJson)
214             if (observedTimestamp != null)
215                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
216             def response = mvc.perform(postRequestBuilder).andReturn().response
217         then: 'a created response is returned'
218             response.status == expectedHttpStatus.value()
219         then: 'the java API was called with the correct parameters'
220             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
221                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
222         where:
223             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
224             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
225             'without observed-timestamp'      | null                           || 1                | HttpStatus.CREATED
226             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
227     }
228
229     def 'Get data node with leaves'() {
230         given: 'the service returns data node leaves'
231             def xpath = 'parent-1'
232             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
233             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
234         when: 'get request is performed through REST API'
235             def response =
236                 mvc.perform(get(endpoint).param('xpath', xpath))
237                     .andReturn().response
238         then: 'a success response is returned'
239             response.status == HttpStatus.OK.value()
240         then: 'the response contains the the datanode in json format'
241             response.getContentAsString() == '{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
242         and: 'response contains expected leaf and value'
243             response.contentAsString.contains('"leaf":"value"')
244         and: 'response contains expected leaf-list and values'
245             response.contentAsString.contains('"leafList":["leaveListElement1","leaveListElement2"]')
246     }
247
248     def 'Get data node with #scenario.'() {
249         given: 'the service returns data node with #scenario'
250             def xpath = 'some xPath'
251             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
252             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
253         when: 'get request is performed through REST API'
254             def response =
255                 mvc.perform(
256                     get(endpoint)
257                         .param('xpath', xpath)
258                         .param('include-descendants', includeDescendantsOption))
259                     .andReturn().response
260         then: 'a success response is returned'
261             response.status == HttpStatus.OK.value()
262         and: 'the response contains the root node identifier: #expectedRootidentifier'
263             response.contentAsString.contains(expectedRootidentifier)
264         and: 'the response contains child is #expectChildInResponse'
265             response.contentAsString.contains('"child"') == expectChildInResponse
266         where:
267             scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
268             'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
269             'no descendant explicitly'  | dataNodeWithLeavesNoChildren | 'false'                  || OMIT_DESCENDANTS             | false                 | 'parent-1'
270             'with descendants'          | dataNodeWithChild            | 'true'                   || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
271     }
272
273     def 'Get all the data trees as json array with root node xPath using V2'() {
274         given: 'the service returns all data node leaves'
275             def xpath = '/'
276             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
277             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
278         when: 'V2 of get request is performed through REST API'
279             def response =
280                 mvc.perform(get(endpoint).param('xpath', xpath))
281                     .andReturn().response
282         then: 'a success response is returned'
283             response.status == HttpStatus.OK.value()
284         and: 'the response contains the datanode in json array format'
285             response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' +
286                 '{"parent-2":{"leaf":"value"}}]'
287         and: 'the json array contains expected number of data trees'
288             def numberOfDataTrees = new JsonSlurper().parseText(response.getContentAsString()).iterator().size()
289             assert numberOfDataTrees == 2
290     }
291
292     def 'Get data node with #scenario using V2.'() {
293         given: 'the service returns data nodes with #scenario'
294             def xpath = 'some xPath'
295             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
296             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
297         when: 'V2 of get request is performed through REST API'
298             def response =
299                 mvc.perform(
300                     get(endpoint)
301                         .param('xpath', xpath)
302                         .param('descendants', includeDescendantsOption))
303                     .andReturn().response
304         then: 'a success response is returned'
305             response.status == HttpStatus.OK.value()
306         and: 'the response contains the root node identifier: #expectedRootidentifier'
307             response.contentAsString.contains(expectedRootidentifier)
308         and: 'the response contains child is #expectChildInResponse'
309             response.contentAsString.contains('"child"') == expectChildInResponse
310         where:
311             scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
312             'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
313             'no descendant explicitly'  | dataNodeWithLeavesNoChildren | '0'                      || OMIT_DESCENDANTS             | false                 | 'parent-1'
314             'with descendants'          | dataNodeWithChild            | '-1'                     || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
315     }
316
317     def 'Get data node using v2 api'() {
318         given: 'the service returns data node'
319             def xpath = 'some xPath'
320             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
321             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, { descendantsOption -> {
322                 assert descendantsOption.depth == 2}} as FetchDescendantsOption) >> [dataNodeWithChild]
323         when: 'get request is performed through REST API'
324             def response =
325                 mvc.perform(
326                     get(endpoint)
327                         .param('xpath', xpath)
328                         .param('descendants', '2'))
329                     .andReturn().response
330         then: 'a success response is returned'
331             assert response.status == HttpStatus.OK.value()
332         and: 'the response contains the root node identifier'
333             assert response.contentAsString.contains('parent')
334         and: 'the response contains child is true'
335             assert response.contentAsString.contains('"child"')
336     }
337
338     def 'Get delta between two anchors'() {
339         given: 'the service returns a list containing delta reports'
340             def deltaReports = new DeltaReportBuilder().actionAdd().withXpath('/bookstore').withSourceData('bookstore-name': 'Easons').withTargetData('bookstore-name': 'Easons').build()
341             def xpath = 'some xpath'
342             def endpoint = "$dataNodeBaseEndpointV2/anchors/sourceAnchor/delta"
343             mockCpsDataService.getDeltaByDataspaceAndAnchors(dataspaceName, 'sourceAnchor', 'targetAnchor', xpath, OMIT_DESCENDANTS) >> [deltaReports]
344         when: 'get delta request is performed using REST API'
345             def response =
346                 mvc.perform(get(endpoint)
347                     .param('target-anchor-name', 'targetAnchor')
348                     .param('xpath', xpath))
349                     .andReturn().response
350         then: 'expected response code is returned'
351             assert response.status == HttpStatus.OK.value()
352         and: 'the response contains expected value'
353             assert response.contentAsString.contains("[{\"action\":\"add\",\"xpath\":\"/bookstore\",\"sourceData\":{\"bookstore-name\":\"Easons\"},\"targetData\":{\"bookstore-name\":\"Easons\"}}]")
354     }
355
356     def 'Update data node leaves: #scenario.'() {
357         given: 'endpoint to update a node '
358             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
359         when: 'patch request is performed'
360             def response =
361                 mvc.perform(
362                     patch(endpoint)
363                         .contentType(MediaType.APPLICATION_JSON)
364                         .content(requestBodyJson)
365                         .param('xpath', inputXpath)
366                 ).andReturn().response
367         then: 'the service method is invoked with expected parameters'
368             1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, null)
369         and: 'response status indicates success'
370             response.status == HttpStatus.OK.value()
371         where:
372             scenario               | inputXpath    || xpathServiceParameter
373             'root node by default' | ''            || '/'
374             'root node by choice'  | '/'           || '/'
375             'some xpath by parent' | '/some/xpath' || '/some/xpath'
376     }
377
378     def 'Update data node leaves with observedTimestamp'() {
379         given: 'endpoint to update a node leaves '
380             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
381         when: 'patch request is performed'
382             def response =
383                 mvc.perform(
384                     patch(endpoint)
385                         .contentType(MediaType.APPLICATION_JSON)
386                         .content(requestBodyJson)
387                         .param('xpath', '/')
388                         .param('observed-timestamp', observedTimestamp)
389                 ).andReturn().response
390         then: 'the service method is invoked with expected parameters'
391             expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
392                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
393         and: 'response status indicates success'
394             response.status == expectedHttpStatus.value()
395         where:
396             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
397             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
398             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
399     }
400
401     def 'Replace data node tree: #scenario.'() {
402         given: 'endpoint to replace node'
403             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
404         when: 'put request is performed'
405             def response =
406                 mvc.perform(
407                     put(endpoint)
408                         .contentType(MediaType.APPLICATION_JSON)
409                         .content(requestBodyJson)
410                         .param('xpath', inputXpath))
411                     .andReturn().response
412         then: 'the service method is invoked with expected parameters'
413             1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
414         and: 'response status indicates success'
415             response.status == HttpStatus.OK.value()
416         where:
417             scenario               | inputXpath    || xpathServiceParameter
418             'root node by default' | ''            || '/'
419             'root node by choice'  | '/'           || '/'
420             'some xpath by parent' | '/some/xpath' || '/some/xpath'
421     }
422
423     def 'Update data node and descendants with observedTimestamp.'() {
424         given: 'endpoint to replace node'
425             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
426         when: 'put request is performed'
427             def response =
428                 mvc.perform(
429                     put(endpoint)
430                         .contentType(MediaType.APPLICATION_JSON)
431                         .content(requestBodyJson)
432                         .param('xpath', '')
433                         .param('observed-timestamp', observedTimestamp))
434                     .andReturn().response
435         then: 'the service method is invoked with expected parameters'
436             expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
437                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
438         and: 'response status indicates success'
439             response.status == expectedHttpStatus.value()
440         where:
441             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
442             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
443             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
444     }
445
446     def 'Replace list content #scenario.'() {
447         when: 'list-nodes endpoint is invoked with put (update) operation'
448             def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
449                 .contentType(MediaType.APPLICATION_JSON)
450                 .param('xpath', 'parent xpath')
451                 .content(requestBodyJson)
452             if (observedTimestamp != null)
453                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
454             def response = mvc.perform(putRequestBuilder).andReturn().response
455         then: 'a success response is returned'
456             response.status == expectedHttpStatus.value()
457         and: 'the java API was called with the correct parameters'
458             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
459                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
460         where:
461             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
462             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
463             'without observed-timestamp'      | null                           || 1                | HttpStatus.OK
464             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
465     }
466
467     def 'Delete list element #scenario.'() {
468         when: 'list-nodes endpoint is invoked with delete operation'
469             def deleteRequestBuilder = delete("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
470                 .param('xpath', 'list element xpath')
471             if (observedTimestamp != null)
472                 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
473             def response = mvc.perform(deleteRequestBuilder).andReturn().response
474         then: 'a success response is returned'
475             response.status == expectedHttpStatus.value()
476         and: 'the java API was called with the correct parameters'
477             expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
478                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
479         where:
480             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
481             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.NO_CONTENT
482             'without observed-timestamp'      | null                           || 1                | HttpStatus.NO_CONTENT
483             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
484     }
485
486     def 'Delete data node #scenario.'() {
487         given: 'data node xpath'
488             def dataNodeXpath = '/dataNodeXpath'
489         when: 'delete data node endpoint is invoked'
490             def deleteDataNodeRequest = delete( "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes")
491                 .param('xpath', dataNodeXpath)
492         and: 'observed timestamp is added to the parameters'
493             if (observedTimestamp != null)
494                 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
495             def response = mvc.perform(deleteDataNodeRequest).andReturn().response
496         then: 'a successful response is returned'
497             response.status == expectedHttpStatus.value()
498         and: 'the api is called with the correct parameters'
499             expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
500                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
501         where:
502             scenario                            | observedTimestamp                 || expectedApiCount | expectedHttpStatus
503             'with observed timestamp'           | '2021-03-03T23:59:59.999-0400'    || 1                | HttpStatus.NO_CONTENT
504             'without observed timestamp'        | null                              || 1                | HttpStatus.NO_CONTENT
505             'with invalid observed timestamp'   | 'invalid'                         || 0                | HttpStatus.BAD_REQUEST
506     }
507 }