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