diff --git a/coriolis/tests/osmorphing/__init__.py b/coriolis/tests/osmorphing/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/coriolis/tests/osmorphing/osdetect/__init__.py b/coriolis/tests/osmorphing/osdetect/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/coriolis/tests/osmorphing/osdetect/test_amazon.py b/coriolis/tests/osmorphing/osdetect/test_amazon.py
new file mode 100644
index 00000000..87730419
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_amazon.py
@@ -0,0 +1,38 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import amazon
+from coriolis.osmorphing.osdetect import base
+from coriolis.tests import test_base
+
+
+class AmazonLinuxOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for AmazonLinuxOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_get_os_release')
+    def test_detect_os(self, mock_get_os_release):
+        mock_get_os_release.return_value = {
+            "ID": "amzn",
+            "VERSION": mock.sentinel.version,
+            "NAME": "Amazon Linux"
+        }
+
+        expected_info = {
+            "os_type": amazon.constants.OS_TYPE_LINUX,
+            "distribution_name": amazon.AMAZON_DISTRO_NAME,
+            "release_version": mock.sentinel.version,
+            "friendly_release_name": "Amazon Linux %s" % mock.sentinel.version
+        }
+
+        amazon_os_detect_tools = amazon.AmazonLinuxOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = amazon_os_detect_tools.detect_os()
+
+        mock_get_os_release.assert_called_once_with()
+
+        self.assertEqual(result, expected_info)
diff --git a/coriolis/tests/osmorphing/osdetect/test_base.py b/coriolis/tests/osmorphing/osdetect/test_base.py
new file mode 100644
index 00000000..df1a3675
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_base.py
@@ -0,0 +1,172 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import os
+from unittest import mock
+
+from coriolis import exception
+from coriolis.osmorphing.osdetect import base
+from coriolis.tests import test_base
+
+
+class BaseOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the BaseOSDetectTools class."""
+
+    @mock.patch.object(base.BaseOSDetectTools, '__abstractmethods__', set())
+    def setUp(self):
+        super(BaseOSDetectToolsTestCase, self).setUp()
+        self.base_os_detect_tools = base.BaseOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    def test_returned_detected_os_info_fields(self):
+        self.assertRaises(
+            NotImplementedError,
+            self.base_os_detect_tools.returned_detected_os_info_fields
+        )
+
+    def test_detect_os(self):
+        self.assertRaises(
+            NotImplementedError,
+            self.base_os_detect_tools.detect_os
+        )
+
+    def test_set_environment(self):
+        self.base_os_detect_tools.set_environment(mock.sentinel.environment)
+
+        self.assertEqual(
+            self.base_os_detect_tools._environment, mock.sentinel.environment
+        )
+
+
+class BaseLinuxOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the BaseLinuxOSDetectTools class."""
+
+    @mock.patch.object(
+        base.BaseLinuxOSDetectTools, '__abstractmethods__', set()
+    )
+    def setUp(self):
+        super(BaseLinuxOSDetectToolsTestCase, self).setUp()
+        self.chroot_path = '/mock/chroot/path'
+        self.os_root_dir = '/mock/os/root/dir'
+        self.base_os_detect = base.BaseLinuxOSDetectTools(
+            mock.sentinel.conn, self.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    def test_returned_detected_os_info_fields(self):
+        result = self.base_os_detect.returned_detected_os_info_fields()
+
+        self.assertEqual(
+            result, base.REQUIRED_DETECTED_OS_FIELDS
+        )
+
+    @mock.patch.object(base.utils, 'read_ssh_file')
+    def test__read_file(self, mock_read_ssh_file):
+        result = self.base_os_detect._read_file(self.chroot_path)
+
+        mocked_full_path = os.path.join(
+            self.base_os_detect._os_root_dir, self.chroot_path)
+
+        mock_read_ssh_file.assert_called_once_with(
+            self.base_os_detect._conn, mocked_full_path)
+
+        self.assertEqual(result, mock_read_ssh_file.return_value)
+
+    @mock.patch.object(base.utils, 'read_ssh_ini_config_file')
+    def test__read_config_file(self, mock_read_ssh_ini_config):
+        result = self.base_os_detect._read_config_file(self.chroot_path)
+
+        mocked_full_path = os.path.join(
+            self.base_os_detect._os_root_dir, self.chroot_path)
+
+        mock_read_ssh_ini_config.assert_called_once_with(
+            self.base_os_detect._conn, mocked_full_path, check_exists=False)
+
+        self.assertEqual(result, mock_read_ssh_ini_config.return_value)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    def test__get_os_release(self, mock_read_config_file):
+        result = self.base_os_detect._get_os_release()
+
+        mock_read_config_file.assert_called_once_with(
+            "etc/os-release", check_exists=True)
+
+        self.assertEqual(result, mock_read_config_file.return_value)
+
+    @mock.patch.object(base.utils, 'test_ssh_path')
+    def test__test_path(self, mock_test_ssh_path):
+        result = self.base_os_detect._test_path(self.chroot_path)
+
+        mocked_full_path = os.path.join(
+            self.base_os_detect._os_root_dir, self.chroot_path)
+        mock_test_ssh_path.assert_called_once_with(
+            self.base_os_detect._conn, mocked_full_path)
+
+        self.assertEqual(result, mock_test_ssh_path.return_value)
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd')
+    def test__exec_cmd(self, mock_exec_ssh_cmd):
+        result = self.base_os_detect._exec_cmd(mock.sentinel.cmd, timeout=120)
+
+        mock_exec_ssh_cmd.assert_called_once_with(
+            self.base_os_detect._conn, mock.sentinel.cmd,
+            environment=self.base_os_detect._environment, get_pty=True,
+            timeout=120)
+
+        self.assertEqual(result, mock_exec_ssh_cmd.return_value)
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd')
+    def test__exec_cmd_without_timeout(self, mock_exec_ssh_cmd):
+        result = self.base_os_detect._exec_cmd(mock.sentinel.cmd)
+
+        mock_exec_ssh_cmd.assert_called_once_with(
+            self.base_os_detect._conn, mock.sentinel.cmd,
+            environment=self.base_os_detect._environment, get_pty=True,
+            timeout=self.base_os_detect._osdetect_operation_timeout)
+
+        self.assertEqual(result, mock_exec_ssh_cmd.return_value)
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd')
+    def test__exec_cmd_with_exception(self, mock_exec_ssh_cmd):
+        mock_exec_ssh_cmd.side_effect = exception.MinionMachineCommandTimeout
+
+        self.assertRaises(
+            exception.OSMorphingSSHOperationTimeout,
+            self.base_os_detect._exec_cmd,
+            mock.sentinel.cmd
+        )
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd_chroot')
+    def test__exec_cmd_chroot(self, mock_exec_ssh_cmd_chroot):
+        result = self.base_os_detect._exec_cmd_chroot(
+            mock.sentinel.cmd, timeout=120)
+
+        mock_exec_ssh_cmd_chroot.assert_called_once_with(
+            self.base_os_detect._conn, self.base_os_detect._os_root_dir,
+            mock.sentinel.cmd, environment=self.base_os_detect._environment,
+            get_pty=True, timeout=120)
+
+        self.assertEqual(result, mock_exec_ssh_cmd_chroot.return_value)
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd_chroot')
+    def test__exec_cmd_chroot_without_timeout(self, mock_exec_ssh_cmd_chroot):
+        result = self.base_os_detect._exec_cmd_chroot(mock.sentinel.cmd)
+
+        mock_exec_ssh_cmd_chroot.assert_called_once_with(
+            self.base_os_detect._conn, self.base_os_detect._os_root_dir,
+            mock.sentinel.cmd, environment=self.base_os_detect._environment,
+            get_pty=True,
+            timeout=self.base_os_detect._osdetect_operation_timeout)
+
+        self.assertEqual(result, mock_exec_ssh_cmd_chroot.return_value)
+
+    @mock.patch.object(base.utils, 'exec_ssh_cmd_chroot')
+    def test__exec_cmd_chroot_with_exception(self, mock_exec_ssh_cmd_chroot):
+        mock_exec_ssh_cmd_chroot.side_effect = [
+            exception.MinionMachineCommandTimeout]
+
+        self.assertRaises(
+            exception.OSMorphingSSHOperationTimeout,
+            self.base_os_detect._exec_cmd_chroot,
+            mock.sentinel.cmd
+        )
diff --git a/coriolis/tests/osmorphing/osdetect/test_centos.py b/coriolis/tests/osmorphing/osdetect/test_centos.py
new file mode 100644
index 00000000..8060c1bc
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_centos.py
@@ -0,0 +1,76 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import logging
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import centos
+from coriolis.tests import test_base
+
+
+class CentOSOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the CentOSOSDetectTools class."""
+
+    def setUp(self):
+        super(CentOSOSDetectToolsTestCase, self).setUp()
+        self.centos_os_detect_tools = centos.CentOSOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"CentOS Linux release 7.9 (Core)"
+
+        expected_info = {
+            "os_type": centos.constants.OS_TYPE_LINUX,
+            "distribution_name": centos.CENTOS_DISTRO_IDENTIFIER,
+            "release_version": '7.9',
+            "friendly_release_name": "%s Version %s" % (
+                centos.CENTOS_DISTRO_IDENTIFIER, '7.9')
+        }
+
+        result = self.centos_os_detect_tools.detect_os()
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os_centos_stream(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"CentOS Stream release 8.3"
+
+        expected_info = {
+            "os_type": centos.constants.OS_TYPE_LINUX,
+            "distribution_name": centos.CENTOS_STREAM_DISTRO_IDENTIFIER,
+            "release_version": '8.3',
+            "friendly_release_name": "%s Version %s" % (
+                centos.CENTOS_STREAM_DISTRO_IDENTIFIER, '8.3')
+        }
+
+        result = self.centos_os_detect_tools.detect_os()
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os_not_centos(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"dummy release 8.3"
+
+        with self.assertLogs('coriolis.osmorphing.osdetect.centos',
+                             level=logging.DEBUG):
+            result = self.centos_os_detect_tools.detect_os()
+
+            self.assertEqual(result, {})
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
diff --git a/coriolis/tests/osmorphing/osdetect/test_coreos.py b/coriolis/tests/osmorphing/osdetect/test_coreos.py
new file mode 100644
index 00000000..935762dc
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_coreos.py
@@ -0,0 +1,36 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import coreos
+from coriolis.tests import test_base
+
+
+class CoreOSOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the CoreOSOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_get_os_release')
+    def test_detect_os(self, mock_get_os_release):
+        mock_get_os_release.return_value = {
+            "ID": "coreos",
+            "VERSION_ID": mock.sentinel.version
+        }
+
+        expected_info = {
+            "os_type": coreos.constants.OS_TYPE_LINUX,
+            "distribution_name": coreos.COREOS_DISTRO_IDENTIFIER,
+            "release_version": mock.sentinel.version,
+            "friendly_release_name": "CoreOS Linux %s" % mock.sentinel.version
+        }
+
+        coreos_os_detect_tools = coreos.CoreOSOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = coreos_os_detect_tools.detect_os()
+
+        mock_get_os_release.assert_called_once_with()
+
+        self.assertEqual(result, expected_info)
diff --git a/coriolis/tests/osmorphing/osdetect/test_debian.py b/coriolis/tests/osmorphing/osdetect/test_debian.py
new file mode 100644
index 00000000..c1f42fcf
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_debian.py
@@ -0,0 +1,88 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import debian
+from coriolis.tests import test_base
+
+
+class DebianOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the DebianOSDetectTools class."""
+
+    def setUp(self):
+        super(DebianOSDetectToolsTestCase, self).setUp()
+        self.debian_os_detect_tools = debian.DebianOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os(self, mock_read_file, mock_read_config_file,
+                       mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_config_file.return_value = {
+            'DISTRIB_ID': 'Debian',
+            'DISTRIB_RELEASE': '10'
+        }
+
+        expected_info = {
+            "os_type": debian.constants.OS_TYPE_LINUX,
+            "distribution_name": debian.DEBIAN_DISTRO_IDENTIFIER,
+            "release_version": '10',
+            "friendly_release_name": "Debian Linux 10"
+        }
+
+        result = self.debian_os_detect_tools.detect_os()
+
+        mock_test_path.assert_called_once_with("etc/lsb-release")
+        mock_read_config_file.assert_called_once_with("etc/lsb-release")
+        mock_read_file.assert_not_called()
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os_debian_version(
+            self, mock_read_file, mock_read_config_file, mock_test_path):
+        mock_test_path.side_effect = [False, True]
+        mock_read_file.return_value = b"10\n"
+
+        expected_info = {
+            "os_type": debian.constants.OS_TYPE_LINUX,
+            "distribution_name": debian.DEBIAN_DISTRO_IDENTIFIER,
+            "release_version": '10',
+            "friendly_release_name": "Debian Linux 10"
+        }
+
+        result = self.debian_os_detect_tools.detect_os()
+
+        mock_read_config_file.assert_not_called()
+        mock_test_path.assert_has_calls([
+            mock.call("etc/lsb-release"),
+            mock.call("etc/debian_version")
+        ])
+        mock_read_file.assert_called_once_with("etc/debian_version")
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    def test_detect_os_no_release(self, mock_test_path, mock_read_file,
+                                  mock_read_config_file):
+        mock_test_path.return_value = False
+
+        result = self.debian_os_detect_tools.detect_os()
+
+        self.assertEqual(result, {})
+
+        mock_test_path.assert_has_calls([
+            mock.call("etc/lsb-release"),
+            mock.call("etc/debian_version")
+        ])
+        mock_read_file.assert_not_called()
+        mock_read_config_file.assert_not_called()
diff --git a/coriolis/tests/osmorphing/osdetect/test_manager.py b/coriolis/tests/osmorphing/osdetect/test_manager.py
new file mode 100644
index 00000000..c8cdc22a
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_manager.py
@@ -0,0 +1,205 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis import exception
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import manager
+from coriolis.tests import test_base
+
+
+class MockOSDetectTool(base.BaseOSDetectTools):
+    def returned_detected_os_info_fields(self):
+        return ['os_type', 'os_version']
+
+    def detect_os(self):
+        return {
+            'os_type': 'mock_os_type',
+            'os_version': 'mock_os_version'
+        }
+
+
+class MockOSDetectToolNoInfo(base.BaseOSDetectTools):
+    def returned_detected_os_info_fields(cls):
+        pass
+
+    def detect_os(self):
+        pass
+
+
+class MockOSDetectToolMissingFields(MockOSDetectTool):
+    def returned_detected_os_info_fields(self):
+        return ['os_type', 'os_version', 'extra_field']
+
+    def detect_os(self):
+        return {
+            'os_type': 'mock_os_type',
+            'os_version': 'mock_os_version'
+        }
+
+
+class MockOSDetectToolExtraFields(MockOSDetectTool):
+    def returned_detected_os_info_fields(self):
+        return ['os_type', 'os_version']
+
+    def detect_os(self):
+        return {
+            'os_type': 'mock_os_type',
+            'os_version': 'mock_os_version',
+            'extra_field': 'extra_value'
+        }
+
+
+class ManagerTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the Coriolis manager module."""
+
+    def setUp(self):
+        super(ManagerTestCase, self).setUp()
+        self.os_type = "linux"
+        self.os_root_dir = "/root"
+        self.conn = mock.MagicMock()
+
+    def test__check_custom_os_detect_tools(self):
+        # Create a mock object that is an instance of BaseOSDetectTools
+        mock_os_detect_tool = mock.MagicMock(spec=base.BaseOSDetectTools)
+
+        result = manager._check_custom_os_detect_tools([mock_os_detect_tool])
+
+        self.assertTrue(result)
+
+    def test_check_custom_os_detect_tools_not_list(self):
+        self.assertRaises(exception.InvalidCustomOSDetectTools,
+                          manager._check_custom_os_detect_tools, "not a list")
+
+    def test_check_custom_os_detect_tools_invalid_type(self):
+        self.assertRaises(exception.InvalidCustomOSDetectTools,
+                          manager._check_custom_os_detect_tools, [object()])
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    def test_detect_os_custom_tools(self, mock_check_custom_tools):
+        mock_os_detect_tool = MockOSDetectTool
+
+        result = manager.detect_os(
+            self.conn, self.os_type, self.os_root_dir,
+            mock.sentinel.operation_timeout,
+            tools_environment=mock.sentinel.tools_environment,
+            custom_os_detect_tools=[mock_os_detect_tool])
+
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
+
+        expected_result = {
+            'os_type': 'mock_os_type',
+            'os_version': 'mock_os_version'
+        }
+
+        self.assertEqual(result, expected_result)
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    def test_detect_os_windows(self, mock_check_custom_tools):
+        mock_os_detect_tool = MockOSDetectTool
+        self.os_type = 'windows'
+
+        result = manager.detect_os(
+            self.conn, self.os_type, self.os_root_dir,
+            mock.sentinel.operation_timeout,
+            custom_os_detect_tools=[mock_os_detect_tool])
+
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
+        expected_result = {
+            'os_type': 'mock_os_type',
+            'os_version': 'mock_os_version'
+        }
+
+        self.assertEqual(result, expected_result)
+
+    def test_detect_os_invalid_os_type(self):
+        self.os_type = 'invalid_os_type'
+
+        self.assertRaises(exception.OSDetectToolsNotFound, manager.detect_os,
+                          self.conn, self.os_type, self.os_root_dir,
+                          mock.sentinel.operation_timeout)
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    @mock.patch.object(manager.rocky.RockyLinuxOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.redhat.RedHatOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.centos.CentOSOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.oracle.OracleOSDetectTools, 'detect_os')
+    def test_detect_os_no_detected_info(
+            self, mock_oracle_detect_os, mock_centos_detect_os,
+            mock_redhat_detect_os, mock_rocky_detect_os,
+            mock_check_custom_tools):
+        mock_rocky_detect_os.return_value = None
+        mock_redhat_detect_os.return_value = None
+        mock_centos_detect_os.return_value = None
+        mock_oracle_detect_os.return_value = None
+        mock_os_detect_tool = MockOSDetectToolNoInfo
+
+        self.assertRaises(exception.OSDetectToolsNotFound, manager.detect_os,
+                          self.conn, self.os_type, self.os_root_dir,
+                          mock.sentinel.operation_timeout,
+                          custom_os_detect_tools=[mock_os_detect_tool])
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    @mock.patch.object(manager.rocky.RockyLinuxOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.redhat.RedHatOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.centos.CentOSOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.oracle.OracleOSDetectTools, 'detect_os')
+    def test_detect_os_invalid_detected_info(
+            self, mock_oracle_detect_os, mock_centos_detect_os,
+            mock_redhat_detect_os, mock_rocky_detect_os,
+            mock_check_custom_tools):
+        mock_rocky_detect_os.return_value = None
+        mock_redhat_detect_os.return_value = None
+        mock_centos_detect_os.return_value = None
+        mock_oracle_detect_os.return_value = "invalid_detected_info"
+        mock_os_detect_tool = MockOSDetectToolNoInfo
+
+        self.assertRaises(exception.InvalidDetectedOSParams, manager.detect_os,
+                          self.conn, self.os_type, self.os_root_dir,
+                          mock.sentinel.operation_timeout,
+                          custom_os_detect_tools=[mock_os_detect_tool])
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    @mock.patch.object(manager.rocky.RockyLinuxOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.redhat.RedHatOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.centos.CentOSOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.oracle.OracleOSDetectTools, 'detect_os')
+    def test_detect_os_missing_detected_info_fields(
+            self, mock_oracle_detect_os, mock_centos_detect_os,
+            mock_redhat_detect_os, mock_rocky_detect_os,
+            mock_check_custom_tools):
+        mock_rocky_detect_os.return_value = None
+        mock_redhat_detect_os.return_value = None
+        mock_centos_detect_os.return_value = None
+        mock_oracle_detect_os.return_value = None
+        mock_os_detect_tool = MockOSDetectToolMissingFields
+
+        self.assertRaises(exception.InvalidDetectedOSParams, manager.detect_os,
+                          self.conn, self.os_type, self.os_root_dir,
+                          mock.sentinel.operation_timeout,
+                          custom_os_detect_tools=[mock_os_detect_tool])
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
+
+    @mock.patch.object(manager, '_check_custom_os_detect_tools')
+    @mock.patch.object(manager.rocky.RockyLinuxOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.redhat.RedHatOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.centos.CentOSOSDetectTools, 'detect_os')
+    @mock.patch.object(manager.oracle.OracleOSDetectTools, 'detect_os')
+    def test_detect_os_extra_detected_info_fields(
+            self, mock_oracle_detect_os, mock_centos_detect_os,
+            mock_redhat_detect_os, mock_rocky_detect_os,
+            mock_check_custom_tools):
+        mock_rocky_detect_os.return_value = None
+        mock_redhat_detect_os.return_value = None
+        mock_centos_detect_os.return_value = None
+        mock_oracle_detect_os.return_value = None
+        mock_os_detect_tool = MockOSDetectToolExtraFields
+
+        self.assertRaises(exception.InvalidDetectedOSParams, manager.detect_os,
+                          self.conn, self.os_type, self.os_root_dir,
+                          mock.sentinel.operation_timeout,
+                          custom_os_detect_tools=[mock_os_detect_tool])
+        mock_check_custom_tools.assert_called_once_with([mock_os_detect_tool])
diff --git a/coriolis/tests/osmorphing/osdetect/test_openwrt.py b/coriolis/tests/osmorphing/osdetect/test_openwrt.py
new file mode 100644
index 00000000..788eda3e
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_openwrt.py
@@ -0,0 +1,39 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import openwrt
+from coriolis.tests import test_base
+
+
+class OpenWRTOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for OpenWRTOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    def test_detect_os(self, mock_read_config_file):
+        mock_read_config_file.return_value = {
+            "DISTRIB_ID": "OpenWrt",
+            "DISTRIB_DESCRIPTION": "OpenWrt Description",
+            "DISTRIB_RELEASE": mock.sentinel.version
+        }
+
+        expected_info = {
+            "os_type": openwrt.constants.OS_TYPE_LINUX,
+            "distribution_name": openwrt.OPENWRT_DISTRO_IDENTIFIER,
+            "release_version": mock.sentinel.version,
+            "friendly_release_name": "OpenWrt Description Version %s" % (
+                mock.sentinel.version)
+        }
+
+        openwrt_os_detect_tools = openwrt.OpenWRTOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = openwrt_os_detect_tools.detect_os()
+
+        mock_read_config_file.assert_called_once_with(
+            "etc/openwrt_release", check_exists=True)
+
+        self.assertEqual(result, expected_info)
diff --git a/coriolis/tests/osmorphing/osdetect/test_oracle.py b/coriolis/tests/osmorphing/osdetect/test_oracle.py
new file mode 100644
index 00000000..9386057a
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_oracle.py
@@ -0,0 +1,35 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import oracle
+from coriolis.tests import test_base
+
+
+class OracleOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the OracleOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"Oracle Linux release 8.4"
+
+        expected_info = {
+            "os_type": oracle.constants.OS_TYPE_LINUX,
+            "distribution_name": oracle.ORACLE_DISTRO_IDENTIFIER,
+            "release_version": '8.4',
+            "friendly_release_name": "Oracle Linux Version 8.4"
+        }
+
+        oracle_os_detect_tools = oracle.OracleOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = oracle_os_detect_tools.detect_os()
+        mock_test_path.assert_called_once_with("etc/oracle-release")
+        mock_read_file.assert_called_once_with("etc/oracle-release")
+
+        self.assertEqual(result, expected_info)
diff --git a/coriolis/tests/osmorphing/osdetect/test_redhat.py b/coriolis/tests/osmorphing/osdetect/test_redhat.py
new file mode 100644
index 00000000..350aaa6a
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_redhat.py
@@ -0,0 +1,56 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import logging
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import redhat
+from coriolis.tests import test_base
+
+
+class RedHatOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the RedHatOSDetectTools class."""
+
+    def setUp(self):
+        super(RedHatOSDetectToolsTestCase, self).setUp()
+        self.redhat_os_detect_tools = redhat.RedHatOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = (
+            b"Red Hat Enterprise Linux release 8.4 (Ootpa)")
+
+        expected_info = {
+            "os_type": redhat.constants.OS_TYPE_LINUX,
+            "distribution_name": redhat.RED_HAT_DISTRO_IDENTIFIER,
+            "release_version": '8.4',
+            "friendly_release_name": "%s Version %s" % (
+                redhat.RED_HAT_DISTRO_IDENTIFIER, '8.4')
+        }
+
+        result = self.redhat_os_detect_tools.detect_os()
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os_no_redhat(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"CentOS Linux release 8.4 (Ootpa)"
+
+        with self.assertLogs('coriolis.osmorphing.osdetect.redhat',
+                             level=logging.DEBUG):
+            result = self.redhat_os_detect_tools.detect_os()
+
+            self.assertEqual(result, {})
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
diff --git a/coriolis/tests/osmorphing/osdetect/test_rocky.py b/coriolis/tests/osmorphing/osdetect/test_rocky.py
new file mode 100644
index 00000000..b5942756
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_rocky.py
@@ -0,0 +1,53 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import rocky
+from coriolis.tests import test_base
+
+
+class RockyLinuxOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the RockyLinuxOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"Rocky Linux release 8.4"
+
+        expected_info = {
+            "os_type": rocky.constants.OS_TYPE_LINUX,
+            "distribution_name": rocky.ROCKY_LINUX_DISTRO_IDENTIFIER,
+            "release_version": '8.4',
+            "friendly_release_name": "Rocky Linux Version 8.4"
+        }
+
+        rocky_os_detect_tools = rocky.RockyLinuxOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = rocky_os_detect_tools.detect_os()
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
+
+        self.assertEqual(result, expected_info)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_test_path')
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_file')
+    def test_detect_os_no_rocky(self, mock_read_file, mock_test_path):
+        mock_test_path.return_value = True
+        mock_read_file.return_value = b"CentOS Linux release 8.4"
+
+        with self.assertLogs('coriolis.osmorphing.osdetect.rocky',
+                             level="DEBUG"):
+            rocky_os_detect_tools = rocky.RockyLinuxOSDetectTools(
+                mock.sentinel.conn, mock.sentinel.os_root_dir,
+                mock.sentinel.operation_timeout)
+            result = rocky_os_detect_tools.detect_os()
+
+            self.assertEqual(result, {})
+
+        mock_test_path.assert_called_once_with("etc/redhat-release")
+        mock_read_file.assert_called_once_with("etc/redhat-release")
diff --git a/coriolis/tests/osmorphing/osdetect/test_suse.py b/coriolis/tests/osmorphing/osdetect/test_suse.py
new file mode 100644
index 00000000..1d19871d
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_suse.py
@@ -0,0 +1,96 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import suse
+from coriolis.tests import test_base
+
+
+class SUSEOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the SUSEOSDetectTools class."""
+
+    def setUp(self):
+        super(SUSEOSDetectToolsTestCase, self).setUp()
+
+        self.suse_os_detect_tools = suse.SUSEOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+    @mock.patch.object(
+        base.BaseLinuxOSDetectTools, 'returned_detected_os_info_fields')
+    def test_returned_detected_os_info_fields(self,
+                                              mock_detected_os_info_fields):
+        mock_detected_os_info_fields.return_value = [
+            "os_type", "distribution_name"]
+
+        result = suse.SUSEOSDetectTools.returned_detected_os_info_fields()
+
+        expected_fields = ["os_type", "distribution_name",
+                           suse.DETECTED_SUSE_RELEASE_FIELD_NAME]
+
+        self.assertEqual(result, expected_fields)
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_get_os_release')
+    def test_detect_os_sles(self, mock_get_os_release):
+        mock_get_os_release.return_value = {
+            "NAME": "SLES",
+            "VERSION_ID": suse.constants.OS_TYPE_UNKNOWN
+        }
+
+        expected_info = {
+            "os_type": suse.constants.OS_TYPE_LINUX,
+            "distribution_name": suse.SLES_DISTRO_IDENTIFIER,
+            suse.DETECTED_SUSE_RELEASE_FIELD_NAME: "SLES",
+            "release_version": suse.constants.OS_TYPE_UNKNOWN,
+            "friendly_release_name": "SLES %s" % suse.constants.OS_TYPE_UNKNOWN
+        }
+
+        result = self.suse_os_detect_tools.detect_os()
+
+        self.assertEqual(result, expected_info)
+
+        mock_get_os_release.assert_called_once()
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_get_os_release')
+    def test_detect_os_opensuse_tumbleweed(self, mock_get_os_release):
+        mock_get_os_release.return_value = {
+            "NAME": "openSUSE tumbleweed",
+            "VERSION_ID": suse.constants.OS_TYPE_UNKNOWN
+        }
+
+        expected_info = {
+            "os_type": suse.constants.OS_TYPE_LINUX,
+            "distribution_name": suse.OPENSUSE_DISTRO_IDENTIFIER,
+            suse.DETECTED_SUSE_RELEASE_FIELD_NAME: "openSUSE tumbleweed",
+            "release_version": suse.OPENSUSE_TUMBLEWEED_VERSION_IDENTIFIER,
+            "friendly_release_name": "openSUSE tumbleweed"
+        }
+
+        result = self.suse_os_detect_tools.detect_os()
+
+        self.assertEqual(result, expected_info)
+
+        mock_get_os_release.assert_called_once()
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_get_os_release')
+    def test_detect_os_opensuse(self, mock_get_os_release):
+        mock_get_os_release.return_value = {
+            "NAME": "openSUSE test",
+            "VERSION_ID": "15.3"
+        }
+
+        expected_info = {
+            "os_type": suse.constants.OS_TYPE_LINUX,
+            "distribution_name": suse.OPENSUSE_DISTRO_IDENTIFIER,
+            suse.DETECTED_SUSE_RELEASE_FIELD_NAME: "openSUSE test",
+            "release_version": "15.3",
+            "friendly_release_name": "openSUSE 15.3"
+        }
+
+        result = self.suse_os_detect_tools.detect_os()
+
+        self.assertEqual(result, expected_info)
+
+        mock_get_os_release.assert_called_once()
diff --git a/coriolis/tests/osmorphing/osdetect/test_ubuntu.py b/coriolis/tests/osmorphing/osdetect/test_ubuntu.py
new file mode 100644
index 00000000..b9c1085c
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_ubuntu.py
@@ -0,0 +1,38 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+from unittest import mock
+
+from coriolis.osmorphing.osdetect import base
+from coriolis.osmorphing.osdetect import ubuntu
+from coriolis.tests import test_base
+
+
+class UbuntuOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the UbuntuOSDetectTools class."""
+
+    @mock.patch.object(base.BaseLinuxOSDetectTools, '_read_config_file')
+    def test_detect_os(self, mock_read_config_file):
+        mock_read_config_file.return_value = {
+            "DISTRIB_ID": "Ubuntu",
+            "DISTRIB_RELEASE": mock.sentinel.version
+        }
+
+        expected_info = {
+            "os_type": ubuntu.constants.OS_TYPE_LINUX,
+            "distribution_name": ubuntu.UBUNTU_DISTRO_IDENTIFIER,
+            "release_version": mock.sentinel.version,
+            "friendly_release_name": "Ubuntu %s" % (
+                mock.sentinel.version)
+        }
+
+        ubuntu_os_detect_tools = ubuntu.UbuntuOSDetectTools(
+            mock.sentinel.conn, mock.sentinel.os_root_dir,
+            mock.sentinel.operation_timeout)
+
+        result = ubuntu_os_detect_tools.detect_os()
+
+        mock_read_config_file.assert_called_once_with(
+            "etc/lsb-release", check_exists=True)
+
+        self.assertEqual(result, expected_info)
diff --git a/coriolis/tests/osmorphing/osdetect/test_windows.py b/coriolis/tests/osmorphing/osdetect/test_windows.py
new file mode 100644
index 00000000..1c20c9fa
--- /dev/null
+++ b/coriolis/tests/osmorphing/osdetect/test_windows.py
@@ -0,0 +1,202 @@
+# Copyright 2024 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import logging
+import os
+from unittest import mock
+
+import ddt
+
+from coriolis import exception
+from coriolis.osmorphing.osdetect import windows
+from coriolis.tests import test_base
+
+
+WIN_VERSION_PS_OUTPUT = """
+CurrentVersion            : 6.3
+CurrentMajorVersionNumber : 10
+CurrentMinorVersionNumber : 0
+CurrentBuildNumber        : 20348
+InstallationType          : Server
+ProductName               : Windows Server 2022 Datacenter Evaluation
+EditionID                 : ServerDatacenterEval
+"""
+
+WIN_VERSION_PS_OUTPUT_MISSING_FIELDS = """
+CurrentMinorVersionNumber : 0
+CurrentBuildNumber        : 20348
+InstallationType          : Server
+ProductName               : Windows Server 2022 Datacenter Evaluation
+EditionID                 : ServerDatacenterEval
+"""
+
+WIN_VERSION_PS_OUTPUT_MISSING_MAJOR_VERSION = """
+CurrentVersion            : 6.3
+CurrentMinorVersionNumber : 0
+CurrentBuildNumber        : 20348
+InstallationType          : Server
+ProductName               : Windows Server 2022 Datacenter Evaluation
+EditionID                 : ServerDatacenterEval
+"""
+
+
+@ddt.ddt
+class WindowsOSDetectToolsTestCase(test_base.CoriolisBaseTestCase):
+    """Test suite for the WindowsOSDetectTools class."""
+
+    def setUp(self):
+        super(WindowsOSDetectToolsTestCase, self).setUp()
+        self.conn = mock.MagicMock()
+        self.os_root_dir = 'C:\\'
+        self.conn.EOL = '\n'
+
+        self.windows_os_detect_tools = windows.WindowsOSDetectTools(
+            self.conn, self.os_root_dir, mock.sentinel.operation_timeout)
+
+    def test_returned_detected_os_info_fields(self):
+        expected_base_fields = [
+            "os_type",
+            "distribution_name",
+            "release_version",
+            "friendly_release_name",
+            "version_number",
+            "edition_id",
+            "installation_type",
+            "product_name"
+        ]
+
+        result = (
+            windows.WindowsOSDetectTools.returned_detected_os_info_fields()
+        )
+
+        self.assertEqual(result, expected_base_fields)
+
+    def test__load_registry_hive(self):
+        self.windows_os_detect_tools._load_registry_hive(
+            mock.sentinel.subkey, mock.sentinel.path)
+
+        self.conn.exec_command.assert_called_once_with(
+            "reg.exe", ["load", mock.sentinel.subkey, mock.sentinel.path])
+
+    def test__unload_registry_hive(self):
+        self.windows_os_detect_tools._unload_registry_hive(
+            mock.sentinel.subkey)
+
+        self.conn.exec_command.assert_called_once_with(
+            "reg.exe", ["unload", mock.sentinel.subkey])
+
+    def test__get_ps_fl_value(self):
+        result = self.windows_os_detect_tools._get_ps_fl_value(
+            WIN_VERSION_PS_OUTPUT, 'CurrentVersion')
+
+        self.assertEqual(result, '6.3')
+
+    @ddt.data(
+        {
+            "ps_output": WIN_VERSION_PS_OUTPUT,
+            "expected_result": (
+                windows.version.LooseVersion("10.0.20348"),
+                "ServerDatacenterEval",
+                "Server",
+                "Windows Server 2022 Datacenter Evaluation"
+            ),
+        },
+        {
+            "ps_output": WIN_VERSION_PS_OUTPUT_MISSING_MAJOR_VERSION,
+            "expected_result": (
+                "6.3.20348",
+                "ServerDatacenterEval",
+                "Server",
+                "Windows Server 2022 Datacenter Evaluation"
+            )
+        }
+    )
+    @mock.patch.object(windows.WindowsOSDetectTools, '_load_registry_hive')
+    @mock.patch.object(windows.WindowsOSDetectTools, '_unload_registry_hive')
+    @mock.patch.object(windows.uuid, 'uuid4')
+    def test__get_image_version_info(
+            self, data, mock_uuid4, mock_unload_registry_hive,
+            mock_load_registry_hive):
+        self.conn.exec_ps_command.return_value = (
+            data["ps_output"].replace('\n', os.linesep)
+        )
+
+        result = self.windows_os_detect_tools._get_image_version_info()
+
+        mock_load_registry_hive.assert_called_once_with(
+            "HKLM\\%s" % mock_uuid4.return_value,
+            "%sWindows\\System32\\config\\SOFTWARE" % self.os_root_dir)
+
+        mock_unload_registry_hive.assert_called_once_with(
+            "HKLM\\%s" % mock_uuid4.return_value)
+
+        self.assertEqual(result, data["expected_result"])
+
+    @mock.patch.object(windows.WindowsOSDetectTools, '_load_registry_hive')
+    @mock.patch.object(windows.WindowsOSDetectTools, '_unload_registry_hive')
+    def test__get_image_version_info_with_exception(
+            self, mock_unload_registry_hive,
+            mock_load_registry_hive):
+        self.conn.exec_ps_command.return_value = (
+            WIN_VERSION_PS_OUTPUT_MISSING_FIELDS.replace('\n', os.linesep))
+
+        mock_unload_registry_hive.assert_not_called()
+        mock_load_registry_hive.assert_not_called()
+
+        self.assertRaises(
+            exception.CoriolisException,
+            self.windows_os_detect_tools._get_image_version_info
+        )
+
+    @ddt.data(
+        {
+            'version_number': mock.sentinel.version_number,
+            'edition_id': 'server',
+            'installation_type': mock.sentinel.installation_type,
+            'product_name': mock.sentinel.product_name,
+            'distribution_name': windows.WINDOWS_SERVER_IDENTIFIER,
+        },
+        {
+            'version_number': mock.sentinel.version_number,
+            'edition_id': 'client',
+            'installation_type': mock.sentinel.installation_type,
+            'product_name': mock.sentinel.product_name,
+            'distribution_name': windows.WINDOWS_CLIENT_IDENTIFIER,
+        }
+    )
+    @mock.patch.object(windows.WindowsOSDetectTools, '_get_image_version_info')
+    def test_detect_os(self, data, mock_get_image_version_info):
+        mock_get_image_version_info.return_value = (
+            data['version_number'],
+            data['edition_id'],
+            data['installation_type'],
+            data['product_name']
+        )
+
+        expected_result = {
+            "version_number": data['version_number'],
+            "edition_id": data['edition_id'],
+            "installation_type": data['installation_type'],
+            "product_name": data['product_name'],
+            "os_type": windows.constants.OS_TYPE_WINDOWS,
+            "distribution_name": data['distribution_name'],
+            "release_version": data['product_name'],
+            "friendly_release_name": "Windows %s" % data['product_name']
+        }
+
+        with self.assertLogs('coriolis.osmorphing.osdetect.windows',
+                             level=logging.DEBUG):
+            result = self.windows_os_detect_tools.detect_os()
+
+            self.assertEqual(result, expected_result)
+
+    @mock.patch.object(windows.WindowsOSDetectTools, '_get_image_version_info')
+    def test_detect_os_with_exception(self, mock_get_image_version_info):
+        mock_get_image_version_info.side_effect = exception.CoriolisException
+
+        with self.assertLogs('coriolis.osmorphing.osdetect.windows',
+                             level=logging.DEBUG):
+            self.assertRaises(exception.CoriolisException,
+                              self.windows_os_detect_tools.detect_os)
+
+        mock_get_image_version_info.assert_called_once_with()