Add unit tests to N2VC refactor 17/9417/1
authorDominik Fleischmann <dominik.fleischmann@canonical.com>
Tue, 7 Jul 2020 11:11:19 +0000 (13:11 +0200)
committergarciadav <david.garcia@canonical.com>
Wed, 15 Jul 2020 10:21:23 +0000 (12:21 +0200)
This commit adds unit tests for the following
modules:
juju_watcher.py 96% coverage
libjuju.py 72% coverage
utils.py 100% coverage

Minor libjuju.py fixes found with unit testing
Fix testing with tox

Change-Id: I9f23ce2f18aac6765edfa955ed200802c27d9047
Signed-off-by: Dominik Fleischmann <dominik.fleischmann@canonical.com>
n2vc/exceptions.py
n2vc/libjuju.py
n2vc/tests/unit/README.md [new file with mode: 0644]
n2vc/tests/unit/test_juju_watcher.py [new file with mode: 0644]
n2vc/tests/unit/test_libjuju.py [new file with mode: 0644]
n2vc/tests/unit/test_utils.py [new file with mode: 0644]
n2vc/tests/unit/utils.py [new file with mode: 0644]
tox.ini

index 59bcc1a..256860e 100644 (file)
@@ -33,6 +33,14 @@ class JujuApplicationNotFound(Exception):
     """The Application cannot be found."""
 
 
+class JujuLeaderUnitNotFound(Exception):
+    """The Application cannot be found."""
+
+
+class JujuActionNotFound(Exception):
+    """The Action cannot be found."""
+
+
 class JujuMachineNotFound(Exception):
     """The machine cannot be found."""
 
index be16e2a..22ba182 100644 (file)
@@ -29,6 +29,8 @@ from n2vc.n2vc_conn import N2VCConnector
 from n2vc.exceptions import (
     JujuMachineNotFound,
     JujuApplicationNotFound,
+    JujuLeaderUnitNotFound,
+    JujuActionNotFound,
     JujuModelAlreadyExists,
     JujuControllerFailedConnecting,
     JujuApplicationExists,
@@ -298,7 +300,7 @@ class Libjuju:
                             machine_id, model_name
                         )
                     )
-                    machine = model.machines[machine_id]
+                    machine = machines[machine_id]
                 else:
                     raise JujuMachineNotFound("Machine {} not found".format(machine_id))
 
@@ -570,7 +572,6 @@ class Libjuju:
 
         :param: application_name:   Application name
         :param: model_name:         Model name
-        :param: cloud_name:         Cloud name
         :param: action_name:        Name of the action
         :param: db_dict:            Dictionary with data of the DB to write the updates
         :param: progress_timeout:   Maximum time between two updates in the model
@@ -601,12 +602,12 @@ class Libjuju:
                 if await u.is_leader_from_status():
                     unit = u
             if unit is None:
-                raise Exception("Cannot execute action: leader unit not found")
+                raise JujuLeaderUnitNotFound("Cannot execute action: leader unit not found")
 
             actions = await application.get_actions()
 
             if action_name not in actions:
-                raise Exception(
+                raise JujuActionNotFound(
                     "Action {} not in available actions".format(action_name)
                 )
 
@@ -637,8 +638,6 @@ class Libjuju:
                     action_name, action.status, application_name, model_name
                 )
             )
-        except Exception as e:
-            raise e
         finally:
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
@@ -819,7 +818,7 @@ class Libjuju:
         """
         machines = await model.get_machines()
         if machine_id in machines:
-            machine = model.machines[machine_id]
+            machine = machines[machine_id]
             await machine.destroy(force=True)
             # max timeout
             end = time.time() + total_timeout
diff --git a/n2vc/tests/unit/README.md b/n2vc/tests/unit/README.md
new file mode 100644 (file)
index 0000000..39c791b
--- /dev/null
@@ -0,0 +1,48 @@
+<!--- Copyright 2020 Canonical 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. --->
+
+
+# N2VC Unit Testing Guideline
+
+## Use `test_libjuju.py` as a guideline
+
+Even though the Test Cases still have plenty of potential improvements we feel like this file is the most polished of all of them. Therefore it should be used as a baseline of any future tests or changes in current tests for what is the minimum standard.
+
+## Try to use mock as much as possible
+
+There are some cases where FakeClasses (which still inherit from Mock classes) are used. This is only for the cases where the construction of the object requires to much additional mocking. Using standard mocks gives more testing possibilities.
+
+## Separate your Test Cases into different classes
+
+It is preferrable to have a TestCase Class for each method and several test methods to test different scenarios. If all of the classes need the same setup a Parent TestCase class can be created with a setUp method and afterwards the other TestCases can inherit from it like this:
+
+```python
+class GetControllerTest(LibjujuTestCase):
+
+    def setUp(self):
+        super(GetControllerTest, self).setUp()
+```
+
+## Things to assert
+
+It is more important to actually assert the important logic than have a high code coverage but not actually testing the code.
+
+These are some of the things that should be always asserted:
+
+* Assert all Exceptions are launched correctly.
+* Assert the return values are the expected ones for **both** succesfull executions and unsuccesful ones.
+* Assert that all important calls have been called the correct amount of time and with the correct arguments.
+* Assert that when the method is failing the correct log messages are posted.
+* Assert that all things to need to be disconnected after execution are correctly disconnected.
+
diff --git a/n2vc/tests/unit/test_juju_watcher.py b/n2vc/tests/unit/test_juju_watcher.py
new file mode 100644 (file)
index 0000000..56b4bbd
--- /dev/null
@@ -0,0 +1,89 @@
+# Copyright 2020 Canonical 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.
+
+import asynctest
+import asyncio
+
+from unittest import mock
+from unittest.mock import Mock
+from n2vc.juju_watcher import JujuModelWatcher
+from n2vc.utils import EntityType
+from n2vc.exceptions import EntityInvalidException
+from .utils import FakeN2VC, AsyncMock, Deltas, FakeWatcher
+
+
+class JujuWatcherTest(asynctest.TestCase):
+    def setUp(self):
+        self.n2vc = FakeN2VC()
+        self.model = Mock()
+        self.loop = asyncio.new_event_loop()
+
+    def test_get_status(self):
+        tests = Deltas
+        for test in tests:
+            (status, message, vca_status) = JujuModelWatcher.get_status(
+                test.delta, test.entity.type
+            )
+            self.assertEqual(status, test.entity_status.status)
+            self.assertEqual(message, test.entity_status.message)
+            self.assertEqual(vca_status, test.entity_status.vca_status)
+
+    @mock.patch("n2vc.juju_watcher.client.AllWatcherFacade.from_connection")
+    def test_model_watcher(self, allwatcher):
+        tests = Deltas
+        allwatcher.return_value = FakeWatcher()
+        for test in tests:
+            with self.assertRaises(asyncio.TimeoutError):
+                allwatcher.return_value.delta_to_return = [test.delta]
+                self.loop.run_until_complete(
+                    JujuModelWatcher.model_watcher(
+                        self.model,
+                        test.filter.entity_id,
+                        test.filter.entity_type,
+                        timeout=0,
+                        db_dict={"something"},
+                        n2vc=self.n2vc,
+                    )
+                )
+
+            self.assertEqual(self.n2vc.last_written_values, test.db.data)
+            self.n2vc.last_written_values = None
+
+    @mock.patch("n2vc.juju_watcher.asyncio.wait")
+    @mock.patch("n2vc.juju_watcher.EntityType.get_entity")
+    def test_wait_for(self, get_entity, wait):
+        wait.return_value = asyncio.Future()
+        wait.return_value.set_result(None)
+        get_entity.return_value = EntityType.MACHINE
+
+        machine = AsyncMock()
+        self.loop.run_until_complete(JujuModelWatcher.wait_for(self.model, machine))
+
+    @mock.patch("n2vc.juju_watcher.asyncio.wait")
+    @mock.patch("n2vc.juju_watcher.EntityType.get_entity")
+    def test_wait_for_exception(self, get_entity, wait):
+        wait.return_value = asyncio.Future()
+        wait.return_value.set_result(None)
+        wait.side_effect = Exception("error")
+        get_entity.return_value = EntityType.MACHINE
+
+        machine = AsyncMock()
+        with self.assertRaises(Exception):
+            self.loop.run_until_complete(JujuModelWatcher.wait_for(self.model, machine))
+
+    def test_wait_for_invalid_entity_exception(self):
+        with self.assertRaises(EntityInvalidException):
+            self.loop.run_until_complete(
+                JujuModelWatcher.wait_for(self.model, AsyncMock(), total_timeout=0)
+            )
diff --git a/n2vc/tests/unit/test_libjuju.py b/n2vc/tests/unit/test_libjuju.py
new file mode 100644 (file)
index 0000000..5669959
--- /dev/null
@@ -0,0 +1,875 @@
+# Copyright 2020 Canonical 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.
+
+import asyncio
+import asynctest
+import juju
+from juju.errors import JujuAPIError
+import logging
+from .utils import FakeN2VC, FakeMachine, FakeApplication
+from n2vc.libjuju import Libjuju
+from n2vc.exceptions import (
+    JujuControllerFailedConnecting,
+    JujuModelAlreadyExists,
+    JujuMachineNotFound,
+    JujuApplicationNotFound,
+    JujuActionNotFound,
+    JujuApplicationExists,
+)
+
+
+class LibjujuTestCase(asynctest.TestCase):
+    @asynctest.mock.patch("juju.controller.Controller.update_endpoints")
+    @asynctest.mock.patch("juju.client.connector.Connector.connect")
+    @asynctest.mock.patch("juju.controller.Controller.connection")
+    @asynctest.mock.patch("n2vc.libjuju.Libjuju._get_api_endpoints_db")
+    def setUp(
+        self,
+        mock__get_api_endpoints_db=None,
+        mock_connection=None,
+        mock_connect=None,
+        mock_update_endpoints=None,
+    ):
+        loop = asyncio.get_event_loop()
+        n2vc = FakeN2VC()
+        mock__get_api_endpoints_db.return_value = ["127.0.0.1:17070"]
+        endpoints = "127.0.0.1:17070"
+        username = "admin"
+        password = "secret"
+        cacert = """
+    -----BEGIN CERTIFICATE-----
+    SOMECERT
+    -----END CERTIFICATE-----"""
+        self.libjuju = Libjuju(
+            endpoints,
+            "192.168.0.155:17070",
+            username,
+            password,
+            cacert,
+            loop,
+            log=None,
+            db={"get_one": []},
+            n2vc=n2vc,
+            apt_mirror="192.168.0.100",
+            enable_os_upgrade=True,
+        )
+        logging.disable(logging.CRITICAL)
+        loop.run_until_complete(self.libjuju.disconnect())
+
+
+@asynctest.mock.patch("juju.controller.Controller.connect")
+@asynctest.mock.patch(
+    "juju.controller.Controller.api_endpoints",
+    new_callable=asynctest.CoroutineMock(return_value=["127.0.0.1:17070"]),
+)
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._update_api_endpoints_db")
+class GetControllerTest(LibjujuTestCase):
+    def setUp(self):
+        super(GetControllerTest, self).setUp()
+
+    def test_diff_endpoint(
+        self, mock__update_api_endpoints_db, mock_api_endpoints, mock_connect
+    ):
+        self.libjuju.endpoints = []
+        controller = self.loop.run_until_complete(self.libjuju.get_controller())
+        mock__update_api_endpoints_db.assert_called_once_with(["127.0.0.1:17070"])
+        self.assertIsInstance(controller, juju.controller.Controller)
+
+    @asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+    def test_exception(
+        self,
+        mock_disconnect_controller,
+        mock__update_api_endpoints_db,
+        mock_api_endpoints,
+        mock_connect,
+    ):
+        self.libjuju.endpoints = []
+        mock__update_api_endpoints_db.side_effect = Exception()
+        with self.assertRaises(JujuControllerFailedConnecting):
+            controller = self.loop.run_until_complete(self.libjuju.get_controller())
+            self.assertIsNone(controller)
+            mock_disconnect_controller.assert_called_once()
+
+    def test_same_endpoint_get_controller(
+        self, mock__update_api_endpoints_db, mock_api_endpoints, mock_connect
+    ):
+        self.libjuju.endpoints = ["127.0.0.1:17070"]
+        controller = self.loop.run_until_complete(self.libjuju.get_controller())
+        mock__update_api_endpoints_db.assert_not_called()
+        self.assertIsInstance(controller, juju.controller.Controller)
+
+
+class DisconnectTest(LibjujuTestCase):
+    def setUp(self):
+        super(DisconnectTest, self).setUp()
+
+    @asynctest.mock.patch("juju.model.Model.disconnect")
+    def test_disconnect_model(self, mock_disconnect):
+        self.loop.run_until_complete(self.libjuju.disconnect_model(juju.model.Model()))
+        mock_disconnect.assert_called_once()
+
+    @asynctest.mock.patch("juju.controller.Controller.disconnect")
+    def test_disconnect_controller(self, mock_disconnect):
+        self.loop.run_until_complete(
+            self.libjuju.disconnect_controller(juju.controller.Controller())
+        )
+        mock_disconnect.assert_called_once()
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.model_exists")
+@asynctest.mock.patch("juju.controller.Controller.add_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+class AddModelTest(LibjujuTestCase):
+    def setUp(self):
+        super(AddModelTest, self).setUp()
+
+    def test_existing_model(
+        self,
+        mock_disconnect_model,
+        mock_disconnect_controller,
+        mock_add_model,
+        mock_model_exists,
+        mock_get_controller,
+    ):
+        mock_model_exists.return_value = True
+
+        with self.assertRaises(JujuModelAlreadyExists):
+            self.loop.run_until_complete(
+                self.libjuju.add_model("existing_model", "cloud")
+            )
+
+            mock_disconnect_controller.assert_called()
+
+    # TODO Check two job executing at the same time and one returning without doing anything.
+
+    def test_non_existing_model(
+        self,
+        mock_disconnect_model,
+        mock_disconnect_controller,
+        mock_add_model,
+        mock_model_exists,
+        mock_get_controller,
+    ):
+        mock_model_exists.return_value = False
+        mock_get_controller.return_value = juju.controller.Controller()
+
+        self.loop.run_until_complete(
+            self.libjuju.add_model("nonexisting_model", "cloud")
+        )
+
+        mock_add_model.assert_called_once()
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+
+@asynctest.mock.patch("juju.controller.Controller.get_model")
+class GetModelTest(LibjujuTestCase):
+    def setUp(self):
+        super(GetModelTest, self).setUp()
+
+    def test_get_model(
+        self, mock_get_model,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        model = self.loop.run_until_complete(
+            self.libjuju.get_model(juju.controller.Controller(), "model")
+        )
+        self.assertIsInstance(model, juju.model.Model)
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("juju.controller.Controller.list_models")
+class ModelExistsTest(LibjujuTestCase):
+    def setUp(self):
+        super(ModelExistsTest, self).setUp()
+
+    async def test_existing_model(
+        self, mock_list_models, mock_get_controller,
+    ):
+        mock_list_models.return_value = ["existing_model"]
+        self.assertTrue(
+            await self.libjuju.model_exists(
+                "existing_model", juju.controller.Controller()
+            )
+        )
+
+    @asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+    async def test_no_controller(
+        self, mock_disconnect_controller, mock_list_models, mock_get_controller,
+    ):
+        mock_list_models.return_value = ["existing_model"]
+        mock_get_controller.return_value = juju.controller.Controller()
+        self.assertTrue(await self.libjuju.model_exists("existing_model"))
+        mock_disconnect_controller.assert_called_once()
+
+    async def test_non_existing_model(
+        self, mock_list_models, mock_get_controller,
+    ):
+        mock_list_models.return_value = ["existing_model"]
+        self.assertFalse(
+            await self.libjuju.model_exists(
+                "not_existing_model", juju.controller.Controller()
+            )
+        )
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("juju.model.Model.get_status")
+class GetModelStatusTest(LibjujuTestCase):
+    def setUp(self):
+        super(GetModelStatusTest, self).setUp()
+
+    def test_success(
+        self,
+        mock_get_status,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_get_status.return_value = {"status"}
+
+        status = self.loop.run_until_complete(self.libjuju.get_model_status("model"))
+
+        mock_get_status.assert_called_once()
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+        self.assertEqual(status, {"status"})
+
+    def test_excpetion(
+        self,
+        mock_get_status,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_get_status.side_effect = Exception()
+
+        with self.assertRaises(Exception):
+            status = self.loop.run_until_complete(
+                self.libjuju.get_model_status("model")
+            )
+
+            mock_disconnect_controller.assert_called_once()
+            mock_disconnect_model.assert_called_once()
+
+            self.assertIsNone(status)
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("juju.model.Model.get_machines")
+@asynctest.mock.patch("juju.model.Model.add_machine")
+@asynctest.mock.patch("n2vc.juju_watcher.JujuModelWatcher.wait_for")
+class CreateMachineTest(LibjujuTestCase):
+    def setUp(self):
+        super(CreateMachineTest, self).setUp()
+
+    def test_existing_machine(
+        self,
+        mock_wait_for,
+        mock_add_machine,
+        mock_get_machines,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_get_machines.return_value = {"existing_machine": FakeMachine()}
+        machine, bool_res = self.loop.run_until_complete(
+            self.libjuju.create_machine("model", "existing_machine")
+        )
+
+        self.assertIsInstance(machine, FakeMachine)
+        self.assertFalse(bool_res)
+
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+    def test_non_existing_machine(
+        self,
+        mock_wait_for,
+        mock_add_machine,
+        mock_get_machines,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        with self.assertRaises(JujuMachineNotFound):
+            machine, bool_res = self.loop.run_until_complete(
+                self.libjuju.create_machine("model", "non_existing_machine")
+            )
+            self.assertIsNone(machine)
+            self.assertIsNone(bool_res)
+
+            mock_disconnect_controller.assert_called()
+            mock_disconnect_model.assert_called()
+
+    def test_no_machine(
+        self,
+        mock_wait_for,
+        mock_add_machine,
+        mock_get_machines,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_add_machine.return_value = FakeMachine()
+
+        machine, bool_res = self.loop.run_until_complete(
+            self.libjuju.create_machine("model")
+        )
+
+        self.assertIsInstance(machine, FakeMachine)
+        self.assertTrue(bool_res)
+
+        mock_wait_for.assert_called_once()
+        mock_add_machine.assert_called_once()
+
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+
+# TODO test provision machine
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch(
+    "juju.model.Model.applications", new_callable=asynctest.PropertyMock
+)
+@asynctest.mock.patch("juju.model.Model.machines", new_callable=asynctest.PropertyMock)
+@asynctest.mock.patch("juju.model.Model.deploy")
+@asynctest.mock.patch("n2vc.juju_watcher.JujuModelWatcher.wait_for")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.create_machine")
+class DeployCharmTest(LibjujuTestCase):
+    def setUp(self):
+        super(DeployCharmTest, self).setUp()
+
+    def test_existing_app(
+        self,
+        mock_create_machine,
+        mock_wait_for,
+        mock_deploy,
+        mock_machines,
+        mock_applications,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_applications.return_value = {"existing_app"}
+
+        with self.assertRaises(JujuApplicationExists):
+            application = self.loop.run_until_complete(
+                self.libjuju.deploy_charm("existing_app", "path", "model", "machine",)
+            )
+            self.assertIsNone(application)
+
+            mock_disconnect_controller.assert_called()
+            mock_disconnect_model.assert_called()
+
+    def test_non_existing_machine(
+        self,
+        mock_create_machine,
+        mock_wait_for,
+        mock_deploy,
+        mock_machines,
+        mock_applications,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_machines.return_value = {"existing_machine": FakeMachine()}
+        with self.assertRaises(JujuMachineNotFound):
+            application = self.loop.run_until_complete(
+                self.libjuju.deploy_charm("app", "path", "model", "machine",)
+            )
+
+            self.assertIsNone(application)
+
+            mock_disconnect_controller.assert_called()
+            mock_disconnect_model.assert_called()
+
+    def test_2_units(
+        self,
+        mock_create_machine,
+        mock_wait_for,
+        mock_deploy,
+        mock_machines,
+        mock_applications,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_machines.return_value = {"existing_machine": FakeMachine()}
+        mock_create_machine.return_value = (FakeMachine(), "other")
+        mock_deploy.return_value = FakeApplication()
+        application = self.loop.run_until_complete(
+            self.libjuju.deploy_charm(
+                "app", "path", "model", "existing_machine", num_units=2,
+            )
+        )
+
+        self.assertIsInstance(application, FakeApplication)
+
+        mock_deploy.assert_called_once()
+        mock_wait_for.assert_called_once()
+
+        mock_create_machine.assert_called_once()
+
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+    def test_1_unit(
+        self,
+        mock_create_machine,
+        mock_wait_for,
+        mock_deploy,
+        mock_machines,
+        mock_applications,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock_machines.return_value = {"existing_machine": FakeMachine()}
+        mock_deploy.return_value = FakeApplication()
+        application = self.loop.run_until_complete(
+            self.libjuju.deploy_charm("app", "path", "model", "existing_machine")
+        )
+
+        self.assertIsInstance(application, FakeApplication)
+
+        mock_deploy.assert_called_once()
+        mock_wait_for.assert_called_once()
+
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+
+@asynctest.mock.patch(
+    "juju.model.Model.applications", new_callable=asynctest.PropertyMock
+)
+class GetApplicationTest(LibjujuTestCase):
+    def setUp(self):
+        super(GetApplicationTest, self).setUp()
+
+    def test_existing_application(
+        self, mock_applications,
+    ):
+        mock_applications.return_value = {"existing_app": "exists"}
+        model = juju.model.Model()
+        result = self.libjuju._get_application(model, "existing_app")
+        self.assertEqual(result, "exists")
+
+    def test_non_existing_application(
+        self, mock_applications,
+    ):
+        mock_applications.return_value = {"existing_app": "exists"}
+        model = juju.model.Model()
+        result = self.libjuju._get_application(model, "nonexisting_app")
+        self.assertIsNone(result)
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+@asynctest.mock.patch("n2vc.juju_watcher.JujuModelWatcher.wait_for")
+@asynctest.mock.patch("juju.model.Model.get_action_output")
+@asynctest.mock.patch("juju.model.Model.get_action_status")
+class ExecuteActionTest(LibjujuTestCase):
+    def setUp(self):
+        super(ExecuteActionTest, self).setUp()
+
+    def test_no_application(
+        self,
+        mock_get_action_status,
+        mock_get_action_output,
+        mock_wait_for,
+        mock__get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock__get_application.return_value = None
+        mock_get_model.return_value = juju.model.Model()
+
+        with self.assertRaises(JujuApplicationNotFound):
+            output, status = self.loop.run_until_complete(
+                self.libjuju.execute_action("app", "model", "action",)
+            )
+            self.assertIsNone(output)
+            self.assertIsNone(status)
+
+            mock_disconnect_controller.assert_called()
+            mock_disconnect_model.assert_called()
+
+    def test_no_action(
+        self,
+        mock_get_action_status,
+        mock_get_action_output,
+        mock_wait_for,
+        mock__get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+
+        mock_get_model.return_value = juju.model.Model()
+        mock__get_application.return_value = FakeApplication()
+        with self.assertRaises(JujuActionNotFound):
+            output, status = self.loop.run_until_complete(
+                self.libjuju.execute_action("app", "model", "action",)
+            )
+            self.assertIsNone(output)
+            self.assertIsNone(status)
+
+            mock_disconnect_controller.assert_called()
+            mock_disconnect_model.assert_called()
+
+    # TODO no leader unit found exception
+
+    def test_succesful_exec(
+        self,
+        mock_get_action_status,
+        mock_get_action_output,
+        mock_wait_for,
+        mock__get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        mock__get_application.return_value = FakeApplication()
+        mock_get_action_output.return_value = "output"
+        mock_get_action_status.return_value = {"id": "status"}
+        output, status = self.loop.run_until_complete(
+            self.libjuju.execute_action("app", "model", "existing_action")
+        )
+        self.assertEqual(output, "output")
+        self.assertEqual(status, "status")
+
+        mock_wait_for.assert_called_once()
+
+        mock_disconnect_controller.assert_called()
+        mock_disconnect_model.assert_called()
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+class GetActionTest(LibjujuTestCase):
+    def setUp(self):
+        super(GetActionTest, self).setUp()
+
+    def test_exception(
+        self,
+        mock_get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_application.side_effect = Exception()
+
+        with self.assertRaises(Exception):
+            actions = self.loop.run_until_complete(
+                self.libjuju.get_actions("app", "model")
+            )
+
+            self.assertIsNone(actions)
+            mock_disconnect_controller.assert_called_once()
+            mock_disconnect_model.assert_called_once()
+
+    def test_success(
+        self,
+        mock_get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_application.return_value = FakeApplication()
+
+        actions = self.loop.run_until_complete(self.libjuju.get_actions("app", "model"))
+
+        self.assertEqual(actions, ["existing_action"])
+
+        mock_get_controller.assert_called_once()
+        mock_get_model.assert_called_once()
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("juju.model.Model.add_relation")
+class AddRelationTest(LibjujuTestCase):
+    def setUp(self):
+        super(AddRelationTest, self).setUp()
+
+    @asynctest.mock.patch("logging.Logger.warning")
+    def test_not_found(
+        self,
+        mock_warning,
+        mock_add_relation,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        # TODO in libjuju.py should this fail only with a log message?
+        result = {"error": "not found", "response": "response", "request-id": 1}
+
+        mock_get_model.return_value = juju.model.Model()
+        mock_add_relation.side_effect = JujuAPIError(result)
+
+        self.loop.run_until_complete(
+            self.libjuju.add_relation(
+                "model", "app1", "app2", "relation1", "relation2",
+            )
+        )
+
+        mock_warning.assert_called_with("Relation not found: not found")
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+    @asynctest.mock.patch("logging.Logger.warning")
+    def test_already_exists(
+        self,
+        mock_warning,
+        mock_add_relation,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        # TODO in libjuju.py should this fail silently?
+        result = {"error": "already exists", "response": "response", "request-id": 1}
+
+        mock_get_model.return_value = juju.model.Model()
+        mock_add_relation.side_effect = JujuAPIError(result)
+
+        self.loop.run_until_complete(
+            self.libjuju.add_relation(
+                "model", "app1", "app2", "relation1", "relation2",
+            )
+        )
+
+        mock_warning.assert_called_with("Relation already exists: already exists")
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+    def test_exception(
+        self,
+        mock_add_relation,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+        result = {"error": "", "response": "response", "request-id": 1}
+        mock_add_relation.side_effect = JujuAPIError(result)
+
+        with self.assertRaises(JujuAPIError):
+            self.loop.run_until_complete(
+                self.libjuju.add_relation(
+                    "model", "app1", "app2", "relation1", "relation2",
+                )
+            )
+
+            mock_disconnect_controller.assert_called_once()
+            mock_disconnect_model.assert_called_once()
+
+    def test_success(
+        self,
+        mock_add_relation,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+        mock_get_model.return_value = juju.model.Model()
+
+        self.loop.run_until_complete(
+            self.libjuju.add_relation(
+                "model", "app1", "app2", "relation1", "relation2",
+            )
+        )
+
+        mock_add_relation.assert_called_with(
+            relation1="app1:relation1", relation2="app2:relation2"
+        )
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+
+# TODO destroy_model testcase
+
+
+@asynctest.mock.patch("juju.model.Model.get_machines")
+@asynctest.mock.patch("logging.Logger.debug")
+class DestroyMachineTest(LibjujuTestCase):
+    def setUp(self):
+        super(DestroyMachineTest, self).setUp()
+
+    def test_success(
+        self, mock_debug, mock_get_machines,
+    ):
+        mock_get_machines.side_effect = [
+            {"machine": FakeMachine()},
+            {"machine": FakeMachine()},
+            {},
+        ]
+        self.loop.run_until_complete(
+            self.libjuju.destroy_machine(juju.model.Model(), "machine", 2,)
+        )
+        calls = [
+            asynctest.call("Waiting for machine machine is destroyed"),
+            asynctest.call("Machine destroyed: machine"),
+        ]
+        mock_debug.assert_has_calls(calls)
+
+    def test_no_machine(
+        self, mock_debug, mock_get_machines,
+    ):
+        mock_get_machines.return_value = {}
+        self.loop.run_until_complete(
+            self.libjuju.destroy_machine(juju.model.Model(), "machine", 2,)
+        )
+        mock_debug.assert_called_with("Machine not found: machine")
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+class ConfigureApplicationTest(LibjujuTestCase):
+    def setUp(self):
+        super(ConfigureApplicationTest, self).setUp()
+
+    def test_success(
+        self,
+        mock_get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+
+        mock_get_application.return_value = FakeApplication()
+
+        self.loop.run_until_complete(
+            self.libjuju.configure_application("model", "app", {"config"},)
+        )
+        mock_get_application.assert_called_once()
+        mock_disconnect_controller.assert_called_once()
+        mock_disconnect_model.assert_called_once()
+
+    def test_exception(
+        self,
+        mock_get_application,
+        mock_disconnect_controller,
+        mock_disconnect_model,
+        mock_get_model,
+        mock_get_controller,
+    ):
+
+        mock_get_application.side_effect = Exception()
+
+        with self.assertRaises(Exception):
+            self.loop.run_until_complete(
+                self.libjuju.configure_application("model", "app", {"config"},)
+            )
+            mock_disconnect_controller.assert_called_once()
+            mock_disconnect_model.assert_called_once()
+
+
+# TODO _get_api_endpoints_db test case
+# TODO _update_api_endpoints_db test case
+# TODO healthcheck test case
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("juju.controller.Controller.list_models")
+class ListModelsTest(LibjujuTestCase):
+    def setUp(self):
+        super(ListModelsTest, self).setUp()
+
+    def test_containing(
+        self, mock_list_models, mock_disconnect_controller, mock_get_controller,
+    ):
+        mock_get_controller.return_value = juju.controller.Controller()
+        mock_list_models.return_value = ["existingmodel"]
+        models = self.loop.run_until_complete(self.libjuju.list_models("existing"))
+
+        mock_disconnect_controller.assert_called_once()
+        self.assertEquals(models, ["existingmodel"])
+
+    def test_not_containing(
+        self, mock_list_models, mock_disconnect_controller, mock_get_controller,
+    ):
+        mock_get_controller.return_value = juju.controller.Controller()
+        mock_list_models.return_value = ["existingmodel", "model"]
+        models = self.loop.run_until_complete(self.libjuju.list_models("mdl"))
+
+        mock_disconnect_controller.assert_called_once()
+        self.assertEquals(models, [])
+
+    def test_no_contains_arg(
+        self, mock_list_models, mock_disconnect_controller, mock_get_controller,
+    ):
+        mock_get_controller.return_value = juju.controller.Controller()
+        mock_list_models.return_value = ["existingmodel", "model"]
+        models = self.loop.run_until_complete(self.libjuju.list_models())
+
+        mock_disconnect_controller.assert_called_once()
+        self.assertEquals(models, ["existingmodel", "model"])
diff --git a/n2vc/tests/unit/test_utils.py b/n2vc/tests/unit/test_utils.py
new file mode 100644 (file)
index 0000000..3bab705
--- /dev/null
@@ -0,0 +1,91 @@
+# Copyright 2020 Canonical 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.
+
+from unittest import TestCase
+
+from n2vc.utils import Dict, EntityType, JujuStatusToOSM, N2VCDeploymentStatus, DB_DATA
+from juju.machine import Machine
+from juju.application import Application
+from juju.action import Action
+from juju.unit import Unit
+
+
+class UtilsTest(TestCase):
+    def test_dict(self):
+        example = Dict({"key": "value"})
+        self.assertEqual(example["key"], example.key)
+
+    def test_entity_type(self):
+        self.assertFalse(EntityType.has_value("machine2"))
+        values = [Machine, Application, Action, Unit]
+        for value in values:
+            self.assertTrue(EntityType.has_value(value))
+
+        self.assertEqual(EntityType.MACHINE, EntityType.get_entity(Machine))
+        self.assertEqual(EntityType.APPLICATION, EntityType.get_entity(Application))
+        self.assertEqual(EntityType.UNIT, EntityType.get_entity(Unit))
+        self.assertEqual(EntityType.ACTION, EntityType.get_entity(Action))
+
+    def test_juju_status_to_osm(self):
+        tests = [
+            {
+                "entity_type": EntityType.MACHINE,
+                "status": [
+                    {"juju": "pending", "osm": N2VCDeploymentStatus.PENDING},
+                    {"juju": "started", "osm": N2VCDeploymentStatus.COMPLETED},
+                ],
+            },
+            {
+                "entity_type": EntityType.APPLICATION,
+                "status": [
+                    {"juju": "waiting", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "maintenance", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "blocked", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "error", "osm": N2VCDeploymentStatus.FAILED},
+                    {"juju": "active", "osm": N2VCDeploymentStatus.COMPLETED},
+                ],
+            },
+            {
+                "entity_type": EntityType.UNIT,
+                "status": [
+                    {"juju": "waiting", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "maintenance", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "blocked", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "error", "osm": N2VCDeploymentStatus.FAILED},
+                    {"juju": "active", "osm": N2VCDeploymentStatus.COMPLETED},
+                ],
+            },
+            {
+                "entity_type": EntityType.ACTION,
+                "status": [
+                    {"juju": "running", "osm": N2VCDeploymentStatus.RUNNING},
+                    {"juju": "completed", "osm": N2VCDeploymentStatus.COMPLETED},
+                ],
+            },
+        ]
+
+        for test in tests:
+            entity_type = test["entity_type"]
+            self.assertTrue(entity_type in JujuStatusToOSM)
+
+            for status in test["status"]:
+                juju_status = status["juju"]
+                osm_status = status["osm"]
+                self.assertTrue(juju_status in JujuStatusToOSM[entity_type])
+                self.assertEqual(osm_status, JujuStatusToOSM[entity_type][juju_status])
+
+    def test_db_data(self):
+        self.assertEqual(DB_DATA.api_endpoints.table, "admin")
+        self.assertEqual(DB_DATA.api_endpoints.filter, {"_id": "juju"})
+        self.assertEqual(DB_DATA.api_endpoints.key, "api_endpoints")
diff --git a/n2vc/tests/unit/utils.py b/n2vc/tests/unit/utils.py
new file mode 100644 (file)
index 0000000..fe7362e
--- /dev/null
@@ -0,0 +1,456 @@
+# Copyright 2020 Canonical 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.
+
+import asyncio
+
+from n2vc.utils import Dict, EntityType, N2VCDeploymentStatus
+from n2vc.n2vc_conn import N2VCConnector
+from unittest.mock import MagicMock
+
+
+async def AsyncMockFunc():
+    await asyncio.sleep(1)
+
+
+class AsyncMock(MagicMock):
+    async def __call__(self, *args, **kwargs):
+        return super(AsyncMock, self).__call__(*args, **kwargs)
+
+
+class FakeN2VC(MagicMock):
+    last_written_values = None
+
+    async def write_app_status_to_db(
+        self,
+        db_dict: dict,
+        status: N2VCDeploymentStatus,
+        detailed_status: str,
+        vca_status: str,
+        entity_type: str,
+    ):
+        self.last_written_values = Dict(
+            {
+                "n2vc_status": status,
+                "message": detailed_status,
+                "vca_status": vca_status,
+                "entity": entity_type,
+            }
+        )
+
+    osm_status = N2VCConnector.osm_status
+
+
+class FakeMachine(MagicMock):
+    entity_id = "2"
+    dns_name = "FAKE ENDPOINT"
+    model_name = "FAKE MODEL"
+    entity_type = EntityType.MACHINE
+
+    async def destroy(self, force):
+        pass
+
+
+class FakeWatcher(AsyncMock):
+
+    delta_to_return = None
+
+    async def Next(self):
+        return Dict({"deltas": self.delta_to_return})
+
+
+class FakeConnection(MagicMock):
+    endpoint = None
+    is_open = False
+
+
+class FakeAction(MagicMock):
+    entity_id = "id"
+    status = "ready"
+
+
+class FakeUnit(MagicMock):
+    async def is_leader_from_status(self):
+        return True
+
+    async def run_action(self, action_name):
+        return FakeAction()
+
+
+class FakeApplication(AsyncMock):
+
+    async def set_config(self, config):
+        pass
+
+    async def add_unit(self, to):
+        pass
+
+    async def get_actions(self):
+        return ["existing_action"]
+
+    units = [FakeUnit(), FakeUnit()]
+
+
+FAKE_DELTA_MACHINE_PENDING = Dict(
+    {
+        "deltas": ["machine", "change", {}],
+        "entity": "machine",
+        "type": "change",
+        "data": {
+            "id": "2",
+            "instance-id": "juju-1b5808-2",
+            "agent-status": {"current": "pending", "message": "", "version": ""},
+            "instance-status": {"current": "running", "message": "Running"},
+        },
+    }
+)
+FAKE_DELTA_MACHINE_STARTED = Dict(
+    {
+        "deltas": ["machine", "change", {}],
+        "entity": "machine",
+        "type": "change",
+        "data": {
+            "id": "2",
+            "instance-id": "juju-1b5808-2",
+            "agent-status": {"current": "started", "message": "", "version": ""},
+            "instance-status": {"current": "running", "message": "Running"},
+        },
+    }
+)
+
+FAKE_DELTA_UNIT_PENDING = Dict(
+    {
+        "deltas": ["unit", "change", {}],
+        "entity": "unit",
+        "type": "change",
+        "data": {
+            "name": "git/0",
+            "application": "git",
+            "machine-id": "6",
+            "workload-status": {"current": "waiting", "message": ""},
+            "agent-status": {"current": "idle", "message": ""},
+        },
+    }
+)
+
+FAKE_DELTA_UNIT_STARTED = Dict(
+    {
+        "deltas": ["unit", "change", {}],
+        "entity": "unit",
+        "type": "change",
+        "data": {
+            "name": "git/0",
+            "application": "git",
+            "machine-id": "6",
+            "workload-status": {"current": "active", "message": ""},
+            "agent-status": {"current": "idle", "message": ""},
+        },
+    }
+)
+
+FAKE_DELTA_APPLICATION_MAINTENANCE = Dict(
+    {
+        "deltas": ["application", "change", {}],
+        "entity": "application",
+        "type": "change",
+        "data": {
+            "name": "git",
+            "status": {
+                "current": "maintenance",
+                "message": "installing charm software",
+            },
+        },
+    }
+)
+
+FAKE_DELTA_APPLICATION_ACTIVE = Dict(
+    {
+        "deltas": ["application", "change", {}],
+        "entity": "application",
+        "type": "change",
+        "data": {"name": "git", "status": {"current": "active", "message": "Ready!"}},
+    }
+)
+
+FAKE_DELTA_ACTION_COMPLETED = Dict(
+    {
+        "deltas": ["action", "change", {}],
+        "entity": "action",
+        "type": "change",
+        "data": {
+            "model-uuid": "af19cdd4-374a-4d9f-86b1-bfed7b1b5808",
+            "id": "1",
+            "receiver": "git/0",
+            "name": "add-repo",
+            "status": "completed",
+            "message": "",
+        },
+    }
+)
+
+Deltas = [
+    Dict(
+        {
+            "entity": Dict({"id": "2", "type": EntityType.MACHINE}),
+            "filter": Dict({"entity_id": "2", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_MACHINE_PENDING,
+            "entity_status": Dict(
+                {"status": "pending", "message": "Running", "vca_status": "running"}
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "Running",
+                            "entity": "machine",
+                            "vca_status": "running",
+                            "n2vc_status": N2VCDeploymentStatus.PENDING,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "2", "type": EntityType.MACHINE}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_MACHINE_PENDING,
+            "entity_status": Dict(
+                {"status": "pending", "message": "Running", "vca_status": "running"}
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "2", "type": EntityType.MACHINE}),
+            "filter": Dict({"entity_id": "2", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_MACHINE_STARTED,
+            "entity_status": Dict(
+                {"status": "started", "message": "Running", "vca_status": "running"}
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "Running",
+                            "entity": "machine",
+                            "vca_status": "running",
+                            "n2vc_status": N2VCDeploymentStatus.COMPLETED,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "2", "type": EntityType.MACHINE}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_MACHINE_STARTED,
+            "entity_status": Dict(
+                {"status": "started", "message": "Running", "vca_status": "running"}
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git/0", "type": EntityType.UNIT}),
+            "filter": Dict({"entity_id": "git", "entity_type": EntityType.APPLICATION}),
+            "delta": FAKE_DELTA_UNIT_PENDING,
+            "entity_status": Dict(
+                {"status": "waiting", "message": "", "vca_status": "waiting"}
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "",
+                            "entity": "unit",
+                            "vca_status": "waiting",
+                            "n2vc_status": N2VCDeploymentStatus.RUNNING,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git/0", "type": EntityType.UNIT}),
+            "filter": Dict({"entity_id": "2", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_UNIT_PENDING,
+            "entity_status": Dict(
+                {"status": "waiting", "message": "", "vca_status": "waiting"}
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git/0", "type": EntityType.UNIT}),
+            "filter": Dict({"entity_id": "git", "entity_type": EntityType.APPLICATION}),
+            "delta": FAKE_DELTA_UNIT_STARTED,
+            "entity_status": Dict(
+                {"status": "active", "message": "", "vca_status": "active"}
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "",
+                            "entity": "unit",
+                            "vca_status": "active",
+                            "n2vc_status": N2VCDeploymentStatus.COMPLETED,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git/0", "type": EntityType.UNIT}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.ACTION}),
+            "delta": FAKE_DELTA_UNIT_STARTED,
+            "entity_status": Dict(
+                {"status": "active", "message": "", "vca_status": "active"}
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git", "type": EntityType.APPLICATION}),
+            "filter": Dict({"entity_id": "git", "entity_type": EntityType.APPLICATION}),
+            "delta": FAKE_DELTA_APPLICATION_MAINTENANCE,
+            "entity_status": Dict(
+                {
+                    "status": "maintenance",
+                    "message": "installing charm software",
+                    "vca_status": "maintenance",
+                }
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "installing charm software",
+                            "entity": "application",
+                            "vca_status": "maintenance",
+                            "n2vc_status": N2VCDeploymentStatus.RUNNING,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git", "type": EntityType.APPLICATION}),
+            "filter": Dict({"entity_id": "2", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_APPLICATION_MAINTENANCE,
+            "entity_status": Dict(
+                {
+                    "status": "maintenance",
+                    "message": "installing charm software",
+                    "vca_status": "maintenance",
+                }
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git", "type": EntityType.APPLICATION}),
+            "filter": Dict({"entity_id": "git", "entity_type": EntityType.APPLICATION}),
+            "delta": FAKE_DELTA_APPLICATION_ACTIVE,
+            "entity_status": Dict(
+                {"status": "active", "message": "Ready!", "vca_status": "active"}
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "Ready!",
+                            "entity": "application",
+                            "vca_status": "active",
+                            "n2vc_status": N2VCDeploymentStatus.COMPLETED,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git", "type": EntityType.APPLICATION}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.ACTION}),
+            "delta": FAKE_DELTA_APPLICATION_ACTIVE,
+            "entity_status": Dict(
+                {"status": "active", "message": "Ready!", "vca_status": "active"}
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "1", "type": EntityType.ACTION}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.ACTION}),
+            "delta": FAKE_DELTA_ACTION_COMPLETED,
+            "entity_status": Dict(
+                {
+                    "status": "completed",
+                    "message": "completed",
+                    "vca_status": "completed",
+                }
+            ),
+            "db": Dict(
+                {
+                    "written": True,
+                    "data": Dict(
+                        {
+                            "message": "completed",
+                            "entity": "action",
+                            "vca_status": "completed",
+                            "n2vc_status": N2VCDeploymentStatus.COMPLETED,
+                        }
+                    ),
+                }
+            ),
+        }
+    ),
+    Dict(
+        {
+            "entity": Dict({"id": "git", "type": EntityType.ACTION}),
+            "filter": Dict({"entity_id": "1", "entity_type": EntityType.MACHINE}),
+            "delta": FAKE_DELTA_ACTION_COMPLETED,
+            "entity_status": Dict(
+                {
+                    "status": "completed",
+                    "message": "completed",
+                    "vca_status": "completed",
+                }
+            ),
+            "db": Dict({"written": False, "data": None}),
+        }
+    ),
+]
diff --git a/tox.ini b/tox.ini
index bc316e1..a0f0503 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -27,9 +27,9 @@ deps =
 commands =
   coverage erase
   nose2 -C --coverage n2vc --plugin nose2.plugins.junitxml -s n2vc
-  coverage report --omit='*tests*'
-  coverage html -d ./cover --omit='*tests*'
-  coverage xml -o coverage.xml --omit='*tests*'
+  coverage report --omit='*n2vc/tests*'
+  coverage html -d ./cover --omit='*n2vc/tests*'
+  coverage xml -o coverage.xml --omit='*n2vc/tests*'
 
 [testenv:pylint]
 basepython = python3