Merge "Fix: Update OpenSSF Scorecard to RelEng reusable"
[cps.git] / cps-rest / src / test / groovy / org / onap / cps / rest / controller / DataRestControllerSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2025 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-2025 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 org.onap.cps.api.CpsDataService
29 import org.onap.cps.api.CpsFacade
30 import org.onap.cps.utils.ContentType
31 import org.onap.cps.utils.DateTimeUtility
32 import org.onap.cps.utils.JsonObjectMapper
33 import org.spockframework.spring.SpringBean
34 import org.springframework.beans.factory.annotation.Autowired
35 import org.springframework.beans.factory.annotation.Value
36 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
37 import org.springframework.http.HttpStatus
38 import org.springframework.http.MediaType
39 import org.springframework.test.web.servlet.MockMvc
40 import spock.lang.Shared
41 import spock.lang.Specification
42
43 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
44 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
45 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
46 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
47 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
48 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
49 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
50
51 @WebMvcTest(DataRestController)
52 class DataRestControllerSpec extends Specification {
53
54     @SpringBean
55     CpsFacade mockCpsFacade = Mock()
56
57     @SpringBean
58     CpsDataService mockCpsDataService = Mock()
59
60     @SpringBean
61     JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
62
63     @Autowired
64     MockMvc mvc
65
66     @Value('${rest.api.cps-base-path}')
67     def basePath
68
69     def dataNodeBaseEndpointV1
70     def dataNodeBaseEndpointV2
71     def dataNodeBaseEndpointV3
72     def dataspaceName = 'my_dataspace'
73     def anchorName = 'my_anchor'
74     def noTimestamp = null
75
76     @Shared
77     def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
78
79     @Shared
80     def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
81
82     @Shared
83     def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
84
85     @Shared
86     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
87
88     def setup() {
89         dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
90         dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
91         dataNodeBaseEndpointV3 = "$basePath/v3/dataspaces/$dataspaceName"
92     }
93
94     def 'Create a node: #scenario.'() {
95         given: 'endpoint to create a node'
96             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
97         when: 'post is invoked with datanode endpoint and json'
98             def response =
99                 mvc.perform(
100                     post(endpoint)
101                         .contentType(contentType)
102                         .param('xpath', parentNodeXpath)
103                         .content(requestBody)
104                 ).andReturn().response
105         then: 'a created response is returned'
106             response.status == HttpStatus.CREATED.value()
107         then: 'the cps data service was called with the correct parameters'
108             1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
109         where: 'following xpath parameters are are used'
110             scenario                                   | parentNodeXpath | contentType                | expectedContentType | requestBody     | expectedData
111             'JSON content: no xpath parameter'         | ''              | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
112             'JSON content: xpath parameter point root' | '/'             | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
113             'XML content: no xpath parameter'          | ''              | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
114             'XML content: xpath parameter point root'  | '/'             | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
115     }
116
117     def 'Create a node with observed-timestamp.'() {
118         given: 'endpoint to create a node'
119             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
120         when: 'post is invoked with datanode endpoint and json'
121             def response =
122                 mvc.perform(
123                     post(endpoint)
124                         .contentType(contentType)
125                         .param('xpath', '')
126                         .param('observed-timestamp', observedTimestamp)
127                         .content(content)
128                 ).andReturn().response
129         then: 'a created response is returned'
130             response.status == expectedHttpStatus.value()
131         then: 'the cps data service was called with the correct parameters'
132             expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
133                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
134         where:
135             scenario                          | observedTimestamp              | contentType                | content         || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
136             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
137             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
138             'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
139     }
140
141     def 'Validate data using create a node API.'() {
142         given: 'an endpoint to create a node'
143             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
144             def parentNodeXpath = '/'
145         and: 'dryRunEnabled flag is set to true'
146             def dryRunEnabled = 'true'
147         when: 'post is invoked with json data and dry-run flag enabled'
148             def response =
149                 mvc.perform(
150                     post(endpoint)
151                         .contentType(MediaType.APPLICATION_JSON)
152                         .param('xpath', parentNodeXpath)
153                         .param('dry-run', dryRunEnabled)
154                         .content(requestBodyJson)
155                 ).andReturn().response
156         then: 'a 200 OK response is returned'
157             response.status == HttpStatus.OK.value()
158         then: 'the cps data service was called with correct parameters'
159             1 * mockCpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, requestBodyJson, ContentType.JSON)
160     }
161
162     def 'Create a child node #scenario.'() {
163         given: 'endpoint to create a node'
164             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
165         and: 'parent node xpath'
166             def parentNodeXpath = 'some xpath'
167         when: 'post is invoked with datanode endpoint and json'
168             def postRequestBuilder = post(endpoint)
169                 .contentType(contentType)
170                 .param('xpath', parentNodeXpath)
171                 .content(requestBody)
172             if (observedTimestamp != null)
173                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
174             def response =
175                 mvc.perform(postRequestBuilder).andReturn().response
176         then: 'a created response is returned'
177             response.status == HttpStatus.CREATED.value()
178         then: 'the cps data service was called with the correct parameters'
179             1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
180                 DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
181         where:
182             scenario                          | observedTimestamp              | contentType                | requestBody     | expectedData     | expectedContentType
183             'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
184             'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
185             'without observed-timestamp JSON' | null                           | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
186             'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
187     }
188
189     def 'save list elements under root node #scenario.'() {
190         given: 'root node xpath '
191             def rootNodeXpath = '/'
192         when: 'list-node endpoint is invoked with post (create) operation'
193             def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
194                 .contentType(contentType)
195                 .param('xpath', rootNodeXpath )
196                 .content(requestBody)
197             if (observedTimestamp != null)
198                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
199             def response = mvc.perform(postRequestBuilder).andReturn().response
200         then: 'a created response is returned'
201             response.status == expectedHttpStatus.value()
202         then: 'the java API was called with the correct parameters'
203             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, rootNodeXpath, expectedData,
204                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
205         where:
206             scenario                                            | observedTimestamp              | contentType                | requestBody     || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
207             'Content type JSON with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
208             'Content type JSON without observed-timestamp'      | null                           | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
209             'Content type JSON with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
210             'Content type XML with observed-timestamp'          | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
211             'Content type XML without observed-timestamp'       | null                           | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
212             'Content type XML with invalid observed-timestamp'  | 'invalid'                      | MediaType.APPLICATION_XML  | requestBodyXml  || 0                | HttpStatus.BAD_REQUEST | expectedXmlData  | ContentType.XML
213     }
214
215     def 'Save list elements #scenario.'() {
216         given: 'parent node xpath '
217             def parentNodeXpath = 'parent node xpath'
218         when: 'list-node endpoint is invoked with post (create) operation'
219             def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
220                 .contentType(contentType)
221                 .param('xpath', parentNodeXpath)
222                 .content(requestBody)
223             if (observedTimestamp != null)
224                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
225             def response = mvc.perform(postRequestBuilder).andReturn().response
226         then: 'a created response is returned'
227             response.status == expectedHttpStatus.value()
228         then: 'the cps data service was called with the correct parameters when needed'
229             expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, expectedData,
230                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
231         where: 'the following parameters are used'
232             scenario                                            | observedTimestamp              | contentType                | requestBody     || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
233             'Content type JSON with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
234             'Content type JSON without observed-timestamp'      | null                           | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
235             'Content type JSON with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
236             'Content type XML with observed-timestamp'          | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
237             'Content type XML without observed-timestamp'       | null                           | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
238             'Content type XML with invalid observed-timestamp'  | 'invalid'                      | MediaType.APPLICATION_XML  | requestBodyXml  || 0                | HttpStatus.BAD_REQUEST | expectedXmlData  | ContentType.XML
239     }
240
241     def 'Validate data using Save list elements API'() {
242         given: 'endpoint to save list elements'
243             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes"
244         and: 'dryRunEnabled flag is set to true'
245             def dryRunEnabled = 'true'
246         when: 'post request is performed'
247             def response =
248                 mvc.perform(
249                     post(endpoint)
250                         .contentType(MediaType.APPLICATION_JSON)
251                         .param('xpath', '/')
252                         .content(requestBodyJson)
253                         .param('dry-run', dryRunEnabled)
254                 ).andReturn().response
255         then: 'a 200 OK response is returned'
256             response.status == HttpStatus.OK.value()
257         then: 'the cps data service was called with correct parameters'
258             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
259     }
260
261     def 'Get data nodes [V1] with #scenario.'() {
262         given: 'the service returns data node with #scenario'
263             def xpath = 'my/path'
264             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
265         when: 'get request is performed through REST API'
266             def response =
267                 mvc.perform(
268                     get(endpoint)
269                         .param('xpath', xpath)
270                         .param('include-descendants', includeDescendantsOption))
271                     .andReturn().response
272         then: 'the cps facade is called with the correct parameters'
273             1 * mockCpsFacade.getFirstDataNodeByAnchor(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [mocked:'result']
274         then: 'a success response is returned'
275             response.status == HttpStatus.OK.value()
276         and: 'the response contains the facade result in json format'
277             response.getContentAsString() == '{"mocked":"result"}'
278         where: 'the following parameters are used'
279             scenario                    | includeDescendantsOption || expectedCpsDataServiceOption
280             'no descendants (default) ' | ''                       || OMIT_DESCENDANTS
281             'with descendants'          | 'true'                   || INCLUDE_ALL_DESCENDANTS
282     }
283
284     def 'Get data node with #scenario using V2. output type #scenario.'() {
285         given: 'the service returns data nodes with #scenario'
286             def xpath = 'some xPath'
287             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
288         when: 'V2 of get request is performed through REST API'
289             def response =
290                 mvc.perform(get(endpoint)
291                         .contentType(contentType)
292                         .param('xpath', xpath)
293                         .param('descendants', 'all'))
294                     .andReturn().response
295         then: 'the cps service facade is called with the correct parameters and returns some data'
296             1 * mockCpsFacade.getDataNodesByAnchor(dataspaceName, anchorName, xpath, INCLUDE_ALL_DESCENDANTS) >> [[mocked:'result1'], [mocked:'result2']]
297         and: 'a success response is returned'
298             assert response.status == HttpStatus.OK.value()
299         and: 'the response is in the expected format'
300             assert response.contentAsString == expectedResult
301         where: 'the following content types are used'
302             scenario | contentType                || expectedResult
303             'XML'    | MediaType.APPLICATION_XML  || '<mocked>result1</mocked><mocked>result2</mocked>'
304             'JSON'   | MediaType.APPLICATION_JSON || '[{"mocked":"result1"},{"mocked":"result2"}]'
305     }
306
307     def 'Get data node with #scenario using V3. output type #scenario.'() {
308         given: 'the service returns data nodes with #scenario'
309             def xpath = 'some xPath'
310             def endpoint = "$dataNodeBaseEndpointV3/anchors/$anchorName/node"
311         when: 'V3 of get request is performed through REST API'
312             def response =
313                 mvc.perform(get(endpoint)
314                     .contentType(contentType)
315                     .param('xpath', xpath)
316                     .param('descendants', 'all'))
317                     .andReturn().response
318         then: 'the cps service facade is called with the correct parameters and returns some data'
319             1 * mockCpsFacade.getDataNodesByAnchorV3(dataspaceName, anchorName, xpath, INCLUDE_ALL_DESCENDANTS) >> [books: [[title: 'Book 1'], [title: 'Book 2']]]
320         and: 'a success response is returned'
321             assert response.status == HttpStatus.OK.value()
322         and: 'the response is in the expected format'
323             assert response.contentAsString == expectedResult
324         where: 'the following content types are used'
325             scenario | contentType                || expectedResult
326             'XML'    | MediaType.APPLICATION_XML  || '<books><title>Book 1</title></books><books><title>Book 2</title></books>'
327             'JSON'   | MediaType.APPLICATION_JSON || '{"books":[{"title":"Book 1"},{"title":"Book 2"}]}'
328     }
329
330     def 'Update data node leaves: #scenario.'() {
331         given: 'endpoint to update a node '
332             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
333         when: 'patch request is performed'
334             def response =
335                 mvc.perform(
336                     patch(endpoint)
337                         .contentType(contentType)
338                         .content(requestBody)
339                         .param('xpath', inputXpath)
340                 ).andReturn().response
341         then: 'the cps data service method is invoked with expected parameters'
342             1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, expectedData, null, expectedContentType)
343         and: 'response status indicates success'
344             response.status == HttpStatus.OK.value()
345         where:
346             scenario                             | inputXpath    | contentType                || xpathServiceParameter | requestBody     | expectedData        | expectedContentType
347             'JSON content: root node by default' | ''            | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData    | ContentType.JSON
348             'JSON content: root node by choice'  | '/'           | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData    | ContentType.JSON
349             'JSON content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_JSON || '/some/xpath'         | requestBodyJson | expectedJsonData    | ContentType.JSON
350             'XML content: root node by default'  | ''            | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData     | ContentType.XML
351             'XML content: root node by choice'   | '/'           | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData     | ContentType.XML
352             'XML content: some xpath by parent'  | '/some/xpath' | MediaType.APPLICATION_XML  || '/some/xpath'         | requestBodyXml  | expectedXmlData     | ContentType.XML
353     }
354
355     def 'Update data node leaves with observedTimestamp.'() {
356         given: 'endpoint to update a node leaves '
357             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
358         when: 'patch request is performed'
359             def response =
360                 mvc.perform(
361                     patch(endpoint)
362                         .contentType(MediaType.APPLICATION_JSON)
363                         .content(requestBodyJson)
364                         .param('xpath', '/')
365                         .param('observed-timestamp', observedTimestamp)
366                 ).andReturn().response
367         then: 'the cps data service method is invoked with expected parameters'
368             expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', expectedJsonData,
369                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
370         and: 'response status indicates success'
371             response.status == expectedHttpStatus.value()
372         where:
373             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
374             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
375             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
376     }
377
378     def 'Validate data using Update a node API.'() {
379         given: 'endpoint to update a node leaves'
380             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
381         and: 'dryRunEnabled flag is set to true'
382             def dryRunEnabled = 'true'
383         when: 'patch request is performed'
384             def response =
385                 mvc.perform(
386                     patch(endpoint)
387                         .contentType(MediaType.APPLICATION_JSON)
388                         .content(requestBodyJson)
389                         .param('xpath', '/')
390                         .param('dry-run', dryRunEnabled)
391                 ).andReturn().response
392         then: 'a 200 OK response is returned'
393             response.status == HttpStatus.OK.value()
394         then: 'the cps data service was called with correct parameters'
395             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
396     }
397
398     def 'Replace data node tree: #scenario.'() {
399         given: 'endpoint to replace node'
400             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
401         when: 'put request is performed'
402             def response =
403                 mvc.perform(
404                     put(endpoint)
405                         .contentType(contentType)
406                         .content(requestBody)
407                         .param('xpath', inputXpath))
408                     .andReturn().response
409         then: 'the cps data service method is invoked with expected parameters'
410             1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedData, noTimestamp, expectedContentType)
411         and: 'response status indicates success'
412             response.status == HttpStatus.OK.value()
413         where:
414             scenario                             | inputXpath    | contentType                || xpathServiceParameter | requestBody     | expectedData     | expectedContentType
415             'JSON content: root node by default' | ''            | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData | ContentType.JSON
416             'JSON content: root node by choice'  | '/'           | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData | ContentType.JSON
417             'JSON content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_JSON || '/some/xpath'         | requestBodyJson | expectedJsonData | ContentType.JSON
418             'XML content: root node by default'  | ''            | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData  | ContentType.XML
419             'XML content: root node by choice'   | '/'           | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData  | ContentType.XML
420             'XML content: some xpath by parent'  | '/some/xpath' | MediaType.APPLICATION_XML  || '/some/xpath'         | requestBodyXml  | expectedXmlData  | ContentType.XML
421     }
422
423     def 'Validate data using Replace data node API.'() {
424         given: 'endpoint to replace node'
425             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
426         and: 'dryRunEnabled flag is set to true'
427             def dryRunEnabled = 'true'
428         when: 'put request is performed'
429             def response =
430                 mvc.perform(
431                     put(endpoint)
432                         .contentType(MediaType.APPLICATION_JSON)
433                         .content(requestBodyJson)
434                         .param('xpath', '/')
435                         .param('dry-run', dryRunEnabled)
436                 ).andReturn().response
437         then: 'a 200 OK response is returned'
438             response.status == HttpStatus.OK.value()
439         then: 'the cps data service was called with correct parameters'
440             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
441     }
442
443     def 'Update data node and descendants with observedTimestamp.'() {
444         given: 'endpoint to replace node'
445             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
446         when: 'put request is performed'
447             def response =
448                 mvc.perform(
449                     put(endpoint)
450                         .contentType(MediaType.APPLICATION_JSON)
451                         .content(requestBodyJson)
452                         .param('xpath', '')
453                         .param('observed-timestamp', observedTimestamp))
454                     .andReturn().response
455         then: 'the cps data service method is invoked with expected parameters'
456             expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
457                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
458         and: 'response status indicates success'
459             response.status == expectedHttpStatus.value()
460         where:
461             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
462             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
463             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
464     }
465
466     def 'Replace list content #scenario.'() {
467         when: 'list-nodes endpoint is invoked with put (update) operation'
468             def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
469                 .contentType(MediaType.APPLICATION_JSON)
470                 .param('xpath', 'parent xpath')
471                 .content(requestBodyJson)
472             if (observedTimestamp != null)
473                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
474             def response = mvc.perform(putRequestBuilder).andReturn().response
475         then: 'a success response is returned'
476             response.status == expectedHttpStatus.value()
477         and: 'the cps data service was called with the correct parameters'
478             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedJsonData,
479                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
480         where:
481             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
482             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
483             'without observed-timestamp'      | null                           || 1                | HttpStatus.OK
484             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
485     }
486
487     def 'Replace list XML content #scenario.'() {
488         when: 'list-nodes endpoint is invoked with put (update) operation'
489             def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
490                 .contentType(MediaType.APPLICATION_XML)
491                 .param('xpath', 'parent xpath')
492                 .content(requestBodyXml)
493             if (observedTimestamp != null)
494                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
495             def response = mvc.perform(putRequestBuilder).andReturn().response
496         then: 'a success response is returned'
497             response.status == expectedHttpStatus.value()
498         and: 'the cps data service was called with the correct parameters'
499             expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', expectedXmlData,
500                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.XML)
501         where:
502             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
503             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.OK
504             'without observed-timestamp'      | null                           || 1                | HttpStatus.OK
505             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
506     }
507
508     def 'Validate data using Replace list content API.'() {
509         given: 'endpoint to replace list-nodes'
510             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes"
511         and: 'dryRunEnabled flag is set to true'
512             def dryRunEnabled = 'true'
513         when: 'put request is performed'
514             def response =
515                 mvc.perform(
516                     put(endpoint)
517                         .contentType(MediaType.APPLICATION_JSON)
518                         .param('xpath', '/')
519                         .content(requestBodyJson)
520                         .param('dry-run', dryRunEnabled)
521                 ).andReturn().response
522         then: 'a 200 OK response is returned'
523             response.status == HttpStatus.OK.value()
524         then: 'the cps data service was called with correct parameters'
525             1 * mockCpsDataService.validateData(dataspaceName, anchorName, '/', requestBodyJson, ContentType.JSON)
526     }
527
528     def 'Delete list element #scenario.'() {
529         when: 'list-nodes endpoint is invoked with delete operation'
530             def deleteRequestBuilder = delete("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
531                 .param('xpath', 'list element xpath')
532             if (observedTimestamp != null)
533                 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
534             def response = mvc.perform(deleteRequestBuilder).andReturn().response
535         then: 'a success response is returned'
536             response.status == expectedHttpStatus.value()
537         and: 'the cps data service was called with the correct parameters'
538             expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
539                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
540         where:
541             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
542             'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.NO_CONTENT
543             'without observed-timestamp'      | null                           || 1                | HttpStatus.NO_CONTENT
544             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
545     }
546
547     def 'Delete data node #scenario.'() {
548         given: 'data node xpath'
549             def dataNodeXpath = '/dataNodeXpath'
550         when: 'delete data node endpoint is invoked'
551             def deleteDataNodeRequest = delete( "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes")
552                 .param('xpath', dataNodeXpath)
553         and: 'observed timestamp is added to the parameters'
554             if (observedTimestamp != null)
555                 deleteDataNodeRequest.param('observed-timestamp', observedTimestamp)
556             def response = mvc.perform(deleteDataNodeRequest).andReturn().response
557         then: 'a successful response is returned'
558             response.status == expectedHttpStatus.value()
559         and: 'the cps data service is called with the correct parameters'
560             expectedApiCount * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath,
561                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
562         where:
563             scenario                            | observedTimestamp                 || expectedApiCount | expectedHttpStatus
564             'with observed timestamp'           | '2021-03-03T23:59:59.999-0400'    || 1                | HttpStatus.NO_CONTENT
565             'without observed timestamp'        | null                              || 1                | HttpStatus.NO_CONTENT
566             'with invalid observed timestamp'   | 'invalid'                         || 0                | HttpStatus.BAD_REQUEST
567     }
568
569 }