chiark / gitweb /
eb59c3d0848d25994f18e48b2306789a510c567b
[fdroidserver.git] / tests / update.TestCase
1 #!/usr/bin/env python3
2
3 # http://www.drdobbs.com/testing/unit-testing-with-python/240165163
4
5 import git
6 import inspect
7 import logging
8 import optparse
9 import os
10 import shutil
11 import subprocess
12 import sys
13 import tempfile
14 import unittest
15 import yaml
16 from binascii import unhexlify
17 from distutils.version import LooseVersion
18
19 localmodule = os.path.realpath(
20     os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
21 print('localmodule: ' + localmodule)
22 if localmodule not in sys.path:
23     sys.path.insert(0, localmodule)
24
25 import fdroidserver.common
26 import fdroidserver.exception
27 import fdroidserver.metadata
28 import fdroidserver.update
29 from fdroidserver.common import FDroidPopen
30
31
32 class UpdateTest(unittest.TestCase):
33     '''fdroid update'''
34
35     def setUp(self):
36         logging.basicConfig(level=logging.INFO)
37         self.basedir = os.path.join(localmodule, 'tests')
38         self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles'))
39         if not os.path.exists(self.tmpdir):
40             os.makedirs(self.tmpdir)
41         os.chdir(self.basedir)
42
43     def testInsertStoreMetadata(self):
44         config = dict()
45         fdroidserver.common.fill_config_defaults(config)
46         config['accepted_formats'] = ('txt', 'yml')
47         fdroidserver.update.config = config
48         fdroidserver.update.options = fdroidserver.common.options
49         os.chdir(os.path.join(localmodule, 'tests'))
50
51         shutil.rmtree(os.path.join('repo', 'info.guardianproject.urzip'), ignore_errors=True)
52
53         shutil.rmtree(os.path.join('build', 'com.nextcloud.client'), ignore_errors=True)
54         shutil.copytree(os.path.join('source-files', 'com.nextcloud.client'),
55                         os.path.join('build', 'com.nextcloud.client'))
56
57         shutil.rmtree(os.path.join('build', 'com.nextcloud.client.dev'), ignore_errors=True)
58         shutil.copytree(os.path.join('source-files', 'com.nextcloud.client.dev'),
59                         os.path.join('build', 'com.nextcloud.client.dev'))
60
61         shutil.rmtree(os.path.join('build', 'eu.siacs.conversations'), ignore_errors=True)
62         shutil.copytree(os.path.join('source-files', 'eu.siacs.conversations'),
63                         os.path.join('build', 'eu.siacs.conversations'))
64
65         apps = dict()
66         for packageName in ('info.guardianproject.urzip', 'org.videolan.vlc', 'obb.mainpatch.current',
67                             'com.nextcloud.client', 'com.nextcloud.client.dev',
68                             'eu.siacs.conversations'):
69             apps[packageName] = fdroidserver.metadata.App()
70             apps[packageName]['id'] = packageName
71             apps[packageName]['CurrentVersionCode'] = 0xcafebeef
72
73         apps['info.guardianproject.urzip']['CurrentVersionCode'] = 100
74
75         buildnextcloudclient = fdroidserver.metadata.Build()
76         buildnextcloudclient.gradle = ['generic']
77         apps['com.nextcloud.client']['builds'] = [buildnextcloudclient]
78
79         buildnextclouddevclient = fdroidserver.metadata.Build()
80         buildnextclouddevclient.gradle = ['versionDev']
81         apps['com.nextcloud.client.dev']['builds'] = [buildnextclouddevclient]
82
83         build_conversations = fdroidserver.metadata.Build()
84         build_conversations.gradle = ['free']
85         apps['eu.siacs.conversations']['builds'] = [build_conversations]
86
87         fdroidserver.update.insert_localized_app_metadata(apps)
88
89         appdir = os.path.join('repo', 'info.guardianproject.urzip', 'en-US')
90         self.assertTrue(os.path.isfile(os.path.join(appdir, 'icon.png')))
91         self.assertTrue(os.path.isfile(os.path.join(appdir, 'featureGraphic.png')))
92
93         self.assertEqual(6, len(apps))
94         for packageName, app in apps.items():
95             self.assertTrue('localized' in app)
96             self.assertTrue('en-US' in app['localized'])
97             self.assertEqual(1, len(app['localized']))
98             if packageName == 'info.guardianproject.urzip':
99                 self.assertEqual(7, len(app['localized']['en-US']))
100                 self.assertEqual('full description\n', app['localized']['en-US']['description'])
101                 self.assertEqual('title\n', app['localized']['en-US']['name'])
102                 self.assertEqual('short description\n', app['localized']['en-US']['summary'])
103                 self.assertEqual('video\n', app['localized']['en-US']['video'])
104                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
105                 self.assertEqual('featureGraphic.png', app['localized']['en-US']['featureGraphic'])
106                 self.assertEqual('100\n', app['localized']['en-US']['whatsNew'])
107             elif packageName == 'org.videolan.vlc':
108                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
109                 self.assertEqual(9, len(app['localized']['en-US']['phoneScreenshots']))
110                 self.assertEqual(15, len(app['localized']['en-US']['sevenInchScreenshots']))
111             elif packageName == 'obb.mainpatch.current':
112                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
113                 self.assertEqual('featureGraphic.png', app['localized']['en-US']['featureGraphic'])
114                 self.assertEqual(1, len(app['localized']['en-US']['phoneScreenshots']))
115                 self.assertEqual(1, len(app['localized']['en-US']['sevenInchScreenshots']))
116             elif packageName == 'com.nextcloud.client':
117                 self.assertEqual('Nextcloud', app['localized']['en-US']['name'])
118                 self.assertEqual(1073, len(app['localized']['en-US']['description']))
119                 self.assertEqual(78, len(app['localized']['en-US']['summary']))
120             elif packageName == 'com.nextcloud.client.dev':
121                 self.assertEqual('Nextcloud Dev', app['localized']['en-US']['name'])
122                 self.assertEqual(586, len(app['localized']['en-US']['description']))
123                 self.assertEqual(79, len(app['localized']['en-US']['summary']))
124             elif packageName == 'eu.siacs.conversations':
125                 self.assertEqual('Conversations', app['localized']['en-US']['name'])
126
127     def test_insert_triple_t_metadata(self):
128         importer = os.path.join(self.basedir, 'tmp', 'importer')
129         packageName = 'org.fdroid.ci.test.app'
130         if not os.path.isdir(importer):
131             logging.warning('skipping test_insert_triple_t_metadata, import.TestCase must run first!')
132             return
133         tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name,
134                                        dir=self.tmpdir)
135         packageDir = os.path.join(tmptestsdir, 'build', packageName)
136         shutil.copytree(importer, packageDir)
137
138         # always use the same commit so these tests work when ci-test-app.git is updated
139         repo = git.Repo(packageDir)
140         for remote in repo.remotes:
141             remote.fetch()
142         repo.git.reset('--hard', 'b9e5d1a0d8d6fc31d4674b2f0514fef10762ed4f')
143         repo.git.clean('-fdx')
144
145         os.mkdir(os.path.join(tmptestsdir, 'metadata'))
146         metadata = dict()
147         metadata['Description'] = 'This is just a test app'
148         with open(os.path.join(tmptestsdir, 'metadata', packageName + '.yml'), 'w') as fp:
149             yaml.dump(metadata, fp)
150
151         config = dict()
152         fdroidserver.common.fill_config_defaults(config)
153         config['accepted_formats'] = ('yml')
154         fdroidserver.common.config = config
155         fdroidserver.update.config = config
156         fdroidserver.update.options = fdroidserver.common.options
157         os.chdir(tmptestsdir)
158
159         apps = fdroidserver.metadata.read_metadata(xref=True)
160         fdroidserver.update.copy_triple_t_store_metadata(apps)
161
162         # TODO ideally, this would compare the whole dict like in metadata.TestCase's test_read_metadata()
163         correctlocales = [
164             'ar', 'ast_ES', 'az', 'ca', 'ca_ES', 'cs-CZ', 'cs_CZ', 'da',
165             'da-DK', 'de', 'de-DE', 'el', 'en-US', 'es', 'es-ES', 'es_ES', 'et',
166             'fi', 'fr', 'fr-FR', 'he_IL', 'hi-IN', 'hi_IN', 'hu', 'id', 'it',
167             'it-IT', 'it_IT', 'iw-IL', 'ja', 'ja-JP', 'kn_IN', 'ko', 'ko-KR',
168             'ko_KR', 'lt', 'nb', 'nb_NO', 'nl', 'nl-NL', 'no', 'pl', 'pl-PL',
169             'pl_PL', 'pt', 'pt-BR', 'pt-PT', 'pt_BR', 'ro', 'ro_RO', 'ru-RU',
170             'ru_RU', 'sv-SE', 'sv_SE', 'te', 'tr', 'tr-TR', 'uk', 'uk_UA', 'vi',
171             'vi_VN', 'zh-CN', 'zh_CN', 'zh_TW',
172         ]
173         locales = sorted(list(apps['org.fdroid.ci.test.app']['localized'].keys()))
174         self.assertEqual(correctlocales, locales)
175
176     def javagetsig(self, apkfile):
177         getsig_dir = os.path.join(os.path.dirname(__file__), 'getsig')
178         if not os.path.exists(getsig_dir + "/getsig.class"):
179             logging.critical("getsig.class not found. To fix: cd '%s' && ./make.sh" % getsig_dir)
180             sys.exit(1)
181         # FDroidPopen needs some config to work
182         config = dict()
183         fdroidserver.common.fill_config_defaults(config)
184         fdroidserver.common.config = config
185         p = FDroidPopen(['java', '-cp', os.path.join(os.path.dirname(__file__), 'getsig'),
186                          'getsig', os.path.join(os.getcwd(), apkfile)])
187         sig = None
188         for line in p.output.splitlines():
189             if line.startswith('Result:'):
190                 sig = line[7:].strip()
191                 break
192         if p.returncode == 0:
193             return sig
194         else:
195             return None
196
197     def testGoodGetsig(self):
198         # config needed to use jarsigner and keytool
199         config = dict()
200         fdroidserver.common.fill_config_defaults(config)
201         fdroidserver.update.config = config
202         apkfile = os.path.join(os.path.dirname(__file__), 'urzip.apk')
203         sig = self.javagetsig(apkfile)
204         self.assertIsNotNone(sig, "sig is None")
205         pysig = fdroidserver.update.getsig(apkfile)
206         self.assertIsNotNone(pysig, "pysig is None")
207         self.assertEqual(sig, fdroidserver.update.getsig(apkfile),
208                          "python sig not equal to java sig!")
209         self.assertEqual(len(sig), len(pysig),
210                          "the length of the two sigs are different!")
211         try:
212             self.assertEqual(unhexlify(sig), unhexlify(pysig),
213                              "the length of the two sigs are different!")
214         except TypeError as e:
215             print(e)
216             self.assertTrue(False, 'TypeError!')
217
218     def testBadGetsig(self):
219         """getsig() should still be able to fetch the fingerprint of bad signatures"""
220         # config needed to use jarsigner and keytool
221         config = dict()
222         fdroidserver.common.fill_config_defaults(config)
223         fdroidserver.update.config = config
224
225         apkfile = os.path.join(os.path.dirname(__file__), 'urzip-badsig.apk')
226         sig = fdroidserver.update.getsig(apkfile)
227         self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
228                          "python sig should be: " + str(sig))
229
230         apkfile = os.path.join(os.path.dirname(__file__), 'urzip-badcert.apk')
231         sig = fdroidserver.update.getsig(apkfile)
232         self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
233                          "python sig should be: " + str(sig))
234
235     def testScanApksAndObbs(self):
236         os.chdir(os.path.join(localmodule, 'tests'))
237         if os.path.basename(os.getcwd()) != 'tests':
238             raise Exception('This test must be run in the "tests/" subdir')
239
240         config = dict()
241         fdroidserver.common.fill_config_defaults(config)
242         config['ndk_paths'] = dict()
243         config['accepted_formats'] = ['json', 'txt', 'yml']
244         fdroidserver.common.config = config
245         fdroidserver.update.config = config
246
247         fdroidserver.update.options = type('', (), {})()
248         fdroidserver.update.options.clean = True
249         fdroidserver.update.options.delete_unknown = True
250         fdroidserver.update.options.rename_apks = False
251         fdroidserver.update.options.allow_disabled_algorithms = False
252
253         apps = fdroidserver.metadata.read_metadata(xref=True)
254         knownapks = fdroidserver.common.KnownApks()
255         apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
256         self.assertEqual(len(apks), 13)
257         apk = apks[0]
258         self.assertEqual(apk['packageName'], 'com.politedroid')
259         self.assertEqual(apk['versionCode'], 3)
260         self.assertEqual(apk['minSdkVersion'], '3')
261         self.assertEqual(apk['targetSdkVersion'], '3')
262         self.assertFalse('maxSdkVersion' in apk)
263         apk = apks[6]
264         self.assertEqual(apk['packageName'], 'obb.main.oldversion')
265         self.assertEqual(apk['versionCode'], 1444412523)
266         self.assertEqual(apk['minSdkVersion'], '4')
267         self.assertEqual(apk['targetSdkVersion'], '18')
268         self.assertFalse('maxSdkVersion' in apk)
269
270         fdroidserver.update.insert_obbs('repo', apps, apks)
271         for apk in apks:
272             if apk['packageName'] == 'obb.mainpatch.current':
273                 self.assertEqual(apk.get('obbMainFile'), 'main.1619.obb.mainpatch.current.obb')
274                 self.assertEqual(apk.get('obbPatchFile'), 'patch.1619.obb.mainpatch.current.obb')
275             elif apk['packageName'] == 'obb.main.oldversion':
276                 self.assertEqual(apk.get('obbMainFile'), 'main.1434483388.obb.main.oldversion.obb')
277                 self.assertIsNone(apk.get('obbPatchFile'))
278             elif apk['packageName'] == 'obb.main.twoversions':
279                 self.assertIsNone(apk.get('obbPatchFile'))
280                 if apk['versionCode'] == 1101613:
281                     self.assertEqual(apk.get('obbMainFile'), 'main.1101613.obb.main.twoversions.obb')
282                 elif apk['versionCode'] == 1101615:
283                     self.assertEqual(apk.get('obbMainFile'), 'main.1101615.obb.main.twoversions.obb')
284                 elif apk['versionCode'] == 1101617:
285                     self.assertEqual(apk.get('obbMainFile'), 'main.1101615.obb.main.twoversions.obb')
286                 else:
287                     self.assertTrue(False)
288             elif apk['packageName'] == 'info.guardianproject.urzip':
289                 self.assertIsNone(apk.get('obbMainFile'))
290                 self.assertIsNone(apk.get('obbPatchFile'))
291
292     def test_scan_apk(self):
293         config = dict()
294         fdroidserver.common.fill_config_defaults(config)
295         fdroidserver.common.config = config
296         fdroidserver.update.config = config
297         os.chdir(os.path.join(localmodule, 'tests'))
298         if os.path.basename(os.getcwd()) != 'tests':
299             raise Exception('This test must be run in the "tests/" subdir')
300
301         apk_info = fdroidserver.update.scan_apk('repo/duplicate.permisssions_9999999.apk')
302         self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable/ic_launcher.png',
303                                                  '-1': 'res/drawable/ic_launcher.png'})
304
305         apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk')
306         self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png',
307                                                  '120': 'res/drawable-ldpi-v4/icon_launcher.png',
308                                                  '160': 'res/drawable-mdpi-v4/icon_launcher.png',
309                                                  '-1': 'res/drawable-mdpi-v4/icon_launcher.png'})
310         self.assertEqual(apk_info['icons'], {})
311         self.assertEqual(apk_info['features'], [])
312         self.assertEqual(apk_info['antiFeatures'], set())
313         self.assertEqual(apk_info['versionName'], 'v1.6pre2')
314         self.assertEqual(apk_info['hash'],
315                          '897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8')
316         self.assertEqual(apk_info['packageName'], 'org.dyndns.fules.ck')
317         self.assertEqual(apk_info['versionCode'], 20)
318         self.assertEqual(apk_info['size'], 132453)
319         self.assertEqual(apk_info['nativecode'],
320                          ['arm64-v8a', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'x86', 'x86_64'])
321         self.assertEqual(apk_info['minSdkVersion'], '7')
322         self.assertEqual(apk_info['sig'], '9bf7a6a67f95688daec75eab4b1436ac')
323         self.assertEqual(apk_info['hashType'], 'sha256')
324         self.assertEqual(apk_info['targetSdkVersion'], '8')
325
326         apk_info = fdroidserver.update.scan_apk('org.bitbucket.tickytacky.mirrormirror_4.apk')
327         self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable-mdpi/mirror.png',
328                                                  '-1': 'res/drawable-mdpi/mirror.png'})
329
330         apk_info = fdroidserver.update.scan_apk('repo/info.zwanenburg.caffeinetile_4.apk')
331         self.assertEqual(apk_info['icons_src'], {'160': 'res/drawable/ic_coffee_on.xml',
332                                                  '-1': 'res/drawable/ic_coffee_on.xml'})
333
334         apk_info = fdroidserver.update.scan_apk('repo/com.politedroid_6.apk')
335         self.assertEqual(apk_info['icons_src'], {'120': 'res/drawable-ldpi-v4/icon.png',
336                                                  '160': 'res/drawable-mdpi-v4/icon.png',
337                                                  '240': 'res/drawable-hdpi-v4/icon.png',
338                                                  '320': 'res/drawable-xhdpi-v4/icon.png',
339                                                  '-1': 'res/drawable-mdpi-v4/icon.png'})
340
341         apk_info = fdroidserver.update.scan_apk('SpeedoMeterApp.main_1.apk')
342         self.assertEqual(apk_info['icons_src'], {})
343
344     def test_scan_apk_no_sig(self):
345         config = dict()
346         fdroidserver.common.fill_config_defaults(config)
347         fdroidserver.update.config = config
348         os.chdir(os.path.join(localmodule, 'tests'))
349         if os.path.basename(os.getcwd()) != 'tests':
350             raise Exception('This test must be run in the "tests/" subdir')
351
352         with self.assertRaises(fdroidserver.exception.BuildException):
353             fdroidserver.update.scan_apk('urzip-release-unsigned.apk')
354
355     def test_process_apk(self):
356
357         def _build_yaml_representer(dumper, data):
358             '''Creates a YAML representation of a Build instance'''
359             return dumper.represent_dict(data)
360
361         config = dict()
362         fdroidserver.common.fill_config_defaults(config)
363         fdroidserver.update.config = config
364         os.chdir(os.path.join(localmodule, 'tests'))
365         if os.path.basename(os.getcwd()) != 'tests':
366             raise Exception('This test must be run in the "tests/" subdir')
367
368         config['ndk_paths'] = dict()
369         config['accepted_formats'] = ['json', 'txt', 'yml']
370         fdroidserver.common.config = config
371         fdroidserver.update.config = config
372
373         fdroidserver.update.options = type('', (), {})()
374         fdroidserver.update.options.clean = True
375         fdroidserver.update.options.rename_apks = False
376         fdroidserver.update.options.delete_unknown = True
377         fdroidserver.update.options.allow_disabled_algorithms = False
378
379         for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'):
380             if not os.path.exists(icon_dir):
381                 os.makedirs(icon_dir)
382
383         knownapks = fdroidserver.common.KnownApks()
384         apkList = ['../urzip.apk', '../org.dyndns.fules.ck_20.apk']
385
386         for apkName in apkList:
387             _, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo', knownapks,
388                                                                    False)
389             # Don't care about the date added to the repo and relative apkName
390             del apk['added']
391             del apk['apkName']
392             # avoid AAPT application name bug
393             del apk['name']
394
395             # ensure that icons have been extracted properly
396             if apkName == '../urzip.apk':
397                 self.assertEqual(apk['icon'], 'info.guardianproject.urzip.100.png')
398             if apkName == '../org.dyndns.fules.ck_20.apk':
399                 self.assertEqual(apk['icon'], 'org.dyndns.fules.ck.20.png')
400             for density in fdroidserver.update.screen_densities:
401                 icon_path = os.path.join(fdroidserver.update.get_icon_dir('repo', density),
402                                          apk['icon'])
403                 self.assertTrue(os.path.isfile(icon_path))
404                 self.assertTrue(os.path.getsize(icon_path) > 1)
405
406             savepath = os.path.join('metadata', 'apk', apk['packageName'] + '.yaml')
407             # Uncomment to save APK metadata
408             # with open(savepath, 'w') as f:
409             #     yaml.add_representer(fdroidserver.metadata.Build, _build_yaml_representer)
410             #     yaml.dump(apk, f, default_flow_style=False)
411
412             with open(savepath, 'r') as f:
413                 frompickle = yaml.load(f)
414             self.maxDiff = None
415             self.assertEqual(apk, frompickle)
416
417     def test_process_apk_signed_by_disabled_algorithms(self):
418         config = dict()
419         fdroidserver.common.fill_config_defaults(config)
420         fdroidserver.update.config = config
421
422         config['ndk_paths'] = dict()
423         config['accepted_formats'] = ['json', 'txt', 'yml']
424         fdroidserver.common.config = config
425         fdroidserver.update.config = config
426
427         fdroidserver.update.options = type('', (), {})()
428         fdroidserver.update.options.clean = True
429         fdroidserver.update.options.verbose = True
430         fdroidserver.update.options.rename_apks = False
431         fdroidserver.update.options.delete_unknown = True
432         fdroidserver.update.options.allow_disabled_algorithms = False
433
434         knownapks = fdroidserver.common.KnownApks()
435
436         tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name,
437                                        dir=self.tmpdir)
438         print('tmptestsdir', tmptestsdir)
439         os.chdir(tmptestsdir)
440         os.mkdir('repo')
441         os.mkdir('archive')
442         # setup the repo, create icons dirs, etc.
443         fdroidserver.update.process_apks({}, 'repo', knownapks)
444         fdroidserver.update.process_apks({}, 'archive', knownapks)
445
446         disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ]
447         for apkName in disabledsigs:
448             shutil.copy(os.path.join(self.basedir, apkName),
449                         os.path.join(tmptestsdir, 'repo'))
450
451             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
452                                                                       knownapks,
453                                                                       allow_disabled_algorithms=True,
454                                                                       archive_bad_sig=False)
455             self.assertFalse(skip)
456             self.assertIsNotNone(apk)
457             self.assertTrue(cachechanged)
458             self.assertFalse(os.path.exists(os.path.join('archive', apkName)))
459             self.assertTrue(os.path.exists(os.path.join('repo', apkName)))
460
461             javac = config['jarsigner'].replace('jarsigner', 'javac')
462             v = subprocess.check_output([javac, '-version'], stderr=subprocess.STDOUT)[6:-1].decode('utf-8')
463             if LooseVersion(v) < LooseVersion('1.8.0_132'):
464                 print('SKIPPING: running tests with old Java (' + v + ')')
465                 return
466
467             # this test only works on systems with fully updated Java/jarsigner
468             # that has MD5 listed in jdk.jar.disabledAlgorithms in java.security
469             # https://blogs.oracle.com/java-platform-group/oracle-jre-will-no-longer-trust-md5-signed-code-by-default
470             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
471                                                                       knownapks,
472                                                                       allow_disabled_algorithms=False,
473                                                                       archive_bad_sig=True)
474             self.assertTrue(skip)
475             self.assertIsNone(apk)
476             self.assertFalse(cachechanged)
477             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
478             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
479
480             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'archive',
481                                                                       knownapks,
482                                                                       allow_disabled_algorithms=False,
483                                                                       archive_bad_sig=False)
484             self.assertFalse(skip)
485             self.assertIsNotNone(apk)
486             self.assertTrue(cachechanged)
487             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
488             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
489
490             # ensure that icons have been moved to the archive as well
491             for density in fdroidserver.update.screen_densities:
492                 icon_path = os.path.join(fdroidserver.update.get_icon_dir('archive', density),
493                                          apk['icon'])
494                 self.assertTrue(os.path.isfile(icon_path))
495                 self.assertTrue(os.path.getsize(icon_path) > 1)
496
497         badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ]
498         for apkName in badsigs:
499             shutil.copy(os.path.join(self.basedir, apkName),
500                         os.path.join(tmptestsdir, 'repo'))
501
502             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
503                                                                       knownapks,
504                                                                       allow_disabled_algorithms=False,
505                                                                       archive_bad_sig=False)
506             self.assertTrue(skip)
507             self.assertIsNone(apk)
508             self.assertFalse(cachechanged)
509
510     def test_process_invalid_apk(self):
511         os.chdir(os.path.join(localmodule, 'tests'))
512         if os.path.basename(os.getcwd()) != 'tests':
513             raise Exception('This test must be run in the "tests/" subdir')
514
515         config = dict()
516         fdroidserver.common.fill_config_defaults(config)
517         fdroidserver.common.config = config
518         fdroidserver.update.config = config
519         fdroidserver.update.options.delete_unknown = False
520
521         knownapks = fdroidserver.common.KnownApks()
522         apk = 'fake.ota.update_1234.zip'  # this is not an APK, scanning should fail
523         (skip, apk, cachechanged) = fdroidserver.update.process_apk({}, apk, 'repo', knownapks,
524                                                                     False)
525
526         self.assertTrue(skip)
527         self.assertIsNone(apk)
528         self.assertFalse(cachechanged)
529
530     def test_translate_per_build_anti_features(self):
531         os.chdir(os.path.join(localmodule, 'tests'))
532         if os.path.basename(os.getcwd()) != 'tests':
533             raise Exception('This test must be run in the "tests/" subdir')
534
535         config = dict()
536         fdroidserver.common.fill_config_defaults(config)
537         config['ndk_paths'] = dict()
538         config['accepted_formats'] = ['json', 'txt', 'yml']
539         fdroidserver.common.config = config
540         fdroidserver.update.config = config
541
542         fdroidserver.update.options = type('', (), {})()
543         fdroidserver.update.options.clean = True
544         fdroidserver.update.options.delete_unknown = True
545         fdroidserver.update.options.rename_apks = False
546         fdroidserver.update.options.allow_disabled_algorithms = False
547
548         apps = fdroidserver.metadata.read_metadata(xref=True)
549         knownapks = fdroidserver.common.KnownApks()
550         apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
551         fdroidserver.update.translate_per_build_anti_features(apps, apks)
552         self.assertEqual(len(apks), 13)
553         foundtest = False
554         for apk in apks:
555             if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3:
556                 antiFeatures = apk.get('antiFeatures')
557                 self.assertTrue('KnownVuln' in antiFeatures)
558                 self.assertEqual(3, len(antiFeatures))
559                 foundtest = True
560         self.assertTrue(foundtest)
561
562     def test_create_metadata_from_template(self):
563         tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name,
564                                        dir=self.tmpdir)
565         print('tmptestsdir', tmptestsdir)
566         os.chdir(tmptestsdir)
567         os.mkdir('repo')
568         os.mkdir('metadata')
569         shutil.copy(os.path.join(localmodule, 'tests', 'urzip.apk'), 'repo')
570
571         config = dict()
572         fdroidserver.common.fill_config_defaults(config)
573         config['ndk_paths'] = dict()
574         config['accepted_formats'] = ['json', 'txt', 'yml']
575         fdroidserver.common.config = config
576         fdroidserver.update.config = config
577
578         fdroidserver.update.options = type('', (), {})()
579         fdroidserver.update.options.clean = True
580         fdroidserver.update.options.delete_unknown = False
581         fdroidserver.update.options.rename_apks = False
582         fdroidserver.update.options.allow_disabled_algorithms = False
583
584         knownapks = fdroidserver.common.KnownApks()
585         apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
586         self.assertEqual(1, len(apks))
587         apk = apks[0]
588
589         testfile = 'metadata/info.guardianproject.urzip.yml'
590         # create empty 0 byte .yml file, run read_metadata, it should work
591         open(testfile, 'a').close()
592         apps = fdroidserver.metadata.read_metadata(xref=True)
593         self.assertEqual(1, len(apps))
594         os.remove(testfile)
595
596         # test using internal template
597         apps = fdroidserver.metadata.read_metadata(xref=True)
598         self.assertEqual(0, len(apps))
599         fdroidserver.update.create_metadata_from_template(apk)
600         self.assertTrue(os.path.exists(testfile))
601         apps = fdroidserver.metadata.read_metadata(xref=True)
602         self.assertEqual(1, len(apps))
603         for app in apps.values():
604             self.assertEqual('urzip', app['Name'])
605             self.assertEqual(1, len(app['Categories']))
606             break
607
608         # test using external template.yml
609         os.remove(testfile)
610         self.assertFalse(os.path.exists(testfile))
611         shutil.copy(os.path.join(localmodule, 'examples', 'template.yml'), tmptestsdir)
612         fdroidserver.update.create_metadata_from_template(apk)
613         self.assertTrue(os.path.exists(testfile))
614         apps = fdroidserver.metadata.read_metadata(xref=True)
615         self.assertEqual(1, len(apps))
616         for app in apps.values():
617             self.assertEqual('urzip', app['Name'])
618             self.assertEqual(1, len(app['Categories']))
619             self.assertEqual('Internet', app['Categories'][0])
620             break
621         with open(testfile) as fp:
622             data = yaml.load(fp)
623         self.assertEqual('urzip', data['Name'])
624         self.assertEqual('urzip', data['Summary'])
625
626     def test_has_known_vulnerability(self):
627         good = [
628             'org.bitbucket.tickytacky.mirrormirror_1.apk',
629             'org.bitbucket.tickytacky.mirrormirror_2.apk',
630             'org.bitbucket.tickytacky.mirrormirror_3.apk',
631             'org.bitbucket.tickytacky.mirrormirror_4.apk',
632             'org.dyndns.fules.ck_20.apk',
633             'urzip.apk',
634             'urzip-badcert.apk',
635             'urzip-badsig.apk',
636             'urzip-release.apk',
637             'urzip-release-unsigned.apk',
638             'repo/com.politedroid_3.apk',
639             'repo/com.politedroid_4.apk',
640             'repo/com.politedroid_5.apk',
641             'repo/com.politedroid_6.apk',
642             'repo/obb.main.oldversion_1444412523.apk',
643             'repo/obb.mainpatch.current_1619_another-release-key.apk',
644             'repo/obb.mainpatch.current_1619.apk',
645             'repo/obb.main.twoversions_1101613.apk',
646             'repo/obb.main.twoversions_1101615.apk',
647             'repo/obb.main.twoversions_1101617.apk',
648             'repo/urzip-; Рахма́нинов, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢尔盖·.apk',
649         ]
650         for f in good:
651             self.assertFalse(fdroidserver.update.has_known_vulnerability(f))
652         with self.assertRaises(fdroidserver.exception.FDroidException):
653             fdroidserver.update.has_known_vulnerability('janus.apk')
654
655     def test_get_apk_icon_when_src_is_none(self):
656         config = dict()
657         fdroidserver.common.fill_config_defaults(config)
658         fdroidserver.common.config = config
659         fdroidserver.update.config = config
660
661         # pylint: disable=protected-access
662         icons_src = fdroidserver.update._get_apk_icons_src('urzip-release.apk', None)
663         assert icons_src == {}
664
665
666 if __name__ == "__main__":
667     parser = optparse.OptionParser()
668     parser.add_option("-v", "--verbose", action="store_true", default=False,
669                       help="Spew out even more information than normal")
670     (fdroidserver.common.options, args) = parser.parse_args(['--verbose'])
671
672     newSuite = unittest.TestSuite()
673     newSuite.addTest(unittest.makeSuite(UpdateTest))
674     unittest.main(failfast=False)