Increase code coverage in cps-service module 76/135576/3
authorToineSiebelink <toine.siebelink@est.tech>
Wed, 26 Jul 2023 16:49:02 +0000 (17:49 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Mon, 31 Jul 2023 07:57:30 +0000 (08:57 +0100)
- 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

29 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CloudEventConstructionException.java [moved from cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java with 83% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java [deleted file]
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy [moved from cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java with 62% similarity]
cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy [moved from cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy with 84% similarity]
cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy
cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy

index 5afc52d..c80b07c 100644 (file)
@@ -28,7 +28,6 @@ import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistence;
 import org.onap.cps.ncmp.api.impl.utils.SubscriptionEventCloudMapper;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.kafka.annotation.KafkaListener;
 import org.springframework.stereotype.Component;
@@ -61,9 +60,9 @@ public class SubscriptionEventConsumer {
         final String eventType = subscriptionEventConsumerRecord.value().getType();
         final SubscriptionEvent subscriptionEvent = SubscriptionEventCloudMapper.toSubscriptionEvent(cloudEvent);
         final String eventDatastore = subscriptionEvent.getData().getPredicates().getDatastore();
-        if (!eventDatastore.equals("passthrough-running")) {
-            throw new OperationNotYetSupportedException(
-                "passthrough-running datastores are currently only supported for event subscriptions");
+        if (!(eventDatastore.equals("passthrough-running") || eventDatastore.equals("passthrough-operational"))) {
+            throw new UnsupportedOperationException(
+                "passthrough datastores are currently only supported for event subscriptions");
         }
         if ("CM".equals(subscriptionEvent.getData().getDataType().getDataCategory())) {
             if (subscriptionModelLoaderEnabled) {
index f196cb0..0eda914 100644 (file)
@@ -47,7 +47,6 @@ import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.Subscription
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.Data;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.CmHandle;
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
@@ -80,7 +79,7 @@ public class SubscriptionEventForwarder {
         final List<String> cmHandleTargets = subscriptionEvent.getData().getPredicates().getTargets();
         if (cmHandleTargets == null || cmHandleTargets.isEmpty()
                 || cmHandleTargets.stream().anyMatch(id -> (id).contains("*"))) {
-            throw new OperationNotYetSupportedException(
+            throw new UnsupportedOperationException(
                     "CMHandle targets are required. \"Wildcard\" operations are not yet supported");
         }
         final Collection<YangModelCmHandle> yangModelCmHandles =
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.spi.exceptions;
+package org.onap.cps.ncmp.api.impl.utils;
+
+import org.onap.cps.spi.exceptions.CpsException;
 
 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.
      *
index df3998f..d0d70cf 100644 (file)
@@ -32,7 +32,6 @@ import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
-import org.onap.cps.spi.exceptions.CloudEventConstructionException;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 @Slf4j
index 92c5656..b6cb039 100644 (file)
@@ -29,7 +29,6 @@ import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome;
-import org.onap.cps.spi.exceptions.CloudEventConstructionException;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 @Slf4j
index 5f60773..7fa8155 100644 (file)
@@ -29,7 +29,6 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent
 import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
 import org.onap.cps.ncmp.utils.TestUtils
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
@@ -100,9 +99,8 @@ class SubscriptionEventConsumerSpec extends MessagingBaseSpec {
             def consumerRecord = new ConsumerRecord<String, SubscriptionEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent)
         when: 'the valid event is consumed'
             objectUnderTest.consumeSubscriptionEvent(consumerRecord)
-        then: 'an operation not yet supported exception is thrown'
-            def exception = thrown(OperationNotYetSupportedException)
-            exception.details == 'passthrough-running datastores are currently only supported for event subscriptions'
+        then: 'an operation not supported exception is thrown'
+            thrown(UnsupportedOperationException)
     }
 
 }
index 4343c23..4193f75 100644 (file)
@@ -39,7 +39,6 @@ import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.Data
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.CmHandle;
 import org.onap.cps.ncmp.utils.TestUtils
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
@@ -116,8 +115,8 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
             testEventSent.getData().getPredicates().setTargets(invalidTargets)
         when: 'the event is forwarded'
             objectUnderTest.forwardCreateSubscriptionEvent(testEventSent, 'some-event-type')
-        then: 'an operation not yet supported exception is thrown'
-            thrown(OperationNotYetSupportedException)
+        then: 'an operation not supported exception is thrown'
+            thrown(UnsupportedOperationException)
         where:
             scenario   | invalidTargets
             'null'     | null
index bc19e2d..4023441 100644 (file)
@@ -24,7 +24,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent
 import org.onap.cps.ncmp.utils.TestUtils
-import org.onap.cps.spi.exceptions.CloudEventConstructionException
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
index c97623f..8bc39b1 100644 (file)
 
   <artifactId>cps-service</artifactId>
 
-  <properties>
-    <minimum-coverage>0.95</minimum-coverage>
-  </properties>
-
   <dependencies>
     <dependency>
       <groupId>com.github.ben-manes.caffeine</groupId>
index 38f8988..696fd60 100644 (file)
@@ -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/SubscriptionOutcomeTypeNotFoundException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java
deleted file mode 100644 (file)
index 6b898e8..0000000
+++ /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);
-    }
-}
index e212933..b040af5 100644 (file)
@@ -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 {
         }
     }
 
-
 }
index 09f2e16..98c7947 100644 (file)
@@ -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() {
index deb5b05..ca90714 100644 (file)
@@ -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);
         }
     }
 
index 4e0349d..eb41e20 100755 (executable)
@@ -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')
index ba43849..cb95fb6 100644 (file)
@@ -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
     }
+
 }
index 3884eda..a794c58 100644 (file)
@@ -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 (file)
index 0000000..8efd485
--- /dev/null
@@ -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
+    }
+
+}
  *  ============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
     }
+
 }
@@ -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)
index d0cd473..89e305a 100644 (file)
@@ -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'
     }
 }
-
index 2ef468b..f07f89b 100644 (file)
@@ -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
+    }
 }
index b095bfd..28bf38f 100644 (file)
@@ -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 (file)
index 0000000..c844690
--- /dev/null
@@ -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"}]}'
+    }
+
+}
index 1559783..fcbae62 100644 (file)
@@ -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");
 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.'() {
index 2332282..8cbd493 100644 (file)
@@ -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)
     }
index b044e2e..3864a52 100644 (file)
@@ -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>'
-
-
     }
 
 }
index 50b6306..e6344d3 100644 (file)
@@ -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) == ''
+    }
 }
index 3b4d57d..2739281 100644 (file)
 
 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()
+    }
 }