diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94d49ed2..de48b3c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -246,6 +246,7 @@ black: tests/lint.TestCase tests/metadata.TestCase tests/ndk-release-checksums.py + tests/nightly.TestCase tests/rewritemeta.TestCase tests/scanner.TestCase tests/signindex.TestCase diff --git a/MANIFEST.in b/MANIFEST.in index 048c139e..87af42b9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -42,6 +42,7 @@ include locale/zh_Hans/LC_MESSAGES/fdroidserver.po include locale/zh_Hant/LC_MESSAGES/fdroidserver.po include makebuildserver include README.md +include tests/aosp_testkey_debug.keystore include tests/apk.embedded_1.apk include tests/bad-unicode-*.apk include tests/build.TestCase diff --git a/fdroidserver/nightly.py b/fdroidserver/nightly.py index bfeca16f..14db7fbd 100644 --- a/fdroidserver/nightly.py +++ b/fdroidserver/nightly.py @@ -25,6 +25,7 @@ import os import paramiko import platform import shutil +import ssl import subprocess import sys import tempfile @@ -47,7 +48,11 @@ DISTINGUISHED_NAME = 'CN=Android Debug,O=Android,C=US' NIGHTLY = '-nightly' -def _ssh_key_from_debug_keystore(keystore=KEYSTORE_FILE): +def _ssh_key_from_debug_keystore(keystore=None): + if keystore is None: + # set this here so it can be overridden in the tests + # TODO convert this to a class to get rid of this nonsense + keystore = KEYSTORE_FILE tmp_dir = tempfile.mkdtemp(prefix='.') privkey = os.path.join(tmp_dir, '.privkey') key_pem = os.path.join(tmp_dir, '.key.pem') @@ -94,10 +99,17 @@ def _ssh_key_from_debug_keystore(keystore=KEYSTORE_FILE): ], env={'LC_ALL': 'C.UTF-8'}, ) + + # OpenSSL 3.0 changed the default output format from PKCS#1 to + # PKCS#8, which paramiko does not support. + # https://www.openssl.org/docs/man3.0/man1/openssl-rsa.html#traditional + # https://github.com/paramiko/paramiko/issues/1015 + openssl_rsa_cmd = ['openssl', 'rsa'] + if ssl.OPENSSL_VERSION_INFO[0] >= 3: + openssl_rsa_cmd += ['-traditional'] subprocess.check_call( - [ - 'openssl', - 'rsa', + openssl_rsa_cmd + + [ '-in', key_pem, '-out', diff --git a/tests/aosp_testkey_debug.keystore b/tests/aosp_testkey_debug.keystore new file mode 100644 index 00000000..ecbdcb4d Binary files /dev/null and b/tests/aosp_testkey_debug.keystore differ diff --git a/tests/nightly.TestCase b/tests/nightly.TestCase index cf3ccf8b..c5fbf0cd 100755 --- a/tests/nightly.TestCase +++ b/tests/nightly.TestCase @@ -1,12 +1,19 @@ #!/usr/bin/env python3 import inspect +import logging import optparse import os import requests +import shutil import sys +import tempfile +import time import unittest +from pathlib import Path +from unittest.mock import patch + localmodule = os.path.realpath( os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..') ) @@ -17,7 +24,51 @@ if localmodule not in sys.path: from fdroidserver import common, nightly +AOSP_TESTKEY_DEBUG_KEYSTORE_KEY_FILE_NAME = ( + 'debug_keystore_k47SVrA85+oMZAexHc62PkgvIgO8TJBYN00U82xSlxc_id_rsa' +) + + +class Options: + allow_disabled_algorithms = False + clean = False + delete_unknown = False + nosign = False + pretty = True + rename_apks = False + verbose = False + + class NightlyTest(unittest.TestCase): + + basedir = Path(__file__).resolve().parent + path = os.environ['PATH'] + + def setUp(self): + logging.basicConfig(level=logging.WARNING) + self.basedir = Path(localmodule) / 'tests' + self.testroot = Path(localmodule) / '.testfiles' + self.testroot.mkdir(exist_ok=True) + os.chdir(self.basedir) + self.tempdir = tempfile.TemporaryDirectory( + str(time.time()), self._testMethodName + '_', self.testroot + ) + self.testdir = Path(self.tempdir.name) + self.home = self.testdir / 'home' + self.home.mkdir() + self.dot_android = self.home / '.android' + nightly.KEYSTORE_FILE = str(self.dot_android / 'debug.keystore') + + def tearDown(self): + self.tempdir.cleanup() + + def _copy_test_debug_keystore(self): + self.dot_android.mkdir() + shutil.copy( + self.basedir / 'aosp_testkey_debug.keystore', + self.dot_android / 'debug.keystore', + ) + def test_get_repo_base_url(self): for clone_url, repo_git_base, result in [ ( @@ -37,6 +88,63 @@ class NightlyTest(unittest.TestCase): # gitlab.com often returns 403 Forbidden from their cloudflare restrictions self.assertTrue(r.status_code in (200, 403), 'should not be a redirect') + @patch.dict(os.environ, clear=True) + def test_ssh_key_from_debug_keystore(self): + os.environ['HOME'] = str(self.home) + os.environ['PATH'] = self.path + ssh_private_key_file = nightly._ssh_key_from_debug_keystore( + self.basedir / 'aosp_testkey_debug.keystore' + ) + with open(ssh_private_key_file) as fp: + assert '-----BEGIN RSA PRIVATE KEY-----' in fp.read() + with open(ssh_private_key_file + '.pub') as fp: + assert fp.read(8) == 'ssh-rsa ' + + @patch.dict(os.environ, clear=True) + @patch('sys.argv', ['fdroid nightly', '--verbose']) + def test_main_empty_dot_android(self): + """Test that it exits with an error when ~/.android is empty""" + os.environ['HOME'] = str(self.home) + os.environ['PATH'] = self.path + with self.assertRaises(SystemExit) as cm: + nightly.main() + self.assertEqual(cm.exception.code, 1) + + @patch.dict(os.environ, clear=True) + @patch('sys.argv', ['fdroid nightly', '--verbose']) + def test_main_empty_dot_ssh(self): + """Test that it does not create ~/.ssh if it does not exist + + Careful! If the test env is wrong, it can mess up the local + SSH setup. + + """ + dot_ssh = self.home / '.ssh' + self._copy_test_debug_keystore() + os.environ['HOME'] = str(self.home) + os.environ['PATH'] = self.path + assert not dot_ssh.exists() + nightly.main() + assert not dot_ssh.exists() + + @patch.dict(os.environ, clear=True) + @patch('sys.argv', ['fdroid nightly', '--verbose']) + def test_main_on_user_machine(self): + """Test that `fdroid nightly` runs on the user's machine + + Careful! If the test env is wrong, it can mess up the local + SSH setup. + + """ + dot_ssh = self.home / '.ssh' + dot_ssh.mkdir() + self._copy_test_debug_keystore() + os.environ['HOME'] = str(self.home) + os.environ['PATH'] = self.path + nightly.main() + assert (dot_ssh / AOSP_TESTKEY_DEBUG_KEYSTORE_KEY_FILE_NAME).exists() + assert (dot_ssh / (AOSP_TESTKEY_DEBUG_KEYSTORE_KEY_FILE_NAME + '.pub')).exists() + if __name__ == "__main__": os.chdir(os.path.dirname(__file__))