105 行
5.2 KiB
Python
105 行
5.2 KiB
Python
"""Run the generated installer migration in isolated volumes, never live paths."""
|
|
import os, pathlib, plistlib, shlex, subprocess, sys, tempfile, unittest
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
from installer_identity import inject_migration, PRODUCT
|
|
|
|
class InstallerIdentityTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory(prefix='beautygirl-installer-test.')
|
|
self.root = pathlib.Path(self.tmp.name)
|
|
self.old = self.root / pathlib.Path(PRODUCT['legacyProducts'][0]['installationPath']).relative_to('/')
|
|
self.new = self.root / pathlib.Path(PRODUCT['installationPath']).relative_to('/')
|
|
for app, info in [(self.old, PRODUCT['legacyProducts'][0]), (self.new, PRODUCT)]:
|
|
(app / 'Contents/PlugIns' / info['pluginName'] / 'Contents').mkdir(parents=True)
|
|
(app / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': info['bundleIdentifier']}))
|
|
(app / 'Contents/PlugIns' / info['pluginName'] / 'Contents/Info.plist').write_bytes(plistlib.dumps({'ProPlugPlugInList': [{'uuid': PRODUCT['filterUUID']}]}))
|
|
(app / 'payload-byte').write_bytes(b'original signed payload stand-in')
|
|
self.signer = self.root / 'codesign-fixture'
|
|
self.signer.write_text('#!/bin/zsh\napp="${argv[-1]}"\nif [[ -f "$app/.invalid" ]]; then exit 1; fi\n/bin/echo TeamIdentifier=B3W25FX5H5 >&2\n')
|
|
self.signer.chmod(0o755)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def run_installer(self, fail_registration=False):
|
|
base = '''#!/bin/zsh
|
|
set -eu
|
|
TARGET_VOLUME=@@VOLUME@@
|
|
CONSOLE_UID=501
|
|
CONSOLE_USER=fixture
|
|
FX_FAILURE=0
|
|
fx_verify_app() {
|
|
/bin/echo "$1" >> @@LOG@@
|
|
if [[ "$1" == "$BG_NEW_APP" && @@FAIL@@ == 1 ]]; then return 1; fi
|
|
return 0
|
|
}
|
|
trap - EXIT
|
|
fx_verify_app "${TARGET_VOLUME}/Library/Plug-Ins/FxPlug/FCP-BeautyGirl.app" || FX_FAILURE=1
|
|
if (( FX_FAILURE )); then exit 1; fi
|
|
exit 0
|
|
'''
|
|
base = base.replace('@@VOLUME@@', shlex.quote(str(self.root))).replace('@@LOG@@', shlex.quote(str(self.root / 'registration-calls'))).replace('@@FAIL@@', '1' if fail_registration else '0')
|
|
code = inject_migration(base)
|
|
# Only signature and user-session registration are isolated. Identity
|
|
# parsing, file moves, deletes, traps and rollback run unchanged.
|
|
code = code.replace('/usr/bin/codesign', shlex.quote(str(self.signer)))
|
|
code = code.replace('/bin/launchctl asuser', '/usr/bin/true asuser')
|
|
script = self.root / 'postinstall'
|
|
script.write_text(code)
|
|
subprocess.run(['/bin/zsh', '-n', str(script)], check=True, capture_output=True)
|
|
return subprocess.run(['/bin/zsh', str(script)], text=True, capture_output=True)
|
|
|
|
def test_upgrade_keeps_new_and_removes_old_and_recovery_copy(self):
|
|
self.assertEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(self.new.exists())
|
|
self.assertFalse(self.old.exists())
|
|
self.assertFalse(list((self.root / 'Library/Application Support').rglob('*.app')))
|
|
|
|
def test_registration_failure_restores_old_and_removes_new(self):
|
|
original = (self.old / 'payload-byte').read_bytes()
|
|
result = self.run_installer(True)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertFalse(self.new.exists())
|
|
self.assertEqual((self.old / 'payload-byte').read_bytes(), original)
|
|
self.assertIn(str(self.old), (self.root / 'registration-calls').read_text())
|
|
|
|
def test_invalid_new_signature_does_not_touch_old(self):
|
|
(self.new / '.invalid').touch()
|
|
self.assertNotEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(self.old.exists())
|
|
|
|
def test_foreign_old_bundle_id_is_preserved(self):
|
|
(self.old / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': 'com.unrelated.product'}))
|
|
self.assertNotEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(self.old.exists())
|
|
|
|
def test_invalid_old_signature_is_preserved(self):
|
|
(self.old / '.invalid').touch()
|
|
self.assertNotEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(self.old.exists())
|
|
|
|
def test_fresh_install_and_repeat_are_idempotent(self):
|
|
import shutil
|
|
shutil.rmtree(self.old)
|
|
self.assertEqual(self.run_installer().returncode, 0)
|
|
self.assertEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(self.new.exists())
|
|
|
|
def test_unrelated_neighbor_is_preserved(self):
|
|
unrelated = self.old.parent / 'OtherProduct.app'
|
|
unrelated.mkdir()
|
|
self.assertEqual(self.run_installer().returncode, 0)
|
|
self.assertTrue(unrelated.exists())
|
|
|
|
@unittest.skipUnless(os.environ.get('FCP_BEAUTYGIRL_POSTINSTALL'), 'Final PKG is not built yet')
|
|
def test_final_package_contains_the_tested_migration_block(self):
|
|
actual = pathlib.Path(os.environ['FCP_BEAUTYGIRL_POSTINSTALL']).read_text()
|
|
generated = inject_migration('\ntrap - EXIT\nfx_verify_app candidate\nexit 0\n')
|
|
start = generated.index('# FCP-BeautyGirl identity migration.')
|
|
end = generated.rindex('\nfx_verify_app ')
|
|
self.assertIn(generated[start:end], actual)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|