2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021 Bell Canada
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.ncmp.api.impl
25 import com.fasterxml.jackson.core.JsonProcessingException
26 import com.fasterxml.jackson.databind.ObjectMapper
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.api.CpsDataService
29 import org.onap.cps.api.CpsModuleService
30 import org.onap.cps.api.CpsQueryService
31 import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration
32 import org.onap.cps.ncmp.api.impl.exception.NcmpException
33 import org.onap.cps.ncmp.api.impl.operation.DmiOperations
34 import org.onap.cps.ncmp.api.models.CmHandle
35 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
36 import org.onap.cps.ncmp.api.models.PersistenceCmHandle
37 import org.onap.cps.ncmp.utils.TestUtils
38 import org.onap.cps.spi.FetchDescendantsOption
39 import org.onap.cps.spi.model.DataNode
40 import org.onap.cps.spi.model.ModuleReference
41 import org.springframework.http.HttpStatus
42 import org.springframework.http.ResponseEntity
43 import spock.lang.Shared
44 import spock.lang.Specification
46 class NetworkCmProxyDataServiceImplSpec extends Specification {
49 def persistenceCmHandle = new CmHandle()
51 def cmHandlesArray = ['cmHandle001']
53 def mockCpsDataService = Mock(CpsDataService)
54 def mockCpsQueryService = Mock(CpsQueryService)
55 def mockDmiOperations = Mock(DmiOperations)
56 def mockCpsModuleService = Mock(CpsModuleService)
57 def mockCpsAdminService = Mock(CpsAdminService)
58 def mockDmiProperties = Mock(NcmpConfiguration.DmiProperties)
60 def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsModuleService,
61 mockCpsDataService, mockCpsQueryService, mockCpsAdminService, new ObjectMapper())
63 def cmHandle = 'some handle'
64 def noTimestamp = null
65 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
66 def cmHandleForModelSync = new PersistenceCmHandle(id:'some cm handle', dmiServiceName: 'some service name')
67 def expectedDataspaceNameForModleSync = 'NCMP-Admin'
68 def NO_NAMESPACE = null
70 def expectedDataspaceName = 'NFP-Operational'
71 def 'Query data nodes by cps path with #fetchDescendantsOption.'() {
72 given: 'a cm Handle and a cps path'
73 def cpsPath = '/cps-path'
74 when: 'queryDataNodes is invoked'
75 objectUnderTest.queryDataNodes(cmHandle, cpsPath, fetchDescendantsOption)
76 then: 'the persistence service is called once with the correct parameters'
77 1 * mockCpsQueryService.queryDataNodes(expectedDataspaceName, cmHandle, cpsPath, fetchDescendantsOption)
78 where: 'all fetch descendants options are supported'
79 fetchDescendantsOption << FetchDescendantsOption.values()
82 def 'Create full data node: #scenario.'() {
83 given: 'a cm handle and root xpath'
84 def jsonData = 'some json'
85 when: 'createDataNode is invoked'
86 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
87 then: 'the CPS service method is invoked once with the expected parameters'
88 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, jsonData, noTimestamp)
89 where: 'following parameters were used'
92 'root level xpath' | '/'
95 def 'Create child data node.'() {
96 given: 'a cm handle and parent node xpath'
97 def jsonData = 'some json'
98 def xpath = '/test-node'
99 when: 'createDataNode is invoked'
100 objectUnderTest.createDataNode(cmHandle, xpath, jsonData)
101 then: 'the CPS service method is invoked once with the expected parameters'
102 1 * mockCpsDataService.saveData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
105 def 'Add list-node elements.'() {
106 given: 'a cm handle and parent node xpath'
107 def jsonData = 'some json'
108 def xpath = '/test-node'
109 when: 'addListNodeElements is invoked'
110 objectUnderTest.addListNodeElements(cmHandle, xpath, jsonData)
111 then: 'the CPS service method is invoked once with the expected parameters'
112 1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
115 def 'Update data node leaves.'() {
116 given: 'a cm Handle and a cps path'
118 def jsonData = 'some json'
119 when: 'updateNodeLeaves is invoked'
120 objectUnderTest.updateNodeLeaves(cmHandle, xpath, jsonData)
121 then: 'the persistence service is called once with the correct parameters'
122 1 * mockCpsDataService.updateNodeLeaves(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
125 def 'Replace data node tree.'() {
126 given: 'a cm Handle and a cps path'
128 def jsonData = 'some json'
129 when: 'replaceNodeTree is invoked'
130 objectUnderTest.replaceNodeTree(cmHandle, xpath, jsonData)
131 then: 'the persistence service is called once with the correct parameters'
132 1 * mockCpsDataService.replaceNodeTree(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
135 def 'Register or re-register a DMI Plugin with #scenario cm handles.'() {
136 given: 'a registration '
137 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
138 def dmiPluginRegistration = new DmiPluginRegistration()
139 dmiPluginRegistration.dmiPlugin = 'my-server'
140 persistenceCmHandle.cmHandleID = '123'
141 persistenceCmHandle.cmHandleProperties = [name1: 'value1', name2: 'value2']
142 dmiPluginRegistration.createdCmHandles = createdCmHandles
143 dmiPluginRegistration.updatedCmHandles = updatedCmHandles
144 dmiPluginRegistration.removedCmHandles = removedCmHandles
145 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[{"name":"name1","value":"value1"},{"name":"name2","value":"value2"}]}]}'
146 when: 'registration is updated'
147 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
148 then: 'the CPS save list node data is invoked with the expected parameters'
149 expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
150 '/dmi-registry', expectedJsonData, noTimestamp)
151 and: 'update Node and Child Data Nodes is invoked with correct parameters'
152 expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin',
153 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp)
154 and : 'delete list data node is invoked with the correct parameters'
155 expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin',
156 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
159 scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToUpdateNode | expectedCallsToDeleteListDataNode
160 'create' | [persistenceCmHandle ] | [] | [] || 1 | 0 | 0
161 'update' | [] | [persistenceCmHandle ] | [] || 0 | 1 | 0
162 'delete' | [] | [] | cmHandlesArray || 0 | 0 | 1
163 'create, update and delete' | [persistenceCmHandle ] | [persistenceCmHandle ] | cmHandlesArray || 1 | 1 | 1
167 def 'Register a DMI Plugin for the given cmHandle without additional properties.'() {
168 given: 'a registration without cmHandle properties '
169 NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
170 def dmiPluginRegistration = new DmiPluginRegistration()
171 dmiPluginRegistration.dmiPlugin = 'my-server'
172 persistenceCmHandle.cmHandleID = '123'
173 persistenceCmHandle.cmHandleProperties = null
174 dmiPluginRegistration.createdCmHandles = [persistenceCmHandle ]
175 def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[]}]}'
176 when: 'registration is updated'
177 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
178 then: 'the CPS save list node data is invoked with the expected parameters'
179 1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
180 '/dmi-registry', expectedJsonData, noTimestamp)
183 def 'Get resource data for pass-through operational from dmi.'() {
184 given: 'data node representing cmHandle and its properties'
185 def cmHandleDataNode = getCmHandleDataNodeForTest()
186 and: 'data node is got from data service'
187 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
188 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
189 and: 'resource data is got from DMI'
190 mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
196 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('result-json', HttpStatus.OK)
197 when: 'get resource data is called'
198 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
203 then: 'dmi returns ok response'
204 response == 'result-json'
207 def 'Get resource data for pass-through operational from dmi threw parsing exception.'() {
208 given: 'data node representing cmHandle and its properties'
209 def cmHandleDataNode = getCmHandleDataNodeForTest()
210 and: 'cps data service returns valid cmHandle data node'
211 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
212 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
213 and: 'objectMapper not able to parse object'
214 def mockObjectMapper = Mock(ObjectMapper)
215 objectUnderTest.objectMapper = mockObjectMapper
216 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
217 when: 'get resource data is called'
218 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
223 then: 'exception is thrown'
224 thrown(NcmpException.class)
227 def 'Get resource data for pass-through operational from dmi return NOK response.'() {
228 given: 'data node representing cmHandle and its properties'
229 def cmHandleDataNode = getCmHandleDataNodeForTest()
230 and: 'cps data service returns valid cmHandle data node'
231 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
232 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
233 and: 'dmi returns NOK response'
234 mockDmiOperations.getResourceDataOperationalFromDmi('testDmiService',
240 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
241 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
242 when: 'get resource data is called'
243 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
248 then: 'exception is thrown'
249 thrown(NcmpException.class)
252 def 'Get resource data for pass-through running from dmi.'() {
253 given: 'data node representing cmHandle and its properties'
254 def cmHandleDataNode = getCmHandleDataNodeForTest()
255 and: 'cpsDataService returns valid dataNode'
256 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
257 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
258 and: 'dmi returns valid response and data'
259 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
265 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}') >> new ResponseEntity<>('{result-json}', HttpStatus.OK)
266 when: 'get resource data is called'
267 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
272 then: 'get resource data returns expected response'
273 response == '{result-json}'
276 def 'Get resource data for pass-through running from dmi threw parsing exception.'() {
277 given: 'data node representing cmHandle and its properties'
278 def cmHandleDataNode = getCmHandleDataNodeForTest()
279 and: 'cpsDataService returns valid dataNode'
280 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
281 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
282 and: 'objectMapper not able to parse object'
283 def mockObjectMapper = Mock(ObjectMapper)
284 objectUnderTest.objectMapper = mockObjectMapper
285 mockObjectMapper.writeValueAsString(_) >> { throw new JsonProcessingException("testException") }
286 when: 'get resource data is called'
287 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
292 then: 'exception is thrown'
293 thrown(NcmpException.class)
296 def 'Get resource data for pass-through running from dmi return NOK response.'() {
297 given: 'data node representing cmHandle and its properties'
298 def cmHandleDataNode = getCmHandleDataNodeForTest()
299 and: 'cpsDataService returns valid dataNode'
300 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
301 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
302 and: 'dmi returns NOK response'
303 mockDmiOperations.getResourceDataPassThroughRunningFromDmi('testDmiService',
309 '{"operation":"read","cmHandleProperties":{"testName":"testValue"}}')
310 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
311 when: 'get resource data is called'
312 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
317 then: 'exception is thrown'
318 thrown(NcmpException.class)
321 def 'Write resource data for pass-through running from dmi using POST.'() {
322 given: 'data node representing cmHandle and its properties'
323 def cmHandleDataNode = getCmHandleDataNodeForTest()
324 and: 'cpsDataService returns valid cm-handle datanode'
325 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
326 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
327 when: 'get resource data is called'
328 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
330 '{some-json}', 'application/json')
331 then: 'dmi called with correct data'
332 1 * mockDmiOperations.createResourceDataPassThroughRunningFromDmi('testDmiService',
335 '{"operation":"create","dataType":"application/json","data":"{some-json}","cmHandleProperties":{"testName":"testValue"}}')
336 >> { new ResponseEntity<>(HttpStatus.CREATED) }
339 def 'Write resource data for pass-through running from dmi using POST "not found" response (from DMI).'() {
340 given: 'data node representing cmHandle and its properties'
341 def cmHandleDataNode = getCmHandleDataNodeForTest()
342 and: 'cpsDataService returns valid dataNode'
343 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
344 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode
345 and: 'dmi throws exception'
346 mockDmiOperations.createResourceDataPassThroughRunningFromDmi(_ as String, _ as String, _ as String, _ as String)
347 >> { new ResponseEntity<>(HttpStatus.NOT_FOUND) }
348 when: 'get resource data is called'
349 objectUnderTest.createResourceDataPassThroughRunningForCmHandle('testCmHandle',
351 '{some-json}', 'application/json')
352 then: 'exception is thrown'
353 thrown(NcmpException.class)
356 def 'Sync model for a (new) cm handle with #scenario'() {
357 given: 'DMI PLug-in returns a list of module references'
358 getModulesForCmHandle()
359 def knownModule1 = new ModuleReference('module1', '1')
360 def knownOtherModule = new ModuleReference('some other module', 'some revision')
361 and: 'CPS-Core returns list of known modules'
362 mockCpsModuleService.getYangResourceModuleReferences(_) >> [knownModule1, knownOtherModule]
363 and: 'DMI-Plugin returns resource(s) for "new" module(s)'
364 def moduleResources = new ResponseEntity<String>(sdncReponseBody, HttpStatus.OK)
365 mockDmiOperations.getResourceFromDmi(_, cmHandleForModelSync.getId(), 'moduleResources') >> moduleResources
366 when: 'module Sync is triggered'
367 objectUnderTest.createAnchorAndSyncModel(cmHandleForModelSync)
368 then: 'the CPS module service is called once with the correct parameters'
369 1 * mockCpsModuleService.createSchemaSetFromModules(expectedDataspaceNameForModleSync, cmHandleForModelSync.getId(), expectedYangResourceToContentMap, [knownModule1])
370 and: 'admin service create anchor method has been called with correct parameters'
371 1 * mockCpsAdminService.createAnchor(expectedDataspaceNameForModleSync, cmHandleForModelSync.getId(), cmHandleForModelSync.getId())
372 where: 'the following responses are recieved from SDNC'
373 scenario | sdncReponseBody || expectedYangResourceToContentMap
374 'one unknown module' | '[{"moduleName" : "someModule", "revision" : "1","yangSource": "someResource"}]' || [someModule: 'someResource']
375 'no unknown module' | '[]' || [:]
378 def getModulesForCmHandle() {
379 def jsonData = TestUtils.getResourceFileContent('cmHandleModules.json')
380 mockDmiProperties.getAuthUsername() >> 'someUser'
381 mockDmiProperties.getAuthPassword() >> 'somePassword'
382 mockDmiProperties.getDmiPluginBasePath() >> 'someUrl'
383 def moduleReferencesFromCmHandleAsJson = new ResponseEntity<String>(jsonData, HttpStatus.OK)
384 mockDmiOperations.getResourceFromDmi(_, cmHandleForModelSync.getId(), 'modules') >> moduleReferencesFromCmHandleAsJson
387 def getObjectUnderTestWithModelSyncDisabled() {
388 def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockDmiOperations, mockCpsModuleService,
389 mockCpsDataService, mockCpsQueryService, mockCpsAdminService, new ObjectMapper()))
390 objectUnderTest.createAnchorAndSyncModel(_) >> null
391 return objectUnderTest
394 def getCmHandleDataNodeForTest() {
395 def cmHandleDataNode = new DataNode()
396 cmHandleDataNode.leaves = ['dmi-service-name': 'testDmiService']
397 def cmHandlePropertyDataNode = new DataNode()
398 cmHandlePropertyDataNode.leaves = ['name': 'testName', 'value': 'testValue']
399 cmHandleDataNode.childDataNodes = [cmHandlePropertyDataNode]
400 return cmHandleDataNode