Distributed datastore solution for Data Sync Watchdog
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / inventory / sync / SyncUtilsSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022 Nordix Foundation
4  *  Modifications Copyright (C) 2022 Bell Canada
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21
22 package org.onap.cps.ncmp.api.inventory.sync
23
24 import com.fasterxml.jackson.databind.JsonNode
25 import com.fasterxml.jackson.databind.ObjectMapper
26 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
27 import org.onap.cps.ncmp.api.impl.operations.DmiOperations
28 import org.onap.cps.ncmp.api.inventory.CmHandleQueries
29 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
30 import org.onap.cps.ncmp.api.inventory.CmHandleState
31 import org.onap.cps.ncmp.api.inventory.CompositeState
32 import org.onap.cps.ncmp.api.inventory.CompositeStateBuilder
33 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
34 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
35 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
36 import org.onap.cps.spi.FetchDescendantsOption
37 import org.onap.cps.spi.model.DataNode
38 import org.onap.cps.utils.JsonObjectMapper
39 import org.springframework.http.HttpStatus
40 import org.springframework.http.ResponseEntity
41 import spock.lang.Shared
42 import spock.lang.Specification
43
44 import java.time.OffsetDateTime
45 import java.time.format.DateTimeFormatter
46 import java.util.stream.Collectors
47
48 class SyncUtilsSpec extends Specification{
49
50     def mockInventoryPersistence = Mock(InventoryPersistence)
51
52     def mockCmHandleQueries = Mock(CmHandleQueries)
53
54     def mockDmiDataOperations = Mock(DmiDataOperations)
55
56     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
57
58     def objectUnderTest = new SyncUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
59
60     @Shared
61     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.now())
62
63     @Shared
64     def dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
65
66     @Shared
67     def dataNodeAdditionalProperties = new DataNode(leaves: ['name': 'dmiProp1', 'value': 'dmiValue1'])
68
69
70     def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() {
71         given: 'the inventory persistence service returns a collection of data nodes'
72             mockCmHandleQueries.queryCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
73         and: 'we have some additional (dmi, private) properties'
74             dataNodeAdditionalProperties.xpath = dataNode.xpath + '/additional-properties[@name="dmiProp1"]'
75             dataNode.childDataNodes = [dataNodeAdditionalProperties]
76         when: 'get advised cm handles are fetched'
77             def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles()
78         then: 'the returned data node collection is the correct size'
79             yangModelCmHandles.size() == expectedDataNodeSize
80         and: 'if there is a data node the additional (dmi, private) properties are included'
81             if (expectedDataNodeSize > 0) {
82                 assert yangModelCmHandles[0].dmiProperties[0].name == 'dmiProp1'
83                 assert yangModelCmHandles[0].dmiProperties[0].value == 'dmiValue1'
84             }
85         and: 'yang model collection contains the correct data'
86             yangModelCmHandles.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) ==
87                 dataNodeCollection.stream().map(dataNode -> dataNode.leaves.get("id")).collect(Collectors.toSet())
88         where: 'the following scenarios are used'
89             scenario         | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
90             'exists'         | [dataNode]         || 1                                   | 1
91             'does not exist' | []                 || 0                                   | 0
92     }
93
94     def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() {
95         given: 'A locked state'
96             def compositeState = new CompositeState(lockReason: lockReason)
97         when: 'update cm handle details and attempts is called'
98             objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, LockReasonCategory.LOCKED_MODULE_SYNC_FAILED, 'new error message')
99         then: 'the composite state lock reason and details are updated'
100             assert compositeState.lockReason.lockReasonCategory == LockReasonCategory.LOCKED_MODULE_SYNC_FAILED
101             assert compositeState.lockReason.details == expectedDetails
102         where:
103             scenario         | lockReason                                                                                   || expectedDetails
104             'does not exist' | null                                                                                         || 'Attempt #1 failed: new error message'
105             'exists'         | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message'
106     }
107
108     def 'Get all locked Cm-Handle where Lock Reason is LOCKED_MODULE_SYNC_FAILED cm handle #scenario'() {
109         given: 'the cps (persistence service) returns a collection of data nodes'
110             mockCmHandleQueries.queryCmHandleDataNodesByCpsPath(
111                 '//lock-reason[@reason="LOCKED_MODULE_SYNC_FAILED"]',
112                 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
113         when: 'get locked Misbehaving cm handle is called'
114             def result = objectUnderTest.getModuleSyncFailedCmHandles()
115         then: 'the returned cm handle collection is the correct size'
116             result.size() == 1
117         and: 'the correct cm handle is returned'
118             result[0].id == 'cm-handle-123'
119     }
120
121     def 'Retry Locked Cm-Handle where the last update time is #scenario'() {
122         when: 'retry locked cm handle is invoked'
123             def result = objectUnderTest.isReadyForRetry(new CompositeStateBuilder()
124                 .withLockReason(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED, details)
125                 .withLastUpdatedTime(lastUpdateTime).build())
126         then: 'result returns #expectedResult'
127             result == expectedResult
128         where:
129             scenario                     | lastUpdateTime                     | details                 || expectedResult
130             'the first attempt'          | '1900-01-01T00:00:00.000+0100'     | 'First Attempt'         || true
131             'greater than one minute'    | '1900-01-01T00:00:00.000+0100'     | 'Attempt #1 failed:'    || true
132             'less than eight minutes'    | formattedDateAndTime               | 'Attempt #3 failed:'    || false
133     }
134
135
136     def 'Get a Cm-Handle where #scenario'() {
137         given: 'the inventory persistence service returns a collection of data nodes'
138             mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
139             mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState
140         when: 'get advised cm handles are fetched'
141             def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles()
142         then: 'the returned data node collection is the correct size'
143             yangModelCollection.size() == expectedDataNodeSize
144         and: 'the result contains the correct data'
145             yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds
146         where: 'the following scenarios are used'
147             scenario                                   | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds
148             'a Cm-Handle unsynchronized and ready'     | [dataNode]              | true             || 1                    | ['cm-handle-123'] as Set
149             'a Cm-Handle unsynchronized but not ready' | [dataNode]              | false            || 0                    | [] as Set
150             'all Cm-Handle synchronized'               | []                      | false            || 0                    | [] as Set
151     }
152
153     def 'Get resource data through DMI Operations #scenario'() {
154         given: 'the inventory persistence service returns a collection of data nodes'
155             def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
156             JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString);
157             def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK)
158             mockDmiDataOperations.getResourceDataFromDmi('cm-handle-123', DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, _) >> responseEntity
159         when: 'get resource data is called'
160             def result = objectUnderTest.getResourceData('cm-handle-123')
161         then: 'the returned data is correct'
162             result == jsonString
163     }
164 }