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