"""Test clone""" import os import re import pytest BOOTSTRAP_CODE = 123 BOOTSTRAP_MSG = "Bootstrap successful" @pytest.mark.usefixtures("remote") @pytest.mark.parametrize( "good_remote, repo_exists, force, conflicts", [ (False, False, False, False), (True, False, False, False), (True, True, False, False), (True, True, True, False), (True, False, False, True), ], ids=[ "bad remote", "simple", "existing repo", "-f", "conflicts", ], ) def test_clone(runner, paths, yadm_cmd, repo_config, ds1, good_remote, repo_exists, force, conflicts): """Test basic clone operation""" # clear out the work path paths.work.remove() paths.work.mkdir() # determine remote url remote_url = f"file://{paths.remote}" if not good_remote: remote_url = "file://bad_remote" old_repo = None if repo_exists: # put a repo in the way paths.repo.mkdir() old_repo = paths.repo.join("old_repo") old_repo.write("old_repo") if conflicts: ds1.tracked[0].relative.write("conflict") assert ds1.tracked[0].relative.exists() # run the clone command args = ["clone", "-w", paths.work] if force: args += ["-f"] args += [remote_url] run = runner(command=yadm_cmd(*args)) if not good_remote: # clone should fail assert run.failure assert run.out == "" assert "Unable to clone the repository" in run.err assert not paths.repo.exists() elif repo_exists and not force: # can't overwrite data assert run.failure assert run.out == "" assert "Git repo already exists" in run.err else: # clone should succeed, and repo should be configured properly assert successful_clone(run, paths, repo_config) # these clones should have master as HEAD verify_head(paths, "master") # ensure conflicts are handled properly if conflicts: assert "NOTE" in run.out assert "Local files with content that differs" in run.out # confirm correct Git origin run = runner(command=("git", "remote", "-v", "show"), env={"GIT_DIR": paths.repo}) assert run.success assert run.err == "" assert f"origin\t{remote_url}" in run.out # ensure conflicts are really preserved if conflicts: # test that the conflicts are preserved in the work tree run = runner(command=yadm_cmd("status", "-uno", "--porcelain"), cwd=paths.work) assert run.success assert run.err == "" assert str(ds1.tracked[0].path) in run.out # verify content of the conflicts run = runner(command=yadm_cmd("diff"), cwd=paths.work) assert run.success assert run.err == "" assert "\n+conflict" in run.out, "conflict overwritten" # another force-related assertion if old_repo: if force: assert not old_repo.exists() else: assert old_repo.exists() @pytest.mark.usefixtures("remote") @pytest.mark.parametrize( "bs_exists, bs_param, answer", [ (False, "--bootstrap", None), (True, "--bootstrap", None), (True, "--no-bootstrap", None), (True, None, "n"), (True, None, "y"), ], ids=[ "force, missing", "force, existing", "prevent", "existing, answer n", "existing, answer y", ], ) def test_clone_bootstrap(runner, paths, yadm_cmd, repo_config, bs_exists, bs_param, answer): """Test bootstrap clone features""" # establish a bootstrap create_bootstrap(paths, bs_exists) # run the clone command args = ["clone", "-w", paths.work] if bs_param: args += [bs_param] args += [f"file://{paths.remote}"] expect = [] if answer: expect.append(("Would you like to execute it now", answer)) run = runner(command=yadm_cmd(*args), expect=expect) if answer: assert "Would you like to execute it now" in run.out expected_code = 0 if bs_exists and bs_param != "--no-bootstrap": expected_code = BOOTSTRAP_CODE if answer == "y": expected_code = BOOTSTRAP_CODE assert BOOTSTRAP_MSG in run.out elif answer == "n": expected_code = 0 assert BOOTSTRAP_MSG not in run.out assert successful_clone(run, paths, repo_config, expected_code) verify_head(paths, "master") if not bs_exists: assert BOOTSTRAP_MSG not in run.out def create_bootstrap(paths, exists): """Create bootstrap file for test""" if exists: paths.bootstrap.write("#!/bin/sh\n" f"echo {BOOTSTRAP_MSG}\n" f"exit {BOOTSTRAP_CODE}\n") paths.bootstrap.chmod(0o775) assert paths.bootstrap.exists() else: assert not paths.bootstrap.exists() @pytest.mark.usefixtures("remote") @pytest.mark.parametrize( "private_type, in_repo, in_work", [ ("ssh", False, True), ("gnupg", False, True), ("ssh", True, True), ("gnupg", True, True), ("ssh", True, False), ("gnupg", True, False), ], ids=[ "open ssh, not tracked", "open gnupg, not tracked", "open ssh, tracked", "open gnupg, tracked", "missing ssh, tracked", "missing gnupg, tracked", ], ) def test_clone_perms(runner, yadm_cmd, paths, repo_config, private_type, in_repo, in_work): """Test clone permission-related functions""" # update remote repo to include private data if in_repo: rpath = paths.work.mkdir(f".{private_type}").join("related") rpath.write("related") os.system(f'GIT_DIR="{paths.remote}" git add {rpath}') os.system(f'GIT_DIR="{paths.remote}" git commit -m "{rpath}"') rpath.remove() # ensure local private data is insecure at the start if in_work: pdir = paths.work.join(f".{private_type}") if not pdir.exists(): pdir.mkdir() pfile = pdir.join("existing") pfile.write("existing") pdir.chmod(0o777) pfile.chmod(0o777) else: paths.work.remove() paths.work.mkdir() env = {"HOME": paths.work} run = runner(yadm_cmd("clone", "-d", "-w", paths.work, f"file://{paths.remote}"), env=env) assert successful_clone(run, paths, repo_config) verify_head(paths, "master") if in_work: # private directories which already exist, should be left as they are, # which in this test is "insecure". assert re.search(f"initial private dir perms drwxrwxrwx.+.{private_type}", run.out) assert re.search(f"pre-checkout private dir perms drwxrwxrwx.+.{private_type}", run.out) assert re.search(f"post-checkout private dir perms drwxrwxrwx.+.{private_type}", run.out) else: # private directories which are created, should be done prior to # checkout, and with secure permissions. assert "initial private dir perms" not in run.out assert re.search(f"pre-checkout private dir perms drwx------.+.{private_type}", run.out) assert re.search(f"post-checkout private dir perms drwx------.+.{private_type}", run.out) # standard perms still apply afterwards unless disabled with auto.perms assert oct(paths.work.join(f".{private_type}").stat().mode).endswith( "00" ), f".{private_type} has not been secured by auto.perms" @pytest.mark.usefixtures("remote") @pytest.mark.parametrize("branch", ["master", "default", "valid", "invalid"]) def test_alternate_branch(runner, paths, yadm_cmd, repo_config, branch): """Test cloning a branch other than master""" # add a "valid" branch to the remote os.system(f'GIT_DIR="{paths.remote}" git checkout -b valid') os.system(f'GIT_DIR="{paths.remote}" git commit ' f'--allow-empty -m "This branch is valid"') if branch != "default": # When branch == 'default', the "default" branch of the remote repo # will remain "valid" to validate identification the correct default # branch by inspecting the repo. Otherwise it will be set back to # "master" os.system(f'GIT_DIR="{paths.remote}" git checkout master') # clear out the work path paths.work.remove() paths.work.mkdir() remote_url = f"file://{paths.remote}" # run the clone command args = ["clone", "-w", paths.work] if branch not in ["master", "default"]: args += ["-b", branch] args += [remote_url] run = runner(command=yadm_cmd(*args)) if branch == "invalid": assert run.failure assert "ERROR: Unable to clone the repository" in run.err assert f"Remote branch {branch} not found in upstream" in run.err else: assert successful_clone(run, paths, repo_config) # confirm correct Git origin run = runner(command=("git", "remote", "-v", "show"), env={"GIT_DIR": paths.repo}) assert run.success assert run.err == "" assert f"origin\t{remote_url}" in run.out run = runner(command=yadm_cmd("show")) if branch == "master": assert "Initial commit" in run.out verify_head(paths, "master") else: assert "This branch is valid" in run.out verify_head(paths, "valid") def successful_clone(run, paths, repo_config, expected_code=0): """Assert clone is successful""" assert run.code == expected_code assert oct(paths.repo.stat().mode).endswith("00"), "Repo is not secured" assert repo_config("core.bare") == "false" assert repo_config("status.showUntrackedFiles") == "no" assert repo_config("yadm.managed") == "true" return True @pytest.fixture() def remote(paths, ds1_repo_copy): """Function scoped remote (based on ds1)""" # pylint: disable=unused-argument # This is ignored because # @pytest.mark.usefixtures('ds1_remote_copy') # cannot be applied to another fixture. paths.remote.remove() paths.repo.move(paths.remote) def test_no_repo( runner, yadm_cmd, ): """Test cloning without specifying a repo""" run = runner(command=yadm_cmd("clone", "-f")) assert run.failure assert run.out == "" assert "ERROR: Unable to clone the repository" in run.err assert "repository 'repo.git' does not exist" in run.err def verify_head(paths, branch): """Assert the local repo has the correct head branch""" assert paths.repo.join("HEAD").read() == f"ref: refs/heads/{branch}\n" @pytest.mark.usefixtures("remote") def test_clone_subdirectory(runner, paths, yadm_cmd, repo_config): """Test clone from sub-directory of YADM_WORK""" # clear out the work path paths.work.remove() paths.work.mkdir() # create sub-directory subdir = paths.work.mkdir("subdir") # determine remote url remote_url = f"file://{paths.remote}" # run the clone command args = ["clone", "-w", paths.work, remote_url] run = runner(command=yadm_cmd(*args), cwd=subdir) # clone should succeed, and repo should be configured properly assert successful_clone(run, paths, repo_config) # ensure that no changes found as this is a clean dotfiles clone run = runner(command=yadm_cmd("status", "-uno", "--porcelain"), cwd=subdir) assert run.success assert run.out == "" assert run.err == ""