2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2024 Nordix Foundation
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.ncmp.impl.data.policyexecutor
23 import ch.qos.logback.classic.Level
24 import ch.qos.logback.classic.Logger
25 import ch.qos.logback.classic.spi.ILoggingEvent
26 import ch.qos.logback.core.read.ListAppender
27 import com.fasterxml.jackson.databind.JsonNode
28 import com.fasterxml.jackson.databind.ObjectMapper
29 import org.onap.cps.ncmp.api.exceptions.NcmpException
30 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
31 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
32 import org.slf4j.LoggerFactory
33 import org.springframework.http.HttpStatus
34 import org.springframework.http.ResponseEntity
35 import org.springframework.web.reactive.function.client.WebClient
36 import reactor.core.publisher.Mono
37 import spock.lang.Specification
39 import java.util.concurrent.TimeoutException
41 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE
42 import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE
43 import static org.onap.cps.ncmp.api.data.models.OperationType.PATCH
44 import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE
46 class PolicyExecutorSpec extends Specification {
48 def mockWebClient = Mock(WebClient)
49 def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
50 def mockResponseSpec = Mock(WebClient.ResponseSpec)
51 def spiedObjectMapper = Spy(ObjectMapper)
53 PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper)
55 def logAppender = Spy(ListAppender<ILoggingEvent>)
57 def someValidJson = '{"Hello":"World"}'
61 objectUnderTest.enabled = true
62 mockWebClient.post() >> mockRequestBodyUriSpec
63 mockRequestBodyUriSpec.uri(*_) >> mockRequestBodyUriSpec
64 mockRequestBodyUriSpec.header(*_) >> mockRequestBodyUriSpec
65 mockRequestBodyUriSpec.body(*_) >> mockRequestBodyUriSpec
66 mockRequestBodyUriSpec.retrieve() >> mockResponseSpec
70 ((Logger) LoggerFactory.getLogger(PolicyExecutor)).detachAndStopAllAppenders()
73 def 'Permission check with "allow" decision.'() {
74 given: 'allow response'
75 mockResponse([decision:'allow'], HttpStatus.OK)
76 when: 'permission is checked for an operation'
77 objectUnderTest.checkPermission(new YangModelCmHandle(), operationType, 'my credentials','my resource',someValidJson)
78 then: 'system logs the operation is allowed'
79 assert getLogEntry(2) == 'Operation allowed.'
80 and: 'no exception occurs'
82 where: 'all write operations are tested'
83 operationType << [ CREATE, DELETE, PATCH, UPDATE ]
86 def 'Permission check with "other" decision (not allowed).'() {
87 given: 'other response'
88 mockResponse([decision:'other', decisionId:123, message:'I dont like Mondays' ], HttpStatus.OK)
89 when: 'permission is checked for an operation'
90 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
91 then: 'Policy Executor exception is thrown'
92 def thrownException = thrown(PolicyExecutorException)
93 assert thrownException.message == 'Operation not allowed. Decision id 123 : other'
94 assert thrownException.details == 'I dont like Mondays'
97 def 'Permission check with non-2xx response and "allow" default decision.'() {
98 given: 'other http response'
99 mockResponse([], HttpStatus.I_AM_A_TEAPOT)
100 and: 'the configured default decision is "allow"'
101 objectUnderTest.defaultDecision = 'allow'
102 when: 'permission is checked for an operation'
103 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
104 then: 'No exception is thrown'
108 def 'Permission check with non-2xx response and "other" default decision.'() {
109 given: 'other http response'
110 mockResponse([], HttpStatus.I_AM_A_TEAPOT)
111 and: 'the configured default decision is NOT "allow"'
112 objectUnderTest.defaultDecision = 'deny by default'
113 when: 'permission is checked for an operation'
114 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials', 'my resource', someValidJson)
115 then: 'Policy Executor exception is thrown'
116 def thrownException = thrown(PolicyExecutorException)
117 assert thrownException.message == 'Operation not allowed. Decision id N/A : deny by default'
118 assert thrownException.details == 'Policy Executor returned HTTP Status code 418. Falling back to configured default decision: deny by default'
121 def 'Permission check with invalid response from Policy Executor.'() {
122 given: 'invalid response from Policy executor'
123 mockResponseSpec.toEntity(*_) >> invalidResponse
124 when: 'permission is checked for an operation'
125 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
126 then: 'system logs the expected message'
127 assert getLogEntry(1) == expectedMessage
128 where: 'following invalid responses are received'
129 invalidResponse || expectedMessage
130 Mono.empty() || 'No valid response from Policy Executor, ignored'
131 Mono.just(new ResponseEntity<>(null, HttpStatus.OK)) || 'No valid response body from Policy Executor, ignored'
134 def 'Permission check with timeout exception.'() {
135 given: 'a timeout during the request'
136 def cause = new TimeoutException()
137 mockResponseSpec.toEntity(*_) >> { throw new RuntimeException(cause) }
138 and: 'the configured default decision is NOT "allow"'
139 objectUnderTest.defaultDecision = 'deny by default'
140 when: 'permission is checked for an operation'
141 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
142 then: 'Policy Executor exception is thrown'
143 def thrownException = thrown(PolicyExecutorException)
144 assert thrownException.message == 'Operation not allowed. Decision id N/A : deny by default'
145 assert thrownException.details == 'Policy Executor request timed out. Falling back to configured default decision: deny by default'
148 def 'Permission check with other runtime exception.'() {
149 given: 'some other runtime exception'
150 def originalException = new RuntimeException()
151 mockResponseSpec.toEntity(*_) >> { throw originalException}
152 when: 'permission is checked for an operation'
153 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
154 then: 'The original exception is thrown'
155 def thrownException = thrown(RuntimeException)
156 assert thrownException == originalException
159 def 'Permission check with an invalid change request json.'() {
160 when: 'permission is checked for an invalid change request'
161 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', 'invalid json string')
162 then: 'an ncmp exception thrown'
163 def ncmpException = thrown(NcmpException)
164 ncmpException.message == 'Cannot convert Change Request data to Object'
165 ncmpException.details.contains('invalid json string')
168 def 'Permission check feature disabled.'() {
169 given: 'feature is disabled'
170 objectUnderTest.enabled = false
171 when: 'permission is checked for an operation'
172 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
173 then: 'system logs that the feature not enabled'
174 assert getLogEntry(0) == 'Policy Executor Enabled: false'
177 def mockResponse(mockResponseAsMap, httpStatus) {
178 JsonNode jsonNode = spiedObjectMapper.readTree(spiedObjectMapper.writeValueAsString(mockResponseAsMap))
179 def mono = Mono.just(new ResponseEntity<>(jsonNode, httpStatus))
180 mockResponseSpec.toEntity(*_) >> mono
184 def logger = LoggerFactory.getLogger(PolicyExecutor)
185 logger.setLevel(Level.TRACE)
186 logger.addAppender(logAppender)
190 def getLogEntry(index) {
191 logAppender.list[index].formattedMessage