Add schema to persist notification subscription information 30/139030/18
authorrajesh.kumar <rk00747546@techmahindra.com>
Mon, 30 Sep 2024 12:41:47 +0000 (18:11 +0530)
committerrajesh.kumar <rk00747546@techmahindra.com>
Fri, 20 Dec 2024 08:17:14 +0000 (13:47 +0530)
Add required schema to persist notification subscription information. It should contain
  - Schema yang file
  - New Dataspace, Anchors or any other database entity
  - Refactore duplicate code in NCMP

Issue-ID:CPS-2427
Change-Id: I56c34400dc73c71b936a51260efd300924ababdc
Signed-off-by: rajesh.kumar <rk00747546@techmahindra.com>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoader.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/InventoryModelLoader.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy
cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/init/AbstractModelLoader.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java with 59% similarity]
cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/init/ModelLoader.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java with 85% similarity]
cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/init/AbstractModelLoaderSpec.groovy [moved from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy with 66% similarity]
cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy [new file with mode: 0644]

index d1145fd..d5238ae 100644 (file)
@@ -30,6 +30,7 @@ import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDataspaceService;
 import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.api.exceptions.AlreadyDefinedException;
+import org.onap.cps.init.AbstractModelLoader;
 import org.onap.cps.ncmp.api.data.models.DatastoreType;
 import org.onap.cps.ncmp.exceptions.NcmpStartUpException;
 import org.springframework.stereotype.Service;
index 76d12f2..58a5f55 100644 (file)
@@ -29,6 +29,7 @@ import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDataspaceService;
 import org.onap.cps.api.CpsModuleService;
+import org.onap.cps.init.AbstractModelLoader;
 import org.springframework.stereotype.Service;
 
 @Slf4j
index 21f5e98..926d000 100644 (file)
@@ -52,7 +52,7 @@ class CmDataSubscriptionModelLoaderSpec extends Specification {
     def loggingListAppender
 
     void setup() {
-        expectedYangResourcesToContentMap = objectUnderTest.createYangResourcesToContentMap('cm-data-subscriptions@2024-02-12.yang')
+        expectedYangResourcesToContentMap = objectUnderTest.mapYangResourcesToContent('cm-data-subscriptions@2024-02-12.yang')
         logger.setLevel(Level.DEBUG)
         loggingListAppender = new ListAppender()
         logger.addAppender(loggingListAppender)
index b37cfba..ffd1d89 100644 (file)
@@ -51,7 +51,7 @@ class InventoryModelLoaderSpec extends Specification {
     def loggingListAppender
 
     void setup() {
-        expectedYangResourceToContentMap = objectUnderTest.createYangResourcesToContentMap('dmi-registry@2024-02-23.yang')
+        expectedYangResourceToContentMap = objectUnderTest.mapYangResourcesToContent('dmi-registry@2024-02-23.yang')
         logger.setLevel(Level.DEBUG)
         loggingListAppender = new ListAppender()
         logger.addAppender(loggingListAppender)
diff --git a/cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java b/cps-service/src/main/java/org/onap/cps/api/exceptions/ModelOnboardingException.java
new file mode 100644 (file)
index 0000000..d4455eb
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 TechMahindra Ltd.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.api.exceptions;
+
+public class ModelOnboardingException extends CpsException {
+
+    public ModelOnboardingException(final String message, final String details) {
+        super(message, details);
+    }
+}
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2023-2024 Nordix Foundation
+ *  Modifications Copyright (C) 2024 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -18,7 +19,7 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.init;
+package org.onap.cps.init;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.InputStream;
@@ -26,7 +27,6 @@ import java.nio.charset.StandardCharsets;
 import java.time.OffsetDateTime;
 import java.util.HashMap;
 import java.util.Map;
-import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsAnchorService;
@@ -34,88 +34,93 @@ import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDataspaceService;
 import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.api.exceptions.AlreadyDefinedException;
+import org.onap.cps.api.exceptions.ModelOnboardingException;
 import org.onap.cps.api.parameters.CascadeDeleteAllowed;
-import org.onap.cps.ncmp.exceptions.NcmpStartUpException;
 import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.context.event.ApplicationStartedEvent;
 
 @Slf4j
 @RequiredArgsConstructor
-abstract class AbstractModelLoader implements ModelLoader {
+public abstract class AbstractModelLoader implements ModelLoader {
 
-    private final CpsDataspaceService cpsDataspaceService;
+    protected final CpsDataspaceService cpsDataspaceService;
     private final CpsModuleService cpsModuleService;
     private final CpsAnchorService cpsAnchorService;
     protected final CpsDataService cpsDataService;
 
-    private static final int EXIT_CODE_ON_ERROR = 1;
-
     private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper());
 
-    @Value("${ncmp.model-loader.maximum-attempt-count:20}")
-    int maximumAttemptCount;
-
-    @Value("${ncmp.timers.model-loader.retry-time-ms:1000}")
-    long retryTimeMs;
+    private static final int EXIT_CODE_ON_ERROR = 1;
 
     @Override
-    public void onApplicationEvent(@NonNull final ApplicationStartedEvent applicationStartedEvent) {
+    public void onApplicationEvent(final ApplicationStartedEvent applicationStartedEvent) {
         try {
             onboardOrUpgradeModel();
-        } catch (final NcmpStartUpException ncmpStartUpException) {
-            log.error("Onboarding model for NCMP failed: {} ", ncmpStartUpException.getMessage());
+        } catch (final Exception modelOnboardUpException) {
+            log.error("Exiting application due to failure in onboarding model: {} ",
+                    modelOnboardUpException.getMessage());
             SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR);
         }
     }
 
-    void createSchemaSet(final String dataspaceName, final String schemaSetName, final String... resourceNames) {
+    /**
+     * Create initial schema set.
+     * @param dataspaceName dataspace name
+     * @param schemaSetName schemaset name
+     * @param resourceNames resource names
+     */
+    public void createSchemaSet(final String dataspaceName, final String schemaSetName, final String... resourceNames) {
         try {
-            final Map<String, String> yangResourcesContentMap = createYangResourcesToContentMap(resourceNames);
-            cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, yangResourcesContentMap);
+            final Map<String, String> yangResourcesContentByResourceName = mapYangResourcesToContent(resourceNames);
+            cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, yangResourcesContentByResourceName);
         } catch (final AlreadyDefinedException alreadyDefinedException) {
             log.warn("Creating new schema set failed as schema set already exists");
         } catch (final Exception exception) {
             log.error("Creating schema set failed: {} ", exception.getMessage());
-            throw new NcmpStartUpException("Creating schema set failed", exception.getMessage());
+            throw new ModelOnboardingException("Creating schema set failed", exception.getMessage());
         }
     }
 
-    void createDataspace(final String dataspaceName) {
+    /**
+     * Create initial dataspace.
+     * @param dataspaceName dataspace name
+     */
+    public void createDataspace(final String dataspaceName) {
         try {
             cpsDataspaceService.createDataspace(dataspaceName);
         } catch (final AlreadyDefinedException alreadyDefinedException) {
             log.debug("Dataspace already exists");
         } catch (final Exception exception) {
             log.error("Creating dataspace failed: {} ", exception.getMessage());
-            throw new NcmpStartUpException("Creating dataspace failed", exception.getMessage());
+            throw new ModelOnboardingException("Creating dataspace failed", exception.getMessage());
         }
     }
 
-    void deleteUnusedSchemaSets(final String dataspaceName, final String... schemaSetNames) {
-        for (final String schemaSetName : schemaSetNames) {
-            try {
-                cpsModuleService.deleteSchemaSet(
-                    dataspaceName, schemaSetName, CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED);
-            } catch (final Exception exception) {
-                log.warn("Deleting schema set failed: {} ", exception.getMessage());
-            }
-        }
-    }
-
-    void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) {
+    /**
+     * Create initial anchor.
+     * @param dataspaceName dataspace name
+     * @param schemaSetName schemaset name
+     * @param anchorName anchor name
+     */
+    public void createAnchor(final String dataspaceName, final String schemaSetName, final String anchorName) {
         try {
             cpsAnchorService.createAnchor(dataspaceName, schemaSetName, anchorName);
         } catch (final AlreadyDefinedException alreadyDefinedException) {
             log.warn("Creating new anchor failed as anchor already exists");
         } catch (final Exception exception) {
             log.error("Creating anchor failed: {} ", exception.getMessage());
-            throw new NcmpStartUpException("Creating anchor failed", exception.getMessage());
+            throw new ModelOnboardingException("Creating anchor failed", exception.getMessage());
         }
     }
 
-    void createTopLevelDataNode(final String dataspaceName, final String anchorName, final String dataNodeName) {
+    /**
+     * Create initial top level data node.
+     * @param dataspaceName dataspace name
+     * @param anchorName anchor name
+     * @param dataNodeName data node name
+     */
+    public void createTopLevelDataNode(final String dataspaceName, final String anchorName, final String dataNodeName) {
         final String nodeData = jsonObjectMapper.asJsonString(Map.of(dataNodeName, Map.of()));
         try {
             cpsDataService.saveData(dataspaceName, anchorName, nodeData, OffsetDateTime.now());
@@ -123,35 +128,56 @@ abstract class AbstractModelLoader implements ModelLoader {
             log.warn("Creating new data node '{}' failed as data node already exists", dataNodeName);
         } catch (final Exception exception) {
             log.error("Creating data node failed: {}", exception.getMessage());
-            throw new NcmpStartUpException("Creating data node failed", exception.getMessage());
+            throw new ModelOnboardingException("Creating data node failed", exception.getMessage());
         }
     }
 
-    void updateAnchorSchemaSet(final String dataspaceName, final String anchorName, final String schemaSetName) {
+    /**
+     * Delete unused schema set.
+     * @param dataspaceName dataspace name
+     * @param schemaSetNames schema set names
+     */
+    public void deleteUnusedSchemaSets(final String dataspaceName, final String... schemaSetNames) {
+        for (final String schemaSetName : schemaSetNames) {
+            try {
+                cpsModuleService.deleteSchemaSet(
+                        dataspaceName, schemaSetName, CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED);
+            } catch (final Exception exception) {
+                log.warn("Deleting schema set failed: {} ", exception.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Update anchor schema set.
+     * @param dataspaceName dataspace name
+     * @param anchorName anchor name
+     * @param schemaSetName schemaset name
+     */
+    public void updateAnchorSchemaSet(final String dataspaceName, final String anchorName, final String schemaSetName) {
         try {
             cpsAnchorService.updateAnchorSchemaSet(dataspaceName, anchorName, schemaSetName);
         } catch (final Exception exception) {
             log.error("Updating schema set failed: {}", exception.getMessage());
-            throw new NcmpStartUpException("Updating schema set failed", exception.getMessage());
+            throw new ModelOnboardingException("Updating schema set failed", exception.getMessage());
         }
     }
 
-    Map<String, String> createYangResourcesToContentMap(final String... resourceNames) {
-        final Map<String, String> yangResourcesToContentMap = new HashMap<>();
+    Map<String, String> mapYangResourcesToContent(final String... resourceNames) {
+        final Map<String, String> yangResourceContentByName = new HashMap<>();
         for (final String resourceName: resourceNames) {
-            yangResourcesToContentMap.put(resourceName, getFileContentAsString("models/" + resourceName));
+            yangResourceContentByName.put(resourceName, getFileContentAsString("models/" + resourceName));
         }
-        return yangResourcesToContentMap;
+        return yangResourceContentByName;
     }
 
     private String getFileContentAsString(final String fileName) {
-        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
+        try (final InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName)) {
             return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
         } catch (final Exception exception) {
             final String message = String.format("Onboarding failed as unable to read file: %s", fileName);
             log.debug(message);
-            throw new NcmpStartUpException(message, exception.getMessage());
+            throw new ModelOnboardingException(message, exception.getMessage());
         }
     }
-
 }
diff --git a/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java
new file mode 100644 (file)
index 0000000..0b7d160
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 TechMahindra Ltd.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.init;
+
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsAnchorService;
+import org.onap.cps.api.CpsDataService;
+import org.onap.cps.api.CpsDataspaceService;
+import org.onap.cps.api.CpsModuleService;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class CpsNotificationSubscriptionModelLoader extends AbstractModelLoader {
+
+    private static final String MODEL_FILENAME = "cps-notification-subscriptions@2024-07-03.yang";
+    private static final String SCHEMASET_NAME = "cps-notification-subscriptions";
+    private static final String ANCHOR_NAME = "cps-notification-subscriptions";
+    private static final String CPS_DATASPACE_NAME = "CPS-Admin";
+    private static final String REGISTRY_DATANODE_NAME = "dataspaces";
+
+    public CpsNotificationSubscriptionModelLoader(final CpsDataspaceService cpsDataspaceService,
+                                                  final CpsModuleService cpsModuleService,
+                                                  final CpsAnchorService cpsAnchorService,
+                                                  final CpsDataService cpsDataService) {
+        super(cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService);
+    }
+
+    @Override
+    public void onboardOrUpgradeModel() {
+        onboardSubscriptionModels();
+        log.info("Subscription models onboarded successfully");
+    }
+
+    private void onboardSubscriptionModels() {
+        createDataspace(CPS_DATASPACE_NAME);
+        createSchemaSet(CPS_DATASPACE_NAME, SCHEMASET_NAME, MODEL_FILENAME);
+        createAnchor(CPS_DATASPACE_NAME, SCHEMASET_NAME, ANCHOR_NAME);
+        createTopLevelDataNode(CPS_DATASPACE_NAME, ANCHOR_NAME, REGISTRY_DATANODE_NAME);
+    }
+
+}
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2024 Nordix Foundation
+ *  Copyright (C) 2024 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.init;
+package org.onap.cps.init;
 
-import lombok.NonNull;
 import org.springframework.boot.context.event.ApplicationStartedEvent;
 import org.springframework.context.ApplicationListener;
 
 public interface ModelLoader extends ApplicationListener<ApplicationStartedEvent> {
 
     @Override
-    void onApplicationEvent(@NonNull ApplicationStartedEvent applicationStartedEvent);
+    void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent);
 
     void onboardOrUpgradeModel();
-
 }
diff --git a/cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang b/cps-service/src/main/resources/models/cps-notification-subscriptions@2024-07-03.yang
new file mode 100644 (file)
index 0000000..1cab792
--- /dev/null
@@ -0,0 +1,48 @@
+module cps-notification-subscriptions {
+    yang-version 1.1;
+    namespace "org:onap:cps";
+
+    prefix cps-notification-subscriptions;
+
+    revision "2024-08-05" {
+        description
+            "First release of cps notification subscriptions model";
+    }
+    container dataspaces {
+
+        list dataspace {
+            key "name";
+
+            leaf name {
+                type string;
+            }
+
+            container anchors {
+
+                list anchor {
+                    key "name";
+
+                    leaf name {
+                        type string;
+                    }
+
+                    container xpaths {
+
+                        list xpath {
+                            key "path";
+                            leaf path {
+                                type string;
+                            }
+                        }
+                    }
+                }
+            }
+            leaf-list subscriptionIds {
+                type string;
+            }
+            leaf topic {
+                type string;
+            }
+        }
+    }
+}
\ No newline at end of file
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2023-2024 Nordix Foundation
+ *  Modification Copyright (C) 2024 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -18,7 +19,7 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.init
+package org.onap.cps.init
 
 import ch.qos.logback.classic.Level
 import ch.qos.logback.classic.Logger
@@ -27,7 +28,7 @@ import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsDataspaceService
 import org.onap.cps.api.CpsModuleService
-import org.onap.cps.ncmp.exceptions.NcmpStartUpException
+import org.onap.cps.api.exceptions.ModelOnboardingException
 import org.onap.cps.api.parameters.CascadeDeleteAllowed
 import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.slf4j.LoggerFactory
@@ -46,12 +47,10 @@ class AbstractModelLoaderSpec extends Specification {
 
     def applicationContext = new AnnotationConfigApplicationContext()
 
-    def yangResourceToContentMap
     def logger = (Logger) LoggerFactory.getLogger(AbstractModelLoader)
     def loggingListAppender
 
     void setup() {
-        yangResourceToContentMap = objectUnderTest.createYangResourcesToContentMap('cm-data-subscriptions@2024-02-12.yang')
         logger.setLevel(Level.DEBUG)
         loggingListAppender = new ListAppender()
         logger.addAppender(loggingListAppender)
@@ -60,20 +59,21 @@ class AbstractModelLoaderSpec extends Specification {
     }
 
     void cleanup() {
-        ((Logger) LoggerFactory.getLogger(CmDataSubscriptionModelLoader.class)).detachAndStopAllAppenders()
+        logger.detachAndStopAllAppenders()
         applicationContext.close()
+        loggingListAppender.stop()
     }
 
-    def 'Application started event'() {
+    def 'Application started event triggers onboarding/upgrade'() {
         when: 'Application (started) event is triggered'
             objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent))
         then: 'the onboard/upgrade method is executed'
             1 * objectUnderTest.onboardOrUpgradeModel()
     }
 
-    def 'Application started event with start up exception'() {
-        given: 'a start up exception is thrown doing model onboarding'
-            objectUnderTest.onboardOrUpgradeModel() >> { throw new NcmpStartUpException('test message','details are not logged') }
+    def 'Application started event handles startup exception'() {
+        given: 'a startup exception is thrown during model onboarding'
+            objectUnderTest.onboardOrUpgradeModel() >> { throw new ModelOnboardingException('test message','details are not logged') }
         when: 'Application (started) event is triggered'
             objectUnderTest.onApplicationEvent(new ApplicationStartedEvent(new SpringApplication(), null, applicationContext, null))
         then: 'the exception message is logged'
@@ -81,116 +81,138 @@ class AbstractModelLoaderSpec extends Specification {
             assert logs.contains('test message')
     }
 
-    def 'Create schema set.'() {
+    def 'Creating a dataspace delegates to the service.'() {
+        when: 'creating a dataspace'
+            objectUnderTest.createDataspace('some dataspace')
+        then: 'the operation is delegated to the dataspace service'
+            1 * mockCpsDataspaceService.createDataspace('some dataspace')
+    }
+
+    def 'Creating a dataspace handles already defined exception.'() {
+        given: 'dataspace service throws an already defined exception'
+            mockCpsDataspaceService.createDataspace(*_) >> { throw AlreadyDefinedException.forDataNodes([], 'some context') }
+        when: 'creating a dataspace'
+            objectUnderTest.createDataspace('some dataspace')
+        then: 'the exception is ignored i.e. no exception thrown up'
+            noExceptionThrown()
+        and: 'the exception is ignored, and a log message is produced'
+            assertLogContains('Dataspace already exists')
+    }
+
+    def 'Creating a dataspace handles other exception.'() {
+        given: 'dataspace service throws a runtime exception'
+            mockCpsDataspaceService.createDataspace(*_) >> { throw new RuntimeException('test message')  }
+        when: 'creating a dataspace'
+            objectUnderTest.createDataspace('some dataspace')
+        then: 'a startup exception with correct message and details is thrown'
+            def thrown = thrown(ModelOnboardingException)
+            assert thrown.message.contains('Creating dataspace failed')
+            assert thrown.details.contains('test message')
+    }
+
+    def 'Creating a schema set delegates to the service.'() {
         when: 'creating a schema set'
-            objectUnderTest.createSchemaSet('some dataspace','new name','cm-data-subscriptions@2024-02-12.yang')
-        then: 'the operation is delegated to the admin service'
+            objectUnderTest.createSchemaSet('some dataspace','new name','cps-notification-subscriptions@2024-07-03.yang')
+        then: 'the operation is delegated to the module service'
             1 * mockCpsModuleService.createSchemaSet('some dataspace','new name',_)
     }
 
-    def 'Create schema set with already defined exception.'() {
+    def 'Creating a schema set handles already defined exception.'() {
         given: 'the module service throws an already defined exception'
             mockCpsModuleService.createSchemaSet(*_) >>  { throw AlreadyDefinedException.forSchemaSet('name','context',null) }
         when: 'attempt to create a schema set'
-            objectUnderTest.createSchemaSet('some dataspace','new name','cm-data-subscriptions@2024-02-12.yang')
-        then: 'the exception is ignored i.e. no exception thrown up'
+            objectUnderTest.createSchemaSet('some dataspace','new name','cps-notification-subscriptions@2024-07-03.yang')
+        then: 'the exception is ignored, and a log message is produced'
             noExceptionThrown()
-        and: 'the exception message is logged'
-            def logs = loggingListAppender.list.toString()
-            assert logs.contains('Creating new schema set failed as schema set already exists')
+            assertLogContains('Creating new schema set failed as schema set already exists')
     }
 
-    def 'Create schema set with non existing yang file.'() {
-        when: 'attempt to create a schema set from a non existing file'
+    def 'Creating a schema set from a non-existing YANG file.'() {
+        when: 'attempting to create a schema set from a non-existing file'
             objectUnderTest.createSchemaSet('some dataspace','some name','no such yang file')
         then: 'a startup exception with correct message and details is thrown'
-            def thrown = thrown(NcmpStartUpException)
+            def thrown = thrown(ModelOnboardingException)
             assert thrown.message.contains('Creating schema set failed')
             assert thrown.details.contains('unable to read file')
     }
 
-    def 'Delete unused schema sets.'() {
-        when: 'several unused schemas are deleted '
-            objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2')
-        then: 'a request to delete each (without cascade) is delegated to the module service'
-            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 1', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
-            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
-
-    }
-
-    def 'Delete unused schema sets with exception.'() {
-        given: 'deleting the first schemaset causes an exception'
-            mockCpsModuleService.deleteSchemaSet(_, 'schema set 1', _) >> { throw new RuntimeException('test message')}
-        when: 'several unused schemas are deleted '
-            objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2')
-        then: 'the exception message is logged'
-            def logs = loggingListAppender.list.toString()
-            assert logs.contains('Deleting schema set failed')
-            assert logs.contains('test message')
-        and: 'the second schema set is still deleted'
-            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
-    }
-
-    def 'Create anchor.'() {
+    def 'Creating an anchor delegates to the service.'() {
         when: 'creating an anchor'
             objectUnderTest.createAnchor('some dataspace','some schema set','new name')
-        then: 'the operation is delegated to the admin service'
+        then: 'the operation is delegated to the anchor service'
             1 * mockCpsAnchorService.createAnchor('some dataspace','some schema set', 'new name')
     }
 
-    def 'Create anchor with already defined exception.'() {
-        given: 'the admin service throws an already defined exception'
+    def 'Creating an anchor handles already defined exception.'() {
+        given: 'the anchor service throws an already defined exception'
             mockCpsAnchorService.createAnchor(*_)>>  { throw AlreadyDefinedException.forAnchor('name','context',null) }
-        when: 'attempt to create anchor'
+        when: 'attempting to create an anchor'
             objectUnderTest.createAnchor('some dataspace','some schema set','new name')
-        then: 'the exception is ignored i.e. no exception thrown up'
+        then: 'the exception is ignored, and a log message is produced'
             noExceptionThrown()
-        and: 'the exception message is logged'
-            def logs = loggingListAppender.list.toString()
-            assert logs.contains('Creating new anchor failed as anchor already exists')
+            assertLogContains('Creating new anchor failed as anchor already exists')
     }
 
-    def 'Create anchor with any other exception.'() {
-        given: 'the admin service throws a exception'
+    def 'Creating an anchor handles other exceptions.'() {
+        given: 'the anchor service throws a runtime exception'
             mockCpsAnchorService.createAnchor(*_)>>  { throw new RuntimeException('test message') }
         when: 'attempt to create anchor'
             objectUnderTest.createAnchor('some dataspace','some schema set','new name')
         then: 'a startup exception with correct message and details is thrown'
-            def thrown = thrown(NcmpStartUpException)
+            def thrown = thrown(ModelOnboardingException)
             assert thrown.message.contains('Creating anchor failed')
             assert thrown.details.contains('test message')
     }
 
-    def 'Create top level node.'() {
-        when: 'top level node is created'
+    def 'Creating a top-level data node delegates to the service.'() {
+        when: 'top-level node is created'
             objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node')
-        then: 'the correct json is saved using the data service'
+        then: 'the correct JSON is saved using the data service'
             1 * mockCpsDataService.saveData('dataspace','anchor', '{"new node":{}}',_)
     }
 
-    def 'Create top level node with already defined exception.'() {
+    def 'Creating a top-level node handles already defined exception.'() {
         given: 'the data service throws an Already Defined exception'
             mockCpsDataService.saveData(*_) >> { throw AlreadyDefinedException.forDataNodes([], 'some context') }
-        when: 'attempt to create top level node'
+        when: 'attempting to create a top-level node'
             objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node')
-        then: 'the exception is ignored i.e. no exception thrown up'
+        then: 'the exception is ignored, and a log message is produced'
             noExceptionThrown()
-        and: 'the exception message is logged'
-            def logs = loggingListAppender.list.toString()
-            assert logs.contains('failed as data node already exists')
+            assertLogContains('failed as data node already exists')
     }
 
-    def 'Create top level node with any other exception.'() {
-        given: 'the data service throws an exception'
+    def 'Create a top-level node with any other exception.'() {
+        given: 'the data service throws a runtime exception'
             mockCpsDataService.saveData(*_) >> { throw new RuntimeException('test message') }
-        when: 'attempt to create top level node'
+        when: 'attempt to create a top-level node'
             objectUnderTest.createTopLevelDataNode('dataspace','anchor','new node')
         then: 'a startup exception with correct message and details is thrown'
-            def thrown = thrown(NcmpStartUpException)
+            def thrown = thrown(ModelOnboardingException)
             assert thrown.message.contains('Creating data node failed')
             assert thrown.details.contains('test message')
     }
 
+    def 'Delete unused schema sets delegates to the service.'() {
+        when: 'unused schema sets get deleted'
+            objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2')
+        then: 'a request to delete each (without cascade) is delegated to the module service'
+            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 1', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
+            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
+    }
+
+    def 'Delete unused schema sets with exception.'() {
+        given: 'deleting the first schemaset causes an exception'
+            mockCpsModuleService.deleteSchemaSet(_, 'schema set 1', _) >> { throw new RuntimeException('test message')}
+        when: 'several unused schemas are deleted '
+            objectUnderTest.deleteUnusedSchemaSets('some dataspace','schema set 1', 'schema set 2')
+        then: 'the exception message is logged'
+            def logs = loggingListAppender.list.toString()
+            assert logs.contains('Deleting schema set failed')
+            assert logs.contains('test message')
+        and: 'the second schema set is still deleted'
+            1 * mockCpsModuleService.deleteSchemaSet('some dataspace', 'schema set 2', CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED)
+    }
+
     def 'Update anchor schema set.'() {
         when: 'a schema set for an anchor is updated'
             objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set')
@@ -204,11 +226,16 @@ class AbstractModelLoaderSpec extends Specification {
         when: 'a schema set for an anchor is updated'
             objectUnderTest.updateAnchorSchemaSet('some dataspace', 'anchor', 'new schema set')
         then: 'a startup exception with correct message and details is thrown'
-            def thrown = thrown(NcmpStartUpException)
+            def thrown = thrown(ModelOnboardingException)
             assert thrown.message.contains('Updating schema set failed')
             assert thrown.details.contains('test message')
     }
 
+    private void assertLogContains(String message) {
+        def logs = loggingListAppender.list.toString()
+        assert logs.contains(message)
+    }
+
     class TestModelLoader extends AbstractModelLoader {
 
         TestModelLoader(final CpsDataspaceService cpsDataspaceService,
@@ -216,12 +243,11 @@ class AbstractModelLoaderSpec extends Specification {
                         final CpsAnchorService cpsAnchorService,
                         final CpsDataService cpsDataService) {
             super(cpsDataspaceService, cpsModuleService, cpsAnchorService, cpsDataService)
-            super.maximumAttemptCount = 2
-            super.retryTimeMs = 1
         }
 
         @Override
-        void onboardOrUpgradeModel() { }
+        void onboardOrUpgradeModel() {
+            // No operation needed for testing
+        }
     }
-
 }
diff --git a/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy
new file mode 100644 (file)
index 0000000..0d515f9
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 TechMahindra Ltd.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.init
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.core.read.ListAppender
+import org.onap.cps.api.CpsAnchorService
+import org.onap.cps.api.CpsDataService
+import org.onap.cps.api.CpsDataspaceService
+import org.onap.cps.api.CpsModuleService
+import org.onap.cps.api.model.Dataspace
+import org.slf4j.LoggerFactory
+import org.springframework.boot.context.event.ApplicationStartedEvent
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import spock.lang.Specification
+
+class CpsNotificationSubscriptionModelLoaderSpec extends Specification {
+    def mockCpsDataspaceService = Mock(CpsDataspaceService)
+    def mockCpsModuleService = Mock(CpsModuleService)
+    def mockCpsDataService = Mock(CpsDataService)
+    def mockCpsAnchorService = Mock(CpsAnchorService)
+    def objectUnderTest = new CpsNotificationSubscriptionModelLoader(mockCpsDataspaceService, mockCpsModuleService, mockCpsAnchorService, mockCpsDataService)
+
+    def applicationContext = new AnnotationConfigApplicationContext()
+
+    def expectedYangResourcesToContents
+    def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
+    def loggingListAppender
+
+    def CPS_DATASPACE_NAME = 'CPS-Admin'
+    def ANCHOR_NAME = 'cps-notification-subscriptions'
+    def SCHEMASET_NAME = 'cps-notification-subscriptions'
+    def MODEL_FILENAME = 'cps-notification-subscriptions@2024-07-03.yang'
+
+    void setup() {
+        expectedYangResourcesToContents = objectUnderTest.mapYangResourcesToContent(MODEL_FILENAME)
+        logger.setLevel(Level.DEBUG)
+        loggingListAppender = new ListAppender()
+        logger.addAppender(loggingListAppender)
+        loggingListAppender.start()
+        applicationContext.refresh()
+    }
+
+    void cleanup() {
+        logger.detachAndStopAllAppenders()
+        applicationContext.close()
+        loggingListAppender.stop()
+    }
+
+    def 'Onboard subscription model via application started event.'() {
+        given: 'dataspace is already present'
+            mockCpsDataspaceService.getAllDataspaces() >> [new Dataspace('test')]
+        when: 'the application is ready'
+            objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent))
+        then: 'the module service to create schema set is called once'
+            1 * mockCpsModuleService.createSchemaSet(CPS_DATASPACE_NAME, SCHEMASET_NAME, expectedYangResourcesToContents)
+        and: 'the anchor service to create an anchor set is called once'
+            1 * mockCpsAnchorService.createAnchor(CPS_DATASPACE_NAME, SCHEMASET_NAME, ANCHOR_NAME)
+        and: 'the data service to create a top level datanode is called once'
+            1 * mockCpsDataService.saveData(CPS_DATASPACE_NAME, ANCHOR_NAME, '{"dataspaces":{}}', _)
+    }
+
+    private void assertLogContains(String message) {
+        def logs = loggingListAppender.list.toString()
+        assert logs.contains(message)
+    }
+
+}