dp-zp-agent/test/test_git_runner.py

245 lines
11 KiB
Python

"""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'
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/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