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