Support operation field for CPS Temporal Query Output API
[cps/cps-temporal.git] / src / test / groovy / org / onap / cps / temporal / controller / rest / QueryControllerSpec.groovy
1 /*
2  * ============LICENSE_START=======================================================
3  * Copyright (c) 2021-2022 Bell Canada.
4  * ================================================================================
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *         http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.temporal.controller.rest
22
23 import org.onap.cps.temporal.controller.utils.DateTimeUtility
24 import com.fasterxml.jackson.databind.ObjectMapper
25 import org.onap.cps.temporal.domain.Operation
26
27 import java.time.OffsetDateTime
28 import org.onap.cps.temporal.controller.rest.model.AnchorDetails
29 import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapperImpl
30 import org.onap.cps.temporal.controller.rest.model.AnchorHistory
31 import org.onap.cps.temporal.controller.rest.model.ErrorMessage
32 import org.onap.cps.temporal.controller.rest.model.SortMapper
33 import org.onap.cps.temporal.domain.NetworkData
34 import org.onap.cps.temporal.domain.SearchCriteria
35 import org.onap.cps.temporal.service.NetworkDataService
36 import org.spockframework.spring.SpringBean
37 import org.springframework.beans.factory.annotation.Autowired
38 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
39 import org.springframework.context.annotation.Import
40 import org.springframework.data.domain.PageRequest
41 import org.springframework.data.domain.SliceImpl
42 import org.springframework.data.domain.Sort
43 import org.springframework.http.HttpStatus
44 import org.springframework.http.MediaType
45 import org.springframework.security.test.context.support.WithMockUser
46 import org.springframework.test.web.servlet.MockMvc
47 import spock.lang.Specification
48 import spock.lang.Shared
49
50 @WebMvcTest(QueryController)
51 @Import([SortMapper, AnchorDetailsMapperImpl])
52 @WithMockUser
53 class QueryControllerSpec extends Specification {
54
55     @SpringBean
56     NetworkDataService mockNetworkDataService = Mock()
57
58     @Autowired
59     MockMvc mvc
60
61     def myDataspace = 'my-dataspace'
62     @Shared
63     def myAnchor = 'my-anchor'
64     @Shared
65     def mySchemaset = 'my-schemaset'
66     @Shared
67     def objectMapper = new ObjectMapper()
68
69     @Shared
70     def observedDescSortOrder = new Sort.Order(Sort.Direction.DESC, 'observed_timestamp')
71     @Shared
72     def anchorAscSortOrder = new Sort.Order(Sort.Direction.ASC, 'anchor')
73
74     def 'Get #endpointName: default values if missing'() {
75
76         def controllerDataBuilder = new QueryControllerDataBuilder(endpointName,
77             [dataspace: myDataspace] << urlSpecifParams)
78         given: 'network data to be returned'
79             def networkData = createNetworkData()
80         when: 'endpoint is called without pageNumber, pageLimit, sort and pointInTime'
81             def requestBuilder = controllerDataBuilder.
82                 createMockHttpRequestBuilder();
83             def response = mvc.perform(requestBuilder).andReturn().response
84         then: 'pageNumber, pageSize and sort has default values'
85             interaction {
86                 def expectedPageable = PageRequest.of(0, 1000,
87                     Sort.by(Sort.Order.desc('observed_timestamp')))
88                 1 * mockNetworkDataService.searchNetworkData(_ as SearchCriteria) >> {
89                     SearchCriteria searchCriteria ->
90                         assert searchCriteria.getPageable() == expectedPageable
91                         assert searchCriteria.getObservedAfter() == null
92                         assert searchCriteria.getCreatedBefore().isAfter(OffsetDateTime.now().minusMinutes(2))
93                         return new SliceImpl([networkData], searchCriteria.getPageable(), false)
94                 }
95             }
96         and: 'response is ok'
97             response.getStatus() == HttpStatus.OK.value()
98             def anchorHistory = objectMapper.readValue(response.getContentAsString(), AnchorHistory)
99         and: 'content has expected values'
100             anchorHistory.getPreviousRecordsLink() == null
101             anchorHistory.getNextRecordsLink() == null
102             anchorHistory.getRecords() == List.of(toAnchorDetails(networkData))
103         where:
104             endpointName           | urlSpecifParams
105             'anchor by name'       | [anchor: myAnchor]
106             'anchors by schemaset' | [schemaSet: mySchemaset]
107     }
108
109     def 'Get #endpointName: query data #scenario'() {
110         def inputParameters = [
111             dataspace   : myDataspace,
112             pointInTime : '2021-07-24T01:00:01.000-0400',
113             pageNumber  : 2, pageLimit: 10,
114             sortAsString: 'observed_timestamp:desc']
115         inputParameters << urlSpecifParams
116         inputParameters << parameters
117         def controllerDataBuilder = new QueryControllerDataBuilder(endpointName, inputParameters)
118         given:
119             def searchCriteria = controllerDataBuilder.createSearchCriteriaBuilder().build()
120             def networkData = createNetworkData()
121             mockNetworkDataService.searchNetworkData(searchCriteria) >> new SliceImpl<NetworkData>(
122                 List.of(networkData), searchCriteria.getPageable(), true)
123         when: 'endpoint is called with all parameters'
124             def requestBuilder = controllerDataBuilder.createMockHttpRequestBuilder()
125             def response = mvc.perform(requestBuilder
126                 .contentType(MediaType.APPLICATION_JSON)).andReturn().response
127             def responseBody = objectMapper.readValue(response.getContentAsString(), AnchorHistory)
128         then: 'status is ok'
129             response.getStatus() == HttpStatus.OK.value()
130         and: 'next and previous record links have expected value'
131             controllerDataBuilder.isExpectedNextRecordsLink(responseBody.getNextRecordsLink())
132             controllerDataBuilder.isExpectedPreviousRecordsLink(responseBody.getPreviousRecordsLink())
133         and: 'has expected network data records'
134             responseBody.getRecords().size() == 1
135             responseBody.getRecords() == [toAnchorDetails(networkData)]
136         where:
137             scenario                                                   | endpointName           | urlSpecifParams          | parameters
138             'without observedTimestampAfter and with payloadFilter'    | 'anchor by name'       | [anchor: myAnchor]       | [observedTimestampAfter: null, payloadFilter: null]
139             'with observedTimestampAfter and without payloadFilter'    | 'anchor by name'       | [anchor: myAnchor]       | [observedTimestampAfter: '2021-07-24T03:00:01.000-0400', payloadFilter: null]
140             'without observedTimestampAfter and with payloadFilter'    | 'anchor by name'       | [anchor: myAnchor]       | [observedTimestampAfter: null, payloadFilter: '{"message" : "hello+world"}']
141             'with observedTimestampAfter and with payloadFilter'       | 'anchor by name'       | [anchor: myAnchor]       | [observedTimestampAfter: '2021-07-24T03:00:01.000+0400', payloadFilter: '{"message" : "hello world"}']
142             'without observedTimestampAfter and without payloadFilter' | 'anchors by schemaset' | [schemaSet: mySchemaset] | [observedTimestampAfter: null, payloadFilter: null]
143             'with observedTimestampAfter and without payloadFilter'    | 'anchors by schemaset' | [schemaSet: mySchemaset] | [observedTimestampAfter: '2021-07-24T03:00:01.000-0400', payloadFilter: null]
144             'without observedTimestampAfter and with payloadFilter'    | 'anchors by schemaset' | [schemaSet: mySchemaset] | [observedTimestampAfter: null, payloadFilter: '{"message" : "hello world"}']
145             'with observedTimestampAfter and with payloadFilter'       | 'anchors by schemaset' | [schemaSet: mySchemaset] | [observedTimestampAfter: '2021-07-24T03:00:01.000+0400', payloadFilter: '{"message" : "hello world"}']
146     }
147
148     def 'Get #endpointName: Sort by #sortAsString'() {
149         given: 'sort parameters'
150             def parameters = [dataspace: myDataspace, sortAsString: sortAsString] << uriSpecificParams
151         when: 'endpoint is called'
152             def controllerDataBuilder = new QueryControllerDataBuilder(endpointName, parameters)
153             def response = mvc.perform(controllerDataBuilder.createMockHttpRequestBuilder())
154                 .andReturn().response
155         then: 'network data service is called with expected sort'
156             1 * mockNetworkDataService.searchNetworkData(_ as SearchCriteria) >> {
157                 SearchCriteria searchCriteria ->
158                     assert searchCriteria.getPageable().getSort() == expectedSort
159                     return new SliceImpl([], searchCriteria.getPageable(), true)
160             }
161         and: 'response is ok'
162             response.getStatus() == HttpStatus.OK.value()
163             def anchorHistory = objectMapper.readValue(response.getContentAsString(), AnchorHistory)
164         and: 'content has expected values'
165             controllerDataBuilder.isExpectedNextRecordsLink(anchorHistory.getNextRecordsLink())
166             anchorHistory.getPreviousRecordsLink() == null
167         where:
168             endpointName           | uriSpecificParams        | sortAsString                         || expectedSort
169             'anchor by name'       | [anchor: myAnchor]       | 'observed_timestamp:desc'            || Sort.by(observedDescSortOrder)
170             'anchor by name'       | [anchor: myAnchor]       | 'anchor:asc,observed_timestamp:desc' || Sort.by(anchorAscSortOrder, observedDescSortOrder)
171             'anchors by schemaset' | [schemaSet: mySchemaset] | 'observed_timestamp:desc'            || Sort.by(observedDescSortOrder)
172             'anchors by schemaset' | [schemaSet: mySchemaset] | 'anchor:asc,observed_timestamp:desc' || Sort.by(anchorAscSortOrder, observedDescSortOrder)
173     }
174
175     def 'Get #endpointName Error handling: invalid date format in #queryParamName '() {
176         given: 'sort parameters'
177             def parameters = [dataspace: myDataspace] << uriSpecificParams
178             parameters[queryParamName] = 'invalid-date-string'
179         when: 'endpoint is called'
180             QueryControllerDataBuilder dataBuilder = new QueryControllerDataBuilder(endpointName, parameters)
181             def response = mvc.perform(dataBuilder.createMockHttpRequestBuilder())
182                 .andReturn().response
183         then: 'received bad request status'
184             response.getStatus() == HttpStatus.BAD_REQUEST.value()
185         and: 'error details'
186             def errorMessage = objectMapper.readValue(response.getContentAsString(), ErrorMessage)
187             errorMessage.getStatus() == HttpStatus.BAD_REQUEST.value().toString()
188             errorMessage.getMessage().contains(queryParamName)
189             errorMessage.getMessage().contains("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
190         where:
191             endpointName           | uriSpecificParams        | queryParamName
192             'anchor by name'       | [anchor: myAnchor]       | 'pointInTime'
193             'anchor by name'       | [anchor: myAnchor]       | 'observedTimestampAfter'
194             'anchors by schemaset' | [schemaSet: mySchemaset] | 'pointInTime'
195             'anchors by schemaset' | [schemaSet: mySchemaset] | 'observedTimestampAfter'
196     }
197
198     def 'Get #endpointName Error handling: invalid sort format #scenario'() {
199         given: 'sort parameters'
200             def parameters = [dataspace: myDataspace, sortAsString: sortAsString] << uriSpecificParams
201         when: 'endpoint is called'
202             def controllerDataBuilder = new QueryControllerDataBuilder(endpointName, parameters)
203             def response = mvc.perform(controllerDataBuilder.createMockHttpRequestBuilder())
204                 .andReturn().response
205         then: 'received bad request status'
206             response.getStatus() == HttpStatus.BAD_REQUEST.value()
207         and: 'error details'
208             def errorMessage = objectMapper.readValue(response.getContentAsString(), ErrorMessage)
209             errorMessage.getStatus() == HttpStatus.BAD_REQUEST.value().toString()
210             errorMessage.getMessage().contains("sort")
211             errorMessage.getMessage().contains("'$sortAsString'")
212             errorMessage.getMessage().contains('<fieldname>:<direction>,...,<fieldname>:<direction>')
213         where:
214             scenario            | sortAsString             | endpointName           | uriSpecificParams
215             'missing direction' | 'observed_timestamp'     | 'anchor by name'       | [anchor: myAnchor]
216             'missing separator' | 'observed_timestampdesc' | 'anchor by name'       | [anchor: myAnchor]
217             'missing direction' | 'observed_timestamp'     | 'anchors by schemaset' | [schemaSet: mySchemaset]
218             'missing separator' | 'observed_timestampdesc' | 'anchors by schemaset' | [schemaSet: mySchemaset]
219     }
220
221     def 'Get #endpointName Error handling: invalid simple payload filter '() {
222         given: 'payload filter parameters'
223             def parameters = [dataspace: myDataspace, payloadFilter: 'invalid-json'] << uriSpecificParams
224         when: 'endpoint is called'
225             def controllerDataBuilder = new QueryControllerDataBuilder(endpointName, parameters)
226             def response = mvc.perform(controllerDataBuilder.createMockHttpRequestBuilder())
227                 .andReturn().response
228         then: 'received bad request status'
229             response.getStatus() == HttpStatus.BAD_REQUEST.value()
230         and: 'error details'
231             def errorMessage = objectMapper.readValue(response.getContentAsString(), ErrorMessage)
232             errorMessage.getStatus() == HttpStatus.BAD_REQUEST.value().toString()
233             errorMessage.getMessage().contains('simplePayloadFilter')
234         where: 'endpoints are provided'
235             endpointName           | uriSpecificParams
236             'anchor by name'       | [anchor: myAnchor]
237             'anchors by schemaset' | [schemaSet: mySchemaset]
238     }
239
240     NetworkData createNetworkData() {
241         return NetworkData.builder().dataspace(myDataspace)
242             .schemaSet(mySchemaset).anchor(myAnchor).payload('{"message" : "Hello World"}')
243             .observedTimestamp(OffsetDateTime.now())
244             .operation(Operation.CREATE)
245             .createdTimestamp(OffsetDateTime.now()).build()
246     }
247
248     AnchorDetails toAnchorDetails(NetworkData networkData) {
249         AnchorDetails anchorDetails = new AnchorDetails()
250         anchorDetails.setDataspace(networkData.getDataspace())
251         anchorDetails.setAnchor(networkData.getAnchor())
252         anchorDetails.setSchemaSet(networkData.getSchemaSet())
253         anchorDetails.setObservedTimestamp(DateTimeUtility.toString(networkData.getObservedTimestamp()))
254         anchorDetails.setOperation(AnchorDetails.OperationEnum.valueOf(networkData.getOperation().toString()))
255         anchorDetails.setData(networkData.getPayload())
256         return anchorDetails
257     }
258
259
260 }