#!/usr/bin/env python3

# http://www.drdobbs.com/testing/unit-testing-with-python/240165163

import inspect
import logging
import os
import shutil
import sys
import tempfile
import textwrap
import unittest
import yaml
from pathlib import Path
from unittest import mock

localmodule = os.path.realpath(
    os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')
)
print('localmodule: ' + localmodule)
if localmodule not in sys.path:
    sys.path.insert(0, localmodule)

from testcommon import TmpCwd

import fdroidserver.build
import fdroidserver.common
import fdroidserver.metadata
import fdroidserver.scanner
import fdroidserver.vmtools
from testcommon import mkdtemp, parse_args_for_test


class FakeProcess:
    output = 'fake output'
    returncode = 0

    def __init__(self, args, **kwargs):
        print('FakeFDroidPopen', args, kwargs)


class Options:
    keep_when_not_allowed = False


class BuildTest(unittest.TestCase):
    '''fdroidserver/build.py'''

    def setUp(self):
        logging.basicConfig(level=logging.DEBUG)
        self.basedir = os.path.join(localmodule, 'tests')
        os.chdir(self.basedir)
        fdroidserver.common.config = None
        fdroidserver.build.config = None
        fdroidserver.build.options = None
        self._td = mkdtemp()
        self.testdir = self._td.name

    def tearDown(self):
        os.chdir(self.basedir)
        self._td.cleanup()

    def create_fake_android_home(self, d):
        os.makedirs(os.path.join(d, 'build-tools'), exist_ok=True)
        os.makedirs(os.path.join(d, 'platform-tools'), exist_ok=True)
        os.makedirs(os.path.join(d, 'tools'), exist_ok=True)

    def test_get_apk_metadata(self):
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        fdroidserver.build.config = config
        try:
            config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
        except fdroidserver.exception.FDroidException:
            pass  # aapt is not required if androguard is present

        testcases = [
            (
                'repo/obb.main.twoversions_1101613.apk',
                'obb.main.twoversions',
                1101613,
                '0.1',
                None,
            ),
            (
                'org.bitbucket.tickytacky.mirrormirror_1.apk',
                'org.bitbucket.tickytacky.mirrormirror',
                1,
                '1.0',
                None,
            ),
            (
                'org.bitbucket.tickytacky.mirrormirror_2.apk',
                'org.bitbucket.tickytacky.mirrormirror',
                2,
                '1.0.1',
                None,
            ),
            (
                'org.bitbucket.tickytacky.mirrormirror_3.apk',
                'org.bitbucket.tickytacky.mirrormirror',
                3,
                '1.0.2',
                None,
            ),
            (
                'org.bitbucket.tickytacky.mirrormirror_4.apk',
                'org.bitbucket.tickytacky.mirrormirror',
                4,
                '1.0.3',
                None,
            ),
            (
                'org.dyndns.fules.ck_20.apk',
                'org.dyndns.fules.ck',
                20,
                'v1.6pre2',
                [
                    'arm64-v8a',
                    'armeabi',
                    'armeabi-v7a',
                    'mips',
                    'mips64',
                    'x86',
                    'x86_64',
                ],
            ),
            ('urzip.apk', 'info.guardianproject.urzip', 100, '0.1', None),
            ('urzip-badcert.apk', 'info.guardianproject.urzip', 100, '0.1', None),
            ('urzip-badsig.apk', 'info.guardianproject.urzip', 100, '0.1', None),
            ('urzip-release.apk', 'info.guardianproject.urzip', 100, '0.1', None),
            (
                'urzip-release-unsigned.apk',
                'info.guardianproject.urzip',
                100,
                '0.1',
                None,
            ),
            ('repo/com.politedroid_3.apk', 'com.politedroid', 3, '1.2', None),
            ('repo/com.politedroid_4.apk', 'com.politedroid', 4, '1.3', None),
            ('repo/com.politedroid_5.apk', 'com.politedroid', 5, '1.4', None),
            ('repo/com.politedroid_6.apk', 'com.politedroid', 6, '1.5', None),
            (
                'repo/duplicate.permisssions_9999999.apk',
                'duplicate.permisssions',
                9999999,
                '',
                None,
            ),
            (
                'repo/info.zwanenburg.caffeinetile_4.apk',
                'info.zwanenburg.caffeinetile',
                4,
                '1.3',
                None,
            ),
            (
                'repo/obb.main.oldversion_1444412523.apk',
                'obb.main.oldversion',
                1444412523,
                '0.1',
                None,
            ),
            (
                'repo/obb.mainpatch.current_1619_another-release-key.apk',
                'obb.mainpatch.current',
                1619,
                '0.1',
                None,
            ),
            (
                'repo/obb.mainpatch.current_1619.apk',
                'obb.mainpatch.current',
                1619,
                '0.1',
                None,
            ),
            (
                'repo/obb.main.twoversions_1101613.apk',
                'obb.main.twoversions',
                1101613,
                '0.1',
                None,
            ),
            (
                'repo/obb.main.twoversions_1101615.apk',
                'obb.main.twoversions',
                1101615,
                '0.1',
                None,
            ),
            (
                'repo/obb.main.twoversions_1101617.apk',
                'obb.main.twoversions',
                1101617,
                '0.1',
                None,
            ),
            (
                'repo/urzip-; Рахма́, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢·.apk',
                'info.guardianproject.urzip',
                100,
                '0.1',
                None,
            ),
        ]
        for apkfilename, appid, versionCode, versionName, nativecode in testcases:
            app = fdroidserver.metadata.App()
            app.id = appid
            build = fdroidserver.metadata.Build()
            build.buildjni = ['yes'] if nativecode else build.buildjni
            build.versionCode = versionCode
            build.versionName = versionName
            vc, vn = fdroidserver.build.get_metadata_from_apk(app, build, apkfilename)
            self.assertEqual(versionCode, vc)
            self.assertEqual(versionName, vn)

    @mock.patch('fdroidserver.common.get_apk_id')
    @mock.patch('fdroidserver.build.FDroidPopen')
    @mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda f: False)
    @mock.patch('fdroidserver.common.get_native_code', lambda f: 'x86')
    def test_build_local_maven(self, fake_FDroidPopen, fake_get_apk_id):
        """Test build_local() with a maven project"""

        # pylint: disable=unused-argument
        def _side_effect(cmd, cwd=None):
            p = mock.MagicMock()
            p.output = '[INFO] fake apkbuilder target/no.apk'
            with open(os.path.join(self.testdir, 'target', 'no.apk'), 'w') as fp:
                fp.write('placeholder')
            p.returncode = 0
            return p

        fake_FDroidPopen.side_effect = _side_effect
        os.chdir(self.testdir)
        os.mkdir('target')
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        fdroidserver.build.config = config
        fdroidserver.build.options = mock.Mock()
        fdroidserver.build.options.scan_binary = False
        fdroidserver.build.options.notarball = True
        fdroidserver.build.options.skipscan = False

        app = fdroidserver.metadata.App()
        app.id = 'mocked.app.id'
        build = fdroidserver.metadata.Build()
        build.commit = '1.0'
        build.versionCode = 1
        build.versionName = '1.0'
        fake_get_apk_id.side_effect = lambda f: (
            app.id,
            build.versionCode,
            build.versionName,
        )
        vcs = mock.Mock()

        build.maven = 'yes@..'
        fdroidserver.build.build_local(
            app,
            build,
            vcs,
            build_dir=self.testdir,
            output_dir=self.testdir,
            log_dir=os.getcwd(),
            srclib_dir=None,
            extlib_dir=None,
            tmp_dir=None,
            force=False,
            onserver=True,
            refresh=False,
        )

        build.maven = 'yes'
        fdroidserver.build.build_local(
            app,
            build,
            vcs,
            build_dir=self.testdir,
            output_dir=self.testdir,
            log_dir=os.getcwd(),
            srclib_dir=None,
            extlib_dir=None,
            tmp_dir=None,
            force=False,
            onserver=True,
            refresh=False,
        )

    @mock.patch('sdkmanager.build_package_list', lambda use_net: None)
    def test_build_local_ndk(self):
        """Test if `fdroid build` detects installed NDKs and auto-installs when missing"""
        with tempfile.TemporaryDirectory() as testdir, TmpCwd(
            testdir
        ), tempfile.TemporaryDirectory() as sdk_path:
            config = {'ndk_paths': {}, 'sdk_path': sdk_path}
            fdroidserver.common.config = config
            fdroidserver.build.config = config
            fdroidserver.build.options = mock.Mock()
            fdroidserver.build.options.scan_binary = False
            fdroidserver.build.options.notarball = True
            fdroidserver.build.options.skipscan = True

            app = fdroidserver.metadata.App()
            app.id = 'mocked.app.id'
            build = fdroidserver.metadata.Build()
            build.commit = '1.0'
            build.output = app.id + '.apk'
            build.versionCode = 1
            build.versionName = '1.0'
            build.ndk = 'r21e'  # aka 21.4.7075529
            ndk_version = '21.4.7075529'
            ndk_dir = Path(config['sdk_path']) / 'ndk' / ndk_version
            vcs = mock.Mock()

            def make_fake_apk(output, build):
                with open(build.output, 'w') as fp:
                    fp.write('APK PLACEHOLDER')
                return output

            # pylint: disable=unused-argument
            def fake_sdkmanager_install(to_install, android_home=None):
                ndk_dir.mkdir(parents=True)
                self.assertNotEqual(ndk_version, to_install)  # converts r21e to version
                with (ndk_dir / 'source.properties').open('w') as fp:
                    fp.write('Pkg.Revision = %s\n' % ndk_version)

            # use "as _ignored" just to make a pretty layout
            with mock.patch(
                'fdroidserver.common.replace_build_vars', wraps=make_fake_apk
            ) as _ignored, mock.patch(
                'fdroidserver.common.get_native_code', return_value='x86'
            ) as _ignored, mock.patch(
                'fdroidserver.common.get_apk_id',
                return_value=(app.id, build.versionCode, build.versionName),
            ) as _ignored, mock.patch(
                'fdroidserver.common.sha256sum',
                return_value='ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e',
            ) as _ignored, mock.patch(
                'fdroidserver.common.is_debuggable_or_testOnly',
                return_value=False,
            ) as _ignored, mock.patch(
                'fdroidserver.build.FDroidPopen', FakeProcess
            ) as _ignored, mock.patch(
                'sdkmanager.install', wraps=fake_sdkmanager_install
            ) as _ignored:
                _ignored  # silence the linters
                with self.assertRaises(
                    fdroidserver.exception.FDroidException,
                    msg="No NDK setup, `fdroid build` should fail with error",
                ):
                    fdroidserver.build.build_local(
                        app,
                        build,
                        vcs,
                        build_dir=testdir,
                        output_dir=testdir,
                        log_dir=None,
                        srclib_dir=None,
                        extlib_dir=None,
                        tmp_dir=None,
                        force=False,
                        onserver=False,
                        refresh=False,
                    )
                # now run `fdroid build --onserver`
                print('now run `fdroid build --onserver`')
                self.assertFalse(ndk_dir.exists())
                self.assertFalse('r21e' in config['ndk_paths'])
                self.assertFalse(ndk_version in config['ndk_paths'])
                fdroidserver.build.build_local(
                    app,
                    build,
                    vcs,
                    build_dir=testdir,
                    output_dir=testdir,
                    log_dir=os.getcwd(),
                    srclib_dir=None,
                    extlib_dir=None,
                    tmp_dir=None,
                    force=False,
                    onserver=True,
                    refresh=False,
                )
                self.assertTrue(ndk_dir.exists())
                self.assertTrue(os.path.exists(config['ndk_paths'][ndk_version]))
                # All paths in the config must be strings, never pathlib.Path instances
                self.assertIsInstance(config['ndk_paths'][ndk_version], str)

    @mock.patch('sdkmanager.build_package_list', lambda use_net: None)
    @mock.patch('fdroidserver.build.FDroidPopen', FakeProcess)
    @mock.patch('fdroidserver.common.get_native_code', lambda _ignored: 'x86')
    @mock.patch('fdroidserver.common.is_debuggable_or_testOnly', lambda _ignored: False)
    @mock.patch(
        'fdroidserver.common.sha256sum',
        lambda f: 'ad7ce5467e18d40050dc51b8e7affc3e635c85bd8c59be62de32352328ed467e',
    )
    def test_build_local_ndk_some_installed(self):
        """Test if `fdroid build` detects installed NDKs and auto-installs when missing"""
        with tempfile.TemporaryDirectory() as testdir, TmpCwd(
            testdir
        ), tempfile.TemporaryDirectory() as sdk_path:
            ndk_r24 = os.path.join(sdk_path, 'ndk', '24.0.8215888')
            os.makedirs(ndk_r24)
            with open(os.path.join(ndk_r24, 'source.properties'), 'w') as fp:
                fp.write('Pkg.Revision = 24.0.8215888\n')
            config = {'ndk_paths': {'r24': ndk_r24}, 'sdk_path': sdk_path}
            fdroidserver.common.config = config
            fdroidserver.build.config = config
            fdroidserver.build.options = mock.Mock()
            fdroidserver.build.options.scan_binary = False
            fdroidserver.build.options.notarball = True
            fdroidserver.build.options.skipscan = True

            app = fdroidserver.metadata.App()
            app.id = 'mocked.app.id'
            build = fdroidserver.metadata.Build()
            build.commit = '1.0'
            build.output = app.id + '.apk'
            build.versionCode = 1
            build.versionName = '1.0'
            build.ndk = 'r21e'  # aka 21.4.7075529
            ndk_version = '21.4.7075529'
            ndk_dir = Path(config['sdk_path']) / 'ndk' / ndk_version
            vcs = mock.Mock()

            def make_fake_apk(output, build):
                with open(build.output, 'w') as fp:
                    fp.write('APK PLACEHOLDER')
                return output

            # pylint: disable=unused-argument
            def fake_sdkmanager_install(to_install, android_home=None):
                ndk_dir.mkdir(parents=True)
                self.assertNotEqual(ndk_version, to_install)  # converts r21e to version
                with (ndk_dir / 'source.properties').open('w') as fp:
                    fp.write('Pkg.Revision = %s\n' % ndk_version)

            # use "as _ignored" just to make a pretty layout
            with mock.patch(
                'fdroidserver.common.replace_build_vars', wraps=make_fake_apk
            ) as _ignored, mock.patch(
                'fdroidserver.common.get_apk_id',
                return_value=(app.id, build.versionCode, build.versionName),
            ) as _ignored, mock.patch(
                'sdkmanager.install', wraps=fake_sdkmanager_install
            ) as _ignored:
                _ignored  # silence the linters
                self.assertFalse(ndk_dir.exists())
                self.assertFalse('r21e' in config['ndk_paths'])
                self.assertFalse(ndk_version in config['ndk_paths'])
                fdroidserver.build.build_local(
                    app,
                    build,
                    vcs,
                    build_dir=testdir,
                    output_dir=testdir,
                    log_dir=os.getcwd(),
                    srclib_dir=None,
                    extlib_dir=None,
                    tmp_dir=None,
                    force=False,
                    onserver=True,
                    refresh=False,
                )
                self.assertTrue(ndk_dir.exists())
                self.assertTrue(os.path.exists(config['ndk_paths'][ndk_version]))

    def test_build_local_clean(self):
        """Test if `fdroid build` cleans ant and gradle build products"""
        os.chdir(self.testdir)
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        fdroidserver.build.config = config
        fdroidserver.build.options = mock.Mock()
        fdroidserver.build.options.scan_binary = False
        fdroidserver.build.options.notarball = True
        fdroidserver.build.options.skipscan = False

        app = fdroidserver.metadata.App()
        app.id = 'mocked.app.id'
        build = fdroidserver.metadata.Build()
        build.commit = '1.0'
        build.output = app.id + '.apk'
        build.scandelete = ['baz.so']
        build.scanignore = ['foo.aar']
        build.versionCode = 1
        build.versionName = '1.0'
        vcs = mock.Mock()

        os.mkdir('reports')
        os.mkdir('target')

        for f in ('baz.so', 'foo.aar', 'gradle-wrapper.jar'):
            with open(f, 'w') as fp:
                fp.write('placeholder')
            self.assertTrue(os.path.exists(f))

        os.mkdir('build')
        os.mkdir('build/reports')
        with open('build.gradle', 'w', encoding='utf-8') as fp:
            fp.write('// placeholder')

        os.mkdir('bin')
        os.mkdir('gen')
        with open('build.xml', 'w', encoding='utf-8') as fp:
            fp.write(
                textwrap.dedent(
                    """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
                <project basedir="." default="clean" name="mockapp">
                <target name="release"/>
                <target name="clean"/>
                </project>"""
                )
            )

        def make_fake_apk(output, build):
            with open(build.output, 'w') as fp:
                fp.write('APK PLACEHOLDER')
            return output

        with mock.patch('fdroidserver.common.replace_build_vars', wraps=make_fake_apk):
            with mock.patch('fdroidserver.common.get_native_code', return_value='x86'):
                with mock.patch(
                    'fdroidserver.common.get_apk_id',
                    return_value=(app.id, build.versionCode, build.versionName),
                ):
                    with mock.patch(
                        'fdroidserver.common.is_debuggable_or_testOnly',
                        return_value=False,
                    ):
                        fdroidserver.build.build_local(
                            app,
                            build,
                            vcs,
                            build_dir=self.testdir,
                            output_dir=self.testdir,
                            log_dir=None,
                            srclib_dir=None,
                            extlib_dir=None,
                            tmp_dir=None,
                            force=False,
                            onserver=False,
                            refresh=False,
                        )

        self.assertTrue(os.path.exists('foo.aar'))
        self.assertTrue(os.path.isdir('build'))
        self.assertTrue(os.path.isdir('reports'))
        self.assertTrue(os.path.isdir('target'))
        self.assertFalse(os.path.exists('baz.so'))
        self.assertFalse(os.path.exists('bin'))
        self.assertFalse(os.path.exists('build/reports'))
        self.assertFalse(os.path.exists('gen'))
        self.assertFalse(os.path.exists('gradle-wrapper.jar'))

    def test_scan_with_extlib(self):
        os.chdir(self.testdir)
        os.mkdir("build")

        config = fdroidserver.common.read_config()
        config['sdk_path'] = os.getenv('ANDROID_HOME')
        config['ndk_paths'] = {'r10d': os.getenv('ANDROID_NDK_HOME')}
        fdroidserver.common.config = config
        app = fdroidserver.metadata.App()
        app.id = 'com.gpl.rpg.AndorsTrail'
        build = fdroidserver.metadata.Build()
        build.commit = 'master'
        build.androidupdate = ['no']
        os.makedirs("extlib/android")
        # write a fake binary jar file the scanner should definitely error on
        with open('extlib/android/android-support-v4r11.jar', 'wb') as file:
            file.write(
                b'PK\x03\x04\x14\x00\x08\x00\x08\x00-\x0eiA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x04\x00META-INF/\xfe\xca\x00\x00\x03\x00PK\x07\x08\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00'
            )

        class FakeVcs:
            # no need to change to the correct commit here
            def gotorevision(self, rev, refresh=True):
                pass

            def getsrclib(self):
                return None

            def deinitsubmodules(self):
                pass

        # Test we trigger a scanner error without extlibs
        build.extlibs = []
        os.makedirs('build/libs')
        shutil.copy('extlib/android/android-support-v4r11.jar', 'build/libs')
        fdroidserver.common.prepare_source(
            FakeVcs(), app, build, "build", "ignore", "extlib"
        )
        count = fdroidserver.scanner.scan_source("build", build)
        self.assertEqual(1, count, "Should produce a scanner error without extlib")

        # Now try again as an extlib
        build.extlibs = ['android/android-support-v4r11.jar']
        fdroidserver.common.prepare_source(
            FakeVcs(), app, build, "build", "ignore", "extlib"
        )
        count = fdroidserver.scanner.scan_source("build", build)
        self.assertEqual(0, count, "Shouldn't error on jar from extlib")

    def test_failed_verifies_are_not_in_unsigned(self):
        os.chdir(self.testdir)
        sdk_path = os.path.join(self.testdir, 'android-sdk')
        self.create_fake_android_home(sdk_path)
        with open('config.yml', 'w') as fp:
            yaml.dump({'sdk_path': sdk_path, 'keep_when_not_allowed': True}, fp)
        os.chmod('config.yml', 0o600)
        fdroidserver.build.config = fdroidserver.common.read_config()

        os.mkdir('metadata')
        appid = 'info.guardianproject.checkey'
        metadata_file = os.path.join('metadata', appid + '.yml')
        shutil.copy(os.path.join(self.basedir, metadata_file), 'metadata')
        with open(metadata_file) as fp:
            app = fdroidserver.metadata.App(yaml.safe_load(fp))
        app['RepoType'] = 'git'
        app[
            'Binaries'
        ] = 'https://example.com/fdroid/repo/info.guardianproject.checkey_%v.apk'
        build = fdroidserver.metadata.Build(
            {
                'versionCode': 123,
                'versionName': '1.2.3',
                'commit': '1.2.3',
                'disable': False,
            }
        )
        app['Builds'] = [build]
        fdroidserver.metadata.write_metadata(metadata_file, app)

        os.makedirs(os.path.join('unsigned', 'binaries'))
        production_result = os.path.join(
            'unsigned', '%s_%d.apk' % (appid, build['versionCode'])
        )
        production_compare_file = os.path.join(
            'unsigned', 'binaries', '%s_%d.binary.apk' % (appid, build['versionCode'])
        )
        os.makedirs(os.path.join('tmp', 'binaries'))
        test_result = os.path.join('tmp', '%s_%d.apk' % (appid, build['versionCode']))
        test_compare_file = os.path.join(
            'tmp', 'binaries', '%s_%d.binary.apk' % (appid, build['versionCode'])
        )
        with mock.patch(
            'fdroidserver.common.force_exit', lambda *args: None
        ) as a, mock.patch(
            'fdroidserver.common.get_android_tools_version_log', lambda: 'fake'
        ) as b, mock.patch(
            'fdroidserver.common.FDroidPopen', FakeProcess
        ) as c, mock.patch(
            'fdroidserver.build.FDroidPopen', FakeProcess
        ) as d, mock.patch(
            'fdroidserver.build.trybuild', lambda *args: True
        ) as e, mock.patch(
            'fdroidserver.net.download_file', lambda *args, **kwargs: None
        ) as f:
            a, b, c, d, e, f  # silence linters' "unused" warnings

            with mock.patch('sys.argv', ['fdroid build', appid]):
                # successful comparison
                open(production_result, 'w').close()
                open(production_compare_file, 'w').close()
                with mock.patch('fdroidserver.common.verify_apks', lambda *args: None):
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(production_result))
                self.assertTrue(os.path.exists(production_compare_file))
                # failed comparison
                open(production_result, 'w').close()
                open(production_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: 'failed'
                ):
                    fdroidserver.build.main()
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))

            with mock.patch('sys.argv', ['fdroid build', '--test', appid]):
                # successful comparison
                open(test_result, 'w').close()
                open(test_compare_file, 'w').close()
                with mock.patch('fdroidserver.common.verify_apks', lambda *args: None):
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(test_result))
                self.assertTrue(os.path.exists(test_compare_file))
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))
                # failed comparison
                open(test_result, 'w').close()
                open(test_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: 'failed'
                ):
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(test_result))
                self.assertFalse(os.path.exists(test_compare_file))
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))

    def test_failed_allowedapksigningkeys_are_not_in_unsigned(self):
        os.chdir(self.testdir)
        os.mkdir('metadata')
        appid = 'info.guardianproject.checkey'
        metadata_file = os.path.join('metadata', appid + '.yml')
        shutil.copy(os.path.join(self.basedir, metadata_file), 'metadata')
        with open(metadata_file) as fp:
            app = fdroidserver.metadata.App(yaml.safe_load(fp))
        app['RepoType'] = 'git'
        app[
            'Binaries'
        ] = 'https://example.com/fdroid/repo/info.guardianproject.checkey_%v.apk'
        build = fdroidserver.metadata.Build(
            {
                'versionCode': 123,
                'versionName': '1.2.3',
                'commit': '1.2.3',
                'disable': False,
            }
        )
        app['Builds'] = [build]
        expected_key = 'a' * 64
        bogus_key = 'b' * 64
        app['AllowedAPKSigningKeys'] = [expected_key]
        fdroidserver.metadata.write_metadata(metadata_file, app)

        os.makedirs(os.path.join('unsigned', 'binaries'))
        production_result = os.path.join(
            'unsigned', '%s_%d.apk' % (appid, build['versionCode'])
        )
        production_compare_file = os.path.join(
            'unsigned', 'binaries', '%s_%d.binary.apk' % (appid, build['versionCode'])
        )
        os.makedirs(os.path.join('tmp', 'binaries'))
        test_result = os.path.join('tmp', '%s_%d.apk' % (appid, build['versionCode']))
        test_compare_file = os.path.join(
            'tmp', 'binaries', '%s_%d.binary.apk' % (appid, build['versionCode'])
        )
        with mock.patch(
            'fdroidserver.common.force_exit', lambda *args: None
        ) as a, mock.patch(
            'fdroidserver.common.get_android_tools_version_log', lambda: 'fake'
        ) as b, mock.patch(
            'fdroidserver.common.FDroidPopen', FakeProcess
        ) as c, mock.patch(
            'fdroidserver.build.FDroidPopen', FakeProcess
        ) as d, mock.patch(
            'fdroidserver.build.trybuild', lambda *args: True
        ) as e, mock.patch(
            'fdroidserver.net.download_file', lambda *args, **kwargs: None
        ) as f:
            a, b, c, d, e, f  # silence linters' "unused" warnings

            with mock.patch('sys.argv', ['fdroid build', appid]):
                # successful comparison, successful signer
                open(production_result, 'w').close()
                open(production_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: None
                ) as g, mock.patch(
                    'fdroidserver.common.apk_signer_fingerprint',
                    lambda *args: expected_key,
                ) as h:
                    g, h
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(production_result))
                self.assertTrue(os.path.exists(production_compare_file))
                # successful comparison, failed signer
                open(production_result, 'w').close()
                open(production_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: None
                ) as g, mock.patch(
                    'fdroidserver.common.apk_signer_fingerprint',
                    lambda *args: bogus_key,
                ) as h:
                    g, h
                    fdroidserver.build.main()
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))
                # failed comparison
                open(production_result, 'w').close()
                open(production_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: 'failed'
                ):
                    fdroidserver.build.main()
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))

            with mock.patch('sys.argv', ['fdroid build', '--test', appid]):
                # successful comparison, successful signer
                open(test_result, 'w').close()
                open(test_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: None
                ) as g, mock.patch(
                    'fdroidserver.common.apk_signer_fingerprint',
                    lambda *args: expected_key,
                ) as h:
                    g, h
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(test_result))
                self.assertTrue(os.path.exists(test_compare_file))
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))
                # successful comparison, failed signer
                open(test_result, 'w').close()
                open(test_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: None
                ) as g, mock.patch(
                    'fdroidserver.common.apk_signer_fingerprint',
                    lambda *args: bogus_key,
                ) as h:
                    g, h
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(test_result))
                self.assertFalse(os.path.exists(test_compare_file))
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))
                # failed comparison
                open(test_result, 'w').close()
                open(test_compare_file, 'w').close()
                with mock.patch(
                    'fdroidserver.common.verify_apks', lambda *args: 'failed'
                ):
                    fdroidserver.build.main()
                self.assertTrue(os.path.exists(test_result))
                self.assertFalse(os.path.exists(test_compare_file))
                self.assertFalse(os.path.exists(production_result))
                self.assertFalse(os.path.exists(production_compare_file))

    @mock.patch('fdroidserver.vmtools.get_build_vm')
    @mock.patch('fdroidserver.vmtools.get_clean_builder')
    @mock.patch('paramiko.SSHClient')
    @mock.patch('subprocess.check_output')
    def test_build_server_cmdline(
        self,
        subprocess_check_output,
        paramiko_SSHClient,
        fdroidserver_vmtools_get_clean_builder,
        fdroidserver_vmtools_get_build_vm,
    ):
        """Test command line flags passed to the buildserver"""
        global cmdline_args
        test_flag = ['', False]

        def _exec_command(args):
            flag = test_flag[0]
            if test_flag[1]:
                self.assertTrue(flag in args, flag + ' should be present')
            else:
                self.assertFalse(flag in args, flag + ' should not be present')

        os.chdir(self.testdir)
        os.mkdir('tmp')

        chan = mock.MagicMock()
        chan.exec_command = _exec_command
        chan.recv_exit_status = lambda: 0
        transport = mock.MagicMock()
        transport.open_session = mock.Mock(return_value=chan)
        sshs = mock.MagicMock()
        sshs.get_transport = mock.Mock(return_value=transport)
        paramiko_SSHClient.return_value = sshs
        subprocess_check_output.return_value = (
            b'0123456789abcdef0123456789abcdefcafebabe'
        )
        fdroidserver_vmtools_get_clean_builder.side_effect = lambda s: {
            'hostname': 'example.com',
            'idfile': '/path/to/id/file',
            'port': 123,
            'user': 'fake',
        }
        fdroidserver.common.config = {'sdk_path': '/fake/android/sdk/path'}
        fdroidserver.build.options = mock.MagicMock()
        vcs = mock.Mock()
        vcs.getsrclib = mock.Mock(return_value=None)
        app = fdroidserver.metadata.App()
        app['metadatapath'] = 'metadata/fake.id.yml'
        app['id'] = 'fake.id'
        app['RepoType'] = 'git'
        build = fdroidserver.metadata.Build(
            {
                'versionCode': 123,
                'versionName': '1.2.3',
                'commit': '1.2.3',
                'disable': False,
            }
        )
        app['Builds'] = [build]

        test_flag = ('--on-server', True)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)
        self.assertTrue(fdroidserver_vmtools_get_build_vm.called)

        for force in (True, False):
            test_flag = ('--force', force)
            fdroidserver.build.build_server(app, build, vcs, '', '', '', force)

        fdroidserver.build.options.notarball = True
        test_flag = ('--no-tarball', True)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)
        fdroidserver.build.options.notarball = False
        test_flag = ('--no-tarball', False)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)

        fdroidserver.build.options.skipscan = False
        test_flag = ('--scan-binary', True)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)
        fdroidserver.build.options.skipscan = True
        test_flag = ('--scan-binary', False)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)
        test_flag = ('--skip-scan', True)
        fdroidserver.build.build_server(app, build, vcs, '', '', '', False)

    @mock.patch('fdroidserver.vmtools.get_build_vm')
    @mock.patch('fdroidserver.vmtools.get_clean_builder')
    @mock.patch('paramiko.SSHClient')
    @mock.patch('subprocess.check_output')
    @mock.patch('fdroidserver.common.getsrclib')
    @mock.patch('fdroidserver.common.prepare_source')
    @mock.patch('fdroidserver.build.build_local')
    @mock.patch('fdroidserver.common.get_android_tools_version_log', lambda: 'versions')
    @mock.patch('fdroidserver.common.deploy_build_log_with_rsync', lambda a, b, c: None)
    def test_build_server_no_local_prepare(
        self,
        build_build_local,
        common_prepare_source,
        common_getsrclib,
        subprocess_check_output,
        paramiko_SSHClient,
        fdroidserver_vmtools_get_clean_builder,
        fdroidserver_vmtools_get_build_vm,
    ):
        """srclibs Prepare: should only be executed in the buildserver"""

        def _exec_command(args):
            print('chan.exec_command', args)

        def _getsrclib(
            spec,
            srclib_dir,
            basepath=False,
            raw=False,
            prepare=True,
            preponly=False,
            refresh=True,
            build=None,
        ):
            name, ref = spec.split('@')
            libdir = os.path.join(srclib_dir, name)
            os.mkdir(libdir)
            self.assertFalse(prepare, 'Prepare: scripts should never run on host')
            return name, None, libdir  # TODO

        os.chdir(self.testdir)

        chan = mock.MagicMock()
        chan.exec_command = _exec_command
        chan.recv_exit_status = lambda: 0
        transport = mock.MagicMock()
        transport.open_session = mock.Mock(return_value=chan)
        sshs = mock.MagicMock()
        sshs.get_transport = mock.Mock(return_value=transport)
        paramiko_SSHClient.return_value = sshs
        subprocess_check_output.return_value = (
            b'0123456789abcdef0123456789abcdefcafebabe'
        )
        fdroidserver_vmtools_get_clean_builder.side_effect = lambda s: {
            'hostname': 'example.com',
            'idfile': '/path/to/id/file',
            'port': 123,
            'user': 'fake',
        }

        fdroidserver.metadata.srclibs = {
            'flutter': {
                'RepoType': 'git',
                'Repo': 'https://github.com/flutter/flutter',
            }
        }
        os.mkdir('srclibs')
        with open('srclibs/flutter.yml', 'w') as fp:
            yaml.dump(fdroidserver.metadata.srclibs, fp)
        common_getsrclib.side_effect = _getsrclib

        options = mock.MagicMock()
        options.force = False
        options.notarball = True
        options.onserver = False
        options.refresh = False
        options.scan_binary = False
        options.server = True
        options.skipscan = True
        options.test = False
        options.verbose = True
        fdroidserver.build.options = options
        fdroidserver.build.config = {'sdk_path': '/fake/android/sdk/path'}

        vcs = mock.Mock()
        vcs.getsrclib = mock.Mock(return_value=None)
        app = fdroidserver.metadata.App()
        app['metadatapath'] = 'metadata/fake.id.yml'
        app['id'] = 'fake.id'
        app['RepoType'] = 'git'
        spec = 'flutter@v1.7.8'
        build = fdroidserver.metadata.Build(
            {
                'versionCode': 123,
                'versionName': '1.2.3',
                'commit': '1.2.3',
                'disable': False,
                'srclibs': [spec],
            }
        )
        app['Builds'] = [build]

        build_dir = 'build'
        srclib_dir = os.path.join(build_dir, 'srclib')
        extlib_dir = os.path.join(build_dir, 'extlib')
        os.mkdir('tmp')
        os.mkdir(build_dir)
        os.mkdir(srclib_dir)

        fdroidserver.build.trybuild(
            app,
            build,
            build_dir,
            'unsigned',
            'logs',
            None,
            srclib_dir,
            extlib_dir,
            'tmp',
            'repo',
            vcs,
            options.test,
            options.server,
            options.force,
            options.onserver,
            options.refresh,
        )

        common_getsrclib.assert_called_once_with(
            spec, srclib_dir, basepath=True, prepare=False
        )
        common_prepare_source.assert_not_called()
        build_build_local.assert_not_called()

    def test_keep_when_not_allowed_default(self):
        self.assertFalse(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_config_true(self):
        fdroidserver.build.config = {'keep_when_not_allowed': True}
        self.assertTrue(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_config_false(self):
        fdroidserver.build.config = {'keep_when_not_allowed': False}
        self.assertFalse(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_options_true(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = True
        self.assertTrue(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_options_false(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = False
        self.assertFalse(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_options_true_override_config(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = True
        fdroidserver.build.config = {'keep_when_not_allowed': False}
        self.assertTrue(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_options_default_does_not_override(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = False
        fdroidserver.build.config = {'keep_when_not_allowed': True}
        self.assertTrue(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_all_true(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = True
        fdroidserver.build.config = {'keep_when_not_allowed': True}
        self.assertTrue(fdroidserver.build.keep_when_not_allowed())

    def test_keep_when_not_allowed_all_false(self):
        fdroidserver.build.options = Options
        fdroidserver.build.options.keep_when_not_allowed = False
        fdroidserver.build.config = {'keep_when_not_allowed': False}
        self.assertFalse(fdroidserver.build.keep_when_not_allowed())


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))

    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        default=False,
        help="Spew out even more information than normal",
    )
    parse_args_for_test(parser, sys.argv)

    newSuite = unittest.TestSuite()
    newSuite.addTest(unittest.makeSuite(BuildTest))
    unittest.main(failfast=False)
