From 4310430012a1bd939d406ec8a53045a128505ec9 Mon Sep 17 00:00:00 2001 From: leventecsanyi Date: Tue, 17 Sep 2024 12:07:32 +0200 Subject: Refactored cps-ri package structure - fixed import order and moved package structure Issue-ID: CPS-2293 Change-Id: Ie2f9f057f261577054530feee7480850ba4b41e1 Signed-off-by: leventecsanyi --- .../ri/CpsDataPersistenceServiceImplSpec.groovy | 281 +++++++++++++++++++++ ...sModulePersistenceServiceConcurrencySpec.groovy | 145 +++++++++++ .../ri/CpsModulePersistenceServiceImplSpec.groovy | 104 ++++++++ .../onap/cps/ri/utils/CpsValidatorImplSpec.groovy | 78 ++++++ .../org/onap/cps/ri/utils/EscapeUtilsSpec.groovy | 41 +++ .../onap/cps/ri/utils/SessionManagerSpec.groovy | 137 ++++++++++ .../spi/impl/CpsDataPersistenceServiceSpec.groovy | 281 --------------------- ...sModulePersistenceServiceConcurrencySpec.groovy | 145 ----------- .../impl/CpsModulePersistenceServiceSpec.groovy | 103 -------- .../cps/spi/impl/utils/CpsValidatorSpec.groovy | 77 ------ .../org/onap/cps/spi/utils/EscapeUtilsSpec.groovy | 41 --- .../onap/cps/spi/utils/SessionManagerSpec.groovy | 139 ---------- 12 files changed, 786 insertions(+), 786 deletions(-) create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/CpsDataPersistenceServiceImplSpec.groovy create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceConcurrencySpec.groovy create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceImplSpec.groovy create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/utils/CpsValidatorImplSpec.groovy create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/utils/EscapeUtilsSpec.groovy create mode 100644 cps-ri/src/test/groovy/org/onap/cps/ri/utils/SessionManagerSpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceConcurrencySpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceSpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy delete mode 100644 cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy (limited to 'cps-ri/src/test/groovy/org') diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/CpsDataPersistenceServiceImplSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsDataPersistenceServiceImplSpec.groovy new file mode 100644 index 0000000000..36bf55e2db --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsDataPersistenceServiceImplSpec.groovy @@ -0,0 +1,281 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (c) 2021 Bell Canada. + * Modifications Copyright (C) 2021-2023 Nordix Foundation + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= +*/ + +package org.onap.cps.ri + +import com.fasterxml.jackson.databind.ObjectMapper +import org.hibernate.StaleStateException +import org.onap.cps.ri.models.AnchorEntity +import org.onap.cps.ri.models.DataspaceEntity +import org.onap.cps.ri.models.FragmentEntity +import org.onap.cps.ri.repository.AnchorRepository +import org.onap.cps.ri.repository.DataspaceRepository +import org.onap.cps.ri.repository.FragmentRepository +import org.onap.cps.ri.utils.SessionManager +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.ConcurrencyException +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.spi.model.DataNode +import org.onap.cps.spi.model.DataNodeBuilder +import org.onap.cps.utils.JsonObjectMapper +import org.springframework.dao.DataIntegrityViolationException +import spock.lang.Specification + +import java.util.stream.Collectors + +class CpsDataPersistenceServiceImplSpec extends Specification { + + def mockDataspaceRepository = Mock(DataspaceRepository) + def mockAnchorRepository = Mock(AnchorRepository) + def mockFragmentRepository = Mock(FragmentRepository) + def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + def mockSessionManager = Mock(SessionManager) + + def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, + mockFragmentRepository, jsonObjectMapper, mockSessionManager)) + + static def anchorEntity = new AnchorEntity(id: 123, dataspace: new DataspaceEntity(id: 1)) + + def setup() { + mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity + mockFragmentRepository.prefetchDescendantsOfFragmentEntities(_, _) >> { fetchDescendantsOption, fragmentEntities -> fragmentEntities } + mockFragmentRepository.findListByAnchorAndXpath(_, [] as Set) >> [] + } + + def 'Storing data nodes individually when batch operation fails'(){ + given: 'two data nodes and supporting repository mock behavior' + def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath1','OK') + def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath2','OK') + and: 'the batch store operation will fail' + mockFragmentRepository.saveAll(*_) >> { throw new DataIntegrityViolationException("Exception occurred") } + when: 'trying to store data nodes' + objectUnderTest.storeDataNodes('dataSpaceName', 'anchorName', [dataNode1, dataNode2]) + then: 'the two data nodes are saved individually' + 2 * mockFragmentRepository.save(_) + } + + def 'Handling of StaleStateException (caused by concurrent updates) during patch operation for data nodes.'() { + given: 'the system can update one datanode and has two more datanodes that throw an exception while updating' + def dataNodes = createDataNodesAndMockRepositoryMethodSupportingThem([ + '/node1': 'OK', + '/node2': 'EXCEPTION', + '/node3': 'EXCEPTION']) + def updatedLeavesPerXPath = dataNodes.stream() + .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)) + and: 'the batch update will therefore also fail' + mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } + when: 'attempt batch update data nodes' + objectUnderTest.batchUpdateDataLeaves('some-dataspace', 'some-anchor', updatedLeavesPerXPath) + then: 'concurrency exception is thrown' + def thrown = thrown(ConcurrencyException) + assert thrown.message == 'Concurrent Transactions' + and: 'it does not contain the successful datanode' + assert !thrown.details.contains('/node1') + and: 'it contains the failed datanodes' + assert thrown.details.contains('/node2') + assert thrown.details.contains('/node3') + } + + def 'Batch update data node leaves and descendants: #scenario'(){ + given: 'the fragment repository returns fragment entities related to the xpath inputs' + mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> [] + mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [ + new FragmentEntity(1, '/test/xpath', null, "{\"id\":\"testId\"}", anchorEntity, [] as Set) + ] + mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ + new FragmentEntity(1, '/test/xpath1', null, "{\"id\":\"testId1\"}", anchorEntity, [] as Set), + new FragmentEntity(2, '/test/xpath2', null, "{\"id\":\"testId2\"}", anchorEntity, [] as Set) + ] + when: 'replace data node tree' + objectUnderTest.batchUpdateDataLeaves('dataspaceName', 'anchorName', + dataNodes.stream().collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves))) + then: 'call fragment repository save all method' + 1 * mockFragmentRepository.saveAll({fragmentEntities -> + assert fragmentEntities.sort() == expectedFragmentEntities.sort() + assert fragmentEntities.size() == expectedSize + }) + where: 'the following Data Type is passed' + scenario | dataNodes | expectedSize || expectedFragmentEntities + 'empty data node list' | [] | 0 || [] + 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'])] | 1 || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', anchor: anchorEntity)] + 'multiple data nodes' | [new DataNode(xpath: '/test/xpath1', leaves: ['id': 'newTestId1']), new DataNode(xpath: '/test/xpath2', leaves: ['id': 'newTestId2'])] | 2 || [new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"newTestId2"}', anchor: anchorEntity), new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"newTestId1"}', anchor: anchorEntity)] + } + + def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() { + given: 'the system can update one datanode and has two more datanodes that throw an exception while updating' + def dataNodes = createDataNodesAndMockRepositoryMethodSupportingThem([ + '/node1': 'OK', + '/node2': 'EXCEPTION', + '/node3': 'EXCEPTION']) + and: 'the batch update will therefore also fail' + mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } + when: 'attempt batch update data nodes' + objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', dataNodes) + then: 'concurrency exception is thrown' + def thrown = thrown(ConcurrencyException) + assert thrown.message == 'Concurrent Transactions' + and: 'it does not contain the successful datanode' + assert !thrown.details.contains('/node1') + and: 'it contains the failed datanodes' + assert thrown.details.contains('/node2') + assert thrown.details.contains('/node3') + } + + def 'Retrieving a data node with a property JSON value of #scenario'() { + given: 'the db has a fragment with an attribute property JSON value of #scenario' + mockFragmentWithJson("{\"some attribute\": ${dataString}}") + when: 'getting the data node represented by this fragment' + def dataNode = objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', + '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + then: 'the leaf is of the correct value and data type' + def attributeValue = dataNode[0].leaves.get('some attribute') + assert attributeValue == expectedValue + assert attributeValue.class == expectedDataClass + where: 'the following Data Type is passed' + scenario | dataString || expectedValue | expectedDataClass + 'just numbers' | '15174' || 15174 | Integer + 'number with dot' | '15174.32' || 15174.32 | Double + 'number with 0 value after dot' | '15174.0' || 15174.0 | Double + 'number with 0 value before dot' | '0.32' || 0.32 | Double + 'number higher than max int' | '2147483648' || 2147483648 | Long + 'just text' | '"Test"' || 'Test' | String + 'number with exponent' | '1.2345e5' || 1.2345e5 | Double + 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double + 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String + 'number as String' | '"12345"' || '12345' | String + } + + def 'Retrieving a data node with invalid JSON'() { + given: 'a fragment with invalid JSON' + mockFragmentWithJson('{invalid json') + when: 'getting the data node represented by this fragment' + objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', + '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + } + + def 'Retrieving multiple data nodes.'() { + given: 'fragment repository returns a collection of fragments' + mockFragmentRepository.findByAnchorAndXpathIn(anchorEntity, ['/xpath1', '/xpath2'] as Set) >> [ + new FragmentEntity(1, '/xpath1', null, null, anchorEntity, [] as Set), + new FragmentEntity(2, '/xpath2', null, null, anchorEntity, [] as Set) + ] + when: 'getting data nodes for 2 xpaths' + def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + then: '2 data nodes are returned' + assert result.size() == 2 + } + + def 'start session'() { + when: 'start session' + objectUnderTest.startSession() + then: 'the session manager method to start session is invoked' + 1 * mockSessionManager.startSession() + } + + def 'close session'() { + given: 'session ID' + def someSessionId = 'someSessionId' + when: 'close session method is called with session ID as parameter' + objectUnderTest.closeSession(someSessionId) + then: 'the session manager method to close session is invoked with parameter' + 1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT) + } + + def 'Lock anchor.'(){ + when: 'lock anchor method is called with anchor entity details' + objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) + then: 'the session manager method to lock anchor is invoked with same parameters' + 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) + } + + def 'Replace data node and descendants: #scenario'(){ + given: 'the fragment repository returns fragment entities related to the xpath inputs' + mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> [] + mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [ + new FragmentEntity(1, '/test/xpath', null, '{"id":"testId"}', anchorEntity, [] as Set) + ] + when: 'replace data node tree' + objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes) + then: 'call fragment repository save all method' + 1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities}) + where: 'the following Data Type is passed' + scenario | dataNodes || expectedFragmentEntities + 'empty data node list' | [] || [] + 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', anchor: anchorEntity, childFragments: [])] + } + + def 'Replace data nodes and descendants'() { + given: 'the fragment repository returns fragment entities related to the xpath inputs' + mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ + new FragmentEntity(1, '/test/xpath1', null, null, anchorEntity, [] as Set), + new FragmentEntity(2, '/test/xpath2', null, null, anchorEntity, [] as Set) + ] + and: 'some data nodes with descendants' + def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])]) + def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])]) + when: 'the fragment entities are update by the data nodes' + objectUnderTest.updateDataNodesAndDescendants('dataspace', 'anchor', [dataNode1, dataNode2]) + then: 'call fragment repository save all method is called with the updated fragments' + 1 * mockFragmentRepository.saveAll({fragmentEntities -> { + assert fragmentEntities.size() == 2 + def fragmentEntityPerXpath = fragmentEntities.collectEntries { [it.xpath, it] } + assert fragmentEntityPerXpath.get('/test/xpath1').childFragments.first().attributes == '{"id":"childTestId1"}' + assert fragmentEntityPerXpath.get('/test/xpath2').childFragments.first().attributes == '{"id":"childTestId2"}' + }}) + } + + def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) { + def dataNode = new DataNodeBuilder().withXpath(xpath).build() + def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: []) + mockFragmentRepository.getByAnchorAndXpath(_, xpath) >> fragmentEntity + if ('EXCEPTION' == scenario) { + mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } + } + return dataNode + } + + def createDataNodesAndMockRepositoryMethodSupportingThem(Map xpathToScenarioMap) { + def dataNodes = [] + def fragmentEntities = [] + def fragmentId = 1 + xpathToScenarioMap.each { + def xpath = it.key + def scenario = it.value + def dataNode = new DataNodeBuilder().withXpath(xpath).build() + dataNodes.add(dataNode) + def fragmentEntity = new FragmentEntity(id: fragmentId, anchor: anchorEntity, xpath: xpath, childFragments: []) + fragmentEntities.add(fragmentEntity) + if ('EXCEPTION' == scenario) { + mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } + } + fragmentId++ + } + mockFragmentRepository.findByAnchorAndXpathIn(_, xpathToScenarioMap.keySet()) >> fragmentEntities + return dataNodes + } + + def mockFragmentWithJson(json) { + def fragmentEntity = new FragmentEntity(456, '/parent-01', null, json, anchorEntity, [] as Set) + mockFragmentRepository.findByAnchorAndXpathIn(_, ['/parent-01'] as Set) >> [fragmentEntity] + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceConcurrencySpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceConcurrencySpec.groovy new file mode 100644 index 0000000000..b892fe4dae --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceConcurrencySpec.groovy @@ -0,0 +1,145 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Bell Canada. + * Modifications Copyright (C) 2021-2023 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ +package org.onap.cps.ri + +import org.hibernate.exception.ConstraintViolationException +import org.onap.cps.ri.models.DataspaceEntity +import org.onap.cps.ri.models.SchemaSetEntity +import org.onap.cps.ri.repository.DataspaceRepository +import org.onap.cps.ri.repository.ModuleReferenceRepository +import org.onap.cps.ri.repository.SchemaSetRepository +import org.onap.cps.ri.repository.YangResourceRepository +import org.onap.cps.spi.CpsAdminPersistenceService +import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DuplicatedYangResourceException +import org.onap.cps.spi.model.ModuleReference +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.retry.annotation.EnableRetry +import spock.lang.Specification + +import java.sql.SQLException + +@SpringBootTest(classes=[CpsModulePersistenceServiceImpl]) +@EnableRetry +class CpsModulePersistenceServiceConcurrencySpec extends Specification { + + @Autowired + CpsModulePersistenceService objectUnderTest + + @SpringBean + DataspaceRepository dataspaceRepository = Mock() + + @SpringBean + YangResourceRepository yangResourceRepository = Mock() + + @SpringBean + SchemaSetRepository schemaSetRepository = Mock() + + @SpringBean + CpsAdminPersistenceService cpsAdminPersistenceService = Mock() + + @SpringBean + ModuleReferenceRepository moduleReferenceRepository = Mock() + + def NEW_RESOURCE_NAME = 'some new resource' + def NEW_RESOURCE_CONTENT = 'module stores {\n' + + ' yang-version 1.1;\n' + + ' namespace "org:onap:ccsdk:sample";\n' + + '}' + + def newYangResourcesNameToContentMap = [(NEW_RESOURCE_NAME):NEW_RESOURCE_CONTENT] + + def yangResourceChecksum = 'b13faef573ed1374139d02c40d8ce09c80ea1dc70e63e464c1ed61568d48d539' + + def yangResourceChecksumDbConstraint = 'yang_resource_checksum_key' + + def sqlExceptionMessage = String.format('(checksum)=(%s)', yangResourceChecksum) + + def checksumIntegrityException = new DataIntegrityViolationException("checksum integrity exception", + new ConstraintViolationException('', new SQLException(sqlExceptionMessage), yangResourceChecksumDbConstraint)) + + def 'Store new schema set, maximum retries.'() { + given: 'no pre-existing schemaset in database' + dataspaceRepository.getByName(_) >> new DataspaceEntity() + yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() + when: 'a new schemaset is stored' + objectUnderTest.storeSchemaSet('some dataspace', 'some new schema set', newYangResourcesNameToContentMap) + then: 'a duplicated yang resource exception is thrown ' + thrown(DuplicatedYangResourceException) + and: 'the system will attempt to save the data 5 times (because checksum integrity exception is thrown each time)' + 5 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } + } + + def 'Store new schema set, succeed on third attempt.'() { + given: 'no pre-existing schemaset in database' + dataspaceRepository.getByName(_) >> new DataspaceEntity() + yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() + when: 'a new schemaset is stored' + objectUnderTest.storeSchemaSet('some dataspace', 'some new schema set', newYangResourcesNameToContentMap) + then: 'no exception is thrown ' + noExceptionThrown() + and: 'the system will attempt to save the data 2 times with checksum integrity exception but then succeed' + 2 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } + 1 * yangResourceRepository.saveAll(_) >> [] + } + + def 'Store schema set using modules, maximum retries.'() { + given: 'map of new modules, a list of existing modules, module reference' + def mapOfNewModules = [newModule1: 'module newmodule { yang-version 1.1; revision "2021-10-12" { } }'] + def moduleReferenceForExistingModule = new ModuleReference("test","2021-10-12") + def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] + and: 'no pre-existing schemaset in database' + dataspaceRepository.getByName(_) >> new DataspaceEntity() + yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() + when: 'a new schemaset is stored from a module' + objectUnderTest.storeSchemaSetFromModules('some dataspace', 'some new schema set' , mapOfNewModules, listOfExistingModulesModuleReference) + then: 'a duplicated yang resource exception is thrown ' + thrown(DuplicatedYangResourceException) + and: 'the system will attempt to save the data 5 times (because checksum integrity exception is thrown each time)' + 5 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } + } + + def 'Store schema set using modules, succeed on third attempt.'() { + given: 'map of new modules, a list of existing modules, module reference' + def mapOfNewModules = [newModule1: 'module newmodule { yang-version 1.1; revision "2021-10-12" { } }'] + def moduleReferenceForExistingModule = new ModuleReference("test","2021-10-12") + def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] + and: 'no pre-existing schemaset in database' + def dataspaceEntity = new DataspaceEntity() + dataspaceRepository.getByName(_) >> new DataspaceEntity() + yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() + yangResourceRepository.getResourceIdsByModuleReferences(_) >> [] + and: 'can retrieve schemaset details after storing it' + def schemaSetEntity = new SchemaSetEntity() + schemaSetRepository.getByDataspaceAndName(dataspaceEntity, 'new schema set') >> schemaSetEntity + when: 'a new schemaset is stored from a module' + objectUnderTest.storeSchemaSetFromModules('some dataspace', 'new schema set' , mapOfNewModules, listOfExistingModulesModuleReference) + then: 'no exception is thrown ' + noExceptionThrown() + and: 'the system will attempt to save the data 2 times with checksum integrity exception but then succeed' + 2 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } + 1 * yangResourceRepository.saveAll(_) >> [] + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceImplSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceImplSpec.groovy new file mode 100644 index 0000000000..1b61ff39c0 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/CpsModulePersistenceServiceImplSpec.groovy @@ -0,0 +1,104 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (c) 2021 Bell Canada. + * Modifications Copyright (C) 2022-2023 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= +*/ + +package org.onap.cps.ri + +import org.hibernate.exception.ConstraintViolationException +import org.onap.cps.ri.models.SchemaSetEntity +import org.onap.cps.ri.repository.DataspaceRepository +import org.onap.cps.ri.repository.ModuleReferenceRepository +import org.onap.cps.ri.repository.SchemaSetRepository +import org.onap.cps.ri.repository.YangResourceRepository +import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DuplicatedYangResourceException +import org.onap.cps.spi.model.ModuleReference +import org.springframework.dao.DataIntegrityViolationException +import spock.lang.Specification + +import java.sql.SQLException + +/** + * Specification unit test class for CPS module persistence service. + */ +class CpsModulePersistenceServiceImplSpec extends Specification { + + CpsModulePersistenceService objectUnderTest + + def mockDataspaceRepository = Mock(DataspaceRepository) + def mockYangResourceRepository = Mock(YangResourceRepository) + def mockSchemaSetRepository = Mock(SchemaSetRepository) + def mockModuleReferenceRepository = Mock(ModuleReferenceRepository) + + def yangResourceName = 'my-yang-resource-name' + def yangResourceContent = 'module stores {\n' + + ' yang-version 1.1;\n' + + ' namespace "org:onap:ccsdk:sample";\n' + + '\n' + + ' prefix book-store;\n' + + '\n' + + ' revision "2020-09-15" {\n' + + ' description\n' + + ' "Sample Model";\n' + + ' }' + + '}' + + static yangResourceChecksum = 'b13faef573ed1374139d02c40d8ce09c80ea1dc70e63e464c1ed61568d48d539' + static yangResourceChecksumDbConstraint = 'yang_resource_checksum_key' + static sqlExceptionMessage = String.format('(checksum)=(%s)', yangResourceChecksum) + static checksumIntegrityException = new DataIntegrityViolationException('checksum integrity exception', + new ConstraintViolationException('', new SQLException(sqlExceptionMessage), yangResourceChecksumDbConstraint)) + static checksumIntegrityExceptionWithoutChecksum = new DataIntegrityViolationException('checksum integrity exception', + new ConstraintViolationException('', new SQLException('no checksum'), yangResourceChecksumDbConstraint)) + static otherIntegrityException = new DataIntegrityViolationException('another integrity exception') + + def setup() { + objectUnderTest = new CpsModulePersistenceServiceImpl(mockYangResourceRepository, mockSchemaSetRepository, + mockDataspaceRepository, mockModuleReferenceRepository) + } + + def 'Store schema set error scenario: #scenario.'() { + given: 'no yang resource are currently saved' + mockYangResourceRepository.findAllByChecksumIn(_ as Collection) >> Collections.emptyList() + and: 'persisting yang resource raises db constraint exception (in case of concurrent requests for example)' + mockYangResourceRepository.saveAll(_) >> { throw dbException } + when: 'attempt to store schema set ' + def newYangResourcesNameToContentMap = [(yangResourceName):yangResourceContent] + objectUnderTest.storeSchemaSet('my-dataspace', 'my-schema-set', newYangResourcesNameToContentMap) + then: 'an #expectedThrownException is thrown' + def e = thrown(expectedThrownException) + assert e.getMessage().contains(expectedThrownExceptionMessage) + where: 'the following data is used' + scenario | dbException || expectedThrownException | expectedThrownExceptionMessage + 'checksum data failure' | checksumIntegrityException || DuplicatedYangResourceException | yangResourceChecksum + 'checksum failure without checksum' | checksumIntegrityExceptionWithoutChecksum || DuplicatedYangResourceException | 'no checksum found' + 'other data failure' | otherIntegrityException || DataIntegrityViolationException | 'another integrity exception' + } + + def 'Upgrade existing schema set'() { + given: 'old schema has empty yang resource' + mockYangResourceRepository.findAllByChecksumIn(_ as Collection) >> Collections.emptyList() + def schemaSetEntity = new SchemaSetEntity(id: 1) + mockSchemaSetRepository.getByDataspaceAndName(_, _) >> schemaSetEntity + when: 'schema set update is requested' + objectUnderTest.updateSchemaSetFromModules('my-dataspace', 'my-schemaset', [:], [new ModuleReference('some module name', 'some revision name')]) + then: 'no exception is thrown ' + noExceptionThrown() + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/utils/CpsValidatorImplSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/CpsValidatorImplSpec.groovy new file mode 100644 index 0000000000..d57bf25058 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/CpsValidatorImplSpec.groovy @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ri.utils + + +import org.onap.cps.spi.PaginationOption +import org.onap.cps.spi.exceptions.DataValidationException +import spock.lang.Specification + +class CpsValidatorImplSpec extends Specification { + + def objectUnderTest = new CpsValidatorImpl() + + def 'Validating a valid string.'() { + when: 'the string is validated using a valid name' + objectUnderTest.validateNameCharacters('name-with-no-spaces') + then: 'no exception is thrown' + noExceptionThrown() + } + + def 'Validating an invalid string.'() { + when: 'the string is validated using an invalid name' + objectUnderTest.validateNameCharacters(name) + then: 'a data validation exception is thrown' + def exceptionThrown = thrown(DataValidationException) + and: 'the error was encountered at the following index in #scenario' + assert exceptionThrown.getDetails().contains(expectedErrorMessage) + where: 'the following names are used' + scenario | name || expectedErrorMessage + 'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5' + 'position 9' | 'nameWith Space' || 'nameWith Space invalid token encountered at position 9' + } + + def 'Validating a list of valid names.'() { + given: 'a list of valid names' + def names = ['valid-name', 'another-valid-name'] + when: 'a list of strings is validated' + objectUnderTest.validateNameCharacters(names) + then: 'no exception is thrown' + noExceptionThrown() + } + + def 'Validating a list of names with invalid names.'() { + given: 'a list of names with an invalid name' + def names = ['valid-name', 'name with spaces'] + when: 'a list of strings is validated' + objectUnderTest.validateNameCharacters(names) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + } + + def 'Validate Pagination option with invalid page index and size.'() { + when: 'the pagination option is validated using invalid options' + objectUnderTest.validatePaginationOption(new PaginationOption(-5, -2)) + then: 'a data validation exception is thrown' + def exceptionThrown = thrown(DataValidationException) + and: 'the error was encountered at the following index in #scenario' + assert exceptionThrown.getDetails().contains("Invalid page index or size") + } +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/utils/EscapeUtilsSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/EscapeUtilsSpec.groovy new file mode 100644 index 0000000000..8afd9695a7 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/EscapeUtilsSpec.groovy @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ri.utils + +import spock.lang.Specification + +class EscapeUtilsSpec extends Specification { + + def 'Escape text for use in SQL LIKE operation.'() { + expect: 'SQL LIKE special characters to be escaped with forward-slash' + assert EscapeUtils.escapeForSqlLike(unescapedText) == escapedText + where: + unescapedText || escapedText + 'Only %, _, and \\ are special' || 'Only \\%, \\_, and \\\\ are special' + 'Others (./?$) are not special' || 'Others (./?$) are not special' + } + + def 'Escape text for use in SQL string literal.'() { + expect: 'single quotes to be doubled' + assert EscapeUtils.escapeForSqlStringLiteral("I'm escaping!") == "I''m escaping!" + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/ri/utils/SessionManagerSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/SessionManagerSpec.groovy new file mode 100644 index 0000000000..b50a20c124 --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/ri/utils/SessionManagerSpec.groovy @@ -0,0 +1,137 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ri.utils + +import com.google.common.util.concurrent.TimeLimiter +import com.google.common.util.concurrent.UncheckedExecutionException +import org.hibernate.HibernateException +import org.hibernate.Session +import org.hibernate.Transaction +import org.onap.cps.ri.models.AnchorEntity +import org.onap.cps.ri.repository.AnchorRepository +import org.onap.cps.ri.repository.DataspaceRepository +import org.onap.cps.spi.exceptions.SessionManagerException +import spock.lang.Specification + +class SessionManagerSpec extends Specification { + + def mockCpsSessionFactory = Mock(CpsSessionFactory) + def spiedTimeLimiterProvider = Spy(TimeLimiterProvider) + def mockDataspaceRepository = Mock(DataspaceRepository) + def mockAnchorRepository = Mock(AnchorRepository) + def mockSession1 = Mock(Session) + def mockSession2 = Mock(Session) + def mockTransaction1 = Mock(Transaction) + def mockTransaction2 = Mock(Transaction) + + def objectUnderTest = new SessionManager(mockCpsSessionFactory, spiedTimeLimiterProvider, mockDataspaceRepository, mockAnchorRepository) + + def setup(){ + mockSession1.getTransaction() >> mockTransaction1 + mockSession2.getTransaction() >> mockTransaction2 + } + + def 'Lock anchor entity with #exceptionDuringTest exception.'() { + given: 'a dummy session' + objectUnderTest.sessionMap.put('dummy-session', mockSession1) + and: 'the anchor name can be resolved' + def mockAnchorEntity = Mock(AnchorEntity) + mockAnchorEntity.getId() > 456 + mockAnchorRepository.getByDataspaceAndName(_, _) >> mockAnchorEntity + and: 'timeLimiter throws an #exceptionDuringTest exception' + def mockTimeLimiter = Mock(TimeLimiter) + spiedTimeLimiterProvider.getTimeLimiter(_) >> mockTimeLimiter + mockTimeLimiter.callWithTimeout(*_) >> { throw exceptionDuringTest } + when: 'session tries to acquire anchor lock' + objectUnderTest.lockAnchor('dummy-session', 'some-dataspace', 'some-anchor', 123L) + then: 'a session manager exception is thrown with the expected detail' + def thrown = thrown(SessionManagerException) + thrown.details.contains(expectedExceptionDetail) + where: + exceptionDuringTest || expectedExceptionDetail + new InterruptedException() || 'interrupted' + new UncheckedExecutionException() || 'aborted' + } + + def 'Close a session' () { + given: 'a session in the session map' + objectUnderTest.sessionMap.putAll([testSessionId1:mockSession1]) + when: 'the session manager closes session' + objectUnderTest.closeSession('testSessionId1', commit) + then: 'commit or rollback is called on the transaction as appropriate' + if (commit) { + 1 * mockTransaction1.commit() + } else { + 1 * mockTransaction1.rollback() + } + and: 'the correct session is closed' + 1 * mockSession1.close() + where: + commit << [SessionManager.WITH_COMMIT, SessionManager.WITH_ROLLBACK] + } + + def 'Close session that does not exist.'() { + when: 'attempt to close session that does not exist' + objectUnderTest.closeSession('unknown session id', SessionManager.WITH_COMMIT) + then: 'a session manager exception is thrown with the unknown id in the details' + def thrown = thrown(SessionManagerException) + assert thrown.details.contains('unknown session id') + } + + def 'Hibernate exception while closing session.'() { + given: 'a test session in session map' + objectUnderTest.sessionMap.put('testSessionId', mockSession1) + and: 'an hibernate exception when closing that session' + def hibernateException = new HibernateException('test') + mockSession1.close() >> { throw hibernateException } + when: 'attempt to close session' + objectUnderTest.closeSession('testSessionId', SessionManager.WITH_COMMIT) + then: 'a session manager exception is thrown with the session id in the details' + def thrown = thrown(SessionManagerException) + assert thrown.details.contains('testSessionId') + and: 'the original exception as cause' + assert thrown.cause == hibernateException + } + + def 'Attempt to lock anchor entity with session Id that does not exist'() { + when: 'attempt to acquire anchor lock with session that does not exist' + objectUnderTest.lockAnchor('unknown session id', '', '', 123L) + then: 'a session manager exception is thrown with the unknown id in the details' + def thrown = thrown(SessionManagerException) + thrown.details.contains('unknown session id') + } + + def 'Close all sessions in shutdown.'() { + given: 'sessions that holds transactions in the session map' + objectUnderTest.sessionMap.putAll([testSessionId1:mockSession1, otherSessionId:mockSession2]) + when: 'shutdown method to close all sessions is called' + objectUnderTest.closeAllSessionsInShutdown() + then: 'commit is called on each transaction' + 1 * mockTransaction1.rollback() + 1 * mockTransaction2.rollback() + and: 'each session is closed' + 1 * mockSession1.close() + 1 * mockSession2.close() + then: 'session factory is closed' + 1 * mockCpsSessionFactory.closeSessionFactory() + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy deleted file mode 100644 index c72c3046e8..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ /dev/null @@ -1,281 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (c) 2021 Bell Canada. - * Modifications Copyright (C) 2021-2023 Nordix Foundation - * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============LICENSE_END========================================================= -*/ - -package org.onap.cps.spi.impl - -import com.fasterxml.jackson.databind.ObjectMapper -import org.hibernate.StaleStateException -import org.onap.cps.spi.FetchDescendantsOption -import org.onap.cps.spi.entities.AnchorEntity -import org.onap.cps.spi.entities.DataspaceEntity -import org.onap.cps.spi.entities.FragmentEntity - -import org.onap.cps.spi.exceptions.ConcurrencyException -import org.onap.cps.spi.exceptions.DataValidationException -import org.onap.cps.spi.model.DataNode -import org.onap.cps.spi.model.DataNodeBuilder -import org.onap.cps.spi.repository.AnchorRepository -import org.onap.cps.spi.repository.DataspaceRepository -import org.onap.cps.spi.repository.FragmentRepository -import org.onap.cps.spi.utils.SessionManager -import org.onap.cps.utils.JsonObjectMapper -import org.springframework.dao.DataIntegrityViolationException -import spock.lang.Specification -import java.util.stream.Collectors - -class CpsDataPersistenceServiceSpec extends Specification { - - def mockDataspaceRepository = Mock(DataspaceRepository) - def mockAnchorRepository = Mock(AnchorRepository) - def mockFragmentRepository = Mock(FragmentRepository) - def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def mockSessionManager = Mock(SessionManager) - - def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, - mockFragmentRepository, jsonObjectMapper, mockSessionManager)) - - static def anchorEntity = new AnchorEntity(id: 123, dataspace: new DataspaceEntity(id: 1)) - - def setup() { - mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity - mockFragmentRepository.prefetchDescendantsOfFragmentEntities(_, _) >> { fetchDescendantsOption, fragmentEntities -> fragmentEntities } - mockFragmentRepository.findListByAnchorAndXpath(_, [] as Set) >> [] - } - - def 'Storing data nodes individually when batch operation fails'(){ - given: 'two data nodes and supporting repository mock behavior' - def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath1','OK') - def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath2','OK') - and: 'the batch store operation will fail' - mockFragmentRepository.saveAll(*_) >> { throw new DataIntegrityViolationException("Exception occurred") } - when: 'trying to store data nodes' - objectUnderTest.storeDataNodes('dataSpaceName', 'anchorName', [dataNode1, dataNode2]) - then: 'the two data nodes are saved individually' - 2 * mockFragmentRepository.save(_) - } - - def 'Handling of StaleStateException (caused by concurrent updates) during patch operation for data nodes.'() { - given: 'the system can update one datanode and has two more datanodes that throw an exception while updating' - def dataNodes = createDataNodesAndMockRepositoryMethodSupportingThem([ - '/node1': 'OK', - '/node2': 'EXCEPTION', - '/node3': 'EXCEPTION']) - def updatedLeavesPerXPath = dataNodes.stream() - .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)) - and: 'the batch update will therefore also fail' - mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } - when: 'attempt batch update data nodes' - objectUnderTest.batchUpdateDataLeaves('some-dataspace', 'some-anchor', updatedLeavesPerXPath) - then: 'concurrency exception is thrown' - def thrown = thrown(ConcurrencyException) - assert thrown.message == 'Concurrent Transactions' - and: 'it does not contain the successful datanode' - assert !thrown.details.contains('/node1') - and: 'it contains the failed datanodes' - assert thrown.details.contains('/node2') - assert thrown.details.contains('/node3') - } - - def 'Batch update data node leaves and descendants: #scenario'(){ - given: 'the fragment repository returns fragment entities related to the xpath inputs' - mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> [] - mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [ - new FragmentEntity(1, '/test/xpath', null, "{\"id\":\"testId\"}", anchorEntity, [] as Set) - ] - mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ - new FragmentEntity(1, '/test/xpath1', null, "{\"id\":\"testId1\"}", anchorEntity, [] as Set), - new FragmentEntity(2, '/test/xpath2', null, "{\"id\":\"testId2\"}", anchorEntity, [] as Set) - ] - when: 'replace data node tree' - objectUnderTest.batchUpdateDataLeaves('dataspaceName', 'anchorName', - dataNodes.stream().collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves))) - then: 'call fragment repository save all method' - 1 * mockFragmentRepository.saveAll({fragmentEntities -> - assert fragmentEntities.sort() == expectedFragmentEntities.sort() - assert fragmentEntities.size() == expectedSize - }) - where: 'the following Data Type is passed' - scenario | dataNodes | expectedSize || expectedFragmentEntities - 'empty data node list' | [] | 0 || [] - 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'])] | 1 || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', anchor: anchorEntity)] - 'multiple data nodes' | [new DataNode(xpath: '/test/xpath1', leaves: ['id': 'newTestId1']), new DataNode(xpath: '/test/xpath2', leaves: ['id': 'newTestId2'])] | 2 || [new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"newTestId2"}', anchor: anchorEntity), new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"newTestId1"}', anchor: anchorEntity)] - } - - def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() { - given: 'the system can update one datanode and has two more datanodes that throw an exception while updating' - def dataNodes = createDataNodesAndMockRepositoryMethodSupportingThem([ - '/node1': 'OK', - '/node2': 'EXCEPTION', - '/node3': 'EXCEPTION']) - and: 'the batch update will therefore also fail' - mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } - when: 'attempt batch update data nodes' - objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', dataNodes) - then: 'concurrency exception is thrown' - def thrown = thrown(ConcurrencyException) - assert thrown.message == 'Concurrent Transactions' - and: 'it does not contain the successful datanode' - assert !thrown.details.contains('/node1') - and: 'it contains the failed datanodes' - assert thrown.details.contains('/node2') - assert thrown.details.contains('/node3') - } - - def 'Retrieving a data node with a property JSON value of #scenario'() { - given: 'the db has a fragment with an attribute property JSON value of #scenario' - mockFragmentWithJson("{\"some attribute\": ${dataString}}") - when: 'getting the data node represented by this fragment' - def dataNode = objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', - '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) - then: 'the leaf is of the correct value and data type' - def attributeValue = dataNode[0].leaves.get('some attribute') - assert attributeValue == expectedValue - assert attributeValue.class == expectedDataClass - where: 'the following Data Type is passed' - scenario | dataString || expectedValue | expectedDataClass - 'just numbers' | '15174' || 15174 | Integer - 'number with dot' | '15174.32' || 15174.32 | Double - 'number with 0 value after dot' | '15174.0' || 15174.0 | Double - 'number with 0 value before dot' | '0.32' || 0.32 | Double - 'number higher than max int' | '2147483648' || 2147483648 | Long - 'just text' | '"Test"' || 'Test' | String - 'number with exponent' | '1.2345e5' || 1.2345e5 | Double - 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double - 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String - 'number as String' | '"12345"' || '12345' | String - } - - def 'Retrieving a data node with invalid JSON'() { - given: 'a fragment with invalid JSON' - mockFragmentWithJson('{invalid json') - when: 'getting the data node represented by this fragment' - objectUnderTest.getDataNodes('my-dataspace', 'my-anchor', - '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) - then: 'a data validation exception is thrown' - thrown(DataValidationException) - } - - def 'Retrieving multiple data nodes.'() { - given: 'fragment repository returns a collection of fragments' - mockFragmentRepository.findByAnchorAndXpathIn(anchorEntity, ['/xpath1', '/xpath2'] as Set) >> [ - new FragmentEntity(1, '/xpath1', null, null, anchorEntity, [] as Set), - new FragmentEntity(2, '/xpath2', null, null, anchorEntity, [] as Set) - ] - when: 'getting data nodes for 2 xpaths' - def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) - then: '2 data nodes are returned' - assert result.size() == 2 - } - - def 'start session'() { - when: 'start session' - objectUnderTest.startSession() - then: 'the session manager method to start session is invoked' - 1 * mockSessionManager.startSession() - } - - def 'close session'() { - given: 'session ID' - def someSessionId = 'someSessionId' - when: 'close session method is called with session ID as parameter' - objectUnderTest.closeSession(someSessionId) - then: 'the session manager method to close session is invoked with parameter' - 1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT) - } - - def 'Lock anchor.'(){ - when: 'lock anchor method is called with anchor entity details' - objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) - then: 'the session manager method to lock anchor is invoked with same parameters' - 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) - } - - def 'Replace data node and descendants: #scenario'(){ - given: 'the fragment repository returns fragment entities related to the xpath inputs' - mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> [] - mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [ - new FragmentEntity(1, '/test/xpath', null, '{"id":"testId"}', anchorEntity, [] as Set) - ] - when: 'replace data node tree' - objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes) - then: 'call fragment repository save all method' - 1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities}) - where: 'the following Data Type is passed' - scenario | dataNodes || expectedFragmentEntities - 'empty data node list' | [] || [] - 'one data node in list' | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', anchor: anchorEntity, childFragments: [])] - } - - def 'Replace data nodes and descendants'() { - given: 'the fragment repository returns fragment entities related to the xpath inputs' - mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ - new FragmentEntity(1, '/test/xpath1', null, null, anchorEntity, [] as Set), - new FragmentEntity(2, '/test/xpath2', null, null, anchorEntity, [] as Set) - ] - and: 'some data nodes with descendants' - def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])]) - def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])]) - when: 'the fragment entities are update by the data nodes' - objectUnderTest.updateDataNodesAndDescendants('dataspace', 'anchor', [dataNode1, dataNode2]) - then: 'call fragment repository save all method is called with the updated fragments' - 1 * mockFragmentRepository.saveAll({fragmentEntities -> { - assert fragmentEntities.size() == 2 - def fragmentEntityPerXpath = fragmentEntities.collectEntries { [it.xpath, it] } - assert fragmentEntityPerXpath.get('/test/xpath1').childFragments.first().attributes == '{"id":"childTestId1"}' - assert fragmentEntityPerXpath.get('/test/xpath2').childFragments.first().attributes == '{"id":"childTestId2"}' - }}) - } - - def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) { - def dataNode = new DataNodeBuilder().withXpath(xpath).build() - def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: []) - mockFragmentRepository.getByAnchorAndXpath(_, xpath) >> fragmentEntity - if ('EXCEPTION' == scenario) { - mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } - } - return dataNode - } - - def createDataNodesAndMockRepositoryMethodSupportingThem(Map xpathToScenarioMap) { - def dataNodes = [] - def fragmentEntities = [] - def fragmentId = 1 - xpathToScenarioMap.each { - def xpath = it.key - def scenario = it.value - def dataNode = new DataNodeBuilder().withXpath(xpath).build() - dataNodes.add(dataNode) - def fragmentEntity = new FragmentEntity(id: fragmentId, anchor: anchorEntity, xpath: xpath, childFragments: []) - fragmentEntities.add(fragmentEntity) - if ('EXCEPTION' == scenario) { - mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } - } - fragmentId++ - } - mockFragmentRepository.findByAnchorAndXpathIn(_, xpathToScenarioMap.keySet()) >> fragmentEntities - return dataNodes - } - - def mockFragmentWithJson(json) { - def fragmentEntity = new FragmentEntity(456, '/parent-01', null, json, anchorEntity, [] as Set) - mockFragmentRepository.findByAnchorAndXpathIn(_, ['/parent-01'] as Set) >> [fragmentEntity] - } - -} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceConcurrencySpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceConcurrencySpec.groovy deleted file mode 100644 index 2e4dba2e9b..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceConcurrencySpec.groovy +++ /dev/null @@ -1,145 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022 Bell Canada. - * Modifications Copyright (C) 2021-2023 Nordix Foundation. - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the 'License'); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an 'AS IS' BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ -package org.onap.cps.spi.impl - -import org.hibernate.exception.ConstraintViolationException -import org.onap.cps.spi.CpsAdminPersistenceService -import org.onap.cps.spi.CpsModulePersistenceService -import org.onap.cps.spi.entities.DataspaceEntity -import org.onap.cps.spi.entities.SchemaSetEntity -import org.onap.cps.spi.exceptions.DuplicatedYangResourceException -import org.onap.cps.spi.model.ModuleReference -import org.onap.cps.spi.repository.DataspaceRepository -import org.onap.cps.spi.repository.ModuleReferenceRepository -import org.onap.cps.spi.repository.SchemaSetRepository -import org.onap.cps.spi.repository.YangResourceRepository -import org.spockframework.spring.SpringBean -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.dao.DataIntegrityViolationException -import org.springframework.retry.annotation.EnableRetry -import spock.lang.Specification - -import java.sql.SQLException - -@SpringBootTest(classes=[CpsModulePersistenceServiceImpl]) -@EnableRetry -class CpsModulePersistenceServiceConcurrencySpec extends Specification { - - @Autowired - CpsModulePersistenceService objectUnderTest - - @SpringBean - DataspaceRepository dataspaceRepository = Mock() - - @SpringBean - YangResourceRepository yangResourceRepository = Mock() - - @SpringBean - SchemaSetRepository schemaSetRepository = Mock() - - @SpringBean - CpsAdminPersistenceService cpsAdminPersistenceService = Mock() - - @SpringBean - ModuleReferenceRepository moduleReferenceRepository = Mock() - - def NEW_RESOURCE_NAME = 'some new resource' - def NEW_RESOURCE_CONTENT = 'module stores {\n' + - ' yang-version 1.1;\n' + - ' namespace "org:onap:ccsdk:sample";\n' + - '}' - - def newYangResourcesNameToContentMap = [(NEW_RESOURCE_NAME):NEW_RESOURCE_CONTENT] - - def yangResourceChecksum = 'b13faef573ed1374139d02c40d8ce09c80ea1dc70e63e464c1ed61568d48d539' - - def yangResourceChecksumDbConstraint = 'yang_resource_checksum_key' - - def sqlExceptionMessage = String.format('(checksum)=(%s)', yangResourceChecksum) - - def checksumIntegrityException = new DataIntegrityViolationException("checksum integrity exception", - new ConstraintViolationException('', new SQLException(sqlExceptionMessage), yangResourceChecksumDbConstraint)) - - def 'Store new schema set, maximum retries.'() { - given: 'no pre-existing schemaset in database' - dataspaceRepository.getByName(_) >> new DataspaceEntity() - yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() - when: 'a new schemaset is stored' - objectUnderTest.storeSchemaSet('some dataspace', 'some new schema set', newYangResourcesNameToContentMap) - then: 'a duplicated yang resource exception is thrown ' - thrown(DuplicatedYangResourceException) - and: 'the system will attempt to save the data 5 times (because checksum integrity exception is thrown each time)' - 5 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } - } - - def 'Store new schema set, succeed on third attempt.'() { - given: 'no pre-existing schemaset in database' - dataspaceRepository.getByName(_) >> new DataspaceEntity() - yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() - when: 'a new schemaset is stored' - objectUnderTest.storeSchemaSet('some dataspace', 'some new schema set', newYangResourcesNameToContentMap) - then: 'no exception is thrown ' - noExceptionThrown() - and: 'the system will attempt to save the data 2 times with checksum integrity exception but then succeed' - 2 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } - 1 * yangResourceRepository.saveAll(_) >> [] - } - - def 'Store schema set using modules, maximum retries.'() { - given: 'map of new modules, a list of existing modules, module reference' - def mapOfNewModules = [newModule1: 'module newmodule { yang-version 1.1; revision "2021-10-12" { } }'] - def moduleReferenceForExistingModule = new ModuleReference("test","2021-10-12") - def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] - and: 'no pre-existing schemaset in database' - dataspaceRepository.getByName(_) >> new DataspaceEntity() - yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() - when: 'a new schemaset is stored from a module' - objectUnderTest.storeSchemaSetFromModules('some dataspace', 'some new schema set' , mapOfNewModules, listOfExistingModulesModuleReference) - then: 'a duplicated yang resource exception is thrown ' - thrown(DuplicatedYangResourceException) - and: 'the system will attempt to save the data 5 times (because checksum integrity exception is thrown each time)' - 5 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } - } - - def 'Store schema set using modules, succeed on third attempt.'() { - given: 'map of new modules, a list of existing modules, module reference' - def mapOfNewModules = [newModule1: 'module newmodule { yang-version 1.1; revision "2021-10-12" { } }'] - def moduleReferenceForExistingModule = new ModuleReference("test","2021-10-12") - def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] - and: 'no pre-existing schemaset in database' - def dataspaceEntity = new DataspaceEntity() - dataspaceRepository.getByName(_) >> new DataspaceEntity() - yangResourceRepository.findAllByChecksumIn(_) >> Collections.emptyList() - yangResourceRepository.getResourceIdsByModuleReferences(_) >> [] - and: 'can retrieve schemaset details after storing it' - def schemaSetEntity = new SchemaSetEntity() - schemaSetRepository.getByDataspaceAndName(dataspaceEntity, 'new schema set') >> schemaSetEntity - when: 'a new schemaset is stored from a module' - objectUnderTest.storeSchemaSetFromModules('some dataspace', 'new schema set' , mapOfNewModules, listOfExistingModulesModuleReference) - then: 'no exception is thrown ' - noExceptionThrown() - and: 'the system will attempt to save the data 2 times with checksum integrity exception but then succeed' - 2 * yangResourceRepository.saveAll(_) >> { throw checksumIntegrityException } - 1 * yangResourceRepository.saveAll(_) >> [] - } - -} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceSpec.groovy deleted file mode 100644 index 3447a1c599..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceSpec.groovy +++ /dev/null @@ -1,103 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (c) 2021 Bell Canada. - * Modifications Copyright (C) 2022-2023 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============LICENSE_END========================================================= -*/ - -package org.onap.cps.spi.impl - -import org.hibernate.exception.ConstraintViolationException -import org.onap.cps.spi.CpsModulePersistenceService -import org.onap.cps.spi.entities.SchemaSetEntity -import org.onap.cps.spi.exceptions.DuplicatedYangResourceException -import org.onap.cps.spi.model.ModuleReference -import org.onap.cps.spi.repository.DataspaceRepository -import org.onap.cps.spi.repository.ModuleReferenceRepository -import org.onap.cps.spi.repository.SchemaSetRepository -import org.onap.cps.spi.repository.YangResourceRepository -import org.springframework.dao.DataIntegrityViolationException -import spock.lang.Specification -import java.sql.SQLException - -/** - * Specification unit test class for CPS module persistence service. - */ -class CpsModulePersistenceServiceSpec extends Specification { - - CpsModulePersistenceService objectUnderTest - - def mockDataspaceRepository = Mock(DataspaceRepository) - def mockYangResourceRepository = Mock(YangResourceRepository) - def mockSchemaSetRepository = Mock(SchemaSetRepository) - def mockModuleReferenceRepository = Mock(ModuleReferenceRepository) - - def yangResourceName = 'my-yang-resource-name' - def yangResourceContent = 'module stores {\n' + - ' yang-version 1.1;\n' + - ' namespace "org:onap:ccsdk:sample";\n' + - '\n' + - ' prefix book-store;\n' + - '\n' + - ' revision "2020-09-15" {\n' + - ' description\n' + - ' "Sample Model";\n' + - ' }' + - '}' - - static yangResourceChecksum = 'b13faef573ed1374139d02c40d8ce09c80ea1dc70e63e464c1ed61568d48d539' - static yangResourceChecksumDbConstraint = 'yang_resource_checksum_key' - static sqlExceptionMessage = String.format('(checksum)=(%s)', yangResourceChecksum) - static checksumIntegrityException = new DataIntegrityViolationException('checksum integrity exception', - new ConstraintViolationException('', new SQLException(sqlExceptionMessage), yangResourceChecksumDbConstraint)) - static checksumIntegrityExceptionWithoutChecksum = new DataIntegrityViolationException('checksum integrity exception', - new ConstraintViolationException('', new SQLException('no checksum'), yangResourceChecksumDbConstraint)) - static otherIntegrityException = new DataIntegrityViolationException('another integrity exception') - - def setup() { - objectUnderTest = new CpsModulePersistenceServiceImpl(mockYangResourceRepository, mockSchemaSetRepository, - mockDataspaceRepository, mockModuleReferenceRepository) - } - - def 'Store schema set error scenario: #scenario.'() { - given: 'no yang resource are currently saved' - mockYangResourceRepository.findAllByChecksumIn(_ as Collection) >> Collections.emptyList() - and: 'persisting yang resource raises db constraint exception (in case of concurrent requests for example)' - mockYangResourceRepository.saveAll(_) >> { throw dbException } - when: 'attempt to store schema set ' - def newYangResourcesNameToContentMap = [(yangResourceName):yangResourceContent] - objectUnderTest.storeSchemaSet('my-dataspace', 'my-schema-set', newYangResourcesNameToContentMap) - then: 'an #expectedThrownException is thrown' - def e = thrown(expectedThrownException) - assert e.getMessage().contains(expectedThrownExceptionMessage) - where: 'the following data is used' - scenario | dbException || expectedThrownException | expectedThrownExceptionMessage - 'checksum data failure' | checksumIntegrityException || DuplicatedYangResourceException | yangResourceChecksum - 'checksum failure without checksum' | checksumIntegrityExceptionWithoutChecksum || DuplicatedYangResourceException | 'no checksum found' - 'other data failure' | otherIntegrityException || DataIntegrityViolationException | 'another integrity exception' - } - - def 'Upgrade existing schema set'() { - given: 'old schema has empty yang resource' - mockYangResourceRepository.findAllByChecksumIn(_ as Collection) >> Collections.emptyList() - def schemaSetEntity = new SchemaSetEntity(id: 1) - mockSchemaSetRepository.getByDataspaceAndName(_, _) >> schemaSetEntity - when: 'schema set update is requested' - objectUnderTest.updateSchemaSetFromModules('my-dataspace', 'my-schemaset', [:], [new ModuleReference('some module name', 'some revision name')]) - then: 'no exception is thrown ' - noExceptionThrown() - } - -} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy deleted file mode 100644 index 8d348443c7..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy +++ /dev/null @@ -1,77 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.spi.impl.utils - -import org.onap.cps.spi.PaginationOption -import org.onap.cps.spi.exceptions.DataValidationException -import spock.lang.Specification - -class CpsValidatorSpec extends Specification { - - def objectUnderTest = new CpsValidatorImpl() - - def 'Validating a valid string.'() { - when: 'the string is validated using a valid name' - objectUnderTest.validateNameCharacters('name-with-no-spaces') - then: 'no exception is thrown' - noExceptionThrown() - } - - def 'Validating an invalid string.'() { - when: 'the string is validated using an invalid name' - objectUnderTest.validateNameCharacters(name) - then: 'a data validation exception is thrown' - def exceptionThrown = thrown(DataValidationException) - and: 'the error was encountered at the following index in #scenario' - assert exceptionThrown.getDetails().contains(expectedErrorMessage) - where: 'the following names are used' - scenario | name || expectedErrorMessage - 'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5' - 'position 9' | 'nameWith Space' || 'nameWith Space invalid token encountered at position 9' - } - - def 'Validating a list of valid names.'() { - given: 'a list of valid names' - def names = ['valid-name', 'another-valid-name'] - when: 'a list of strings is validated' - objectUnderTest.validateNameCharacters(names) - then: 'no exception is thrown' - noExceptionThrown() - } - - def 'Validating a list of names with invalid names.'() { - given: 'a list of names with an invalid name' - def names = ['valid-name', 'name with spaces'] - when: 'a list of strings is validated' - objectUnderTest.validateNameCharacters(names) - then: 'a data validation exception is thrown' - thrown(DataValidationException) - } - - def 'Validate Pagination option with invalid page index and size.'() { - when: 'the pagination option is validated using invalid options' - objectUnderTest.validatePaginationOption(new PaginationOption(-5, -2)) - then: 'a data validation exception is thrown' - def exceptionThrown = thrown(DataValidationException) - and: 'the error was encountered at the following index in #scenario' - assert exceptionThrown.getDetails().contains("Invalid page index or size") - } -} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy deleted file mode 100644 index 52330e6251..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/EscapeUtilsSpec.groovy +++ /dev/null @@ -1,41 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.spi.utils - -import spock.lang.Specification - -class EscapeUtilsSpec extends Specification { - - def 'Escape text for use in SQL LIKE operation.'() { - expect: 'SQL LIKE special characters to be escaped with forward-slash' - assert EscapeUtils.escapeForSqlLike(unescapedText) == escapedText - where: - unescapedText || escapedText - 'Only %, _, and \\ are special' || 'Only \\%, \\_, and \\\\ are special' - 'Others (./?$) are not special' || 'Others (./?$) are not special' - } - - def 'Escape text for use in SQL string literal.'() { - expect: 'single quotes to be doubled' - assert EscapeUtils.escapeForSqlStringLiteral("I'm escaping!") == "I''m escaping!" - } - -} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy deleted file mode 100644 index feda338b80..0000000000 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/utils/SessionManagerSpec.groovy +++ /dev/null @@ -1,139 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.spi.utils - -import com.google.common.util.concurrent.TimeLimiter -import com.google.common.util.concurrent.UncheckedExecutionException -import org.hibernate.HibernateException -import org.hibernate.Transaction -import org.onap.cps.spi.config.CpsSessionFactory -import org.onap.cps.spi.entities.AnchorEntity -import org.onap.cps.spi.exceptions.SessionManagerException -import org.onap.cps.spi.repository.AnchorRepository -import org.onap.cps.spi.repository.DataspaceRepository -import spock.lang.Specification -import org.hibernate.Session -import java.util.concurrent.ExecutionException - -class SessionManagerSpec extends Specification { - - def mockCpsSessionFactory = Mock(CpsSessionFactory) - def spiedTimeLimiterProvider = Spy(TimeLimiterProvider) - def mockDataspaceRepository = Mock(DataspaceRepository) - def mockAnchorRepository = Mock(AnchorRepository) - def mockSession1 = Mock(Session) - def mockSession2 = Mock(Session) - def mockTransaction1 = Mock(Transaction) - def mockTransaction2 = Mock(Transaction) - - def objectUnderTest = new SessionManager(mockCpsSessionFactory, spiedTimeLimiterProvider, mockDataspaceRepository, mockAnchorRepository) - - def setup(){ - mockSession1.getTransaction() >> mockTransaction1 - mockSession2.getTransaction() >> mockTransaction2 - } - - def 'Lock anchor entity with #exceptionDuringTest exception.'() { - given: 'a dummy session' - objectUnderTest.sessionMap.put('dummy-session', mockSession1) - and: 'the anchor name can be resolved' - def mockAnchorEntity = Mock(AnchorEntity) - mockAnchorEntity.getId() > 456 - mockAnchorRepository.getByDataspaceAndName(_, _) >> mockAnchorEntity - and: 'timeLimiter throws an #exceptionDuringTest exception' - def mockTimeLimiter = Mock(TimeLimiter) - spiedTimeLimiterProvider.getTimeLimiter(_) >> mockTimeLimiter - mockTimeLimiter.callWithTimeout(*_) >> { throw exceptionDuringTest } - when: 'session tries to acquire anchor lock' - objectUnderTest.lockAnchor('dummy-session', 'some-dataspace', 'some-anchor', 123L) - then: 'a session manager exception is thrown with the expected detail' - def thrown = thrown(SessionManagerException) - thrown.details.contains(expectedExceptionDetail) - where: - exceptionDuringTest || expectedExceptionDetail - new InterruptedException() || 'interrupted' - new UncheckedExecutionException() || 'aborted' - } - - def 'Close a session' () { - given: 'a session in the session map' - objectUnderTest.sessionMap.putAll([testSessionId1:mockSession1]) - when: 'the session manager closes session' - objectUnderTest.closeSession('testSessionId1', commit) - then: 'commit or rollback is called on the transaction as appropriate' - if (commit) { - 1 * mockTransaction1.commit() - } else { - 1 * mockTransaction1.rollback() - } - and: 'the correct session is closed' - 1 * mockSession1.close() - where: - commit << [SessionManager.WITH_COMMIT, SessionManager.WITH_ROLLBACK] - } - - def 'Close session that does not exist.'() { - when: 'attempt to close session that does not exist' - objectUnderTest.closeSession('unknown session id', SessionManager.WITH_COMMIT) - then: 'a session manager exception is thrown with the unknown id in the details' - def thrown = thrown(SessionManagerException) - assert thrown.details.contains('unknown session id') - } - - def 'Hibernate exception while closing session.'() { - given: 'a test session in session map' - objectUnderTest.sessionMap.put('testSessionId', mockSession1) - and: 'an hibernate exception when closing that session' - def hibernateException = new HibernateException('test') - mockSession1.close() >> { throw hibernateException } - when: 'attempt to close session' - objectUnderTest.closeSession('testSessionId', SessionManager.WITH_COMMIT) - then: 'a session manager exception is thrown with the session id in the details' - def thrown = thrown(SessionManagerException) - assert thrown.details.contains('testSessionId') - and: 'the original exception as cause' - assert thrown.cause == hibernateException - } - - def 'Attempt to lock anchor entity with session Id that does not exist'() { - when: 'attempt to acquire anchor lock with session that does not exist' - objectUnderTest.lockAnchor('unknown session id', '', '', 123L) - then: 'a session manager exception is thrown with the unknown id in the details' - def thrown = thrown(SessionManagerException) - thrown.details.contains('unknown session id') - } - - def 'Close all sessions in shutdown.'() { - given: 'sessions that holds transactions in the session map' - objectUnderTest.sessionMap.putAll([testSessionId1:mockSession1, otherSessionId:mockSession2]) - when: 'shutdown method to close all sessions is called' - objectUnderTest.closeAllSessionsInShutdown() - then: 'commit is called on each transaction' - 1 * mockTransaction1.rollback() - 1 * mockTransaction2.rollback() - and: 'each session is closed' - 1 * mockSession1.close() - 1 * mockSession2.close() - then: 'session factory is closed' - 1 * mockCpsSessionFactory.closeSessionFactory() - } - -} -- cgit 1.2.3-korg