diff options
author | ToineSiebelink <toine.siebelink@est.tech> | 2023-07-26 17:49:02 +0100 |
---|---|---|
committer | ToineSiebelink <toine.siebelink@est.tech> | 2023-07-31 08:57:30 +0100 |
commit | e3cdc8a0591553da6d022337fa69c8dd507510f6 (patch) | |
tree | 6c72936bc39e00d2b9821def0622e83165c1cb8d /cps-service | |
parent | 92bf624e75673f8027ba48bf4f8c2d28b3b01552 (diff) |
Increase code coverage in cps-service module
- After last rebase I had to remove 3 unused recent cloud eventd specific exceptions/constructors
- Moved the only used new exception from SPI to the relevant util package
(please NOTE not all exceptions belong in SPI and always question need for new exception
when there is no specific handling, try to use standard or existign CPS exception instead!)
- Increased cps-service module (line) coverage from 95 to 100%
- Added tests for missing exceptions (handling i.e. thrown up)
- Removed incorrect SPI defined OperationNotYetSupportedException
(replaced with standard java exception instead)
- Fixed some legacy issues with existign test classes I modified
(unnecessary setup, conventions etc)
- Increased coverage for DataNodeBuilder
- Added or modified test to include more spi models
- Added tests for Hazelcast Configs
- Added more tests for json object mapper
- Added test and fixed error handling in YangUtils/XmlFileUtils
(it was incorrectly converting a config exception to a data validation exception)
Issue-ID: CPS-475
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
Change-Id: I5852ba01bc5b33ae361b8f29daae9868f05baa35
Diffstat (limited to 'cps-service')
22 files changed, 421 insertions, 285 deletions
diff --git a/cps-service/pom.xml b/cps-service/pom.xml index c97623f2a1..8bc39b1d48 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -35,10 +35,6 @@ <artifactId>cps-service</artifactId> - <properties> - <minimum-coverage>0.95</minimum-coverage> - </properties> - <dependencies> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java index 38f8988279..696fd60f8c 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java +++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. - * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (c) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; import lombok.AllArgsConstructor; +import lombok.SneakyThrows; import org.onap.cps.api.CpsDataService; import org.onap.cps.event.model.Content; import org.onap.cps.event.model.CpsDataUpdatedEvent; @@ -44,22 +45,9 @@ import org.springframework.stereotype.Component; @AllArgsConstructor(onConstructor = @__(@Lazy)) public class CpsDataUpdatedEventFactory { - private static final URI EVENT_SCHEMA; - private static final URI EVENT_SOURCE; - private static final String EVENT_TYPE = "org.onap.cps.data-updated-event"; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - static { - try { - EVENT_SCHEMA = new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1"); - EVENT_SOURCE = new URI("urn:cps:org.onap.cps"); - } catch (final URISyntaxException e) { - // As it is fixed string, I don't expect to see this error - throw new IllegalArgumentException(e); - } - } - @Lazy private final CpsDataService cpsDataService; @@ -82,14 +70,17 @@ public class CpsDataUpdatedEventFactory { return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp, operation); } - private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode, - final OffsetDateTime observedTimestamp, final Operation operation) { - final var cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); + @SneakyThrows(URISyntaxException.class) + private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, + final DataNode dataNode, + final OffsetDateTime observedTimestamp, + final Operation operation) { + final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode, observedTimestamp, operation)); cpsDataUpdatedEvent.withId(UUID.randomUUID().toString()); - cpsDataUpdatedEvent.withSchema(EVENT_SCHEMA); - cpsDataUpdatedEvent.withSource(EVENT_SOURCE); - cpsDataUpdatedEvent.withType(EVENT_TYPE); + cpsDataUpdatedEvent.withSchema(new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1")); + cpsDataUpdatedEvent.withSource(new URI("urn:cps:org.onap.cps")); + cpsDataUpdatedEvent.withType("org.onap.cps.data-updated-event"); return cpsDataUpdatedEvent; } diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java deleted file mode 100644 index 1d520e7b0c..0000000000 --- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2020 Pantheon.tech - * Modifications Copyright (C) 2020 Bell Canada - * Modifications Copyright (C) 2020-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.exceptions; - -public class CloudEventConstructionException extends CpsException { - - private static final long serialVersionUID = 7747941311132087621L; - - /** - * Constructor. - * - * @param message the error message - * @param details the error details - */ - public CloudEventConstructionException(final String message, final String details) { - super(message, details); - } - - /** - * Constructor. - * - * @param message the error message - * @param details the error details - * @param cause the error cause - */ - public CloudEventConstructionException(final String message, final String details, final Throwable cause) { - super(message, details, cause); - } -} diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java deleted file mode 100644 index 6b898e853b..0000000000 --- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2020 Pantheon.tech - * Modifications Copyright (C) 2020 Bell Canada - * Modifications Copyright (C) 2020-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.exceptions; - -public class SubscriptionOutcomeTypeNotFoundException extends CpsException { - - private static final long serialVersionUID = 7747941311132087621L; - - /** - * Constructor. - * - * @param message the error message - * @param details the error details - */ - public SubscriptionOutcomeTypeNotFoundException(final String message, final String details) { - super(message, details); - } - - /** - * Constructor. - * - * @param message the error message - * @param details the error details - * @param cause the error cause - */ - public SubscriptionOutcomeTypeNotFoundException(final String message, final String details, final Throwable cause) { - super(message, details, cause); - } -} diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java index e212933388..b040af5bb4 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java @@ -184,9 +184,8 @@ public class DataNodeBuilder { private DataNode buildFromContainerNode() { final Collection<DataNode> dataNodeCollection = buildCollectionFromContainerNode(); - if (!dataNodeCollection.iterator().hasNext()) { - throw new DataValidationException( - "Unsupported xpath: ", "Unsupported xpath as it is referring to one element"); + if (dataNodeCollection.isEmpty()) { + throw new DataValidationException("Unsupported Normalized Node", "No valid node found"); } return dataNodeCollection.iterator().next(); } @@ -278,5 +277,4 @@ public class DataNodeBuilder { } } - } diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java index 09f2e16c6a..98c7947e1c 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG + * Modifications 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. @@ -39,7 +40,6 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import org.onap.cps.spi.exceptions.DataValidationException; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.w3c.dom.Document; @@ -102,10 +102,8 @@ public class XmlFileUtils { final Map<String, String> rootNodeProperty) throws IOException, SAXException, ParserConfigurationException, TransformerException { final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); - final StringBuilder xmlStringBuilder = new StringBuilder(); - xmlStringBuilder.append(xmlContent); - final Document document = documentBuilder.parse( - new ByteArrayInputStream(xmlStringBuilder.toString().getBytes(StandardCharsets.UTF_8))); + final Document document = + documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))); final Element root = document.getDocumentElement(); if (!root.getTagName().equals(rootNodeTagName) && !root.getTagName().equals(YangUtils.DATA_ROOT_NODE_TAG_NAME)) { @@ -143,22 +141,19 @@ public class XmlFileUtils { static Document addDataRootNode(final Element node, final String tagName, final String namespace, - final Map<String, String> rootNodeProperty) { - try { - final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); - final Document document = documentBuilder.newDocument(); - final Element rootElement = document.createElementNS(namespace, tagName); - for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) { - final Element propertyElement = document.createElement(entry.getKey()); - propertyElement.setTextContent(entry.getValue()); - rootElement.appendChild(propertyElement); - } - rootElement.appendChild(document.adoptNode(node)); - document.appendChild(rootElement); - return document; - } catch (final ParserConfigurationException exception) { - throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception); + final Map<String, String> rootNodeProperty) + throws ParserConfigurationException { + final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); + final Document document = documentBuilder.newDocument(); + final Element rootElement = document.createElementNS(namespace, tagName); + for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) { + final Element propertyElement = document.createElement(entry.getKey()); + propertyElement.setTextContent(entry.getValue()); + rootElement.appendChild(propertyElement); } + rootElement.appendChild(document.adoptNode(node)); + document.appendChild(rootElement); + return document; } private static DocumentBuilderFactory getDocumentBuilderFactory() { diff --git a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java index deb5b05752..ca907148dd 100644 --- a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java @@ -27,7 +27,6 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import io.micrometer.core.annotation.Timed; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -37,7 +36,6 @@ import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.NoArgsConstructor; -import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.spi.exceptions.ModelValidationException; import org.onap.cps.spi.model.ModuleReference; import org.opendaylight.yangtools.yang.common.Revision; @@ -45,7 +43,6 @@ import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier; import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource; -import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException; import org.opendaylight.yangtools.yang.parser.rfc7950.reactor.RFC7950Reactors; import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangStatementStreamSource; import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; @@ -144,23 +141,20 @@ public final class YangTextSchemaSourceSetBuilder { final String resourceName = yangTextSchemaSource.getIdentifier().getName(); try { reactor.addSource(YangStatementStreamSource.create(yangTextSchemaSource)); - } catch (final IOException e) { - throw new CpsException("Failed to read yang resource.", - String.format("Exception occurred on reading resource %s.", resourceName), e); - } catch (final YangSyntaxErrorException e) { - throw new ModelValidationException("Yang resource is invalid.", - String.format( - "Yang syntax validation failed for resource %s:%n%s", resourceName, e.getMessage()), e); + } catch (final Exception exception) { + throw new ModelValidationException("Yang resource processing exception.", + String.format("Could not process resource %s:%n%s", resourceName, exception.getMessage()), + exception); } } try { return reactor.buildEffective(); - } catch (final ReactorException e) { + } catch (final ReactorException reactorException) { final List<String> resourceNames = yangResourceNameToContent.keySet().stream().collect(Collectors.toList()); Collections.sort(resourceNames); throw new ModelValidationException("Invalid schema set.", - String.format("Effective schema context build failed for resources %s.", resourceNames.toString()), - e); + String.format("Effective schema context build failed for resources %s.", resourceNames), + reactorException); } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy index 4e0349d2b8..eb41e2085f 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy @@ -25,6 +25,7 @@ package org.onap.cps.api.impl import org.onap.cps.api.CpsDataService import org.onap.cps.spi.CpsAdminPersistenceService +import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.Dataspace import org.onap.cps.spi.utils.CpsValidator @@ -154,6 +155,21 @@ class CpsAdminServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters('some-dataspace-name') } + def 'Query all anchors with Module Names Not Found Exception in persistence layer.'() { + given: 'the persistence layer throws a Module Names Not Found Exception' + def originalException = new ModuleNamesNotFoundException('exception-ds', [ 'm1', 'm2']) + mockCpsAdminPersistenceService.queryAnchors(*_) >> { throw originalException} + when: 'attempt query anchors' + objectUnderTest.queryAnchorNames('some-dataspace-name', []) + then: 'the same exception is thrown (up)' + def thrownUp = thrown(ModuleNamesNotFoundException) + assert thrownUp == originalException + and: 'the exception details contains the relevant data' + assert thrownUp.details.contains('exception-ds') + assert thrownUp.details.contains('m1') + assert thrownUp.details.contains('m2') + } + def 'Delete dataspace.'() { when: 'delete dataspace is invoked' objectUnderTest.deleteDataspace('someDataspace') diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index ba438496fd..cb95fb6bfd 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -29,7 +29,11 @@ import org.onap.cps.notification.NotificationService import org.onap.cps.notification.Operation import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.ConcurrencyException +import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.spi.exceptions.SessionManagerException +import org.onap.cps.spi.exceptions.SessionTimeoutException import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder @@ -333,6 +337,18 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"] } + def 'Replace data node with concurrency exception in persistence layer.'() { + given: 'the persistence layer throws an concurrency exception' + def originalException = new ConcurrencyException('message', 'details') + mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException } + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to replace data node' + objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp) + then: 'the same exception is thrown up' + def thrownUp = thrown(ConcurrencyException) + assert thrownUp == originalException + } + def 'Replace list content data fragment under parent node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -366,8 +382,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete list element under existing node.'() { - given: 'schema set for given anchor and dataspace references test-tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete list data method is invoked with list element json data' objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp) then: 'the persistence service method is invoked with correct parameters' @@ -379,8 +393,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete multiple list elements under existing node.'() { - given: 'schema set for given anchor and dataspace references test-tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete multiple list data method is invoked with list element json data' objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp) then: 'the persistence service method is invoked with correct parameters' @@ -392,8 +404,6 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete data node under anchor and dataspace.'() { - given: 'schema set for given anchor and dataspace references test tree model' - setupSchemaSetMocks('test-tree.yang') when: 'delete data node method is invoked with correct parameters' objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp) then: 'the persistence service method is invoked with the correct parameters' @@ -405,9 +415,7 @@ class CpsDataServiceImplSpec extends Specification { } def 'Delete all data nodes for a given anchor and dataspace.'() { - given: 'schema set for given anchor and dataspace references test tree model' - setupSchemaSetMocks('test-tree.yang') - when: 'delete data node method is invoked with correct parameters' + when: 'delete data nodes method is invoked with correct parameters' objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp) then: 'data updated event is sent to notification service before the delete' 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp) @@ -417,6 +425,20 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName) } + def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() { + given: 'a batch exception in persistence layer' + def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[]) + mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException } + when: 'attempt to delete data nodes' + objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp) + then: 'the original exception is thrown up' + def thrownUp = thrown(DataNodeNotFoundExceptionBatch) + assert thrownUp == originalException + and: 'the exception details contain the expected data' + assert thrownUp.details.contains('ds1') + assert thrownUp.details.contains('a1') + } + def 'Delete all data nodes for given dataspace and multiple anchors.'() { given: 'schema set for given anchors and dataspace references test tree model' setupSchemaSetMocks('test-tree.yang') @@ -433,22 +455,28 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>) } - def setupSchemaSetMocks(String... yangResources) { - def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) - mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext - } - - def 'start session'() { + def 'Start session.'() { when: 'start session method is called' objectUnderTest.startSession() then: 'the persistence service method to start session is invoked' 1 * mockCpsDataPersistenceService.startSession() } - def 'close session'(){ + def 'Start session with Session Manager Exceptions.'() { + given: 'the persistence layer throws an Session Manager Exception' + mockCpsDataPersistenceService.startSession() >> { throw originalException } + when: 'attempt to start session' + objectUnderTest.startSession() + then: 'the original exception is thrown up' + def thrownUp = thrown(SessionManagerException) + assert thrownUp == originalException + where: 'variations of Session Manager Exception are used' + originalException << [ new SessionManagerException('message','details'), + new SessionManagerException('message','details', new Exception('cause')), + new SessionTimeoutException('message','details', new Exception('cause'))] + } + + def 'Close session.'(){ given: 'session Id from calling the start session method' def sessionId = objectUnderTest.startSession() when: 'close session method is called' @@ -457,20 +485,26 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.closeSession(sessionId) } - def 'lock anchor with no timeout parameter'(){ + def 'Lock anchor with no timeout parameter.'(){ when: 'lock anchor method with no timeout parameter with details of anchor entity to lock' objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName') then: 'the persistence service method to lock anchor is invoked with default timeout' - 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 300L) + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L) } - def 'lock anchor with timeout parameter'(){ + def 'Lock anchor with timeout parameter.'(){ when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock' - objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 250L) + objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) then: 'the persistence service method to lock anchor is invoked with the given timeout' - 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', - 'some-anchorName', 250L) + 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy index 3884eda661..a794c58fc6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy @@ -26,8 +26,10 @@ package org.onap.cps.api.impl import org.onap.cps.TestUtils import org.onap.cps.api.CpsAdminService import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.exceptions.DuplicatedYangResourceException import org.onap.cps.spi.exceptions.ModelValidationException import org.onap.cps.spi.exceptions.SchemaSetInUseException +import org.onap.cps.spi.model.ModuleDefinition import org.onap.cps.spi.utils.CpsValidator import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.ModuleReference @@ -50,24 +52,22 @@ class CpsModuleServiceImplSpec extends Specification { def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAdminService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def 'Create schema set.'() { - given: 'Valid yang resource as name-to-content map' - def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') when: 'Create schema set method is invoked' - objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) + objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:]) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) + 1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', [:]) and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('someDataspace', 'someSchemaSet') } def 'Create schema set from new modules and existing modules.'() { given: 'a list of existing modules module reference' - def moduleReferenceForExistingModule = new ModuleReference("test", "2021-10-12","test.org") + def moduleReferenceForExistingModule = new ModuleReference('test', '2021-10-12','test.org') def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule] when: 'create schema set from modules method is invoked' - objectUnderTest.createSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference) + objectUnderTest.createSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference) then: 'processing is delegated to persistence service' - 1 * mockCpsModulePersistenceService.storeSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference) + 1 * mockCpsModulePersistenceService.storeSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference) and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someSchemaSetName') } @@ -78,7 +78,21 @@ class CpsModuleServiceImplSpec extends Specification { when: 'Create schema set method is invoked' objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap) then: 'Model validation exception is thrown' - thrown(ModelValidationException.class) + thrown(ModelValidationException) + } + + def 'Create schema set with duplicate yang resource exception in persistence layer.'() { + given: 'the persistence layer throws an duplicated yang resource exception' + def originalException = new DuplicatedYangResourceException('name', '123', null) + mockCpsModulePersistenceService.storeSchemaSet(*_) >> { throw originalException } + when: 'attempt to create schema set' + objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:]) + then: 'the same duplicated yang resource exception is thrown (up)' + def thrownUp = thrown(DuplicatedYangResourceException) + assert thrownUp == originalException + and: 'the exception message contains the relevant data' + assert thrownUp.message.contains('name') + assert thrownUp.message.contains('123') } def 'Get schema set by name and dataspace.'() { @@ -212,20 +226,23 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someAnchorName') } - def 'Identifying new module references'(){ + def 'Identifying new module references.'(){ given: 'module references from cm handle' def moduleReferencesToCheck = [new ModuleReference('some-module', 'some-revision')] when: 'identifyNewModuleReferences is called' objectUnderTest.identifyNewModuleReferences(moduleReferencesToCheck) then: 'cps module persistence service is called with module references to check' - 1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck); + 1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck) } def 'Getting module definitions.'() { + given: 'the module persistence service returns a collection of module definitions' + def moduleDefinitionsFromPersistenceService = [ new ModuleDefinition('name', 'revision', 'content' ) ] + mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name') >> moduleDefinitionsFromPersistenceService when: 'get module definitions method is called with a valid dataspace and anchor name' - objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name') - then: 'CPS module persistence service is invoked the correct number of times' - 1 * mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name') + def result = objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name') + then: 'the result is the same collection returned by the persistence service' + assert result == moduleDefinitionsFromPersistenceService and: 'the CpsValidator is called on the dataspaceName and schemaSetName' 1 * mockCpsValidator.validateNameCharacters('some-dataspace-name', 'some-anchor-name') } diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy new file mode 100644 index 0000000000..8efd48547e --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy @@ -0,0 +1,54 @@ +/* + * ============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.cache + +import spock.lang.Specification + +class HazelcastCacheConfigSpec extends Specification { + + def objectUnderTest = new HazelcastCacheConfig() + + def 'Create Hazelcast instance with a #scenario'() { + given: 'a cluster name' + objectUnderTest.clusterName = 'my cluster' + when: 'an hazelcast instance is created (name has to be unique)' + def result = objectUnderTest.createHazelcastInstance(scenario, config) + then: 'the instance is created and has the correct name' + assert result.name == scenario + and: 'if applicable it has a map config with the expected name' + if (expectMapConfig) { + assert result.config.mapConfigs.values()[0].name == 'my map config' + } else { + assert result.config.mapConfigs.isEmpty() + } + and: 'if applicable it has a queue config with the expected name' + if (expectQueueConfig) { + assert result.config.queueConfigs.values()[0].name == 'my queue config' + } else { + assert result.config.queueConfigs.isEmpty() + } + where: 'the following configs are used' + scenario | config || expectMapConfig | expectQueueConfig + 'Map Config' | HazelcastCacheConfig.createMapConfig('my map config') || true | false + 'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false | true + } + +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy index 6a4e2a098f..b1880d50fb 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java +++ b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy @@ -18,23 +18,15 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.spi.exceptions; +package org.onap.cps.config -/** - * Operation Not Yet Supported Exception. - * Indicates the operation is not supported and has intention to be supported in the future. - */ - -public class OperationNotYetSupportedException extends CpsException { +import spock.lang.Specification - private static final long serialVersionUID = 1517903069236383746L; +class CacheConfigSpec extends Specification { - /** - * Constructor. - * - * @param details reason for the exception - */ - public OperationNotYetSupportedException(final String details) { - super("Operation Not Yet Supported Exception", details); + def 'Create Cache Config. (easiest test ever)'() { + expect: 'can create a Cache Config' + new CacheConfig() != null } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy index 5dbc2bb04b..49f4bf3850 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2021-2022 Bell Canada. - * Modifications Copyright (c) 2022 Nordix Foundation + * Modifications Copyright (c) 2022-2023 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,8 @@ package org.onap.cps.notification +import org.onap.cps.spi.model.DataNode + import java.time.OffsetDateTime import java.time.format.DateTimeFormatter import org.onap.cps.utils.DateTimeUtility @@ -35,7 +37,7 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.springframework.util.StringUtils import spock.lang.Specification -class CpsDataUpdateEventFactorySpec extends Specification { +class CpsDataUpdatedEventFactorySpec extends Specification { def mockCpsDataService = Mock(CpsDataService) @@ -112,6 +114,22 @@ class CpsDataUpdateEventFactorySpec extends Specification { } } + def 'Create CPS Data Event with URI Syntax Exception'() { + given: 'an anchor' + def anchor = new Anchor('my-anchorname', 'my-dataspace', 'my-schemaset-name') + and: 'a mocked data Node (collection)' + def mockDataNode = Mock(DataNode) + mockCpsDataService.getDataNodes(*_) >> [ mockDataNode ] + and: 'a URI syntax exception is thrown somewhere (using datanode as cannot manipulate hardcoded URIs' + def originalException = new URISyntaxException('input', 'reason', 0) + mockDataNode.getXpath() >> { throw originalException } + when: 'attempt to create data updated event' + objectUnderTest.createCpsDataUpdatedEvent(anchor, OffsetDateTime.now(), Operation.UPDATE) + then: 'the same exception is thrown up' + def thrownUp = thrown(URISyntaxException) + assert thrownUp == originalException + } + def isExpectedDateTimeFormat(String observedTimestamp) { try { DateTimeFormatter.ofPattern(dateTimeFormat).parse(observedTimestamp) diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy index d0cd47383f..89e305aedb 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation + * 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. @@ -44,15 +44,17 @@ class NotificationErrorHandlerSpec extends Specification{ ((Logger) LoggerFactory.getLogger(NotificationErrorHandler.class)).detachAndStopAllAppenders(); } - def 'Logging exception via notification error handler'() { - when: 'some exception occurs' - objectUnderTest.onException(new Exception('sample exception'), 'some context') + def 'Logging exception via notification error handler #scenario'() { + when: 'exception #scenario occurs' + objectUnderTest.onException(exception, 'some context') then: 'log output results contains the correct error details' - def logMessage = logWatcher.list.get(0).getFormattedMessage() - logMessage.contains( - "Failed to process \n" + - " Error cause: sample exception \n" + - " Error context: [some context]") + def logMessage = logWatcher.list[0].getFormattedMessage() + assert logMessage.contains('Failed to process') + assert logMessage.contains("Error cause: ${exptectedCauseString}") + assert logMessage.contains("Error context: [some context]") + where: + scenario | exception || exptectedCauseString + 'with cause' | new Exception('message') || 'message' + 'without cause' | new Exception('message', new RuntimeException('cause')) || 'java.lang.RuntimeException: cause' } } - diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy index 2ef468bb53..f07f89b391 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy @@ -42,14 +42,14 @@ import java.util.concurrent.TimeUnit @ContextConfiguration(classes = [NotificationProperties, NotificationService, NotificationErrorHandler, AsyncConfig]) class NotificationServiceSpec extends Specification { + @SpringSpy + NotificationProperties spyNotificationProperties @SpringBean NotificationPublisher mockNotificationPublisher = Mock() @SpringBean CpsDataUpdatedEventFactory mockCpsDataUpdatedEventFactory = Mock() @SpringSpy NotificationErrorHandler spyNotificationErrorHandler - @SpringSpy - NotificationProperties spyNotificationProperties @SpringBean CpsAdminService mockCpsAdminService = Mock() @@ -146,4 +146,13 @@ class NotificationServiceSpec extends Specification { notThrown Exception 1 * spyNotificationErrorHandler.onException(_, _, _, '/', Operation.CREATE) } + + def 'Disabled Notification services'() { + given: 'a notification service that is disabled' + spyNotificationProperties.enabled >> false + NotificationService notificationService = new NotificationService(spyNotificationProperties, mockNotificationPublisher, mockCpsDataUpdatedEventFactory, spyNotificationErrorHandler, mockCpsAdminService) + notificationService.init() + expect: 'it will not send notifications' + assert notificationService.shouldSendNotification('') == false + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy index b095bfd3d1..28bf38fb5e 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy @@ -21,6 +21,7 @@ package org.onap.cps.spi +import org.onap.cps.spi.exceptions.DataValidationException import spock.lang.Specification class FetchDescendantsOptionSpec extends Specification { @@ -74,10 +75,10 @@ class FetchDescendantsOptionSpec extends Specification { thrown IllegalArgumentException } - def 'Create fetch descendant option with descendant using #scenario.'() { - when: 'the next level of depth is not allowed' - def FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString) - then: 'fetch descendant object created' + def 'Create fetch descendant option from string scenario: #scenario.'() { + when: 'create fetch descendant option from string' + def fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString) + then: 'fetch descendant object created with correct depth' assert fetchDescendantsOption.depth == expectedDepth where: 'following parameters are used' scenario | fetchDescendantsOptionAsString || expectedDepth @@ -85,12 +86,21 @@ class FetchDescendantsOptionSpec extends Specification { 'all descendants using all' | 'all' || -1 'No descendants by default' | '' || 0 'No descendants using none' | 'none' || 0 + 'No descendants using number' | '0' || 0 'direct child using number' | '1' || 1 'direct child using direct' | 'direct' || 1 'til 10th descendants using number' | '10' || 10 } - def 'String values.'() { + def 'Create fetch descendant option from string with invalid string.'() { + when: 'attempt to create fetch descendant option from invalid string' + FetchDescendantsOption.getFetchDescendantsOption('invalid-string') + then: 'a validation exception is thrown with the invalid string in the details' + def thrown = thrown(DataValidationException) + thrown.details.contains('invalid-string') + } + + def 'Convert to string.'() { expect: 'each fetch descendant option has the correct String value' assert fetchDescendantsOption.toString() == expectedStringValue where: 'the following option is used' diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy new file mode 100644 index 0000000000..c8446902d5 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy @@ -0,0 +1,38 @@ +/* + * ============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.model + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.utils.JsonObjectMapper +import spock.lang.Specification + +class ConditionPropertiesSpec extends Specification { + + ObjectMapper objectMapper = new ObjectMapper() + + def 'Condition Properties JSON conversion.'() { + given: 'a condition properties' + def objectUnderTest = new ConditionProperties(conditionName: 'test', conditionParameters: [ [ key : 'value' ] ]) + expect: 'the name is blank' + assert objectMapper.writeValueAsString(objectUnderTest) == '{"conditionName":"test","conditionParameters":[{"key":"value"}]}' + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy index 1559783e97..fcbae628e6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021-2022 Nordix Foundation. + * Modifications Copyright (C) 2021-2023 Nordix Foundation. * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,15 +22,19 @@ package org.onap.cps.spi.model import org.onap.cps.TestUtils +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.utils.DataMapUtils import org.onap.cps.utils.YangUtils import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode +import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode import spock.lang.Specification class DataNodeBuilderSpec extends Specification { - Map<String, Map<String, Serializable>> expectedLeavesByXpathMap = [ + def objectUnderTest = new DataNodeBuilder() + + def expectedLeavesByXpathMap = [ '/test-tree' : [], '/test-tree/branch[@name=\'Left\']' : [name: 'Left'], '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']], @@ -56,7 +60,7 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('test-tree.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: '6 DataNode objects with unique xpath were created in total' mappedResult.size() == 6 @@ -76,16 +80,12 @@ class DataNodeBuilderSpec extends Specification { def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") when: 'the container node is converted to a data node with parent node xpath defined' - def result = new DataNodeBuilder() - .withContainerNode(containerNode) - .withParentNodeXpath("/test-tree") - .build() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: '2 DataNode objects with unique xpath were created in total' mappedResult.size() == 2 and: 'all expected xpaths were built' - mappedResult.keySet() - .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest']) + mappedResult.keySet().containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest']) } def 'Converting ContainerNode (tree) to a DataNode (tree) -- augmentation case.'() { @@ -96,11 +96,10 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node ' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'all expected data nodes are populated' mappedResult.size() == 32 - println(mappedResult.keySet().sort()) and: 'xpaths for augmentation nodes (link and termination-point nodes) were built correctly' mappedResult.keySet().containsAll([ "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']", @@ -130,8 +129,7 @@ class DataNodeBuilderSpec extends Specification { def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) when: 'the container node is converted to a data node with given parent node xpath' - def result = new DataNodeBuilder().withContainerNode(containerNode) - .withParentNodeXpath(parentNodeXpath).build() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build() then: 'the resulting data node represents a child of augmentation node' assert result.xpath == "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']/source" assert result.leaves['source-node'] == 'D1' @@ -146,15 +144,13 @@ class DataNodeBuilderSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json') def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) when: 'the container node is converted to a data node' - def result = new DataNodeBuilder().withContainerNode(containerNode).build() + def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'the resulting data node contains only one xpath with 3 leaves' - mappedResult.keySet().containsAll([ - "/container-with-choice-leaves" - ]) - assert result.leaves['leaf-1'] == "test" - assert result.leaves['choice-case1-leaf-a'] == "test" - assert result.leaves['choice-case1-leaf-b'] == "test" + mappedResult.keySet().containsAll([ '/container-with-choice-leaves' ]) + assert result.leaves['leaf-1'] == 'test' + assert result.leaves['choice-case1-leaf-a'] == 'test' + assert result.leaves['choice-case1-leaf-b'] == 'test' } def 'Converting ContainerNode into DataNode collection: #scenario.'() { @@ -162,12 +158,11 @@ class DataNodeBuilderSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'parent node xpath referencing parent of list element' - def parentNodeXpath = "/test-tree" + def parentNodeXpath = '/test-tree' and: 'the json data fragment (list element) parsed into container node object' def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) when: 'the container node is converted to a data node collection' - def result = new DataNodeBuilder().withContainerNode(containerNode) - .withParentNodeXpath(parentNodeXpath).buildCollection() + def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection() def resultXpaths = result.collect { it.getXpath() } then: 'the resulting collection contains data nodes for expected list elements' assert resultXpaths.size() == expectedSize @@ -178,15 +173,43 @@ class DataNodeBuilderSpec extends Specification { 'multiple entries' | '{"branch": [{"name": "One"}, {"name": "Two"}]}' | 2 | ['/test-tree/branch[@name=\'One\']', '/test-tree/branch[@name=\'Two\']'] } - def 'Converting ContainerNode to a DataNode collection -- edge cases: #scenario.'() { - when: 'the container node is #node' - def result = new DataNodeBuilder().withContainerNode(containerNode).buildCollection() - then: 'the resulting collection contains data nodes for expected list elements' - assert result.isEmpty() - where: 'following parameters are used' - scenario | containerNode - 'ContainerNode is null' | null - 'ContainerNode is an unsupported type' | Mock(ContainerNode) + def 'Converting ContainerNode to a Collection with #scenario.'() { + expect: 'converting null to a collection returns an empty collection' + assert objectUnderTest.withContainerNode(containerNode).buildCollection().isEmpty() + where: 'the following container node is used' + scenario | containerNode + 'null object' | null + 'object without body' | Mock(ContainerNode) + } + + def 'Converting ContainerNode to a DataNode with unsupported Normalized Node.'() { + given: 'a container node of an unsupported type' + def mockContainerNode = Mock(ContainerNode) + mockContainerNode.body() >> [ Mock(ForeignDataNode) ] + when: 'attempt to convert it' + objectUnderTest.withContainerNode(mockContainerNode).build() + then: 'a data validation exception is thrown' + thrown(DataValidationException) + } + + def 'Build datanode from attributes.'() { + when: 'data node is built' + def result = new DataNodeBuilder() + .withDataspace('my dataspace') + .withAnchor('my anchor') + .withModuleNamePrefix('my prefix') + .withXpath('some xpath') + .withLeaves([leaf1: 'value1']) + .withChildDataNodes([Mock(DataNode)]) + .build() + then: 'the datanode has all the defined attributes' + assert result.dataspace == 'my dataspace' + assert result.anchorName == 'my anchor' + assert result.moduleNamePrefix == 'my prefix' + assert result.moduleNamePrefix == 'my prefix' + assert result.xpath == 'some xpath' + assert result.leaves == [leaf1: 'value1'] + assert result.childDataNodes.size() == 1 } def 'Use of adding the module name prefix attribute of data node.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy index 2332282e2b..8cbd493550 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy @@ -46,13 +46,23 @@ class JsonObjectMapperSpec extends Specification { type << ['String', 'bytes'] } + def 'Convert to bytes with processing exception.'() { + given: 'the object mapper throws an processing exception' + spiedObjectMapper.writeValueAsBytes(_) >> { throw new JsonProcessingException('message from cause')} + when: 'attempt to convert an object to bytes' + jsonObjectMapper.asJsonBytes('does not matter') + then: 'a data validation exception is thrown with the original exception message as details' + def thrown = thrown(DataValidationException) + assert thrown.details == 'message from cause' + } + def 'Map a structured object to json String error.'() { given: 'some object' def object = new Object() and: 'the Object mapper throws an exception' spiedObjectMapper.writeValueAsString(object) >> { throw new JsonProcessingException('Sample problem'){} } when: 'attempting to convert the object to a string' - jsonObjectMapper.asJsonString(object); + jsonObjectMapper.asJsonString(object) then: 'a Data Validation Exception is thrown' def thrown = thrown(DataValidationException) and: 'the details containing the original error message' @@ -63,21 +73,27 @@ class JsonObjectMapperSpec extends Specification { given: 'a map object model' def contentMap = new JsonSlurper().parseText(TestUtils.getResourceFileContent('bookstore.json')) when: 'converted into a Map' - def result = jsonObjectMapper.convertToValueType(contentMap, Map); + def result = jsonObjectMapper.convertToValueType(contentMap, Map) then: 'the result is a mapped into class of type Map' assert result instanceof Map and: 'the map contains the expected key' assert result.containsKey('test:bookstore') assert result.'test:bookstore'.categories[0].name == 'SciFi' + } + def 'Mapping a valid json string to class object of specific class type T.'() { + given: 'a json string representing a map' + def content = '{"key":"value"}' + expect: 'the string is converted correctly to a map' + jsonObjectMapper.convertJsonString(content, Map) == [ key: 'value' ] } def 'Mapping an unstructured json string to class object of specific class type T.'() { given: 'Unstructured json string' - def content = '{ "nest": { "birds": "bird"] } }' + def content = '{invalid json' when: 'mapping json string to given class type' - jsonObjectMapper.convertJsonString(content, Map); - then: 'an exception is thrown' + jsonObjectMapper.convertJsonString(content, Map) + then: 'a data validation exception is thrown' thrown(DataValidationException) } @@ -87,7 +103,7 @@ class JsonObjectMapperSpec extends Specification { and: 'Object mapper throws an exception' spiedObjectMapper.convertValue(*_) >> { throw new IllegalArgumentException() } when: 'converted into specific class type' - jsonObjectMapper.convertToValueType(contentMap, Object); + jsonObjectMapper.convertToValueType(contentMap, Object) then: 'an exception is thrown' thrown(DataValidationException) } @@ -96,9 +112,9 @@ class JsonObjectMapperSpec extends Specification { given: 'Unstructured object' def object = new Object() and: 'disable serialization failure on empty bean' - spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) when: 'the object is mapped to string' - jsonObjectMapper.asJsonString(object); + jsonObjectMapper.asJsonString(object) then: 'no exception is thrown' noExceptionThrown() } @@ -107,16 +123,16 @@ class JsonObjectMapperSpec extends Specification { given: 'Unstructured object' def content = '{ "nest": { "birds": "bird" } }' when: 'the object is mapped to string' - def result = jsonObjectMapper.convertToJsonNode(content); + def result = jsonObjectMapper.convertToJsonNode(content) then: 'the result is a valid JsonNode' - result.fieldNames().next() == "nest" + result.fieldNames().next() == 'nest' } def 'Map a unstructured json String to JsonNode.'() { given: 'Unstructured object' def content = '{ "nest": { "birds": "bird" }] }' when: 'the object is mapped to string' - jsonObjectMapper.convertToJsonNode(content); + jsonObjectMapper.convertToJsonNode(content) then: 'a data validation exception is thrown' thrown(DataValidationException) } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy index b044e2e727..3864a5253a 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG + * Modifications 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. @@ -21,16 +22,18 @@ package org.onap.cps.utils import org.onap.cps.TestUtils import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.xml.sax.SAXParseException import spock.lang.Specification class XmlFileUtilsSpec extends Specification { + def 'Parse a valid xml content #scenario'(){ given: 'YANG model schema context' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the XML data is parsed' + when: 'the xml data is parsed' def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext) - then: 'the result XML is wrapped by root node defined in YANG schema' + then: 'the result xml is wrapped by root node defined in YANG schema' assert parsedXmlContent == expectedOutput where: scenario | xmlData || expectedOutput @@ -39,13 +42,22 @@ class XmlFileUtilsSpec extends Specification { 'no xml header' | '<stores><class> </class></stores>' || '<stores><class> </class></stores>' } + def 'Parse a invalid xml content'(){ + given: 'YANG model schema context' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'attempt to parse invalid xml' + XmlFileUtils.prepareXmlContent('invalid-xml', schemaContext) + then: 'a Sax Parser exception is thrown' + thrown(SAXParseException) + } + def 'Parse a xml content with XPath container #scenario'() { given: 'YANG model schema context' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() and: 'Parent schema node by xPath' - def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext) - .get("dataSchemaNode") + def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get("dataSchemaNode") when: 'the XML data is parsed' def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) then: 'the result XML is wrapped by xPath defined parent root node' @@ -54,8 +66,6 @@ class XmlFileUtilsSpec extends Specification { scenario | xmlData | xPath || expectedOutput 'XML element test tree' | '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' | '/test-tree' || '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>' | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' - - } } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy index 50b6306439..e6344d3035 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2022 Nordix Foundation + * Copyright (C) 2020-2023 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -27,6 +27,7 @@ import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.common.QName +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode import spock.lang.Specification @@ -162,4 +163,12 @@ class YangUtilsSpec extends Specification { 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] } + + def 'Get key attribute statement without key attributes'() { + given: 'a path argument without key attributes' + def mockPathArgument = Mock(YangInstanceIdentifier.NodeIdentifierWithPredicates) + mockPathArgument.entrySet() >> [ ] + expect: 'the result is an empty string' + YangUtils.getKeyAttributesStatement(mockPathArgument) == '' + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy index 3b4d57d3a6..2739281bc7 100644 --- a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy @@ -23,13 +23,13 @@ package org.onap.cps.yang - import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.ModelValidationException -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.common.Revision import spock.lang.Specification +import java.nio.charset.StandardCharsets + class YangTextSchemaSourceSetBuilderSpec extends Specification { def 'Building a valid YangTextSchemaSourceSet using #filenameCase filename.'() { @@ -62,4 +62,16 @@ class YangTextSchemaSourceSetBuilderSpec extends Specification { 'invalid-empty.yang' | 'no valid content' || ModelValidationException 'invalid-missing-import.yang' | 'no dependency module' || ModelValidationException } + + def 'Convert yang source to a YangTextSchemaSource.'() { + given: 'a yang source text' + def yangSourceText = TestUtils.getResourceFileContent('bookstore.yang') + when: 'convert it to a YangTextSchemaSource' + def result = YangTextSchemaSourceSetBuilder.toYangTextSchemaSource('some name', yangSourceText) + then: 'the converted object has correct properties' + assert result.toString() == '{identifier=RevisionSourceIdentifier [name=some name]}' + assert new String(result.openStream().readAllBytes(), StandardCharsets.UTF_8) == yangSourceText + and: 'it has no symbolic name' + assert result.getSymbolicName().isEmpty() + } } |