Add Support for Activation and Deactivation

* Add support for reconfiguration of the administrativeState field
* Add support for policy feedback handling
* Fix network function filter applying to non active network functions

Signed-off-by: ERIMROB <robertas.rimkus@est.tech>
Change-Id: Ic1cfc3207b2495c1d8d10acd0ed1c40114cf4643
Issue-ID: DCAEGEN2-1830
diff --git a/components/pm-subscription-handler/pmsh_service/mod/aai_client.py b/components/pm-subscription-handler/pmsh_service/mod/aai_client.py
index 747846f..f0f2056 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/aai_client.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/aai_client.py
@@ -119,10 +119,12 @@
     try:
         for nf in nf_data['results']:
             name_identifier = 'pnf-name' if nf['node-type'] == 'pnf' else 'vnf-name'
-            if nf_filter.is_nf_in_filter(nf['properties'].get(name_identifier)):
+            orchestration_status = nf['properties'].get('orchestration-status')
+            if nf_filter.is_nf_in_filter(nf['properties'].get(name_identifier)) \
+                    and orchestration_status == 'Active':
                 nf_set.add(NetworkFunction(
                     nf_name=nf['properties'].get(name_identifier),
-                    orchestration_status=nf['properties'].get('orchestration-status')))
+                    orchestration_status=orchestration_status))
     except KeyError as e:
         logger.debug(f'Failed to parse AAI data: {e}')
         raise
diff --git a/components/pm-subscription-handler/pmsh_service/mod/config_handler.py b/components/pm-subscription-handler/pmsh_service/mod/config_handler.py
index 1ce4b70..acf5b76 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/config_handler.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/config_handler.py
@@ -15,12 +15,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
-
-import json
 from os import environ
 
 import requests
-from tenacity import retry, wait_fixed, stop_after_attempt
+from tenacity import retry, wait_fixed, stop_after_attempt, retry_if_exception_type
 
 import mod.pmsh_logging as logger
 
@@ -45,7 +43,7 @@
     def hostname(self):
         return _get_environment_variable('HOSTNAME')
 
-    @retry(wait=wait_fixed(2), stop=stop_after_attempt(5))
+    @retry(wait=wait_fixed(2), stop=stop_after_attempt(5), retry=retry_if_exception_type(Exception))
     def get_config(self):
         """ Retrieves PMSH's configuration from Configbinding service. If a non-2xx response
         is received, it retries after 2 seconds for 5 times before raising an exception.
@@ -56,18 +54,15 @@
         Raises:
             Exception: If any error occurred pulling configuration from Configbinding service.
         """
-        if self._config is None:
-            logger.debug('No configuration found, pulling from Configbinding Service.')
-            try:
-                response = requests.get(self.cbs_url)
-                response.raise_for_status()
-                self._config = response.json()
-                logger.debug(f'PMSH Configuration from Configbinding Service: {self._config}')
-                return json.loads(self._config)
-            except Exception as err:
-                raise Exception(f'Error retrieving configuration from CBS: {err}')
-        else:
+
+        try:
+            response = requests.get(self.cbs_url)
+            response.raise_for_status()
+            self._config = response.json()
+            logger.debug(f'PMSH Configuration from Configbinding Service: {self._config}')
             return self._config
+        except Exception as err:
+            raise Exception(f'Error retrieving configuration from CBS: {err}')
 
 
 def _get_environment_variable(env_var_key):
diff --git a/components/pm-subscription-handler/pmsh_service/mod/network_function.py b/components/pm-subscription-handler/pmsh_service/mod/network_function.py
index 64f614a..9f21cc6 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/network_function.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/network_function.py
@@ -15,16 +15,13 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
-
 from mod import pmsh_logging as logger, db
 from mod.db_models import NetworkFunctionModel
 
 
 class NetworkFunction:
     def __init__(self, **kwargs):
-        """
-        Object representation of the NetworkFunction.
-        """
+        """ Object representation of the NetworkFunction. """
         self.nf_name = kwargs.get('nf_name')
         self.orchestration_status = kwargs.get('orchestration_status')
 
@@ -36,8 +33,7 @@
         return f'nf-name: {self.nf_name}, orchestration-status: {self.orchestration_status}'
 
     def create(self):
-        """ Creates a NetworkFunction database entry
-        """
+        """ Creates a NetworkFunction database entry """
         existing_nf = NetworkFunctionModel.query.filter(
             NetworkFunctionModel.nf_name == self.nf_name).one_or_none()
 
@@ -71,3 +67,13 @@
             list: NetworkFunctionModel objects else empty
         """
         return NetworkFunctionModel.query.all()
+
+    @staticmethod
+    def delete(**kwargs):
+        """ Deletes a network function from the database """
+        nf_name = kwargs['nf_name']
+        NetworkFunctionModel.query.filter(
+            NetworkFunctionModel.nf_name == nf_name). \
+            delete(synchronize_session='evaluate')
+
+        db.session.commit()
diff --git a/components/pm-subscription-handler/pmsh_service/mod/pmsh_logging.py b/components/pm-subscription-handler/pmsh_service/mod/pmsh_logging.py
index f2d11d4..885644b 100644
--- a/components/pm-subscription-handler/pmsh_service/mod/pmsh_logging.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/pmsh_logging.py
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2019 Nordix Foundation.
+#  Copyright (C) 2019-2020 Nordix Foundation.
 # ============================================================================
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -15,11 +15,10 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
-
+import datetime
 import logging as log
 from logging.handlers import RotatingFileHandler
 from os import makedirs
-import datetime
 
 # These loggers will be overwritten with EELF logging when running in Docker
 _AUDIT_LOGGER = log.getLogger("defaultlogger")
diff --git a/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py b/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
index b665691..4a77543 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
@@ -15,12 +15,17 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
+import json
+import threading
 import uuid
 
 import requests
 from requests.auth import HTTPBasicAuth
+from tenacity import retry, wait_fixed, retry_if_exception_type
 
 import mod.pmsh_logging as logger
+from mod.subscription import Subscription, SubNfState, AdministrativeState
+from mod.network_function import NetworkFunction
 
 
 class AppConfig:
@@ -168,3 +173,71 @@
         except Exception as e:
             logger.debug(e)
         return topic_data
+
+    @staticmethod
+    def _handle_response(subscription_name, administrative_state, nf_name, response_message):
+        """
+        Handles the response from Policy, updating the DB
+
+        Args:
+            subscription_name (str): The subscription name
+            administrative_state (str): The administrative state of the subscription
+            nf_name (str): The network function name
+            response_message (str): The message in the response regarding the state (success|failed)
+        """
+        logger.debug(f'Response from MR: Sub: {subscription_name} for '
+                     f'NF: {nf_name} received, updating the DB')
+        try:
+            sub_nf_status = subscription_nf_states[administrative_state][response_message].value
+            policy_response_handle_functions[administrative_state][response_message](
+                subscription_name=subscription_name, status=sub_nf_status, nf_name=nf_name)
+        except Exception as err:
+            raise Exception(f'Error changing nf_sub status in the DB: {err}')
+
+    @retry(wait=wait_fixed(5), retry=retry_if_exception_type(Exception))
+    def poll_policy_topic(self, subscription_name, app):
+        """
+        This method polls MR for response from policy. It checks whether the message is for the
+        relevant subscription and then handles the response
+
+        Args:
+            subscription_name (str): The subscription name
+            app (app): Needed to push context for the db
+        """
+        app.app_context().push()
+        administrative_state = Subscription.get(subscription_name).status
+        try:
+            response_data = self.get_from_topic('policy_response_consumer')
+            for data in response_data:
+                data = json.loads(data)
+                if data['status']['subscriptionName'] == subscription_name:
+                    nf_name = data['status']['nfName']
+                    response_message = data['status']['message']
+                    self._handle_response(subscription_name, administrative_state,
+                                          nf_name, response_message)
+            threading.Timer(5, self.poll_policy_topic, [subscription_name, app]).start()
+        except Exception as err:
+            raise Exception(f'Error trying to poll MR: {err}')
+
+
+subscription_nf_states = {
+    AdministrativeState.LOCKED.value: {
+        'success': SubNfState.CREATED,
+        'failed': SubNfState.DELETE_FAILED
+    },
+    AdministrativeState.UNLOCKED.value: {
+        'success': SubNfState.CREATED,
+        'failed': SubNfState.CREATE_FAILED
+    }
+}
+
+policy_response_handle_functions = {
+    AdministrativeState.LOCKED.value: {
+        'success': NetworkFunction.delete,
+        'failed': Subscription.update_sub_nf_status
+    },
+    AdministrativeState.UNLOCKED.value: {
+        'success': Subscription.update_sub_nf_status,
+        'failed': Subscription.update_sub_nf_status
+    }
+}
diff --git a/components/pm-subscription-handler/pmsh_service/mod/subscription.py b/components/pm-subscription-handler/pmsh_service/mod/subscription.py
index 265d90b..031609a 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/subscription.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/subscription.py
@@ -16,10 +16,25 @@
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
 import re
+from enum import Enum
 
 import mod.pmsh_logging as logger
 from mod import db
 from mod.db_models import SubscriptionModel, NfSubRelationalModel
+from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
+
+
+class SubNfState(Enum):
+    PENDING_CREATE = 'PENDING_CREATE'
+    CREATE_FAILED = 'CREATE_FAILED'
+    CREATED = 'CREATED'
+    PENDING_DELETE = 'PENDING_DELETE'
+    DELETE_FAILED = 'DELETE_FAILED'
+
+
+class AdministrativeState(Enum):
+    UNLOCKED = 'UNLOCKED'
+    LOCKED = 'LOCKED'
 
 
 class Subscription:
@@ -42,7 +57,10 @@
             dict: the Subscription event to be published.
         """
         clean_sub = {k: v for k, v in self.__dict__.items() if k != 'nfFilter'}
-        clean_sub.update({'nfName': xnf_name, 'policyName': f'OP-{self.subscriptionName}'})
+        clean_sub.update({'nfName': xnf_name, 'policyName': f'OP-{self.subscriptionName}',
+                          'changeType': 'DELETE'
+                          if self.administrativeState == AdministrativeState.LOCKED.value
+                          else 'CREATE'})
         return clean_sub
 
     def create(self):
@@ -84,7 +102,8 @@
                 NfSubRelationalModel.subscription_name == current_sub.subscription_name,
                 NfSubRelationalModel.nf_name == current_nf.nf_name).one_or_none()
             if existing_entry is None:
-                new_nf_sub = NfSubRelationalModel(current_sub.subscription_name, nf.nf_name)
+                new_nf_sub = NfSubRelationalModel(current_sub.subscription_name,
+                                                  nf.nf_name, SubNfState.PENDING_CREATE.value)
                 new_nf_sub.nf = current_nf
                 logger.debug(current_nf)
                 current_sub.nfs.append(new_nf_sub)
@@ -114,6 +133,44 @@
         """
         return SubscriptionModel.query.all()
 
+    def update_subscription_status(self):
+        """ Updates the status of subscription in subscription table """
+        SubscriptionModel.query.filter(
+            SubscriptionModel.subscription_name == self.subscriptionName). \
+            update({SubscriptionModel.status: self.administrativeState},
+                   synchronize_session='evaluate')
+
+        db.session.commit()
+
+    def delete_subscription(self):
+        """ Deletes a subscription from the database """
+        SubscriptionModel.query.filter(
+            SubscriptionModel.subscription_name == self.subscriptionName). \
+            delete(synchronize_session='evaluate')
+
+        db.session.commit()
+
+    @retry(wait=wait_exponential(multiplier=1, min=30, max=120), stop=stop_after_attempt(3),
+           retry=retry_if_exception_type(Exception))
+    def process_subscription(self, nfs, mr_pub):
+        action = 'Deactivate'
+        sub_nf_state = SubNfState.PENDING_DELETE.value
+        self.update_subscription_status()
+
+        if self.administrativeState == AdministrativeState.UNLOCKED.value:
+            action = 'Activate'
+            sub_nf_state = SubNfState.PENDING_CREATE.value
+
+        try:
+            for nf in nfs:
+                mr_pub.publish_subscription_event_data(self, nf.nf_name)
+                logger.debug(f'Publishing Event to {action} '
+                             f'Sub: {self.subscriptionName} for the nf: {nf.nf_name}')
+                self.add_network_functions_to_subscription(nfs)
+                self.update_sub_nf_status(self.subscriptionName, sub_nf_state, nf.nf_name)
+        except Exception as err:
+            raise Exception(f'Error publishing activation event to MR: {err}')
+
     @staticmethod
     def get_all_nfs_subscription_relations():
         """ Retrieves all network function to subscription relations
@@ -125,6 +182,22 @@
 
         return nf_per_subscriptions
 
+    @staticmethod
+    def update_sub_nf_status(subscription_name, status, nf_name):
+        """ Updates the status of the subscription for a particular nf
+
+        Args:
+            subscription_name (str): The subscription name
+            nf_name (str): The network function name
+            status (str): Status of the subscription
+        """
+        NfSubRelationalModel.query.filter(
+            NfSubRelationalModel.subscription_name == subscription_name,
+            NfSubRelationalModel.nf_name == nf_name). \
+            update({NfSubRelationalModel.nf_sub_status: status}, synchronize_session='evaluate')
+
+        db.session.commit()
+
 
 class NetworkFunctionFilter:
     def __init__(self, **kwargs):
diff --git a/components/pm-subscription-handler/pmsh_service/pmsh_service.py b/components/pm-subscription-handler/pmsh_service/pmsh_service.py
deleted file mode 100755
index c564a5e..0000000
--- a/components/pm-subscription-handler/pmsh_service/pmsh_service.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# ============LICENSE_START===================================================
-#  Copyright (C) 2019-2020 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=====================================================
-import sys
-import time
-
-import mod.aai_client as aai_client
-import mod.pmsh_logging as logger
-from mod import db, create_app
-from mod.config_handler import ConfigHandler
-from mod.subscription import Subscription
-
-
-def main():
-
-    try:
-        app = create_app()
-        app.app_context().push()
-        db.create_all(app=app)
-
-        config_handler = ConfigHandler()
-        cbs_data = config_handler.get_config()
-        subscription, xnfs = aai_client.get_pmsh_subscription_data(cbs_data)
-        subscription.add_network_functions_to_subscription(xnfs)
-    except Exception as e:
-        logger.debug(f'Failed to Init PMSH: {e}')
-        sys.exit(e)
-
-    while True:
-        logger.debug(Subscription.get_all_nfs_subscription_relations())
-        time.sleep(5)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/components/pm-subscription-handler/pmsh_service/pmsh_service_main.py b/components/pm-subscription-handler/pmsh_service/pmsh_service_main.py
new file mode 100755
index 0000000..ab33032
--- /dev/null
+++ b/components/pm-subscription-handler/pmsh_service/pmsh_service_main.py
@@ -0,0 +1,94 @@
+# ============LICENSE_START===================================================
+#  Copyright (C) 2019-2020 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=====================================================
+import sys
+import time
+import threading
+
+import mod.aai_client as aai
+import mod.pmsh_logging as logger
+from mod import db, create_app
+from mod.config_handler import ConfigHandler
+from mod.pmsh_utils import AppConfig
+from mod.subscription import Subscription, AdministrativeState
+
+
+def subscription_processor(config_handler, administrative_state, mr_pub, app):
+    """
+    Checks for changes of administrative state in config and proceeds to process
+    the Subscription if a change has occurred
+
+    Args:
+        config_handler (ConfigHandler): Configuration Handler used to get config
+        administrative_state (str): The administrative state
+        mr_pub (_MrPub): MR publisher
+        app (db): DB application
+    """
+    app.app_context().push()
+    config = config_handler.get_config()
+    sub, nfs = aai.get_pmsh_subscription_data(config)
+    new_administrative_state = config['policy']['subscription']['administrativeState']
+    polling_period = 30.0
+
+    try:
+        if administrative_state == new_administrative_state:
+            logger.debug('Administrative State did not change in the Config')
+        else:
+            sub.process_subscription(nfs, mr_pub)
+
+    except Exception as err:
+        logger.debug(f'Error occurred during the activation/deactivation process {err}')
+
+    threading.Timer(polling_period, subscription_processor,
+                    [config_handler, new_administrative_state, mr_pub, app]).start()
+
+
+def main():
+
+    try:
+        config_handler = ConfigHandler()
+        config = config_handler.get_config()
+        app_conf = AppConfig(**config['config'])
+        app = create_app()
+        app.app_context().push()
+        db.create_all(app=app)
+        sub, nfs = aai.get_pmsh_subscription_data(config)
+        mr_pub = app_conf.get_mr_pub('policy_pm_publisher')
+        mr_sub = app_conf.get_mr_sub('policy_pm_subscriber')
+        initial_start_delay = 5.0
+
+        administrative_state = AdministrativeState.LOCKED.value
+        subscription_in_db = Subscription.get(sub.subscriptionName)
+        if subscription_in_db is not None:
+            administrative_state = subscription_in_db.status
+
+        threading.Timer(initial_start_delay, subscription_processor,
+                        [config_handler, administrative_state, mr_pub, app]).start()
+
+        threading.Timer(20.0, mr_sub.poll_policy_topic, [sub.subscriptionName, app]).start()
+
+    except Exception as e:
+        logger.debug(f'Failed to Init PMSH: {e}')
+        sys.exit(e)
+
+    while True:
+        logger.debug(Subscription.get_all_nfs_subscription_relations())
+        time.sleep(5)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/components/pm-subscription-handler/tests/data/aai_xnfs.json b/components/pm-subscription-handler/tests/data/aai_xnfs.json
index 78fc654..d0c2043 100644
--- a/components/pm-subscription-handler/tests/data/aai_xnfs.json
+++ b/components/pm-subscription-handler/tests/data/aai_xnfs.json
@@ -36,7 +36,8 @@
         "serial-number": "6061ZW3",
         "ipaddress-v6-oam": "2001:0db8:0:0:0:0:1428:57ab",
         "resource-version": "1573053304574",
-        "nf-role": "gNB"
+        "nf-role": "gNB",
+        "orchestration-status": "Active"
       }
     },
     {
@@ -50,7 +51,7 @@
         "equip-vendor": "Nokia",
         "equip-model": "val6",
         "ipaddress-v4-oam": "10.10.10.66",
-         "orchestration-status": "Inventoried",
+        "orchestration-status": "Active",
         "sw-version": "val7",
         "in-maint": false,
         "serial-number": "6061ZW3",
@@ -70,6 +71,7 @@
         "equip-vendor": "Nokia",
         "equip-model": "val6",
         "ipaddress-v4-oam": "10.10.13.26",
+        "orchestration-status": "Active",
         "sw-version": "val7",
         "in-maint": false,
         "serial-number": "6061ZW3",
@@ -89,7 +91,7 @@
         "equip-vendor": "Ericsson",
         "equip-model": "val6",
         "ipaddress-v4-oam": "10.40.10.26",
-         "orchestration-status": "Inventoried",
+        "orchestration-status": "Inventoried",
         "sw-version": "val7",
         "in-maint": false,
         "serial-number": "6061ZW3",
diff --git a/components/pm-subscription-handler/tests/test_aai_service.py b/components/pm-subscription-handler/tests/test_aai_service.py
index 7f4735a..aaf2bb1 100644
--- a/components/pm-subscription-handler/tests/test_aai_service.py
+++ b/components/pm-subscription-handler/tests/test_aai_service.py
@@ -17,9 +17,9 @@
 # ============LICENSE_END=====================================================
 import json
 import os
-import unittest
 from test.support import EnvironmentVarGuard
-from unittest import mock
+from unittest import mock, TestCase
+from unittest.mock import patch
 
 import responses
 from requests import Session
@@ -27,7 +27,7 @@
 import mod.aai_client as aai_client
 
 
-class AaiClientTestCase(unittest.TestCase):
+class AaiClientTestCase(TestCase):
 
     def setUp(self):
         self.env = EnvironmentVarGuard()
@@ -38,16 +38,16 @@
         with open(os.path.join(os.path.dirname(__file__), 'data/aai_xnfs.json'), 'r') as data:
             self.aai_response_data = data.read()
 
-    @mock.patch.object(Session, 'put')
+    @patch.object(Session, 'put')
     def test_aai_client_get_pm_sub_data_success(self, mock_session):
         mock_session.return_value.status_code = 200
         mock_session.return_value.text = self.aai_response_data
         sub, xnfs = aai_client.get_pmsh_subscription_data(self.cbs_data)
         self.assertEqual(sub.subscriptionName, 'ExtraPM-All-gNB-R2B')
         self.assertEqual(sub.administrativeState, 'UNLOCKED')
-        self.assertEqual(len(xnfs), 6)
+        self.assertEqual(len(xnfs), 3)
 
-    @mock.patch.object(Session, 'put')
+    @patch.object(Session, 'put')
     def test_aai_client_get_pm_sub_data_fail(self, mock_session):
         mock_session.return_value.status_code = 404
         with mock.patch('mod.aai_client._get_all_aai_nf_data', return_value=None):
@@ -73,5 +73,5 @@
         with self.assertRaises(KeyError):
             aai_client._get_aai_service_url()
 
-    def test_aai_client_get_aai_service_url_succses(self):
+    def test_aai_client_get_aai_service_url_success(self):
         self.assertEqual('https://1.2.3.4:8443', aai_client._get_aai_service_url())
diff --git a/components/pm-subscription-handler/tests/test_config_handler.py b/components/pm-subscription-handler/tests/test_config_handler.py
index 5e80db5..dce48fc 100755
--- a/components/pm-subscription-handler/tests/test_config_handler.py
+++ b/components/pm-subscription-handler/tests/test_config_handler.py
@@ -24,7 +24,7 @@
 import responses
 from tenacity import wait_none
 
-from pmsh_service.mod.config_handler import ConfigHandler
+from mod.config_handler import ConfigHandler
 
 
 class ConfigHandlerTestCase(unittest.TestCase):
@@ -49,7 +49,7 @@
 
     @responses.activate
     def test_get_config_success(self):
-        responses.add(responses.GET, self.cbs_url, json=json.dumps(self.expected_config),
+        responses.add(responses.GET, self.cbs_url, json=self.expected_config,
                       status=200)
 
         config_handler = ConfigHandler()
diff --git a/components/pm-subscription-handler/tests/test_network_function.py b/components/pm-subscription-handler/tests/test_network_function.py
index 267851d..e9394b4 100755
--- a/components/pm-subscription-handler/tests/test_network_function.py
+++ b/components/pm-subscription-handler/tests/test_network_function.py
@@ -15,17 +15,17 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
-import unittest
 from test.support import EnvironmentVarGuard
-from unittest import mock
+from unittest import TestCase
+from unittest.mock import patch
 
 from mod import db, create_app
 from mod.network_function import NetworkFunction
 
 
-class NetworkFunctionTests(unittest.TestCase):
+class NetworkFunctionTests(TestCase):
 
-    @mock.patch('mod.get_db_connection_url')
+    @patch('mod.get_db_connection_url')
     def setUp(self, mock_get_db_url):
         mock_get_db_url.return_value = 'sqlite://'
         self.nf_1 = NetworkFunction(nf_name='pnf_1', orchestration_status='Inventoried')
@@ -65,3 +65,11 @@
         same_nf = self.nf_1.create()
 
         self.assertEqual(nf, same_nf)
+
+    def test_delete_network_function(self):
+        self.nf_1.create()
+        self.nf_2.create()
+        self.nf_1.delete(nf_name='pnf_1')
+        nfs = NetworkFunction.get_all()
+
+        self.assertEqual(1, len(nfs))
diff --git a/components/pm-subscription-handler/tests/test_pmsh_service.py b/components/pm-subscription-handler/tests/test_pmsh_service.py
new file mode 100644
index 0000000..4a6032b
--- /dev/null
+++ b/components/pm-subscription-handler/tests/test_pmsh_service.py
@@ -0,0 +1,82 @@
+# ============LICENSE_START===================================================
+#  Copyright (C) 2020 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=====================================================
+import os
+import json
+from unittest import TestCase
+from unittest.mock import patch
+
+import pmsh_service_main as pmsh_service
+from mod.network_function import NetworkFunction
+
+
+class PMSHServiceTest(TestCase):
+
+    @patch('mod.create_app')
+    @patch('mod.subscription.Subscription')
+    @patch('mod.pmsh_utils._MrPub')
+    @patch('mod.config_handler.ConfigHandler')
+    def setUp(self, mock_config_handler, mock_mr_pub,
+              mock_sub, mock_app):
+        with open(os.path.join(os.path.dirname(__file__), 'data/cbs_data_1.json'), 'r') as data:
+            self.cbs_data_1 = json.load(data)
+        self.mock_app = mock_app
+        self.mock_sub = mock_sub
+        self.mock_mr_pub = mock_mr_pub
+        self.mock_config_handler = mock_config_handler
+        self.nf_1 = NetworkFunction(nf_name='pnf_1')
+        self.nf_2 = NetworkFunction(nf_name='pnf_2')
+        self.nfs = [self.nf_1, self.nf_2]
+
+    @patch('threading.Timer')
+    @patch('mod.aai_client.get_pmsh_subscription_data')
+    def test_subscription_processor_changed_state(self, mock_get_aai, mock_thread):
+        self.mock_config_handler.get_config.return_value = self.cbs_data_1
+        mock_get_aai.return_value = self.mock_sub, self.nfs
+        mock_thread.start.return_value = 1
+
+        pmsh_service.subscription_processor(self.mock_config_handler, 'LOCKED',
+                                            self.mock_mr_pub, self.mock_app)
+
+        self.mock_sub.process_subscription.assert_called_with(self.nfs, self.mock_mr_pub)
+
+    @patch('threading.Timer')
+    @patch('mod.pmsh_logging.debug')
+    @patch('mod.aai_client.get_pmsh_subscription_data')
+    def test_subscription_processor_unchanged_state(self, mock_get_aai, mock_logger, mock_thread):
+        self.mock_config_handler.get_config.return_value = self.cbs_data_1
+        mock_get_aai.return_value = self.mock_sub, self.nfs
+        mock_thread.start.return_value = 1
+
+        pmsh_service.subscription_processor(self.mock_config_handler, 'UNLOCKED', self.mock_mr_pub,
+                                            self.mock_app)
+
+        mock_logger.assert_called_with('Administrative State did not change in the Config')
+
+    @patch('threading.Timer')
+    @patch('mod.pmsh_logging.debug')
+    @patch('mod.aai_client.get_pmsh_subscription_data')
+    def test_subscription_processor_exception(self, mock_get_aai, mock_logger, mock_thread):
+        self.mock_config_handler.get_config.return_value = self.cbs_data_1
+        mock_get_aai.return_value = self.mock_sub, self.nfs
+        mock_thread.start.return_value = 1
+        self.mock_sub.process_subscription.side_effect = Exception
+
+        pmsh_service.subscription_processor(self.mock_config_handler, 'LOCKED', self.mock_mr_pub,
+                                            self.mock_app)
+        mock_logger.assert_called_with(f'Error occurred during the '
+                                       f'activation/deactivation process ')
diff --git a/components/pm-subscription-handler/tests/test_pmsh_utils.py b/components/pm-subscription-handler/tests/test_pmsh_utils.py
index 8df2c62..03e8c69 100644
--- a/components/pm-subscription-handler/tests/test_pmsh_utils.py
+++ b/components/pm-subscription-handler/tests/test_pmsh_utils.py
@@ -17,26 +17,39 @@
 # ============LICENSE_END=====================================================
 import json
 import os
-import unittest
 from test.support import EnvironmentVarGuard
-from unittest import mock
+from unittest import TestCase
 from unittest.mock import patch
 
 import responses
 from requests import Session
+from tenacity import stop_after_attempt
 
-from mod import get_db_connection_url
-from mod.pmsh_utils import AppConfig
+from mod import db, get_db_connection_url, create_app
+from mod.db_models import SubscriptionModel
+from mod.pmsh_utils import AppConfig, policy_response_handle_functions
 from mod.subscription import Subscription
+from mod.network_function import NetworkFunction
 
 
-class PmshUtilsTestCase(unittest.TestCase):
+class PmshUtilsTestCase(TestCase):
 
-    def setUp(self):
+    @patch('mod.create_app')
+    @patch('mod.get_db_connection_url')
+    def setUp(self, mock_get_db_url, mock_app):
+        mock_get_db_url.return_value = 'sqlite://'
         with open(os.path.join(os.path.dirname(__file__), 'data/cbs_data_1.json'), 'r') as data:
             self.cbs_data = json.load(data)
         self.app_conf = AppConfig(**self.cbs_data['config'])
         self.sub = Subscription(**self.cbs_data['policy']['subscription'])
+        self.env = EnvironmentVarGuard()
+        self.env.set('LOGS_PATH', './unit_test_logs')
+        self.policy_mr_sub = self.app_conf.get_mr_sub('policy_pm_subscriber')
+        self.mock_app = mock_app
+        self.app = create_app()
+        self.app_context = self.app.app_context()
+        self.app_context.push()
+        db.create_all()
 
     def test_utils_get_mr_sub(self):
         mr_policy_sub = self.app_conf.get_mr_sub('policy_pm_subscriber')
@@ -58,7 +71,7 @@
         self.assertTrue(self.app_conf.cert_params, ('/opt/app/pm-mapper/etc/certs/cert.pem',
                                                     '/opt/app/pm-mapper/etc/certs/key.pem'))
 
-    @mock.patch.object(Session, 'post')
+    @patch.object(Session, 'post')
     def test_mr_pub_publish_to_topic_success(self, mock_session):
         mock_session.return_value.status_code = 200
         mr_policy_pub = self.app_conf.get_mr_pub('policy_pm_publisher')
@@ -87,8 +100,7 @@
                       'https://node:30226/events/org.onap.dmaap.mr.PM_SUBSCRIPTIONS/'
                       'dcae_pmsh_cg/1?timeout=1000',
                       json={"dummy_val": "43c4ee19-6b8d-4279-a80f-c507850aae47"}, status=200)
-        mr_policy_sub = self.app_conf.get_mr_sub('policy_pm_subscriber')
-        mr_topic_data = mr_policy_sub.get_from_topic(1)
+        mr_topic_data = self.policy_mr_sub.get_from_topic(1)
         self.assertIsNotNone(mr_topic_data)
 
     @responses.activate
@@ -97,8 +109,7 @@
                       'https://node:30226/events/org.onap.dmaap.mr.PM_SUBSCRIPTIONS/'
                       'dcae_pmsh_cg/1?timeout=1000',
                       json={"dummy_val": "43c4ee19-6b8d-4279-a80f-c507850aae47"}, status=400)
-        mr_policy_sub = self.app_conf.get_mr_sub('policy_pm_subscriber')
-        mr_topic_data = mr_policy_sub.get_from_topic(1)
+        mr_topic_data = self.policy_mr_sub.get_from_topic(1)
         self.assertIsNone(mr_topic_data)
 
     def test_get_db_connection_url_success(self):
@@ -115,3 +126,93 @@
         self.env.set('PMSH_PG_PASSWORD', 'pass')
         with self.assertRaises(Exception):
             get_db_connection_url()
+
+    @patch('mod.pmsh_utils.NetworkFunction.delete')
+    def test_handle_response_locked_success(self, mock_delete):
+        with patch.dict(policy_response_handle_functions, {'LOCKED': {'success': mock_delete}}):
+            administrative_state = 'LOCKED'
+            nf = NetworkFunction(nf_name='nf1')
+            self.policy_mr_sub._handle_response(self.sub.subscriptionName, administrative_state,
+                                                nf.nf_name, 'success')
+
+            mock_delete.assert_called()
+
+    @patch('mod.subscription.Subscription.update_sub_nf_status')
+    def test_handle_response_locked_failed(self, mock_update_sub_nf):
+        with patch.dict(policy_response_handle_functions,
+                        {'LOCKED': {'failed': mock_update_sub_nf}}):
+            administrative_state = 'LOCKED'
+            nf = NetworkFunction(nf_name='nf1')
+            self.policy_mr_sub._handle_response(self.sub.subscriptionName, administrative_state,
+                                                nf.nf_name, 'failed')
+            mock_update_sub_nf.assert_called()
+
+    @patch('mod.subscription.Subscription.update_sub_nf_status')
+    def test_handle_response_unlocked_success(self, mock_update_sub_nf):
+        with patch.dict(policy_response_handle_functions,
+                        {'UNLOCKED': {'success': mock_update_sub_nf}}):
+            nf = NetworkFunction(nf_name='nf1')
+            self.policy_mr_sub._handle_response(self.sub.subscriptionName,
+                                                self.sub.administrativeState,
+                                                nf.nf_name, 'success')
+            mock_update_sub_nf.assert_called()
+
+    @patch('mod.subscription.Subscription.update_sub_nf_status')
+    def test_handle_response_unlocked_failed(self, mock_update_sub_nf):
+        with patch.dict(policy_response_handle_functions,
+                        {'UNLOCKED': {'failed': mock_update_sub_nf}}):
+            nf = NetworkFunction(nf_name='nf1')
+            self.policy_mr_sub._handle_response(self.sub.subscriptionName,
+                                                self.sub.administrativeState,
+                                                nf.nf_name, 'failed')
+            mock_update_sub_nf.assert_called()
+
+    def test_handle_response_exception(self):
+        self.assertRaises(Exception, self.policy_mr_sub._handle_response, 'sub1', 'wrong_state',
+                          'nf1', 'wrong_message')
+
+    @patch('mod.pmsh_utils._MrSub.get_from_topic')
+    @patch('mod.pmsh_utils._MrSub._handle_response')
+    @patch('mod.subscription.Subscription.get')
+    @patch('threading.Timer')
+    def test_poll_policy_topic_calls_methods_correct_sub(self, mock_thread, mock_get_sub,
+                                                         mock_handle_response, mock_get_from_topic):
+        result_data = ['{"name": "ResponseEvent","status": { "subscriptionName": '
+                       '"ExtraPM-All-gNB-R2B", "nfName": "pnf300", "message": "success" } }']
+        mock_get_from_topic.return_value = result_data
+        mock_thread.start.return_value = 1
+        mock_get_sub.return_value = SubscriptionModel(subscription_name='ExtraPM-All-gNB-R2B',
+                                                      status='UNLOCKED')
+        self.policy_mr_sub.poll_policy_topic(self.sub.subscriptionName, self.mock_app)
+
+        mock_get_from_topic.assert_called()
+        mock_handle_response.assert_called_with(self.sub.subscriptionName,
+                                                'UNLOCKED', 'pnf300', 'success')
+
+    @patch('mod.pmsh_utils._MrSub.get_from_topic')
+    @patch('mod.pmsh_utils._MrSub._handle_response')
+    @patch('mod.subscription.Subscription.get')
+    @patch('threading.Timer')
+    def test_poll_policy_topic_no_method_calls_incorrect_sub(self, mock_thread, mock_get_sub,
+                                                             mock_handle_response,
+                                                             mock_get_from_topic):
+        result_data = ['{"name": "ResponseEvent","status": { "subscriptionName": '
+                       '"demo-subscription", "nfName": "pnf300", "message": "success" } }']
+        mock_get_from_topic.return_value = result_data
+        mock_thread.start.return_value = 1
+        mock_get_sub.return_value = SubscriptionModel(subscription_name='ExtraPM-All-gNB-R2B',
+                                                      status='UNLOCKED')
+        self.policy_mr_sub.poll_policy_topic(self.sub, self.mock_app)
+
+        mock_get_from_topic.assert_called()
+        mock_handle_response.assert_not_called()
+
+    @patch('mod.subscription.Subscription.get')
+    @patch('mod.pmsh_utils._MrSub.get_from_topic')
+    def test_poll_policy_topic_exception(self, mock_get_from_topic, mock_get_sub):
+        mock_get_from_topic.return_value = 'wrong_return'
+        mock_get_sub.return_value = SubscriptionModel(subscription_name='ExtraPM-All-gNB-R2B',
+                                                      status='UNLOCKED')
+        self.policy_mr_sub.poll_policy_topic.retry.stop = stop_after_attempt(1)
+
+        self.assertRaises(Exception, self.policy_mr_sub.poll_policy_topic, 'sub1', self.mock_app)
diff --git a/components/pm-subscription-handler/tests/test_subscription.py b/components/pm-subscription-handler/tests/test_subscription.py
index 97c1d6a..8fe233e 100755
--- a/components/pm-subscription-handler/tests/test_subscription.py
+++ b/components/pm-subscription-handler/tests/test_subscription.py
@@ -17,11 +17,12 @@
 # ============LICENSE_END=====================================================
 import json
 import os
-import unittest
 from test.support import EnvironmentVarGuard
-from unittest import mock
+from unittest import TestCase
+from unittest.mock import patch
 
 from requests import Session
+from tenacity import stop_after_attempt
 
 import mod.aai_client as aai_client
 from mod import db, create_app
@@ -29,11 +30,12 @@
 from mod.subscription import Subscription, NetworkFunctionFilter
 
 
-class SubscriptionTest(unittest.TestCase):
-
-    @mock.patch('mod.get_db_connection_url')
-    @mock.patch.object(Session, 'put')
-    def setUp(self, mock_session, mock_get_db_url):
+class SubscriptionTest(TestCase):
+    @patch('mod.pmsh_utils._MrPub')
+    @patch('mod.pmsh_utils._MrSub')
+    @patch('mod.get_db_connection_url')
+    @patch.object(Session, 'put')
+    def setUp(self, mock_session, mock_get_db_url, mock_mr_sub, mock_mr_pub):
         mock_get_db_url.return_value = 'sqlite://'
         with open(os.path.join(os.path.dirname(__file__), 'data/aai_xnfs.json'), 'r') as data:
             self.aai_response_data = data.read()
@@ -54,6 +56,8 @@
         self.nf_1 = NetworkFunction(nf_name='pnf_1', orchestration_status='Inventoried')
         self.nf_2 = NetworkFunction(nf_name='pnf_2', orchestration_status='Active')
         self.xnf_filter = NetworkFunctionFilter(**self.sub_1.nfFilter)
+        self.mock_mr_sub = mock_mr_sub
+        self.mock_mr_pub = mock_mr_pub
         self.app = create_app()
         self.app_context = self.app.app_context()
         self.app_context.push()
@@ -100,12 +104,6 @@
         self.assertEqual(sub1, same_sub1)
         self.assertEqual(1, len(self.sub_1.get_all()))
 
-    def test_get_nfs_per_subscription(self):
-        nf_array = [self.nf_1, self.nf_2]
-        self.sub_1.add_network_functions_to_subscription(nf_array)
-        nfs_for_sub_1 = Subscription.get_all_nfs_subscription_relations()
-        self.assertEqual(2, len(nfs_for_sub_1))
-
     def test_add_network_functions_per_subscription(self):
         nf_array = [self.nf_1, self.nf_2]
         self.sub_1.add_network_functions_to_subscription(nf_array)
@@ -125,3 +123,65 @@
         self.sub_1.add_network_functions_to_subscription(nf_array)
         nf_subs = Subscription.get_all_nfs_subscription_relations()
         self.assertEqual(1, len(nf_subs))
+
+    def test_update_subscription_status(self):
+        sub_name = 'ExtraPM-All-gNB-R2B'
+        self.sub_1.create()
+        self.sub_1.administrativeState = 'new_status'
+        self.sub_1.update_subscription_status()
+        sub = Subscription.get(sub_name)
+
+        self.assertEqual('new_status', sub.status)
+
+    def test_delete_subscription(self):
+        self.sub_1.create()
+        subs = self.sub_1.get_all()
+        self.assertEqual(1, len(subs))
+
+        self.sub_1.delete_subscription()
+        new_subs = self.sub_1.get_all()
+        self.assertEqual(0, len(new_subs))
+
+    def test_update_sub_nf_status(self):
+        sub_name = 'ExtraPM-All-gNB-R2B'
+        nf_array = [self.nf_1, self.nf_2]
+        self.sub_1.add_network_functions_to_subscription(nf_array)
+        sub_nfs = Subscription.get_all_nfs_subscription_relations()
+        self.assertEqual('PENDING_CREATE', sub_nfs[0].nf_sub_status)
+
+        Subscription.update_sub_nf_status(sub_name, 'Active', 'pnf_1')
+        sub_nfs = Subscription.get_all_nfs_subscription_relations()
+        self.assertEqual('Active', sub_nfs[0].nf_sub_status)
+        self.assertEqual('PENDING_CREATE', sub_nfs[1].nf_sub_status)
+
+    @patch('mod.subscription.Subscription.add_network_functions_to_subscription')
+    @patch('mod.subscription.Subscription.update_sub_nf_status')
+    @patch('mod.subscription.Subscription.update_subscription_status')
+    def test_process_activate_subscription(self, mock_update_sub_status,
+                                           mock_update_sub_nf, mock_add_nfs):
+        self.sub_1.process_subscription.retry.stop = stop_after_attempt(1)
+        self.sub_1.process_subscription([self.nf_1], self.mock_mr_pub)
+
+        mock_update_sub_status.assert_called()
+        mock_add_nfs.assert_called()
+        self.assertTrue(self.mock_mr_pub.publish_subscription_event_data.called)
+        mock_update_sub_nf.assert_called_with(self.sub_1.subscriptionName,
+                                              'PENDING_CREATE', self.nf_1.nf_name)
+
+    @patch('mod.subscription.Subscription.update_sub_nf_status')
+    @patch('mod.subscription.Subscription.update_subscription_status')
+    def test_process_deactivate_subscription(self, mock_update_sub_status,
+                                             mock_update_sub_nf):
+        self.sub_1.administrativeState = 'LOCKED'
+        self.sub_1.process_subscription.retry.stop = stop_after_attempt(1)
+        self.sub_1.process_subscription([self.nf_1], self.mock_mr_pub)
+
+        self.assertTrue(self.mock_mr_pub.publish_subscription_event_data.called)
+        mock_update_sub_nf.assert_called_with(self.sub_1.subscriptionName,
+                                              'PENDING_DELETE', self.nf_1.nf_name)
+        mock_update_sub_status.assert_called()
+
+    def test_process_subscription_exception(self):
+        self.sub_1.process_subscription.retry.stop = stop_after_attempt(1)
+        self.assertRaises(Exception, self.sub_1.process_subscription,
+                          [self.nf_1], 'not_mr_pub')
diff --git a/components/pm-subscription-handler/tox.ini b/components/pm-subscription-handler/tox.ini
index 6e83d55..5ba25d5 100644
--- a/components/pm-subscription-handler/tox.ini
+++ b/components/pm-subscription-handler/tox.ini
@@ -30,7 +30,8 @@
 setenv =
     PYTHONPATH={toxinidir}/pmsh_service:{toxinidir}/pmsh_service/mod:{toxinidir}/tests
 commands=
-    pytest --junitxml xunit-results.xml --cov pmsh_service --cov-report xml --cov-report term tests --verbose --cov-fail-under=70
+    pytest --junitxml xunit-results.xml --cov pmsh_service --cov-report xml --cov-report term \
+    tests --verbose --cov-fail-under=70
 
 [testenv:flake8]
 basepython = python3