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)