"""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 / 'scripts/git_runner' 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 test_runner_init_preserves_existing_private_token(tmp_path): for name in ['runner.sh', 'runner.env.example']: shutil.copy2(RUNNER / name, tmp_path / name) command = ['bash', str(tmp_path / 'runner.sh'), 'init'] subprocess.run(command, check=True, capture_output=True) env = tmp_path / '.env' env.write_text('RUNNER_REGISTRATION_TOKEN=private-test-value\n') env.chmod(0o644) result = subprocess.run(command, check=True, capture_output=True, text=True) assert env.read_text() == 'RUNNER_REGISTRATION_TOKEN=private-test-value\n' assert stat.S_IMODE(env.stat().st_mode) == 0o600 assert stat.S_IMODE((tmp_path / 'data').stat().st_mode) == 0o700 assert 'private-test-value' not in result.stdout + result.stderr def test_runner_helper_targets_only_its_own_compose_project(tmp_path): for name in ['runner.sh', 'runner.env.example', 'compose.yml']: shutil.copy2(RUNNER / name, tmp_path / name) subprocess.run(['bash', str(tmp_path / 'runner.sh'), 'init'], check=True, capture_output=True) (tmp_path / '.env').write_text('RUNNER_REGISTRATION_TOKEN=private-test-value\n') 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(tmp_path / 'runner.sh'), '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(tmp_path / 'compose.yml') assert 'private-test-value' not in ' '.join(command) 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['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/scripts/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', ['scripts/git_runner/.env','scripts/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