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.api.exceptions.ServerNcmpException
32 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
33 import org.slf4j.LoggerFactory
34 import org.springframework.http.HttpStatus
35 import org.springframework.http.ResponseEntity
36 import org.springframework.web.reactive.function.client.WebClient
37 import reactor.core.publisher.Mono
38 import spock.lang.Specification
40 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE
41 import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE
42 import static org.onap.cps.ncmp.api.data.models.OperationType.PATCH
43 import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE
45 class PolicyExecutorSpec extends Specification {
47 def mockWebClient = Mock(WebClient)
48 def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
49 def mockResponseSpec = Mock(WebClient.ResponseSpec)
50 def spiedObjectMapper = Spy(ObjectMapper)
52 PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper)
54 def logAppender = Spy(ListAppender<ILoggingEvent>)
56 def someValidJson = '{"Hello":"World"}'
60 objectUnderTest.enabled = true
61 mockWebClient.post() >> mockRequestBodyUriSpec
62 mockRequestBodyUriSpec.uri(*_) >> mockRequestBodyUriSpec
63 mockRequestBodyUriSpec.header(*_) >> mockRequestBodyUriSpec
64 mockRequestBodyUriSpec.body(*_) >> mockRequestBodyUriSpec
65 mockRequestBodyUriSpec.retrieve() >> mockResponseSpec
69 ((Logger) LoggerFactory.getLogger(PolicyExecutor)).detachAndStopAllAppenders()
72 def 'Permission check with allow response.'() {
73 given: 'allow response'
74 mockResponse([decision:'allow'], HttpStatus.OK)
75 when: 'permission is checked for an operation'
76 objectUnderTest.checkPermission(new YangModelCmHandle(), operationType, 'my credentials','my resource',someValidJson)
77 then: 'system logs the operation is allowed'
78 assert getLogEntry(2) == 'Policy Executor allows the operation'
79 and: 'no exception occurs'
81 where: 'all write operations are tested'
82 operationType << [ CREATE, DELETE, PATCH, UPDATE ]
85 def 'Permission check with other response (not allowed).'() {
86 given: 'other response'
87 mockResponse([decision:'other', decisionId:123, message:'I dont like Mondays' ], HttpStatus.OK)
88 when: 'permission is checked for an operation'
89 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
90 then: 'Policy Executor exception is thrown'
91 def thrownException = thrown(PolicyExecutorException)
92 assert thrownException.message == 'Policy Executor did not allow request. Decision #123 : other'
93 assert thrownException.details == 'I dont like Mondays'
96 def 'Permission check with non 2xx response.'() {
97 given: 'other response'
98 mockResponse([], HttpStatus.I_AM_A_TEAPOT)
99 when: 'permission is checked for an operation'
100 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
101 then: 'Server Ncmp exception is thrown'
102 def thrownException = thrown(ServerNcmpException)
103 assert thrownException.message == 'Policy Executor invocation failed'
104 assert thrownException.details == 'HTTP status code: 418'
107 def 'Permission check with invalid response from Policy Executor.'() {
108 given: 'invalid response from Policy executor'
109 mockResponseSpec.toEntity(*_) >> invalidResponse
110 when: 'permission is checked for an operation'
111 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials','my resource',someValidJson)
112 then: 'system logs the expected message'
113 assert getLogEntry(1) == expectedMessage
114 where: 'following invalid responses are received'
115 invalidResponse || expectedMessage
116 Mono.empty() || 'No valid response from policy, ignored'
117 Mono.just(new ResponseEntity<>(null, HttpStatus.OK)) || 'No valid response body from policy, ignored'
120 def 'Permission check with an invalid change request json.'() {
121 when: 'permission is checked for an invalid change request'
122 objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', 'invalid json string')
123 then: 'an ncmp exception thrown'
124 def ncmpException = thrown(NcmpException)
125 ncmpException.message == 'Cannot convert Change Request data to Object'
126 ncmpException.details.contains('invalid json string')
129 def 'Permission check feature disabled.'() {
130 given: 'feature is disabled'
131 objectUnderTest.enabled = false
132 when: 'permission is checked for an operation'
133 objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
134 then: 'system logs that the feature not enabled'
135 assert getLogEntry(0) == 'Policy Executor Enabled: false'
138 def mockResponse(mockResponseAsMap, httpStatus) {
139 JsonNode jsonNode = spiedObjectMapper.readTree(spiedObjectMapper.writeValueAsString(mockResponseAsMap))
140 def mono = Mono.just(new ResponseEntity<>(jsonNode, httpStatus))
141 mockResponseSpec.toEntity(*_) >> mono
145 def logger = LoggerFactory.getLogger(PolicyExecutor)
146 logger.setLevel(Level.TRACE)
147 logger.addAppender(logAppender)
151 def getLogEntry(index) {
152 logAppender.list[index].formattedMessage