"""Exercise real local Git remotes; never register a runner or contact Gitea.""" import json import os from pathlib import Path import shutil import stat import subprocess import pytest import yaml ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "tools/git_runner" LAUNCHER = ROOT / "scripts/runner.sh" FORK = "git@git.kemt.fei.tuke.sk:jp170na/zpwiki.git" UPSTREAM = "git@git.kemt.fei.tuke.sk:KEMT/zpwiki.git" def git(repo, *args): return subprocess.run( ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True ).stdout.strip() def commit(repo, name): (repo / name).write_text(name) git(repo, "add", name) git( repo, "-c", "user.name=Sync Test", "-c", "user.email=sync@example.invalid", "commit", "-m", name, ) return git(repo, "rev-parse", "HEAD") @pytest.fixture def repositories(tmp_path): source = tmp_path / "upstream" subprocess.run(["git", "init", "-b", "master", str(source)], check=True, capture_output=True) commit(source, "initial") fork = tmp_path / "fork.git" subprocess.run( ["git", "clone", "--bare", str(source), str(fork)], check=True, capture_output=True ) script = tmp_path / "sync_fork.sh" # Only transport endpoints differ; execute the unmodified production algorithm. script.write_text( (RUNNER / "sync_fork.sh") .read_text() .replace(FORK, str(fork)) .replace(UPSTREAM, str(source)) ) shutil.copy2(RUNNER / "ssh_known_hosts", tmp_path / "ssh_known_hosts") return source, fork, script def sync(repositories, *args, token="test-token-never-print"): source, fork, script = repositories env = {**os.environ, "ZPWIKI_SYNC_SSH_KEY": token} result = subprocess.run(["bash", str(script), *args], env=env, capture_output=True, text=True) assert not token or token not in result.stdout + result.stderr return result def test_fast_forward_preserves_upstream_commit_and_other_fork_branches(repositories): source, fork, _ = repositories before = git(fork, "rev-parse", "master") git(fork, "branch", "keep-me", before) after = commit(source, "upstream-update") result = sync(repositories) assert result.returncode == 0, result.stderr assert git(fork, "rev-parse", "master") == after assert git(fork, "rev-parse", "keep-me") == before assert git(source, "rev-parse", "HEAD") == after assert git(fork, "rev-list", "--count", "master") == "2" def test_repeated_sync_is_a_noop(repositories): _, fork, _ = repositories before = git(fork, "rev-parse", "master") for _ in range(2): result = sync(repositories) assert result.returncode == 0 and "Bez zapisu" in result.stdout assert git(fork, "rev-parse", "master") == before def test_check_mode_needs_no_token_and_does_not_push(repositories): source, fork, _ = repositories before = git(fork, "rev-parse", "master") commit(source, "pending-update") result = sync(repositories, "--check", token="") assert result.returncode == 0 and "potrebuje fast-forward" in result.stdout assert git(fork, "rev-parse", "master") == before def test_missing_token_stops_before_any_write(repositories): source, fork, _ = repositories before = git(fork, "rev-parse", "master") commit(source, "pending-update") result = sync(repositories, token="") assert result.returncode != 0 and "ZPWIKI_SYNC_SSH_KEY" in result.stderr assert git(fork, "rev-parse", "master") == before def test_ssh_transport_keeps_key_private_and_pushes_only_the_fork(repositories, tmp_path): source, fork, _ = repositories wanted = commit(source, "ssh-update") script = tmp_path / "original-sync.sh" shutil.copy2(RUNNER / "sync_fork.sh", script) executable = tmp_path / "bin/ssh" executable.parent.mkdir() executable.write_text("""#!/usr/bin/env python3 import json,os,shlex,stat,sys from pathlib import Path args=sys.argv[1:] for value in ['BatchMode=yes','StrictHostKeyChecking=yes','IdentitiesOnly=yes']: assert value in args assert args[args.index('-F')+1]=='/dev/null' assert 'ZPWIKI_SYNC_SSH_KEY' not in os.environ key=Path(args[args.index('-i')+1]) assert stat.S_IMODE(key.stat().st_mode)==0o600 assert key.read_text()=='fixture-private-key\\n' assert any(a.startswith('UserKnownHostsFile=') for a in args) command,repo=shlex.split(args[-1]) assert command in ['git-upload-pack','git-receive-pack'] assert command!='git-receive-pack' or repo=='jp170na/zpwiki.git' target={'jp170na/zpwiki.git':os.environ['FORK_TEST_PATH'], 'KEMT/zpwiki.git':os.environ['UPSTREAM_TEST_PATH']}[repo] with open(os.environ['SSH_TEST_RECORD'],'a') as f: f.write(json.dumps({'command':command,'repo':repo,'key':str(key)})+'\\n') os.execvp(command,[command,target]) """) executable.chmod(0o700) record = tmp_path / "ssh-calls.jsonl" env = { **os.environ, "PATH": str(executable.parent) + os.pathsep + os.environ["PATH"], "FORK_TEST_PATH": str(fork), "UPSTREAM_TEST_PATH": str(source / ".git"), "SSH_TEST_RECORD": str(record), "ZPWIKI_SYNC_SSH_KEY": "fixture-private-key\r\n", } result = subprocess.run(["bash", str(script)], env=env, capture_output=True, text=True) assert result.returncode == 0, result.stderr assert git(fork, "rev-parse", "master") == wanted calls = [json.loads(row) for row in record.read_text().splitlines()] assert len(calls) == 3 assert calls[-1]["command"] == "git-receive-pack" and calls[-1]["repo"] == "jp170na/zpwiki.git" assert all(not Path(row["key"]).exists() for row in calls) assert "fixture-private-key" not in result.stdout + result.stderr def fork_edit(fork, tmp_path): edit = tmp_path / "fork-edit" subprocess.run(["git", "clone", str(fork), str(edit)], check=True, capture_output=True) own = commit(edit, "own-fork-change") git(edit, "push", "origin", "master") return own def test_fork_own_commits_are_preserved_when_upstream_is_already_contained(repositories, tmp_path): source, fork, _ = repositories own = fork_edit(fork, tmp_path) result = sync(repositories) assert result.returncode == 0 and git(fork, "rev-parse", "master") == own @pytest.mark.parametrize("args", [(), ("--check",)]) def test_divergence_is_rejected_without_rewriting_either_repository(repositories, tmp_path, args): source, fork, _ = repositories own = fork_edit(fork, tmp_path) upstream = commit(source, "different-upstream-change") result = sync(repositories, *args) assert result.returncode != 0 and "Vetvy sa rozisli" in result.stderr assert git(fork, "rev-parse", "master") == own assert git(source, "rev-parse", "HEAD") == upstream def test_upstream_transport_failure_does_not_update_fork(repositories): source, fork, _ = repositories before = git(fork, "rev-parse", "master") shutil.rmtree(source) assert sync(repositories).returncode != 0 assert git(fork, "rev-parse", "master") == before @pytest.mark.parametrize("args", [("--force",), ("--check", "unexpected")]) def test_unknown_options_never_change_fork(repositories, args): _, fork, _ = repositories before = git(fork, "rev-parse", "master") assert sync(repositories, *args).returncode == 64 assert git(fork, "rev-parse", "master") == before def helper_layout(root): launcher = root / "scripts/runner.sh" launcher.parent.mkdir() shutil.copy2(LAUNCHER, launcher) config = root / "tools/git_runner" config.mkdir(parents=True) shutil.copy2(RUNNER / "compose.yml", config / "compose.yml") return launcher, config def test_runner_init_preserves_main_env_and_existing_private_token(tmp_path): launcher, config = helper_layout(tmp_path) command = ["bash", str(launcher), "init"] subprocess.run(command, check=True, capture_output=True) env = tmp_path / ".env" content = "SEARCH_API_KEY=existing-api-value\nRUNNER_REGISTRATION_TOKEN=private-test-value\n" env.write_text(content) env.chmod(0o644) result = subprocess.run(command, check=True, capture_output=True, text=True) assert env.read_text() == content assert stat.S_IMODE(env.stat().st_mode) == 0o600 assert stat.S_IMODE((config / "data").stat().st_mode) == 0o700 assert not (config / ".env").exists() assert "private-test-value" not in result.stdout + result.stderr def test_runner_init_adds_token_setting_without_changing_backend_settings(tmp_path): launcher, config = helper_layout(tmp_path) env = tmp_path / ".env" content = "SEARCH_API_KEY=existing-api-value\nNEO4J_DATABASE=neo4j\n" env.write_text(content) command = ["bash", str(launcher), "init"] subprocess.run(command, check=True, capture_output=True) updated = env.read_text() assert updated.startswith(content) assert updated.count("RUNNER_REGISTRATION_TOKEN=") == 1 subprocess.run(command, check=True, capture_output=True) assert env.read_text() == updated assert not (config / ".env").exists() def test_runner_helper_targets_only_its_own_compose_project(tmp_path): launcher, config = helper_layout(tmp_path) subprocess.run(["bash", str(launcher), "init"], check=True, capture_output=True) (tmp_path / ".env").write_text("RUNNER_REGISTRATION_TOKEN=private-test-value\n") registration = config / "data/.runner" registration.write_text('{"id":1,"token":"keep-existing-registration"}') executable = tmp_path / "bin/docker" executable.parent.mkdir() executable.write_text( "#!/usr/bin/env python3\nimport json,os,sys\n" 'with open(os.environ["RECORDED_COMMANDS"],"a") as f: ' 'f.write(json.dumps(sys.argv[1:])+"\\n")\n' ) executable.chmod(0o700) record = tmp_path / "commands.jsonl" env = { **os.environ, "PATH": str(executable.parent) + os.pathsep + os.environ["PATH"], "ZP_DOCKER_CONTEXT": "fixture-context", "RECORDED_COMMANDS": str(record), } subprocess.run(["bash", str(launcher), "up"], env=env, check=True, capture_output=True) commands = [json.loads(row) for row in record.read_text().splitlines()] assert len(commands) == 3 assert commands[0][-2:] == ["config", "--quiet"] assert commands[1][-2:] == ["up", "-d"] and commands[2][-1] == "ps" for command in commands: assert command[:2] == ["--context", "fixture-context"] assert command[command.index("--project-name") + 1] == "zp-git-runner" assert command[command.index("-f") + 1] == str(config / "compose.yml") assert command[command.index("--env-file") + 1] == str(tmp_path / ".env") assert "private-test-value" not in " ".join(command) assert registration.read_text() == '{"id":1,"token":"keep-existing-registration"}' assert not (config / ".env").exists() def test_daily_workflow_and_runner_configuration_match(): workflow = yaml.safe_load((RUNNER / "sync-zpwiki.yml").read_text()) assert workflow["on"]["schedule"] == [{"cron": "0 3 * * *"}] assert workflow["on"]["push"]["branches"] == ["master"] assert not {"pull_request", "pull_request_target", "workflow_dispatch"} & workflow["on"].keys() job = workflow["jobs"]["sync-wiki"] compose = yaml.safe_load((RUNNER / "compose.yml").read_text()) runner = compose["services"]["runner"] assert runner["container_name"] == "zpwiki-sync" assert runner["environment"]["GITEA_RUNNER_NAME"] == "zpwiki-sync" assert runner["environment"]["GITEA_RUNNER_LABELS"].startswith(job["runs-on"] + ":docker://") assert "RUNNER_REGISTRATION_TOKEN" in runner["environment"]["GITEA_RUNNER_REGISTRATION_TOKEN"] step = job["steps"][-1] assert step["env"]["ZPWIKI_SYNC_SSH_KEY"] == "${{ secrets.ZPWIKI_SYNC_SSH_KEY }}" assert "raw/commit/$WORKFLOW_SHA/tools/git_runner/$file" in step["run"] assert "for file in sync_fork.sh ssh_known_hosts" in step["run"] assert "set +x" in step["run"] for step in job["steps"]: subprocess.run([step["shell"], "-n"], input=step["run"], text=True, check=True) @pytest.mark.parametrize("private_path", ["tools/git_runner/.env", "tools/git_runner/data/.runner"]) def test_runner_credentials_and_state_are_ignored_by_git(private_path): result = subprocess.run( ["git", "check-ignore", "--no-index", private_path], cwd=ROOT, capture_output=True, text=True, ) assert result.returncode == 0 def test_existing_key_file_updates_only_fork(repositories, tmp_path): source, fork, _ = repositories upstream_head = commit(source, "pending-with-local-key") key = tmp_path / "private-key" key.write_text("not-an-actual-key-local-transport-only") key.chmod(0o600) result = sync(repositories, "--key-file", str(key), token="") assert result.returncode == 0, result.stderr assert git(fork, "rev-parse", "master") == upstream_head assert git(source, "rev-parse", "HEAD") == upstream_head assert key.read_text() == "not-an-actual-key-local-transport-only"