import importlib.util import json from pathlib import Path import subprocess import tempfile import unittest from unittest.mock import patch spec = importlib.util.spec_from_file_location('auto_deploy', Path(__file__).with_name('auto_deploy.py')) deploy = importlib.util.module_from_spec(spec) spec.loader.exec_module(deploy) class DeployTest(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.base = Path(self.temp.name) self.repo = self.base / 'source' self.repo.mkdir() self.git('init') self.git('symbolic-ref', 'HEAD', 'refs/heads/master') self.config = {'state': str(self.base / 'state'), 'backups': str(self.base / 'backups'), 'host': 'localhost', 'php': 'php', 'health': []} self.target = self.base / 'site' self.target.mkdir() self.settings = {'name': 'backend', 'url': str(self.repo), 'target': str(self.target)} def git(self, *args): return subprocess.check_output(['git', '-C', str(self.repo)] + list(args), stderr=subprocess.STDOUT) def write(self, name, value): p = self.repo / name p.parent.mkdir(parents=True, exist_ok=True) p.write_text(value) def commit(self): self.git('add', '-A') self.git('-c', 'user.name=Deploy Test', '-c', 'user.email=deploy@example.invalid', 'commit', '-m', 'fixture') def test_publish_and_repeat_without_republishing(self): self.write('public/app.txt', 'first') self.commit() self.assertTrue(deploy.publish(self.config, self.settings)) self.assertEqual((self.target / 'public/app.txt').read_text(), 'first') self.assertFalse(deploy.publish(self.config, self.settings)) def test_preserves_production_config_uploads_and_screen(self): for name in ['application/database.php', 'application/config.php', 'application/extra/site.php', 'public/uploads/photo.jpg', 'public/screen/index.html']: self.write(name, 'repository version') p = self.target / name p.parent.mkdir(parents=True, exist_ok=True) p.write_text('production version') self.write('public/app.txt', 'deploy me') self.commit() deploy.publish(self.config, self.settings) self.assertEqual((self.target / 'application/database.php').read_text(), 'production version') self.assertEqual((self.target / 'public/uploads/photo.jpg').read_text(), 'production version') self.assertEqual((self.target / 'public/screen/index.html').read_text(), 'production version') def test_health_failure_restores_files_and_does_not_advance_revision(self): self.write('public/app.txt', 'first') self.commit() deploy.publish(self.config, self.settings) state = (Path(self.config['state']) / 'backend.json').read_text() self.write('public/app.txt', 'broken') self.write('public/new.txt', 'new') self.commit() self.config['health'] = ['http://localhost/health'] with patch.object(deploy, 'healthcheck', side_effect=RuntimeError('unhealthy')): with self.assertRaises(RuntimeError): deploy.publish(self.config, self.settings) self.assertEqual((self.target / 'public/app.txt').read_text(), 'first') self.assertFalse((self.target / 'public/new.txt').exists()) self.assertEqual((Path(self.config['state']) / 'backend.json').read_text(), state) def test_deletion_only_removes_previously_deployed_files(self): self.write('public/app.txt', 'first') self.write('public/old.txt', 'old') self.commit() deploy.publish(self.config, self.settings) (self.target / 'public/unmanaged.txt').write_text('retain') (self.repo / 'public/old.txt').unlink() self.commit() deploy.publish(self.config, self.settings) self.assertFalse((self.target / 'public/old.txt').exists()) self.assertTrue((self.target / 'public/unmanaged.txt').exists()) def test_frontend_deploys_source_instead_of_stale_dist(self): self.settings['name'] = 'frontend' self.write('index.html', 'new screen') self.write('dist/index.html', 'stale build') self.write('js/app.js', 'export const value = 1;') self.commit() deploy.publish(self.config, self.settings) self.assertEqual((self.target / 'index.html').read_text(), 'new screen') self.assertFalse((self.target / 'dist').exists()) def test_rejects_paths_outside_the_site(self): with self.assertRaises(RuntimeError): deploy.destination(self.target, '../outside') if __name__ == '__main__': unittest.main()