chiark / gitweb /
aapt 26.0.0 is required to properly parse permissions and label
[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), 12)
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[5]
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.update.config = config
296         os.chdir(os.path.join(localmodule, 'tests'))
297         if os.path.basename(os.getcwd()) != 'tests':
298             raise Exception('This test must be run in the "tests/" subdir')
299
300         apk_info = fdroidserver.update.scan_apk('org.dyndns.fules.ck_20.apk')
301
302         self.assertEqual(apk_info['icons_src'], {'240': 'res/drawable-hdpi-v4/icon_launcher.png',
303                                                  '120': 'res/drawable-ldpi-v4/icon_launcher.png',
304                                                  '160': 'res/drawable-mdpi-v4/icon_launcher.png',
305                                                  '-1': 'res/drawable-mdpi-v4/icon_launcher.png'})
306         self.assertEqual(apk_info['icons'], {})
307         self.assertEqual(apk_info['features'], [])
308         self.assertEqual(apk_info['antiFeatures'], set())
309         self.assertEqual(apk_info['versionName'], 'v1.6pre2')
310         self.assertEqual(apk_info['hash'],
311                          '897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8')
312         self.assertEqual(apk_info['packageName'], 'org.dyndns.fules.ck')
313         self.assertEqual(apk_info['versionCode'], 20)
314         self.assertEqual(apk_info['size'], 132453)
315         self.assertEqual(apk_info['nativecode'],
316                          ['arm64-v8a', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'x86', 'x86_64'])
317         self.assertEqual(apk_info['minSdkVersion'], '7')
318         self.assertEqual(apk_info['sig'], '9bf7a6a67f95688daec75eab4b1436ac')
319         self.assertEqual(apk_info['hashType'], 'sha256')
320         self.assertEqual(apk_info['targetSdkVersion'], '8')
321
322     def test_scan_apk_no_sig(self):
323         config = dict()
324         fdroidserver.common.fill_config_defaults(config)
325         fdroidserver.update.config = config
326         os.chdir(os.path.join(localmodule, 'tests'))
327         if os.path.basename(os.getcwd()) != 'tests':
328             raise Exception('This test must be run in the "tests/" subdir')
329
330         with self.assertRaises(fdroidserver.exception.BuildException):
331             fdroidserver.update.scan_apk('urzip-release-unsigned.apk')
332
333     def test_process_apk(self):
334
335         def _build_yaml_representer(dumper, data):
336             '''Creates a YAML representation of a Build instance'''
337             return dumper.represent_dict(data)
338
339         config = dict()
340         fdroidserver.common.fill_config_defaults(config)
341         fdroidserver.update.config = config
342         os.chdir(os.path.join(localmodule, 'tests'))
343         if os.path.basename(os.getcwd()) != 'tests':
344             raise Exception('This test must be run in the "tests/" subdir')
345
346         config['ndk_paths'] = dict()
347         config['accepted_formats'] = ['json', 'txt', 'yml']
348         fdroidserver.common.config = config
349         fdroidserver.update.config = config
350
351         fdroidserver.update.options = type('', (), {})()
352         fdroidserver.update.options.clean = True
353         fdroidserver.update.options.rename_apks = False
354         fdroidserver.update.options.delete_unknown = True
355         fdroidserver.update.options.allow_disabled_algorithms = False
356
357         for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'):
358             if not os.path.exists(icon_dir):
359                 os.makedirs(icon_dir)
360
361         knownapks = fdroidserver.common.KnownApks()
362         apkList = ['../urzip.apk', '../org.dyndns.fules.ck_20.apk']
363
364         for apkName in apkList:
365             _, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo', knownapks,
366                                                                    False)
367             # Don't care about the date added to the repo and relative apkName
368             del apk['added']
369             del apk['apkName']
370             # avoid AAPT application name bug
371             del apk['name']
372
373             # ensure that icons have been extracted properly
374             if apkName == '../urzip.apk':
375                 self.assertEqual(apk['icon'], 'info.guardianproject.urzip.100.png')
376             if apkName == '../org.dyndns.fules.ck_20.apk':
377                 self.assertEqual(apk['icon'], 'org.dyndns.fules.ck.20.png')
378             for density in fdroidserver.update.screen_densities:
379                 icon_path = os.path.join(fdroidserver.update.get_icon_dir('repo', density),
380                                          apk['icon'])
381                 self.assertTrue(os.path.isfile(icon_path))
382                 self.assertTrue(os.path.getsize(icon_path) > 1)
383
384             savepath = os.path.join('metadata', 'apk', apk['packageName'] + '.yaml')
385             # Uncomment to save APK metadata
386             # with open(savepath, 'w') as f:
387             #     yaml.add_representer(fdroidserver.metadata.Build, _build_yaml_representer)
388             #     yaml.dump(apk, f, default_flow_style=False)
389
390             with open(savepath, 'r') as f:
391                 frompickle = yaml.load(f)
392             self.maxDiff = None
393             self.assertEqual(apk, frompickle)
394
395     def test_process_apk_signed_by_disabled_algorithms(self):
396         config = dict()
397         fdroidserver.common.fill_config_defaults(config)
398         fdroidserver.update.config = config
399
400         config['ndk_paths'] = dict()
401         config['accepted_formats'] = ['json', 'txt', 'yml']
402         fdroidserver.common.config = config
403         fdroidserver.update.config = config
404
405         fdroidserver.update.options = type('', (), {})()
406         fdroidserver.update.options.clean = True
407         fdroidserver.update.options.verbose = True
408         fdroidserver.update.options.rename_apks = False
409         fdroidserver.update.options.delete_unknown = True
410         fdroidserver.update.options.allow_disabled_algorithms = False
411
412         knownapks = fdroidserver.common.KnownApks()
413
414         tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name,
415                                        dir=self.tmpdir)
416         print('tmptestsdir', tmptestsdir)
417         os.chdir(tmptestsdir)
418         os.mkdir('repo')
419         os.mkdir('archive')
420         # setup the repo, create icons dirs, etc.
421         fdroidserver.update.process_apks({}, 'repo', knownapks)
422         fdroidserver.update.process_apks({}, 'archive', knownapks)
423
424         disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ]
425         for apkName in disabledsigs:
426             shutil.copy(os.path.join(self.basedir, apkName),
427                         os.path.join(tmptestsdir, 'repo'))
428
429             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
430                                                                       knownapks,
431                                                                       allow_disabled_algorithms=True,
432                                                                       archive_bad_sig=False)
433             self.assertFalse(skip)
434             self.assertIsNotNone(apk)
435             self.assertTrue(cachechanged)
436             self.assertFalse(os.path.exists(os.path.join('archive', apkName)))
437             self.assertTrue(os.path.exists(os.path.join('repo', apkName)))
438
439             javac = config['jarsigner'].replace('jarsigner', 'javac')
440             v = subprocess.check_output([javac, '-version'], stderr=subprocess.STDOUT)[6:-1].decode('utf-8')
441             if LooseVersion(v) < LooseVersion('1.8.0_132'):
442                 print('SKIPPING: running tests with old Java (' + v + ')')
443                 return
444
445             # this test only works on systems with fully updated Java/jarsigner
446             # that has MD5 listed in jdk.jar.disabledAlgorithms in java.security
447             # https://blogs.oracle.com/java-platform-group/oracle-jre-will-no-longer-trust-md5-signed-code-by-default
448             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
449                                                                       knownapks,
450                                                                       allow_disabled_algorithms=False,
451                                                                       archive_bad_sig=True)
452             self.assertTrue(skip)
453             self.assertIsNone(apk)
454             self.assertFalse(cachechanged)
455             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
456             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
457
458             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'archive',
459                                                                       knownapks,
460                                                                       allow_disabled_algorithms=False,
461                                                                       archive_bad_sig=False)
462             self.assertFalse(skip)
463             self.assertIsNotNone(apk)
464             self.assertTrue(cachechanged)
465             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
466             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
467
468             # ensure that icons have been moved to the archive as well
469             for density in fdroidserver.update.screen_densities:
470                 icon_path = os.path.join(fdroidserver.update.get_icon_dir('archive', density),
471                                          apk['icon'])
472                 self.assertTrue(os.path.isfile(icon_path))
473                 self.assertTrue(os.path.getsize(icon_path) > 1)
474
475         badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ]
476         for apkName in badsigs:
477             shutil.copy(os.path.join(self.basedir, apkName),
478                         os.path.join(tmptestsdir, 'repo'))
479
480             skip, apk, cachechanged = fdroidserver.update.process_apk({}, apkName, 'repo',
481                                                                       knownapks,
482                                                                       allow_disabled_algorithms=False,
483                                                                       archive_bad_sig=False)
484             self.assertTrue(skip)
485             self.assertIsNone(apk)
486             self.assertFalse(cachechanged)
487
488     def test_process_invalid_apk(self):
489         os.chdir(os.path.join(localmodule, 'tests'))
490         if os.path.basename(os.getcwd()) != 'tests':
491             raise Exception('This test must be run in the "tests/" subdir')
492
493         config = dict()
494         fdroidserver.common.fill_config_defaults(config)
495         fdroidserver.common.config = config
496         fdroidserver.update.config = config
497         fdroidserver.update.options.delete_unknown = False
498
499         knownapks = fdroidserver.common.KnownApks()
500         apk = 'fake.ota.update_1234.zip'  # this is not an APK, scanning should fail
501         (skip, apk, cachechanged) = fdroidserver.update.process_apk({}, apk, 'repo', knownapks,
502                                                                     False)
503
504         self.assertTrue(skip)
505         self.assertIsNone(apk)
506         self.assertFalse(cachechanged)
507
508     def test_translate_per_build_anti_features(self):
509         os.chdir(os.path.join(localmodule, 'tests'))
510         if os.path.basename(os.getcwd()) != 'tests':
511             raise Exception('This test must be run in the "tests/" subdir')
512
513         config = dict()
514         fdroidserver.common.fill_config_defaults(config)
515         config['ndk_paths'] = dict()
516         config['accepted_formats'] = ['json', 'txt', 'yml']
517         fdroidserver.common.config = config
518         fdroidserver.update.config = config
519
520         fdroidserver.update.options = type('', (), {})()
521         fdroidserver.update.options.clean = True
522         fdroidserver.update.options.delete_unknown = True
523         fdroidserver.update.options.rename_apks = False
524         fdroidserver.update.options.allow_disabled_algorithms = False
525
526         apps = fdroidserver.metadata.read_metadata(xref=True)
527         knownapks = fdroidserver.common.KnownApks()
528         apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
529         fdroidserver.update.translate_per_build_anti_features(apps, apks)
530         self.assertEqual(len(apks), 12)
531         foundtest = False
532         for apk in apks:
533             if apk['packageName'] == 'com.politedroid' and apk['versionCode'] == 3:
534                 antiFeatures = apk.get('antiFeatures')
535                 self.assertTrue('KnownVuln' in antiFeatures)
536                 self.assertEqual(3, len(antiFeatures))
537                 foundtest = True
538         self.assertTrue(foundtest)
539
540     def test_create_metadata_from_template(self):
541         tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name,
542                                        dir=self.tmpdir)
543         print('tmptestsdir', tmptestsdir)
544         os.chdir(tmptestsdir)
545         os.mkdir('repo')
546         os.mkdir('metadata')
547         shutil.copy(os.path.join(localmodule, 'tests', 'urzip.apk'), 'repo')
548
549         config = dict()
550         fdroidserver.common.fill_config_defaults(config)
551         config['ndk_paths'] = dict()
552         config['accepted_formats'] = ['json', 'txt', 'yml']
553         fdroidserver.common.config = config
554         fdroidserver.update.config = config
555
556         fdroidserver.update.options = type('', (), {})()
557         fdroidserver.update.options.clean = True
558         fdroidserver.update.options.delete_unknown = False
559         fdroidserver.update.options.rename_apks = False
560         fdroidserver.update.options.allow_disabled_algorithms = False
561
562         knownapks = fdroidserver.common.KnownApks()
563         apks, cachechanged = fdroidserver.update.process_apks({}, 'repo', knownapks, False)
564         self.assertEqual(1, len(apks))
565         apk = apks[0]
566
567         testfile = 'metadata/info.guardianproject.urzip.yml'
568         # create empty 0 byte .yml file, run read_metadata, it should work
569         open(testfile, 'a').close()
570         apps = fdroidserver.metadata.read_metadata(xref=True)
571         self.assertEqual(1, len(apps))
572         os.remove(testfile)
573
574         # test using internal template
575         apps = fdroidserver.metadata.read_metadata(xref=True)
576         self.assertEqual(0, len(apps))
577         fdroidserver.update.create_metadata_from_template(apk)
578         self.assertTrue(os.path.exists(testfile))
579         apps = fdroidserver.metadata.read_metadata(xref=True)
580         self.assertEqual(1, len(apps))
581         for app in apps.values():
582             self.assertEqual('urzip', app['Name'])
583             self.assertEqual(1, len(app['Categories']))
584             break
585
586         # test using external template.yml
587         os.remove(testfile)
588         self.assertFalse(os.path.exists(testfile))
589         shutil.copy(os.path.join(localmodule, 'examples', 'template.yml'), tmptestsdir)
590         fdroidserver.update.create_metadata_from_template(apk)
591         self.assertTrue(os.path.exists(testfile))
592         apps = fdroidserver.metadata.read_metadata(xref=True)
593         self.assertEqual(1, len(apps))
594         for app in apps.values():
595             self.assertEqual('urzip', app['Name'])
596             self.assertEqual(1, len(app['Categories']))
597             self.assertEqual('Internet', app['Categories'][0])
598             break
599         with open(testfile) as fp:
600             data = yaml.load(fp)
601         self.assertEqual('urzip', data['Name'])
602         self.assertEqual('urzip', data['Summary'])
603
604     def test_has_known_vulnerability(self):
605         good = [
606             'org.bitbucket.tickytacky.mirrormirror_1.apk',
607             'org.bitbucket.tickytacky.mirrormirror_2.apk',
608             'org.bitbucket.tickytacky.mirrormirror_3.apk',
609             'org.bitbucket.tickytacky.mirrormirror_4.apk',
610             'org.dyndns.fules.ck_20.apk',
611             'urzip.apk',
612             'urzip-badcert.apk',
613             'urzip-badsig.apk',
614             'urzip-release.apk',
615             'urzip-release-unsigned.apk',
616             'repo/com.politedroid_3.apk',
617             'repo/com.politedroid_4.apk',
618             'repo/com.politedroid_5.apk',
619             'repo/com.politedroid_6.apk',
620             'repo/obb.main.oldversion_1444412523.apk',
621             'repo/obb.mainpatch.current_1619_another-release-key.apk',
622             'repo/obb.mainpatch.current_1619.apk',
623             'repo/obb.main.twoversions_1101613.apk',
624             'repo/obb.main.twoversions_1101615.apk',
625             'repo/obb.main.twoversions_1101617.apk',
626             'repo/urzip-; Рахма́нинов, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢尔盖·.apk',
627         ]
628         for f in good:
629             self.assertFalse(fdroidserver.update.has_known_vulnerability(f))
630         with self.assertRaises(fdroidserver.exception.FDroidException):
631             fdroidserver.update.has_known_vulnerability('janus.apk')
632
633
634 if __name__ == "__main__":
635     parser = optparse.OptionParser()
636     parser.add_option("-v", "--verbose", action="store_true", default=False,
637                       help="Spew out even more information than normal")
638     (fdroidserver.common.options, args) = parser.parse_args(['--verbose'])
639
640     newSuite = unittest.TestSuite()
641     newSuite.addTest(unittest.makeSuite(UpdateTest))
642     unittest.main(failfast=False)