Repository: cobbler/cobbler

Issue:
Reposync tests are flaky
### Describe the bug

We have wonderful `cobbler reposync` tests thanks to @tpw56j! However in our CI they are flaky.

### Steps to reproduce

1. Look at the history of our CI testruns
2. See that many failed runs are due to the mirrors not being available

### Expected behavior

The Python Tests are thought as Unit Tests. Thus they should be refactored and we should test the integration tests with our bash testsuite.

### Cobbler version

````paste below
Cobbler 3.4.0
  source: 0e20f01b, Mon Jun 13 16:13:33 2022 +0200
  build time: Sun Jul  3 07:59:48 2022
````

### Operating system

openSUSE Leap/Tumbleweed

### Cobbler log

````paste below
Not applicable
````

### Screenshots

None

### Additional information

None



Discussion:

Resolving patch:
diff --git a/cobbler/actions/reposync.py b/cobbler/actions/reposync.py
--- a/cobbler/actions/reposync.py
+++ b/cobbler/actions/reposync.py
@@ -13,7 +13,7 @@
 import pipes
 import stat
 import shutil
-from typing import Optional, Union
+from typing import Any, Dict, Optional, Tuple
 
 from cobbler import utils
 from cobbler import download_manager
@@ -48,6 +48,8 @@ def repo_walker(top, func, arg):
     """
     try:
         names = os.listdir(top)
+        # The order of the return is not guaranteed and seems to depend on the fileystem thus sort the list
+        names.sort()
     except os.error:
         return
     func(arg, top, names)
@@ -134,13 +136,14 @@ def run(self, name: Optional[str] = None, verbose: bool = True):
 
             # Which may actually NOT reposync if the repo is set to not mirror locally but that's a technicality.
 
+            success = False
             for x in range(self.tries + 1, 1, -1):
-                success = False
                 try:
                     self.sync(repo)
                     success = True
                     break
-                except:
+                except Exception:
+                    success = False
                     utils.log_exc()
                     self.logger.warning("reposync failed, tries left: %s", (x - 2))
 
@@ -219,8 +222,7 @@ def librepo_getinfo(self, dirname: str) -> dict:
         except librepo.LibrepoException as e:
             raise CX("librepo error: " + dirname + " - " + e.args[1]) from e
 
-        rmd = r.getinfo(librepo.LRR_RPMMD_REPOMD)["records"]
-        return rmd
+        return r.getinfo(librepo.LRR_RPMMD_REPOMD).get("records", {})
 
     # ====================================================================================
 
@@ -496,7 +498,9 @@ def rhn_sync(self, repo):
 
     # ====================================================================================
 
-    def gen_urlgrab_ssl_opts(self, yumopts) -> Union[str, bool]:
+    def gen_urlgrab_ssl_opts(
+        self, yumopts: Dict[str, Any]
+    ) -> Tuple[Optional[Tuple], bool]:
         """
         This function translates yum repository options into the appropriate options for python-requests
 
@@ -519,7 +523,7 @@ def gen_urlgrab_ssl_opts(self, yumopts) -> Union[str, bool]:
             else:
                 verify = False
 
-        return (cert, verify)
+        return cert, verify
 
     # ====================================================================================
 
@@ -845,8 +849,7 @@ def update_permissions(self, repo_path):
         elif dist in ("debian", "ubuntu"):
             owner = "root:www-data"
 
-        cmd1 = "chown -R " + owner + " %s" % repo_path
-
+        cmd1 = "chown -R %s %s" % (owner, repo_path)
         utils.subprocess_call(cmd1)
 
         cmd2 = "chmod -R 755 %s" % repo_path


Test patch:
diff --git a/tests/actions/reposync_test.py b/tests/actions/reposync_test.py
--- a/tests/actions/reposync_test.py
+++ b/tests/actions/reposync_test.py
@@ -1,256 +1,527 @@
 import os
-import glob
 
 import pytest
 
 from cobbler import enums
-from cobbler.api import CobblerAPI
-from cobbler.actions.reposync import RepoSync
+from cobbler.actions import reposync
 from cobbler.items.repo import Repo
 from cobbler import cexceptions
 from tests.conftest import does_not_raise
 
 
-@pytest.fixture(scope="class")
-def api():
-    return CobblerAPI()
-
-
-@pytest.fixture(scope="class")
-def reposync(api):
-    test_reposync = RepoSync(api, tries=2, nofail=False)
+@pytest.fixture(scope="function")
+def reposync_object(mocker, cobbler_api):
+    settings_mock = mocker.MagicMock()
+    settings_mock.webdir = "/srv/www/cobbler"
+    settings_mock.server = "localhost"
+    settings_mock.http_port = 80
+    settings_mock.proxy_url_ext = ""
+    settings_mock.yumdownloader_flags = "--testflag"
+    settings_mock.reposync_rsync_flags = "--testflag"
+    settings_mock.reposync_flags = "--testflag"
+    mocker.patch.object(cobbler_api, "settings", return_value=settings_mock)
+    test_reposync = reposync.RepoSync(cobbler_api, tries=2, nofail=False)
     return test_reposync
 
 
 @pytest.fixture
-def repo(api):
+def repo(cobbler_api):
     """
     Creates a Repository "testrepo0" with a keep_updated=True and mirror_locally=True".
     """
-    test_repo = Repo(api)
+    test_repo = Repo(cobbler_api)
     test_repo.name = "testrepo0"
     test_repo.mirror_locally = True
     test_repo.keep_updated = True
-    api.add_repo(test_repo)
     return test_repo
 
 
 @pytest.fixture
-def remove_repo(api):
+def remove_repo(cobbler_api):
     """
     Removes the Repository "testrepo0" which can be created with repo.
     """
     yield
-    test_repo = api.find_repo("testrepo0")
+    test_repo = cobbler_api.find_repo("testrepo0")
     if test_repo is not None:
-        api.remove_repo(test_repo.name)
+        cobbler_api.remove_repo(test_repo.name)
 
 
-class TestRepoSync:
-    @pytest.mark.usefixtures("remove_repo")
-    @pytest.mark.parametrize(
-        "input_mirror_type,input_mirror,expected_exception",
-        [
-            (
-                enums.MirrorType.BASEURL,
-                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os",
-                does_not_raise(),
-            ),
-            (
-                enums.MirrorType.MIRRORLIST,
-                "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-35&arch=x86_64",
-                does_not_raise(),
-            ),
-            (
-                enums.MirrorType.METALINK,
-                "https://mirrors.fedoraproject.org/metalink?repo=fedora-35&arch=x86_64",
-                does_not_raise(),
-            ),
-            (
-                enums.MirrorType.BASEURL,
-                "http://www.example.com/path/to/some/repo",
-                pytest.raises(cexceptions.CX),
-            ),
+@pytest.fixture(scope="function", autouse=True)
+def reset_librepo():
+    has_librepo = reposync.HAS_LIBREPO
+    yield
+    reposync.HAS_LIBREPO = has_librepo
+
+
+def test_repo_walker(mocker, tmp_path):
+    # Arrange
+    def test_fun(arg, top, names):
+        pass
+
+    subdir1 = tmp_path / "sub1"
+    subdir2 = tmp_path / "sub2"
+    subdir1.mkdir()
+    subdir2.mkdir()
+    spy = mocker.Mock(wraps=test_fun)
+
+    # Act
+    reposync.repo_walker(tmp_path, spy, None)
+
+    # Assert
+    assert spy.mock_calls == [
+        # settings.yaml is here because of our autouse fixture that we use to restore the settings
+        mocker.call(None, tmp_path, ["settings.yaml", "sub1", "sub2"]),
+        mocker.call(None, str(subdir1), []),
+        mocker.call(None, str(subdir2), []),
+    ]
+
+
+@pytest.mark.parametrize(
+    "input_has_librepo,input_path_exists_side_effect,expected_exception,expected_result",
+    [
+        (True, [True, False], does_not_raise(), "/usr/bin/dnf reposync"),
+        (True, [False, True], does_not_raise(), "/usr/bin/reposync"),
+        (True, [False, False], pytest.raises(cexceptions.CX), ""),
+        (False, [False, True], pytest.raises(cexceptions.CX), ""),
+    ],
+)
+def test_reposync_cmd(
+    mocker,
+    reposync_object,
+    input_has_librepo,
+    input_path_exists_side_effect,
+    expected_exception,
+    expected_result,
+):
+    # Arrange
+    mocker.patch("os.path.exists", side_effect=input_path_exists_side_effect)
+    reposync.HAS_LIBREPO = input_has_librepo
+
+    # Act
+    with expected_exception:
+        result = reposync_object.reposync_cmd()
+
+        # Assert
+        assert result == expected_result
+
+
+def test_run(mocker, reposync_object, repo):
+    # Arrange
+    env_vars = {}
+    mocker.patch("os.makedirs")
+    mocker.patch("os.path.isdir", return_value=True)
+    mocker.patch(
+        "os.path.join",
+        side_effect=[
+            "/srv/www/cobbler/repo_mirror",
+            "/srv/www/cobbler/repo_mirror/%s" % repo.name,
         ],
     )
-    def test_reposync_yum(
-        self, input_mirror_type, input_mirror, expected_exception, api, repo, reposync
-    ):
-        # Arrange
-        test_repo = repo
-        test_repo.breed = enums.RepoBreeds.YUM
-        test_repo.mirror = input_mirror
-        test_repo.mirror_type = input_mirror_type
-        test_repo.rpm_list = "fedora-gpg-keys"
-        test_settings = api.settings()
-        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
-
-        # Act & Assert
-        with expected_exception:
-            reposync.run(test_repo.name)
-            result = os.path.exists(repo_path)
-            if test_repo.rpm_list and test_repo.rpm_list != []:
-                for rpm in test_repo.rpm_list:
-                    assert (
-                        glob.glob(
-                            os.path.join(repo_path, "**", rpm) + "*.rpm", recursive=True
-                        )
-                        != []
-                    )
-            assert result
-            # Test that re-downloading the metadata in .origin/repodata will not result in an error
-            reposync.run(test_repo.name)
-
-    @pytest.mark.usefixtures("remove_repo")
-    @pytest.mark.parametrize(
-        "input_mirror_type,input_mirror,input_arch,input_rpm_list,expected_exception",
-        [
-            (
-                enums.MirrorType.BASEURL,
-                "http://ftp.debian.org/debian",
-                enums.RepoArchs.X86_64,
-                "",
-                does_not_raise(),
-            ),
-            (
-                enums.MirrorType.MIRRORLIST,
-                "http://ftp.debian.org/debian",
-                enums.RepoArchs.X86_64,
-                "",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.METALINK,
-                "http://ftp.debian.org/debian",
-                enums.RepoArchs.X86_64,
-                "",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.BASEURL,
-                "http://www.example.com/path/to/some/repo",
-                enums.RepoArchs.X86_64,
-                "",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.BASEURL,
-                "http://ftp.debian.org/debian",
-                enums.RepoArchs.NONE,
-                "",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.BASEURL,
-                "http://ftp.debian.org/debian",
-                enums.RepoArchs.X86_64,
-                "dpkg",
-                pytest.raises(cexceptions.CX),
-            ),
-        ],
+    mocker.patch("os.environ", return_value=env_vars)
+    mocker.patch.object(reposync_object, "repos", return_value=[repo])
+    mocker.patch.object(reposync_object, "sync")
+    mocker.patch.object(reposync_object, "update_permissions")
+    reposync_object.repos = [repo]
+
+    # Act
+    reposync_object.run()
+
+    # Assert
+    # This has to be 0 since all env vars need to be removed after reposync has run.
+    assert len(env_vars) == 0
+
+
+def test_gen_urlgrab_ssl_opts(reposync_object):
+    # Arrange
+    input_dict = {}
+
+    # Act
+    result = reposync_object.gen_urlgrab_ssl_opts(input_dict)
+
+    # Assert
+    assert isinstance(result, tuple)
+    assert len(result) == 2
+    # The data of the first element is kind of flexible let's skip asserting it for now
+    assert isinstance(result[1], bool)
+
+
+@pytest.mark.usefixtures("remove_repo")
+@pytest.mark.parametrize(
+    "input_mirror_type,input_mirror,expected_exception",
+    [
+        (
+            enums.MirrorType.BASEURL,
+            "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os",
+            does_not_raise(),
+        ),
+        (
+            enums.MirrorType.MIRRORLIST,
+            "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-35&arch=x86_64",
+            does_not_raise(),
+        ),
+        (
+            enums.MirrorType.METALINK,
+            "https://mirrors.fedoraproject.org/metalink?repo=fedora-35&arch=x86_64",
+            does_not_raise(),
+        ),
+    ],
+)
+def test_reposync_yum(
+    mocker,
+    input_mirror_type,
+    input_mirror,
+    expected_exception,
+    cobbler_api,
+    repo,
+    reposync_object,
+):
+    # Arrange
+    test_repo = repo
+    test_repo.breed = enums.RepoBreeds.YUM
+    test_repo.mirror = input_mirror
+    test_repo.mirror_type = input_mirror_type
+    test_repo.rpm_list = "fedora-gpg-keys"
+    test_settings = cobbler_api.settings()
+    repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
     )
-    def test_reposync_apt(
-        self,
-        input_mirror_type,
-        input_mirror,
-        input_arch,
-        input_rpm_list,
-        expected_exception,
-        api,
-        repo,
-        reposync,
-    ):
-        # Arrange
-        test_repo = repo
-        test_repo.breed = enums.RepoBreeds.APT
-        test_repo.arch = input_arch
-        test_repo.apt_components = "main"
-        test_repo.apt_dists = "stable"
-        test_repo.mirror = input_mirror
-        test_repo.mirror_type = input_mirror_type
-        test_repo.rpm_list = input_rpm_list
-        test_repo.yumopts = (
-            "--exclude=.* --include=dpkg.* --no-check-gpg --rsync-extra=none"
+    mocker.patch.object(
+        reposync_object, "create_local_file", return_value="/create/local/file"
+    )
+    mocker.patch.object(
+        reposync_object, "reposync_cmd", return_value="/my/fake/dnf reposync"
+    )
+    mocker.patch.object(reposync_object, "rflags", return_value="--fake-r-flakg")
+    mocker.patch.object(
+        reposync_object,
+        "gen_urlgrab_ssl_opts",
+        return_value=(("TODO", "TODO", "TODO"), False),
+    )
+    mocker.patch("os.path.exists", return_value=True)
+    mocker.patch("shutil.rmtree")
+    mocker.patch("os.makedirs")
+    mocked_repo_walker = mocker.patch("cobbler.actions.reposync.repo_walker")
+    handle_mock = mocker.MagicMock()
+    result_mock = mocker.MagicMock()
+    mocker.patch("librepo.Handle", return_value=handle_mock)
+    mocker.patch("librepo.Result", return_value=result_mock)
+
+    # Act & Assert
+    with expected_exception:
+        reposync_object.yum_sync(repo)
+
+        mocked_subprocess.assert_called_with(
+            "/usr/bin/dnf download --testflag  --disablerepo=* --enablerepo=%s -c /create/local/file --destdir=%s"
+            " fedora-gpg-keys" % (repo.name, repo_path)
         )
-        test_settings = api.settings()
-        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
-
-        # Act & Assert
-        with expected_exception:
-            reposync.run(test_repo.name)
-            result = os.path.exists(repo_path)
-            for rpm in ["dpkg"]:
-                assert (
-                    glob.glob(
-                        os.path.join(repo_path, "**", "dpkg") + "*", recursive=True
-                    )
-                    != []
-                )
-            assert result
-
-    @pytest.mark.skip(
-        "To flaky and thus not reliable. Needs to be mocked to be of use."
+        handle_mock.perform.assert_called_with(result_mock)
+        assert mocked_repo_walker.call_count == 1
+
+
+@pytest.mark.usefixtures("remove_repo")
+@pytest.mark.parametrize(
+    "input_mirror_type,input_mirror,input_arch,input_rpm_list,expected_exception",
+    [
+        (
+            enums.MirrorType.BASEURL,
+            "http://ftp.debian.org/debian",
+            enums.RepoArchs.X86_64,
+            "",
+            does_not_raise(),
+        ),
+        (
+            enums.MirrorType.MIRRORLIST,
+            "http://ftp.debian.org/debian",
+            enums.RepoArchs.X86_64,
+            "",
+            pytest.raises(cexceptions.CX),
+        ),
+        (
+            enums.MirrorType.METALINK,
+            "http://ftp.debian.org/debian",
+            enums.RepoArchs.X86_64,
+            "",
+            pytest.raises(cexceptions.CX),
+        ),
+        (
+            enums.MirrorType.BASEURL,
+            "http://ftp.debian.org/debian",
+            enums.RepoArchs.NONE,
+            "",
+            pytest.raises(cexceptions.CX),
+        ),
+        (
+            enums.MirrorType.BASEURL,
+            "http://ftp.debian.org/debian",
+            enums.RepoArchs.X86_64,
+            "dpkg",
+            pytest.raises(cexceptions.CX),
+        ),
+    ],
+)
+def test_reposync_apt(
+    mocker,
+    input_mirror_type,
+    input_mirror,
+    input_arch,
+    input_rpm_list,
+    expected_exception,
+    cobbler_api,
+    repo,
+    reposync_object,
+):
+    # Arrange
+    test_repo = repo
+    test_repo.breed = enums.RepoBreeds.APT
+    test_repo.arch = input_arch
+    test_repo.apt_components = "main"
+    test_repo.apt_dists = "stable"
+    test_repo.mirror = input_mirror
+    test_repo.mirror_type = input_mirror_type
+    test_repo.rpm_list = input_rpm_list
+    test_settings = cobbler_api.settings()
+    repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
     )
-    @pytest.mark.usefixtures("remove_repo")
-    @pytest.mark.parametrize(
-        "input_mirror_type,input_mirror,expected_exception",
-        [
-            (
-                enums.MirrorType.BASEURL,
-                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
-                does_not_raise(),
-            ),
-            (
-                enums.MirrorType.MIRRORLIST,
-                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.METALINK,
-                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
-                pytest.raises(cexceptions.CX),
-            ),
-            (
-                enums.MirrorType.BASEURL,
-                "http://www.example.com/path/to/some/repo",
-                pytest.raises(cexceptions.CX),
-            ),
-        ],
+    mocker.patch("os.path.exists", return_value=True)
+
+    # Act
+    with expected_exception:
+        reposync_object.apt_sync(repo)
+
+        # Assert
+        mocked_subprocess.assert_called_with(
+            "/usr/bin/debmirror --nocleanup --method=http --host=%s --root=%s --dist=stable --section=main %s "
+            "--nosource -a amd64" % ("ftp.debian.org", "/debian", repo_path)
+        )
+
+
+@pytest.mark.usefixtures("remove_repo")
+@pytest.mark.parametrize(
+    "input_mirror_type,input_mirror,expected_exception",
+    [
+        (
+            enums.MirrorType.BASEURL,
+            "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+            does_not_raise(),
+        ),
+        (
+            enums.MirrorType.MIRRORLIST,
+            "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+            pytest.raises(cexceptions.CX),
+        ),
+        (
+            enums.MirrorType.METALINK,
+            "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+            pytest.raises(cexceptions.CX),
+        ),
+    ],
+)
+def test_reposync_wget(
+    mocker,
+    input_mirror_type,
+    input_mirror,
+    expected_exception,
+    cobbler_api,
+    repo,
+    reposync_object,
+):
+    # Arrange
+    test_repo = repo
+    test_repo.breed = enums.RepoBreeds.WGET
+    test_repo.mirror = input_mirror
+    test_repo.mirror_type = input_mirror_type
+    repo_path = os.path.join(
+        reposync_object.settings.webdir, "repo_mirror", test_repo.name
+    )
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
+    )
+    mocker.patch("cobbler.actions.reposync.repo_walker")
+    mocker.patch.object(reposync_object, "create_local_file")
+
+    # Act
+    with expected_exception:
+        reposync_object.wget_sync(test_repo)
+
+        # Assert
+        mocked_subprocess.assert_called_with(
+            [
+                "wget",
+                "-N",
+                "-np",
+                "-r",
+                "-l",
+                "inf",
+                "-nd",
+                "-P",
+                repo_path,
+                input_mirror,
+            ],
+            shell=False,
+        )
+
+
+def test_reposync_rhn(mocker, reposync_object, repo):
+    # Arrange
+    repo.mirror = "rhn://%s" % repo.name
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
+    )
+    mocker.patch("os.path.isdir", return_value=True)
+    mocker.patch("os.makedirs")
+    mocker.patch("cobbler.actions.reposync.repo_walker")
+    mocker.patch.object(reposync_object, "create_local_file")
+    mocker.patch.object(
+        reposync_object, "reposync_cmd", return_value="/my/fake/reposync"
     )
-    def test_reposync_wget(
-        self, input_mirror_type, input_mirror, expected_exception, api, repo, reposync
-    ):
-        # Arrange
-        test_repo = repo
-        test_repo.breed = enums.RepoBreeds.WGET
-        test_repo.mirror = input_mirror
-        test_repo.mirror_type = input_mirror_type
-        test_settings = api.settings()
-        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
-
-        # Act & Assert
-        with expected_exception:
-            reposync.run(test_repo.name)
-            result = os.path.exists(repo_path)
-            for rpm in ["rpm"]:
-                assert (
-                    glob.glob(os.path.join(repo_path, "**", "2") + "*", recursive=True)
-                    != []
-                )
-            assert result
-
-
-@pytest.mark.skip("TODO")
-def test_reposync_rhn():
+
+    # Act
+    reposync_object.rhn_sync(repo)
+
+    # Assert
+    # TODO: Check this more and document how its actually working
+    mocked_subprocess.assert_called_with(
+        "/my/fake/reposync --testflag --repo=testrepo0 -p /srv/www/cobbler/repo_mirror"
+    )
+
+
+def test_reposync_rsync(mocker, reposync_object, repo):
     # Arrange
+    mocked_subprocess = mocker.patch("cobbler.utils.subprocess_call", return_value=0)
+    mocker.patch("cobbler.actions.reposync.repo_walker")
+    mocker.patch.object(reposync_object, "create_local_file")
+    repo_path = os.path.join(reposync_object.settings.webdir, "repo_mirror", repo.name)
+
     # Act
+    reposync_object.rsync_sync(repo)
+
     # Assert
-    assert False
+    mocked_subprocess.assert_called_with(
+        "rsync --testflag --delete-after -e ssh --delete --exclude-from=/etc/cobbler/rsync.exclude / %s"
+        % repo_path
+    )
 
 
-@pytest.mark.skip("TODO")
-def test_reposync_rsync():
+def test_createrepo_walker(mocker, reposync_object, repo):
     # Arrange
+    input_repo = repo
+    input_repo.breed = enums.RepoBreeds.RSYNC
+    input_dirname = ""
+    input_fnames = []
+    expected_call = "createrepo  --testflags '%s'" % input_dirname
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
+    )
+    mocker.patch(
+        "cobbler.utils.blender",
+        autospec=True,
+        return_value={"createrepo_flags": "--testflags"},
+    )
+    mocker.patch("cobbler.utils.remove_yum_olddata")
+    mocker.patch("cobbler.utils.subprocess_get", return_value="5")
+    mocker.patch("cobbler.utils.get_family", return_value="TODO")
+    mocker.patch("os.path.exists", return_value=True)
+    mocker.patch("os.path.isfile", return_value=True)
+    mocker.patch.object(reposync_object, "librepo_getinfo", return_value={})
+
     # Act
+    reposync_object.createrepo_walker(input_repo, input_dirname, input_fnames)
+
+    # Assert
+    # TODO: Improve coverage over different cases in method
+    mocked_subprocess.assert_called_with(expected_call)
+
+
+@pytest.mark.parametrize(
+    "input_repotype,expected_exception",
+    [
+        (enums.RepoBreeds.YUM, does_not_raise()),
+        (enums.RepoBreeds.RHN, does_not_raise()),
+        (enums.RepoBreeds.APT, does_not_raise()),
+        (enums.RepoBreeds.RSYNC, does_not_raise()),
+        (enums.RepoBreeds.WGET, does_not_raise()),
+        (enums.RepoBreeds.NONE, pytest.raises(cexceptions.CX)),
+    ],
+)
+def test_sync(mocker, cobbler_api, reposync_object, input_repotype, expected_exception):
+    # Arrange
+    test_repo = Repo(cobbler_api)
+    test_repo.breed = input_repotype
+    rhn_sync_mock = mocker.patch.object(reposync_object, "rhn_sync")
+    yum_sync_mock = mocker.patch.object(reposync_object, "yum_sync")
+    apt_sync_mock = mocker.patch.object(reposync_object, "apt_sync")
+    rsync_sync_mock = mocker.patch.object(reposync_object, "rsync_sync")
+    wget_sync_mock = mocker.patch.object(reposync_object, "wget_sync")
+
+    # Act
+    with expected_exception:
+        reposync_object.sync(test_repo)
+
+        # Assert
+        call_count = sum(
+            (
+                rhn_sync_mock.call_count,
+                yum_sync_mock.call_count,
+                apt_sync_mock.call_count,
+                rsync_sync_mock.call_count,
+                wget_sync_mock.call_count,
+            )
+        )
+        assert call_count == 1
+
+
+def test_librepo_getinfo(mocker, reposync_object, tmp_path):
+    # Arrange
+    handle_mock = mocker.MagicMock()
+    result_mock = mocker.MagicMock()
+    mocker.patch("librepo.Handle", return_value=handle_mock)
+    mocker.patch("librepo.Result", return_value=result_mock)
+
+    # Act
+    reposync_object.librepo_getinfo(str(tmp_path))
+
+    # Assert
+    handle_mock.perform.assert_called_with(result_mock)
+    result_mock.getinfo.assert_called()
+
+
+def test_create_local_file(mocker, reposync_object, repo):
+    # Arrange
+    mocker.patch("cobbler.utils.filesystem_helpers.mkdir", autospec=True)
+    mock_open = mocker.patch("builtins.open", mocker.mock_open())
+    input_dest_path = ""
+    input_repo = repo
+    input_output = True
+
+    # Act
+    reposync_object.create_local_file(input_dest_path, input_repo, output=input_output)
+
+    # Assert
+    # TODO: Extend checks
+    assert mock_open.call_count == 1
+    assert mock_open.mock_calls[0] == mocker.call("config.repo", "w+")
+    mock_open_handle = mock_open()
+    assert mock_open_handle.write.mock_calls[0] == mocker.call("[testrepo0]\n")
+    assert mock_open_handle.write.mock_calls[1] == mocker.call("name=testrepo0\n")
+
+
+def test_update_permissions(mocker, reposync_object):
+    # Arrange
+    mocked_subprocess = mocker.patch(
+        "cobbler.utils.subprocess_call", autospec=True, return_value=0
+    )
+    path_to_update = "/my/fake/path"
+    expected_calls = [
+        mocker.call("chown -R root:www %s" % path_to_update),
+        mocker.call("chmod -R 755 %s" % path_to_update),
+    ]
+
+    # Act
+    reposync_object.update_permissions(path_to_update)
+
     # Assert
-    assert False
+    assert mocked_subprocess.mock_calls == expected_calls