Adapt the audited publisher refusal changes, derive metadata from the signed APK, and constrain all regression staging to disposable repositories.
95 lines
5.2 KiB
Python
95 lines
5.2 KiB
Python
"""Publisher checks use a disposable Git repository and fake SDK only."""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
spec = importlib.util.spec_from_file_location('apk_metadata', ROOT / 'scripts/companion_apk_metadata.py')
|
|
metadata = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(metadata)
|
|
|
|
|
|
class PublisherTests(unittest.TestCase):
|
|
def test_actual_variant_version_is_retained(self):
|
|
for version in ('0.5.40-debug', '0.5.40-bump28-debug'):
|
|
self.assertEqual(metadata.parse_badging(
|
|
f"package: name='com.archipelago.app.debug' versionCode='60' versionName='{version}' platformBuildVersionName='15'"
|
|
), {'versionName': version, 'versionCode': 60})
|
|
|
|
def test_invalid_or_different_package_is_refused(self):
|
|
for line in ('', "package: name='foreign.app' versionCode='60' versionName='1'",
|
|
"package: name='com.archipelago.app.debug' versionCode='0' versionName='1'",
|
|
"package: name='com.archipelago.app.debug' versionCode='x' versionName='1'"):
|
|
with self.assertRaises(ValueError):
|
|
metadata.parse_badging(line)
|
|
|
|
def fixture(self, root, badging):
|
|
subprocess.run(['git', 'init', '-q', str(root)], check=True)
|
|
for name in ('scripts', 'Android/app/build/outputs/apk/debug', 'Android/app/src/main/res',
|
|
'neode-ui/public/packages', 'jdk/bin', 'sdk/build-tools/35.0'):
|
|
(root / name).mkdir(parents=True, exist_ok=True)
|
|
for name in ('publish-companion-apk.sh', 'companion_apk_metadata.py'):
|
|
shutil.copyfile(ROOT / 'scripts' / name, root / 'scripts' / name)
|
|
(root / 'Android/app/debug.keystore').write_text('fixture; not a key')
|
|
# Deliberately disagrees with the built APK: extraction must ignore it.
|
|
(root / 'Android/app/build.gradle.kts').write_text('versionName = "old"\nversionCode = 1\n')
|
|
(root / 'neode-ui/public/packages/archipelago-companion.apk').write_bytes(b'old apk')
|
|
(root / 'neode-ui/public/packages/archipelago-companion.json').write_text('{"old":true}\n')
|
|
programs = {
|
|
'jdk/bin/java': '#!/bin/sh\nexit 0\n',
|
|
'Android/gradlew': '#!/bin/sh\nprintf "new apk" > app/build/outputs/apk/debug/app-debug.apk\n',
|
|
'sdk/build-tools/35.0/zipalign': '#!/bin/sh\ncp "$4" "$5"\n',
|
|
'sdk/build-tools/35.0/apksigner': '''#!/bin/sh
|
|
if [ "$1" = verify ]; then
|
|
echo 'Verified using v1 scheme: true'
|
|
echo 'Verified using v2 scheme: true'
|
|
echo 'Verified using v3 scheme: true'
|
|
echo 'Signer #1 certificate SHA-256 digest: d622e07e7f474246e8e2bcee828ca9bf6c6cdf9434c065c58fe89bb4eec2664d'
|
|
fi
|
|
''',
|
|
'sdk/build-tools/35.0/aapt': '#!/bin/sh\ncat "$(dirname "$0")/badging.txt"\n',
|
|
}
|
|
for name, source in programs.items():
|
|
(root / name).write_text(source)
|
|
(root / name).chmod(0o700)
|
|
(root / 'sdk/build-tools/35.0/badging.txt').write_text(badging)
|
|
return dict(os.environ, JAVA_HOME=str(root / 'jdk'), ANDROID_HOME=str(root / 'sdk'))
|
|
|
|
def run_publisher(self, root, env):
|
|
return subprocess.run(['bash', str(root / 'scripts/publish-companion-apk.sh')],
|
|
cwd=root, env=env, capture_output=True, text=True, timeout=30)
|
|
|
|
def test_success_stages_exact_apk_version_only_in_fixture(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
env = self.fixture(root, "package: name='com.archipelago.app.debug' versionCode='60' versionName='0.5.40-bump28-debug'\n")
|
|
result = self.run_publisher(root, env)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(json.loads((root / 'neode-ui/public/packages/archipelago-companion.json').read_text()),
|
|
{'versionName': '0.5.40-bump28-debug', 'versionCode': 60})
|
|
staged = subprocess.check_output(['git', 'diff', '--cached', '--name-only'], cwd=root, text=True).splitlines()
|
|
self.assertEqual(set(staged), {'neode-ui/public/packages/archipelago-companion.apk',
|
|
'neode-ui/public/packages/archipelago-companion.json'})
|
|
|
|
def test_refusal_preserves_old_pair_and_stages_nothing(self):
|
|
for missing_sdk in (False, True):
|
|
with self.subTest(missing_sdk=missing_sdk), tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
env = self.fixture(root, 'invalid badging')
|
|
if missing_sdk:
|
|
env['ANDROID_HOME'] = str(root / 'missing')
|
|
result = self.run_publisher(root, env)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual((root / 'neode-ui/public/packages/archipelago-companion.apk').read_bytes(), b'old apk')
|
|
self.assertEqual(json.loads((root / 'neode-ui/public/packages/archipelago-companion.json').read_text()), {'old': True})
|
|
self.assertEqual(subprocess.check_output(['git', 'diff', '--cached', '--name-only'], cwd=root), b'')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|