81 lines
4.7 KiB
Python
81 lines
4.7 KiB
Python
import copy
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
SOURCE=Path(__file__).with_name('activate_receive_hooks.py').resolve()
|
|
spec=importlib.util.spec_from_file_location('activation',SOURCE)
|
|
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
|
|
BASE=json.loads(SOURCE.with_name('receive-hook-proposal.json').read_text())
|
|
|
|
class Native:
|
|
def __init__(self, root, proposal, fail=False): self.root=root;self.p=proposal;self.fail=fail
|
|
def preflight(self,p):
|
|
for x in p['artifacts']:
|
|
if not Path(x['source']).is_file(): raise ValueError('missing-artifact')
|
|
return {'rehearsal':True}
|
|
def archive(self,p):
|
|
out=io.BytesIO();checks=[]
|
|
with tarfile.open(fileobj=out,mode='w') as t:
|
|
for item in p['artifacts']:
|
|
content=Path(item['source']).read_bytes();i=tarfile.TarInfo(item['name']);i.size=len(content);i.mode=item['mode'];t.addfile(i,io.BytesIO(content));checks.append(item['sha256']+' '+item['name']+'\n')
|
|
content=''.join(checks).encode();i=tarfile.TarInfo('SHA256SUMS');i.size=len(content);t.addfile(i,io.BytesIO(content))
|
|
return out.getvalue()
|
|
def execute(self,p,*args,payload=None,timeout=45):
|
|
if args[0]=='wget': return b''
|
|
mapped=[x.replace('/data/',str(self.root/'data')+'/').replace('/tmp/'+p['directory_name'],str(self.root/'stage')) for x in args]
|
|
if self.fail and args[0]=='bash' and 'mktemp' in args[-1]:
|
|
self.fail=False
|
|
path=self.root/'data/git/gitea-repositories/coulomb'/ (p['repositories'][0]+'.git/hooks/pre-receive.d')
|
|
for field,source in [('hook_name','receive-profile-hook'),('veto_name','receive-probe-veto')]:
|
|
dest=path/p[field];dest.write_bytes(SOURCE.with_name(source).read_bytes());dest.chmod(0o755)
|
|
raise ValueError('injected-partial-install-failure')
|
|
r=subprocess.run(mapped,input=payload,capture_output=True,timeout=timeout)
|
|
if r.returncode: raise ValueError('local-command-refused')
|
|
return r.stdout.replace((str(self.root/'data')+'/').encode(),b'/data/')
|
|
|
|
class Tests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp=tempfile.TemporaryDirectory();self.root=Path(self.tmp.name);self.p=copy.deepcopy(BASE)
|
|
# Small inert tools are sufficient for installer/rollback protocol testing.
|
|
binary=self.root/'tool';binary.write_text('#!/bin/sh\nexit 0\n')
|
|
for item in self.p['artifacts'][:2]: item.update(source=str(binary),sha256=a.sha(binary))
|
|
self.p['existing_hook_hashes']={k:a.hashlib.sha256(b'#!/bin/sh\nexit 0\n').hexdigest() for k in self.p['existing_hook_hashes']}
|
|
for name in self.p['repositories']:
|
|
repo=self.root/'data/git/gitea-repositories/coulomb'/(name+'.git')
|
|
subprocess.run(['git','init','--bare','-q',str(repo)],check=True)
|
|
for rel in self.p['existing_hook_hashes']:
|
|
f=repo/'hooks'/rel;f.parent.mkdir(exist_ok=True);f.write_bytes(binary.read_bytes());f.chmod(0o755)
|
|
self.native=Native(self.root,self.p);self.proposal=self.root/'proposal.json';self.receipt=self.root/'receipt.json'
|
|
def tearDown(self):self.tmp.cleanup()
|
|
def runmain(self,*args):
|
|
self.proposal.write_text(json.dumps(self.p))
|
|
with patch.object(a,'helper',return_value=self.native),patch.object(a,'verify_pushes',return_value=[{'rehearsal_only':True}]),patch.object(sys,'argv',['activate','--proposal',str(self.proposal),'--receipt',str(self.receipt),*args]):a.main()
|
|
def test_activation_and_rollback_without_build_files(self):
|
|
self.runmain('--apply');a.inspect(self.native,self.p,applied=True)
|
|
self.assertEqual(json.loads(self.receipt.read_text())['result'],'verified')
|
|
for x in self.p['artifacts']:x['source']='/missing/build/artifact'
|
|
self.runmain('--rollback');a.inspect(self.native,self.p)
|
|
def test_partial_install_rolls_back_all_added_hooks(self):
|
|
self.native.fail=True
|
|
with self.assertRaises(ValueError):self.runmain('--apply')
|
|
a.inspect(self.native,self.p)
|
|
r=json.loads(self.receipt.read_text());self.assertEqual(r['rollback'],'prior hook inventory restored');self.assertTrue(r['temporary_directory_removed']);self.assertEqual(r['result'],'unverified')
|
|
def test_non_executable_hook_refused(self):
|
|
self.runmain('--apply')
|
|
f=self.root/'data/git/gitea-repositories/coulomb'/(self.p['repositories'][0]+'.git/hooks/pre-receive.d')/self.p['hook_name'];f.chmod(0o644)
|
|
with self.assertRaises(ValueError):a.inspect(self.native,self.p,applied=True)
|
|
def test_rollback_preserves_unknown_modification(self):
|
|
self.runmain('--apply')
|
|
f=self.root/'data/git/gitea-repositories/coulomb'/(self.p['repositories'][0]+'.git/hooks/pre-receive.d')/self.p['hook_name'];f.write_text('unknown change\n')
|
|
with self.assertRaises(ValueError):a.rollback(self.native,self.p)
|
|
self.assertEqual(f.read_text(),'unknown change\n')
|
|
|
|
if __name__=='__main__':unittest.main(verbosity=2)
|