import itertools import os import platform import re import shlex import shutil import subprocess import sys import threading import unittest from datetime import datetime, timezone from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path try: from androguard.core.bytecodes.apk import get_apkid # androguard <4 except ModuleNotFoundError: from androguard.core.apk import get_apkid from fdroidserver._yaml import yaml, yaml_dumper from .shared_test_code import mkdir_testfiles # TODO: port generic tests that use index.xml to index-v2 (test that # explicitly test index-v0 should still use index.xml) basedir = Path(__file__).parent FILES = basedir try: WORKSPACE = Path(os.environ["WORKSPACE"]) except KeyError: WORKSPACE = basedir.parent from fdroidserver import common conf = {"sdk_path": os.getenv("ANDROID_HOME", "")} common.find_apksigner(conf) USE_APKSIGNER = "apksigner" in conf @unittest.skipIf(sys.byteorder == 'big', 'androguard is not ported to big-endian') class IntegrationTest(unittest.TestCase): @classmethod def setUpClass(cls): try: cls.fdroid_cmd = shlex.split(os.environ["fdroid"]) except KeyError: cls.fdroid_cmd = [WORKSPACE / "fdroid"] os.environ.update( { "GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "no@mail", "GIT_COMMITTER_NAME": "Test", "GIT_COMMITTER_EMAIL": "no@mail", "GIT_ALLOW_PROTOCOL": "file:https", } ) def setUp(self): self.prev_cwd = Path() self.testdir = mkdir_testfiles(WORKSPACE, self) self.tmp_repo_root = self.testdir / "fdroid" self.tmp_repo_root.mkdir(parents=True) os.chdir(self.tmp_repo_root) def tearDown(self): os.chdir(self.prev_cwd) shutil.rmtree(self.testdir) def assert_run(self, *args, **kwargs): proc = subprocess.run(*args, **kwargs) self.assertEqual(proc.returncode, 0) return proc def assert_run_fail(self, *args, **kwargs): proc = subprocess.run(*args, **kwargs) self.assertNotEqual(proc.returncode, 0) return proc @staticmethod def update_yaml(path, items, replace=False): """Update a .yml file, e.g. config.yml, with the given items.""" doc = {} if not replace: try: with open(path) as f: doc = yaml.load(f) except FileNotFoundError: pass doc.update(items) with open(path, "w") as f: yaml_dumper.dump(doc, f) @staticmethod def remove_lines(path, unwanted_strings): """Remove the lines in the path that contain the unwanted strings.""" def contains_unwanted(line, unwanted_strings): for str in unwanted_strings: if str in line: return True return False with open(path) as f: filtered = [ line for line in f if not contains_unwanted(line, unwanted_strings) ] with open(path, "w") as f: for line in filtered: f.write(line) @staticmethod def copy_apks_into_repo(): def to_skip(name): for str in [ "unaligned", "unsigned", "badsig", "badcert", "bad-unicode", "janus.apk", ]: if str in name: return True return False for f in FILES.glob("*.apk"): if not to_skip(f.name): appid, versionCode, _ignored = get_apkid(f) shutil.copy( f, Path("repo") / common.get_release_apk_filename(appid, versionCode), ) @staticmethod def create_fake_android_home(path): (path / "tools").mkdir() (path / "platform-tools").mkdir() (path / "build-tools/34.0.0").mkdir(parents=True) (path / "build-tools/34.0.0/aapt").touch() def fdroid_init_with_prebuilt_keystore(self, keystore_path=FILES / "keystore.jks"): self.assert_run( self.fdroid_cmd + ["init", "--keystore", keystore_path, "--repo-keyalias", "sova"] ) self.update_yaml( common.CONFIG_FILE, { "keystorepass": "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=", "keypass": "r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=", }, ) @unittest.skipUnless(USE_APKSIGNER, "requires apksigner") def test_run_process_when_building_and_signing_are_on_separate_machines(self): shutil.copy(FILES / "keystore.jks", "keystore.jks") self.fdroid_init_with_prebuilt_keystore("keystore.jks") self.update_yaml( common.CONFIG_FILE, { "make_current_version_link": True, "keydname": "CN=Birdman, OU=Cell, O=Alcatraz, L=Alcatraz, S=California, C=US", }, ) Path("metadata").mkdir() shutil.copy(FILES / "metadata/info.guardianproject.urzip.yml", "metadata") Path("unsigned").mkdir() shutil.copy( FILES / "urzip-release-unsigned.apk", "unsigned/info.guardianproject.urzip_100.apk", ) self.assert_run(self.fdroid_cmd + ["publish", "--verbose"]) self.assert_run(self.fdroid_cmd + ["update", "--verbose", "--nosign"]) self.assert_run(self.fdroid_cmd + ["signindex", "--verbose"]) self.assertIn( '', Path("repo/index.xml").read_text(), ) self.assertTrue(Path("repo/index.jar").is_file()) self.assertTrue(Path("repo/index-v1.jar").is_file()) apkcache = Path("tmp/apkcache.json") self.assertTrue(apkcache.is_file()) self.assertTrue(apkcache.stat().st_size > 0) self.assertTrue(Path("urzip.apk").is_symlink()) def test_utf8_metadata(self): self.fdroid_init_with_prebuilt_keystore() self.update_yaml( common.CONFIG_FILE, { "repo_description": "获取已安装在您的设备上的应用的", "mirrors": ["https://foo.bar/fdroid", "http://secret.onion/fdroid"], }, ) shutil.copy(FILES / "urzip.apk", "repo") shutil.copy(FILES / "bad-unicode-πÇÇ现代通用字-български-عربي1.apk", "repo") Path("metadata").mkdir() shutil.copy(FILES / "metadata/info.guardianproject.urzip.yml", "metadata") self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assert_run(self.fdroid_cmd + ["update"]) def test_copy_git_import_and_run_fdroid_scanner_on_it(self): url = "https://gitlab.com/fdroid/ci-test-app.git" Path("metadata").mkdir() self.update_yaml( "metadata/org.fdroid.ci.test.app.yml", { "AutoName": "Just A Test", "WebSite": None, "Builds": [ { "versionName": "0.3", "versionCode": 300, "commit": "0.3", "subdir": "app", "gradle": ["yes"], } ], "Repo": url, "RepoType": "git", }, ) self.assert_run(["git", "clone", url, "build/org.fdroid.ci.test.app"]) self.assert_run( self.fdroid_cmd + ["scanner", "org.fdroid.ci.test.app", "--verbose"] ) @unittest.skipUnless(shutil.which("gpg"), "requires command line gpg") def test_copy_repo_generate_java_gpg_keys_update_and_gpgsign(self): """Needs tricks to make gpg-agent run in a test harness.""" self.fdroid_init_with_prebuilt_keystore() shutil.copytree(FILES / "repo", "repo", dirs_exist_ok=True) for dir in ["config", "metadata"]: shutil.copytree(FILES / dir, dir) # gpg requires a short path to the socket to talk to gpg-agent gnupghome = (WORKSPACE / '.testfiles/gnupghome').resolve() shutil.rmtree(gnupghome, ignore_errors=True) shutil.copytree(FILES / "gnupghome", gnupghome) os.chmod(gnupghome, 0o700) self.update_yaml( common.CONFIG_FILE, { "install_list": "org.adaway", "uninstall_list": ["com.android.vending", "com.facebook.orca"], "gpghome": str(gnupghome), "gpgkey": "CE71F7FB", "mirrors": [ "http://foobarfoobarfoobar.onion/fdroid", "https://foo.bar/fdroid", ], }, ) self.assert_run( self.fdroid_cmd + ["update", "--verbose", "--pretty"], env=os.environ | {"LC_MESSAGES": "C.UTF-8"}, ) index_xml = Path("repo/index.xml").read_text() self.assertIn("" in line) with open("repo/index.xml") as f: repo_cnt = sum(1 for line in f if "" in line) if USE_APKSIGNER: self.assertEqual(archive_cnt, 2) self.assertEqual(repo_cnt, 10) else: # This will fail when jarsigner allows MD5 for APK signatures self.assertEqual(archive_cnt, 5) self.assertEqual(repo_cnt, 7) @unittest.skipIf(USE_APKSIGNER, "runs only without apksigner") def test_per_app_archive_policy(self): self.fdroid_init_with_prebuilt_keystore() Path("metadata").mkdir() shutil.copy(FILES / "metadata/com.politedroid.yml", "metadata") for f in FILES.glob("repo/com.politedroid_[0-9].apk"): shutil.copy(f, "repo") self.update_yaml(common.CONFIG_FILE, {"archive_older": 3}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 4) self.assertEqual(archive_cnt, 0) self.assertIn("com.politedroid_3.apk", repo) self.assertIn("com.politedroid_4.apk", repo) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_6.apk", repo) self.assertTrue(Path("repo/com.politedroid_3.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_4.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) # enable one app in the repo self.update_yaml("metadata/com.politedroid.yml", {"ArchivePolicy": 1}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertEqual(archive_cnt, 3) self.assertIn("com.politedroid_6.apk", repo) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_5.apk", archive) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_4.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_5.apk").is_file()) # remove all apps from the repo self.update_yaml("metadata/com.politedroid.yml", {"ArchivePolicy": 0}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 0) self.assertEqual(archive_cnt, 4) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_5.apk", archive) self.assertIn("com.politedroid_6.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_4.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_5.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_6.apk").is_file()) self.assertFalse(Path("repo/com.politedroid_6.apk").exists()) # move back one from archive to the repo self.update_yaml("metadata/com.politedroid.yml", {"ArchivePolicy": 1}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertEqual(archive_cnt, 3) self.assertIn("com.politedroid_6.apk", repo) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_5.apk", archive) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_4.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_5.apk").is_file()) self.assertFalse(Path("archive/com.politedroid_6.apk").exists()) # set an earlier version as CVC and test that it's the only one not archived self.update_yaml("metadata/com.politedroid.yml", {"CurrentVersionCode": 5}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertEqual(archive_cnt, 3) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_6.apk", archive) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertFalse(Path("repo/com.politedroid_6.apk").exists()) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_4.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_6.apk").is_file()) def test_moving_old_apks_to_and_from_the_archive(self): self.fdroid_init_with_prebuilt_keystore() Path("metadata").mkdir() shutil.copy(FILES / "metadata/com.politedroid.yml", "metadata") self.remove_lines("metadata/com.politedroid.yml", ["ArchivePolicy:"]) for f in FILES.glob("repo/com.politedroid_[0-9].apk"): shutil.copy(f, "repo") self.update_yaml(common.CONFIG_FILE, {"archive_older": 3}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) self.assertEqual(repo_cnt, 3) self.assertIn("com.politedroid_4.apk", repo) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_6.apk", repo) self.assertTrue(Path("repo/com.politedroid_4.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(archive_cnt, 1) self.assertIn("com.politedroid_3.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.update_yaml(common.CONFIG_FILE, {"archive_older": 1}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertIn("com.politedroid_6.apk", repo) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(archive_cnt, 3) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_5.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_4.apk").is_file()) self.assertTrue(Path("archive/com.politedroid_5.apk").is_file()) # disabling deletes from the archive metadata_path = Path("metadata/com.politedroid.yml") metadata = metadata_path.read_text() metadata = re.sub( "versionCode: 4", "versionCode: 4\n disable: testing deletion", metadata ) metadata_path.write_text(metadata) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertIn("com.politedroid_6.apk", repo) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(archive_cnt, 2) self.assertIn("com.politedroid_3.apk", archive) self.assertNotIn("com.politedroid_4.apk", archive) self.assertIn("com.politedroid_5.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertFalse(Path("archive/com.politedroid_4.apk").exists()) self.assertTrue(Path("archive/com.politedroid_5.apk").is_file()) # disabling deletes from the repo, and promotes one from the archive metadata = re.sub( "versionCode: 6", "versionCode: 6\n disable: testing deletion", metadata ) metadata_path.write_text(metadata) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) self.assertEqual(repo_cnt, 1) self.assertIn("com.politedroid_5.apk", repo) self.assertNotIn("com.politedroid_6.apk", repo) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertFalse(Path("repo/com.politedroid_6.apk").exists()) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(archive_cnt, 1) self.assertIn("com.politedroid_3.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertFalse(Path("archive/com.politedroid_6.apk").exists()) def test_that_verify_can_succeed_and_fail(self): Path("tmp").mkdir() Path("unsigned").mkdir() shutil.copy(FILES / "repo/com.politedroid_6.apk", "tmp") shutil.copy(FILES / "repo/com.politedroid_6.apk", "unsigned") self.assert_run( self.fdroid_cmd + ["verify", "--reuse-remote-apk", "--verbose", "com.politedroid"] ) # force a fail shutil.copy( FILES / "repo/com.politedroid_5.apk", "unsigned/com.politedroid_6.apk" ) self.assert_run_fail( self.fdroid_cmd + ["verify", "--reuse-remote-apk", "--verbose", "com.politedroid"] ) def test_allowing_disabled_signatures_in_repo_and_archive(self): self.fdroid_init_with_prebuilt_keystore() self.update_yaml( common.CONFIG_FILE, {"allow_disabled_algorithms": True, "archive_older": 3} ) Path("metadata").mkdir() shutil.copy(FILES / "metadata/com.politedroid.yml", "metadata") self.update_yaml( "metadata/info.guardianproject.urzip.yml", {"Summary": "good test version of urzip"}, replace=True, ) self.update_yaml( "metadata/org.bitbucket.tickytacky.mirrormirror.yml", {"Summary": "good MD5 sig, disabled algorithm"}, replace=True, ) for f in Path("metadata").glob("*.yml"): self.remove_lines(f, ["ArchivePolicy:"]) for f in itertools.chain( FILES.glob("urzip-badsig.apk"), FILES.glob("org.bitbucket.tickytacky.mirrormirror_[0-9].apk"), FILES.glob("repo/com.politedroid_[0-9].apk"), ): shutil.copy(f, "repo") self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 6) self.assertEqual(archive_cnt, 2) self.assertIn("com.politedroid_4.apk", repo) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_6.apk", repo) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("org.bitbucket.tickytacky.mirrormirror_2.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_3.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_4.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_1.apk", archive) self.assertNotIn("urzip-badsig.apk", repo) self.assertNotIn("urzip-badsig.apk", archive) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_4.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_1.apk").is_file() ) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_2.apk").is_file() ) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_3.apk").is_file() ) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_4.apk").is_file() ) self.assertTrue(Path("archive/urzip-badsig.apk").is_file()) if not USE_APKSIGNER: self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) repo = Path("repo/index.xml").read_text() repo_cnt = sum(1 for line in repo.splitlines() if "" in line) archive = Path("archive/index.xml").read_text() archive_cnt = sum(1 for line in archive.splitlines() if "" in line) self.assertEqual(repo_cnt, 3) self.assertEqual(archive_cnt, 5) self.assertIn("com.politedroid_4.apk", repo) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_6.apk", repo) self.assertNotIn("urzip-badsig.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_1.apk", archive) self.assertIn("org.bitbucket.tickytacky.mirrormirror_2.apk", archive) self.assertIn("org.bitbucket.tickytacky.mirrormirror_3.apk", archive) self.assertIn("org.bitbucket.tickytacky.mirrormirror_4.apk", archive) self.assertIn("com.politedroid_3.apk", archive) self.assertNotIn("urzip-badsig.apk", archive) self.assertTrue(Path("repo/com.politedroid_4.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_1.apk").is_file() ) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_2.apk").is_file() ) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_3.apk").is_file() ) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_4.apk").is_file() ) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue(Path("archive/urzip-badsig.apk").is_file()) # test unarchiving when disabled_algorithms are allowed again self.update_yaml(common.CONFIG_FILE, {"allow_disabled_algorithms": True}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) with open("archive/index.xml") as f: archive_cnt = sum(1 for line in f if "" in line) with open("repo/index.xml") as f: repo_cnt = sum(1 for line in f if "" in line) self.assertEqual(repo_cnt, 6) self.assertEqual(archive_cnt, 2) self.assertIn("com.politedroid_4.apk", repo) self.assertIn("com.politedroid_5.apk", repo) self.assertIn("com.politedroid_6.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_2.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_3.apk", repo) self.assertIn("org.bitbucket.tickytacky.mirrormirror_4.apk", repo) self.assertNotIn("urzip-badsig.apk", repo) self.assertIn("com.politedroid_3.apk", archive) self.assertIn("org.bitbucket.tickytacky.mirrormirror_1.apk", archive) self.assertNotIn("urzip-badsig.apk", archive) self.assertTrue(Path("repo/com.politedroid_4.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_5.apk").is_file()) self.assertTrue(Path("repo/com.politedroid_6.apk").is_file()) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_2.apk").is_file() ) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_3.apk").is_file() ) self.assertTrue( Path("repo/org.bitbucket.tickytacky.mirrormirror_4.apk").is_file() ) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertTrue( Path("archive/org.bitbucket.tickytacky.mirrormirror_1.apk").is_file() ) self.assertTrue(Path("archive/urzip-badsig.apk").is_file()) def test_rename_apks_with_fdroid_update_rename_apks_opt_nosign_opt_for_speed(self): self.fdroid_init_with_prebuilt_keystore() self.update_yaml( common.CONFIG_FILE, { "keydname": "CN=Birdman, OU=Cell, O=Alcatraz, L=Alcatraz, S=California, C=US" }, ) Path("metadata").mkdir() shutil.copy(FILES / "metadata/info.guardianproject.urzip.yml", "metadata") shutil.copy( FILES / "urzip.apk", "repo/asdfiuhk urzip-πÇÇπÇÇ现代汉语通用字-български-عربي1234 ö.apk", ) self.assert_run( self.fdroid_cmd + ["update", "--rename-apks", "--pretty", "--nosign"] ) self.assertTrue(Path("repo/info.guardianproject.urzip_100.apk").is_file()) index_xml = Path("repo/index.xml").read_text() index_v1_json = Path("repo/index-v1.json").read_text() self.assertIn("info.guardianproject.urzip_100.apk", index_v1_json) self.assertIn("info.guardianproject.urzip_100.apk", index_xml) shutil.copy(FILES / "urzip-release.apk", "repo") self.assert_run( self.fdroid_cmd + ["update", "--rename-apks", "--pretty", "--nosign"] ) self.assertTrue(Path("repo/info.guardianproject.urzip_100.apk").is_file()) self.assertTrue( Path("repo/info.guardianproject.urzip_100_b4964fd.apk").is_file() ) index_xml = Path("repo/index.xml").read_text() index_v1_json = Path("repo/index-v1.json").read_text() self.assertIn("info.guardianproject.urzip_100.apk", index_v1_json) self.assertIn("info.guardianproject.urzip_100.apk", index_xml) self.assertIn("info.guardianproject.urzip_100_b4964fd.apk", index_v1_json) self.assertNotIn("info.guardianproject.urzip_100_b4964fd.apk", index_xml) shutil.copy(FILES / "urzip-release.apk", "repo") self.assert_run( self.fdroid_cmd + ["update", "--rename-apks", "--pretty", "--nosign"] ) self.assertTrue(Path("repo/info.guardianproject.urzip_100.apk").is_file()) self.assertTrue( Path("repo/info.guardianproject.urzip_100_b4964fd.apk").is_file() ) self.assertTrue( Path("duplicates/repo/info.guardianproject.urzip_100_b4964fd.apk").is_file() ) index_xml = Path("repo/index.xml").read_text() index_v1_json = Path("repo/index-v1.json").read_text() self.assertIn("info.guardianproject.urzip_100.apk", index_v1_json) self.assertIn("info.guardianproject.urzip_100.apk", index_xml) self.assertIn("info.guardianproject.urzip_100_b4964fd.apk", index_v1_json) self.assertNotIn("info.guardianproject.urzip_100_b4964fd.apk", index_xml) def test_for_added_date_being_set_correctly_for_repo_and_archive(self): self.fdroid_init_with_prebuilt_keystore() self.update_yaml(common.CONFIG_FILE, {"archive_older": 3}) Path("metadata").mkdir() Path("archive").mkdir() shutil.copy(FILES / "repo/com.politedroid_6.apk", "repo") shutil.copy(FILES / "repo/index-v2.json", "repo") shutil.copy(FILES / "repo/com.politedroid_5.apk", "archive") shutil.copy(FILES / "metadata/com.politedroid.yml", "metadata") # TODO: the timestamp of the oldest apk in the file should be used, even # if that doesn't exist anymore self.update_yaml("metadata/com.politedroid.yml", {"ArchivePolicy": 1}) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) timestamp = int(datetime(2017, 6, 23, tzinfo=timezone.utc).timestamp()) * 1000 index_v1_json = Path("repo/index-v1.json").read_text() self.assertIn(f'"added": {timestamp}', index_v1_json) # the archive will have the added timestamp for the app and for the apk, # both need to be there with open("archive/index-v1.json") as f: count = sum(1 for line in f if f'"added": {timestamp}' in line) self.assertEqual(count, 2) def test_whatsnew_from_fastlane_without_cvc_set(self): self.fdroid_init_with_prebuilt_keystore() Path("metadata/com.politedroid/en-US/changelogs").mkdir(parents=True) shutil.copy(FILES / "repo/com.politedroid_6.apk", "repo") shutil.copy(FILES / "metadata/com.politedroid.yml", "metadata") self.remove_lines("metadata/com.politedroid.yml", ["CurrentVersion:"]) Path("metadata/com.politedroid/en-US/changelogs/6.txt").write_text( "whatsnew test" ) self.assert_run(self.fdroid_cmd + ["update", "--pretty", "--nosign"]) index_v1_json = Path("repo/index-v1.json").read_text() self.assertIn("whatsnew test", index_v1_json) def test_metadata_checks(self): Path("repo").mkdir() shutil.copy(FILES / "urzip.apk", "repo") # this should fail because there is no metadata self.assert_run_fail(self.fdroid_cmd + ["build"]) Path("metadata").mkdir() shutil.copy(FILES / "metadata/org.smssecure.smssecure.yml", "metadata") self.assert_run(self.fdroid_cmd + ["readmeta"]) def test_ensure_commands_that_dont_need_the_jdk_work_without_a_jdk_configured(self): Path("repo").mkdir() Path("metadata").mkdir() self.update_yaml( "metadata/fake.yml", { "License": "GPL-2.0-only", "Summary": "Yup still fake", "Categories": ["Internet"], "Description": "this is fake", }, ) # fake that no JDKs are available self.update_yaml( common.CONFIG_FILE, {"categories": ["Internet"], "java_paths": {}}, replace=True, ) local_copy_dir = self.testdir / "local_copy_dir/fdroid" (local_copy_dir / "repo").mkdir(parents=True) self.update_yaml( common.CONFIG_FILE, {"local_copy_dir": str(local_copy_dir.resolve())} ) subprocess.run(self.fdroid_cmd + ["checkupdates", "--allow-dirty"]) if shutil.which("gpg"): self.assert_run(self.fdroid_cmd + ["gpgsign"]) self.assert_run(self.fdroid_cmd + ["lint"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assert_run(self.fdroid_cmd + ["rewritemeta", "fake"]) self.assert_run(self.fdroid_cmd + ["deploy"]) self.assert_run(self.fdroid_cmd + ["scanner"]) # run these to get their output, but the are not setup, so don't fail subprocess.run(self.fdroid_cmd + ["build"]) subprocess.run(self.fdroid_cmd + ["import"]) subprocess.run(self.fdroid_cmd + ["install", "-n"]) def test_config_checks_of_local_copy_dir(self): self.assert_run(self.fdroid_cmd + ["init"]) self.assert_run(self.fdroid_cmd + ["update", "--create-metadata", "--verbose"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) local_copy_dir = (self.testdir / "local_copy_dir/fdroid").resolve() local_copy_dir.mkdir(parents=True) self.assert_run( self.fdroid_cmd + ["deploy", "--local-copy-dir", local_copy_dir] ) self.assert_run( self.fdroid_cmd + ["deploy", "--local-copy-dir", local_copy_dir, "--verbose"] ) # this should fail because thisisnotanabsolutepath is not an absolute path self.assert_run_fail( self.fdroid_cmd + ["deploy", "--local-copy-dir", "thisisnotanabsolutepath"] ) # this should fail because the path doesn't end with "fdroid" self.assert_run_fail( self.fdroid_cmd + [ "deploy", "--local-copy-dir", "/tmp/IReallyDoubtThisPathExistsasdfasdf", # nosec B108 ] ) # this should fail because the dirname path does not exist self.assert_run_fail( self.fdroid_cmd + [ "deploy", "--local-copy-dir", "/tmp/IReallyDoubtThisPathExistsasdfasdf/fdroid", # nosec B108 ] ) def test_setup_a_new_repo_from_scratch_using_android_home_and_do_a_local_sync(self): self.fdroid_init_with_prebuilt_keystore() self.copy_apks_into_repo() self.assert_run(self.fdroid_cmd + ["update", "--create-metadata", "--verbose"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assertIn(" 0) def test_check_duplicate_files_are_properly_handled_by_fdroid_update(self): self.fdroid_init_with_prebuilt_keystore() Path("metadata").mkdir() shutil.copy(FILES / "metadata/obb.mainpatch.current.yml", "metadata") shutil.copy(FILES / "repo/obb.mainpatch.current_1619.apk", "repo") shutil.copy( FILES / "repo/obb.mainpatch.current_1619_another-release-key.apk", "repo" ) self.assert_run(self.fdroid_cmd + ["update", "--pretty"]) index_xml = Path("repo/index.xml").read_text() index_v1_json = Path("repo/index-v1.json").read_text() self.assertNotIn( "obb.mainpatch.current_1619_another-release-key.apk", index_xml ) self.assertIn("obb.mainpatch.current_1619.apk", index_xml) self.assertIn("obb.mainpatch.current_1619.apk", index_v1_json) self.assertIn( "obb.mainpatch.current_1619_another-release-key.apk", index_v1_json ) # die if there are exact duplicates shutil.copy(FILES / "repo/obb.mainpatch.current_1619.apk", "repo/duplicate.apk") self.assert_run_fail(self.fdroid_cmd + ["update"]) def test_setup_new_repo_from_scratch_using_android_home_env_var_putting_apks_in_repo_first( self, ): Path("repo").mkdir() self.copy_apks_into_repo() self.fdroid_init_with_prebuilt_keystore() self.assert_run(self.fdroid_cmd + ["update", "--create-metadata", "--verbose"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assertIn(" 0) def test_setup_a_new_repo_manually_and_generate_a_keystore(self): self.assertFalse(Path("keystore.p12").exists()) # this should fail because this repo has no keystore self.assert_run_fail(self.fdroid_cmd + ["update"]) self.assert_run(self.fdroid_cmd + ["update", "--create-key"]) self.assertTrue(Path("keystore.p12").is_file()) self.copy_apks_into_repo() self.assert_run(self.fdroid_cmd + ["update", "--create-metadata", "--verbose"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assertIn(" 0) def test_setup_a_new_repo_from_scratch_generate_a_keystore_then_add_apk_and_update( self, ): self.assert_run(self.fdroid_cmd + ["init", "--keystore", "keystore.p12"]) self.assertTrue(Path("keystore.p12").is_file()) self.copy_apks_into_repo() self.assert_run(self.fdroid_cmd + ["update", "--create-metadata", "--verbose"]) self.assert_run(self.fdroid_cmd + ["readmeta"]) self.assertIn(" 0) self.assertIn(" 0) # now set fake repo_keyalias self.update_yaml(common.CONFIG_FILE, {"repo_keyalias": "fake"}) # this should fail because this repo has a bad repo_keyalias self.assert_run_fail(self.fdroid_cmd + ["update"]) # this should fail because a keystore is already there self.assert_run_fail(self.fdroid_cmd + ["update", "--create-key"]) # now actually create the key with the existing settings Path("keystore.jks").unlink() self.assert_run(self.fdroid_cmd + ["update", "--create-key"]) self.assertTrue(Path("keystore.jks").is_file()) def test_setup_a_new_repo_from_scratch_using_android_home_env_var_with_git_mirror( self, ): server_git_mirror = self.testdir / "server_git_mirror" server_git_mirror.mkdir() self.assert_run( ["git", "-C", server_git_mirror, "init", "--initial-branch", "master"] ) self.assert_run( [ "git", "-C", server_git_mirror, "config", "receive.denyCurrentBranch", "updateInstead", ] ) self.fdroid_init_with_prebuilt_keystore() self.update_yaml( common.CONFIG_FILE, {"archive_older": 3, "servergitmirrors": str(server_git_mirror)}, ) for f in FILES.glob("repo/com.politedroid_[345].apk"): shutil.copy(f, "repo") self.assert_run(self.fdroid_cmd + ["update", "--create-metadata"]) self.assert_run(self.fdroid_cmd + ["deploy"]) git_mirror = Path("git-mirror") self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_3.apk").is_file()) self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_4.apk").is_file()) self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_5.apk").is_file()) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_3.apk").is_file() ) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_4.apk").is_file() ) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_5.apk").is_file() ) (git_mirror / ".git/test-stamp").write_text(str(datetime.now())) # add one more APK to trigger archiving shutil.copy(FILES / "repo/com.politedroid_6.apk", "repo") self.assert_run(self.fdroid_cmd + ["update"]) self.assert_run(self.fdroid_cmd + ["deploy"]) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertFalse((git_mirror / "fdroid/archive/com.politedroid_3.apk").exists()) self.assertFalse( (server_git_mirror / "fdroid/archive/com.politedroid_3.apk").exists() ) self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_4.apk").is_file()) self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_5.apk").is_file()) self.assertTrue((git_mirror / "fdroid/repo/com.politedroid_6.apk").is_file()) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_4.apk").is_file() ) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_5.apk").is_file() ) self.assertTrue( (server_git_mirror / "fdroid/repo/com.politedroid_6.apk").is_file() ) before = sum( f.stat().st_size for f in (git_mirror / ".git").glob("**/*") if f.is_file() ) self.update_yaml(common.CONFIG_FILE, {"git_mirror_size_limit": "60kb"}) self.assert_run(self.fdroid_cmd + ["update"]) self.assert_run(self.fdroid_cmd + ["deploy"]) self.assertTrue(Path("archive/com.politedroid_3.apk").is_file()) self.assertFalse( (server_git_mirror / "fdroid/archive/com.politedroid_3.apk").exists() ) after = sum( f.stat().st_size for f in (git_mirror / ".git").glob("**/*") if f.is_file() ) self.assertFalse((git_mirror / ".git/test-stamp").exists()) self.assert_run(["git", "-C", git_mirror, "gc"]) self.assert_run(["git", "-C", server_git_mirror, "gc"]) self.assertGreater(before, after) def test_sign_binary_repo_in_offline_box_then_publishing_from_online_box(self): offline_root = self.testdir / "offline_root" offline_root.mkdir() local_copy_dir = self.testdir / "local_copy_dir/fdroid" local_copy_dir.mkdir(parents=True) online_root = self.testdir / "online_root" online_root.mkdir() server_web_root = self.testdir / "server_web_root/fdroid" server_web_root.mkdir(parents=True) # create offline binary transparency log (offline_root / "binary_transparency").mkdir() os.chdir(offline_root / "binary_transparency") self.assert_run(["git", "init", "--initial-branch", "master"]) # fake git remote server for binary transparency log binary_transparency_remote = self.testdir / "binary_transparency_remote" binary_transparency_remote.mkdir() # fake git remote server for repo mirror server_git_mirror = self.testdir / "server_git_mirror" server_git_mirror.mkdir() os.chdir(server_git_mirror) self.assert_run(["git", "init", "--initial-branch", "master"]) self.assert_run(["git", "config", "receive.denyCurrentBranch", "updateInstead"]) os.chdir(offline_root) self.fdroid_init_with_prebuilt_keystore() shutil.copytree(FILES / "repo", "repo", dirs_exist_ok=True) shutil.copytree(FILES / "metadata", "metadata") Path("unsigned").mkdir() shutil.copy(FILES / "urzip-release-unsigned.apk", "unsigned") self.update_yaml( common.CONFIG_FILE, { "archive_older": 3, "mirrors": [ "http://foo.bar/fdroid", "http://asdflkdsfjafdsdfhkjh.onion/fdroid", ], "servergitmirrors": str(server_git_mirror), "local_copy_dir": str(local_copy_dir), }, ) self.assert_run(self.fdroid_cmd + ["update", "--pretty"]) index_xml = Path("repo/index.xml").read_text() self.assertIn("", index_xml) mirror_cnt = sum(1 for line in index_xml.splitlines() if "" in line) self.assertEqual(mirror_cnt, 2) archive_xml = Path("archive/index.xml").read_text() self.assertIn("/fdroid/archive", archive_xml) mirror_cnt = sum(1 for line in archive_xml.splitlines() if "" in line) self.assertEqual(mirror_cnt, 2) os.chdir("binary_transparency") proc = self.assert_run( ["git", "rev-list", "--count", "HEAD"], capture_output=True ) self.assertEqual(int(proc.stdout), 1) os.chdir(offline_root) self.assert_run(self.fdroid_cmd + ["deploy", "--verbose"]) self.assertTrue( Path(local_copy_dir / "unsigned/urzip-release-unsigned.apk").is_file() ) self.assertIn( "