chiark / gitweb /
update: allow_disabled_algorithms option to keep MD5 sigs in repo
[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 sys
12 import tempfile
13 import unittest
14 import yaml
15 from binascii import unhexlify
16
17 localmodule = os.path.realpath(
18     os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
19 print('localmodule: ' + localmodule)
20 if localmodule not in sys.path:
21     sys.path.insert(0, localmodule)
22
23 import fdroidserver.common
24 import fdroidserver.metadata
25 import fdroidserver.update
26 from fdroidserver.common import FDroidPopen
27
28
29 class UpdateTest(unittest.TestCase):
30     '''fdroid update'''
31
32     def testInsertStoreMetadata(self):
33         config = dict()
34         fdroidserver.common.fill_config_defaults(config)
35         config['accepted_formats'] = ('txt', 'yml')
36         fdroidserver.update.config = config
37         fdroidserver.update.options = fdroidserver.common.options
38         os.chdir(os.path.join(localmodule, 'tests'))
39
40         shutil.rmtree(os.path.join('repo', 'info.guardianproject.urzip'), ignore_errors=True)
41
42         apps = dict()
43         for packageName in ('info.guardianproject.urzip', 'org.videolan.vlc', 'obb.mainpatch.current'):
44             apps[packageName] = dict()
45             apps[packageName]['id'] = packageName
46             apps[packageName]['CurrentVersionCode'] = 0xcafebeef
47         apps['info.guardianproject.urzip']['CurrentVersionCode'] = 100
48         fdroidserver.update.insert_localized_app_metadata(apps)
49
50         appdir = os.path.join('repo', 'info.guardianproject.urzip', 'en-US')
51         self.assertTrue(os.path.isfile(os.path.join(appdir, 'icon.png')))
52         self.assertTrue(os.path.isfile(os.path.join(appdir, 'featureGraphic.png')))
53
54         self.assertEqual(3, len(apps))
55         for packageName, app in apps.items():
56             self.assertTrue('localized' in app)
57             self.assertTrue('en-US' in app['localized'])
58             self.assertEqual(1, len(app['localized']))
59             if packageName == 'info.guardianproject.urzip':
60                 self.assertEqual(7, len(app['localized']['en-US']))
61                 self.assertEqual('full description\n', app['localized']['en-US']['description'])
62                 self.assertEqual('title\n', app['localized']['en-US']['name'])
63                 self.assertEqual('short description\n', app['localized']['en-US']['summary'])
64                 self.assertEqual('video\n', app['localized']['en-US']['video'])
65                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
66                 self.assertEqual('featureGraphic.png', app['localized']['en-US']['featureGraphic'])
67                 self.assertEqual('100\n', app['localized']['en-US']['whatsNew'])
68             elif packageName == 'org.videolan.vlc':
69                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
70                 self.assertEqual(9, len(app['localized']['en-US']['phoneScreenshots']))
71                 self.assertEqual(15, len(app['localized']['en-US']['sevenInchScreenshots']))
72             elif packageName == 'obb.mainpatch.current':
73                 self.assertEqual('icon.png', app['localized']['en-US']['icon'])
74                 self.assertEqual('featureGraphic.png', app['localized']['en-US']['featureGraphic'])
75                 self.assertEqual(1, len(app['localized']['en-US']['phoneScreenshots']))
76                 self.assertEqual(1, len(app['localized']['en-US']['sevenInchScreenshots']))
77
78     def test_insert_triple_t_metadata(self):
79         importer = os.path.join(localmodule, 'tests', 'tmp', 'importer')
80         packageName = 'org.fdroid.ci.test.app'
81         if not os.path.isdir(importer):
82             logging.warning('skipping test_insert_triple_t_metadata, import.TestCase must run first!')
83             return
84         tmpdir = os.path.join(localmodule, '.testfiles')
85         if not os.path.exists(tmpdir):
86             os.makedirs(tmpdir)
87         tmptestsdir = tempfile.mkdtemp(prefix='test_insert_triple_t_metadata-', dir=tmpdir)
88         packageDir = os.path.join(tmptestsdir, 'build', packageName)
89         shutil.copytree(importer, packageDir)
90
91         # always use the same commit so these tests work when ci-test-app.git is updated
92         repo = git.Repo(packageDir)
93         for remote in repo.remotes:
94             remote.fetch()
95         repo.git.reset('--hard', 'b9e5d1a0d8d6fc31d4674b2f0514fef10762ed4f')
96         repo.git.clean('-fdx')
97
98         os.mkdir(os.path.join(tmptestsdir, 'metadata'))
99         metadata = dict()
100         metadata['Description'] = 'This is just a test app'
101         with open(os.path.join(tmptestsdir, 'metadata', packageName + '.yml'), 'w') as fp:
102             yaml.dump(metadata, fp)
103
104         config = dict()
105         fdroidserver.common.fill_config_defaults(config)
106         config['accepted_formats'] = ('yml')
107         fdroidserver.common.config = config
108         fdroidserver.update.config = config
109         fdroidserver.update.options = fdroidserver.common.options
110         os.chdir(tmptestsdir)
111
112         apps = fdroidserver.metadata.read_metadata(xref=True)
113         fdroidserver.update.copy_triple_t_store_metadata(apps)
114
115         # TODO ideally, this would compare the whole dict like in metadata.TestCase's test_read_metadata()
116         correctlocales = [
117             'ar', 'ast_ES', 'az', 'ca', 'ca_ES', 'cs-CZ', 'cs_CZ', 'da',
118             'da-DK', 'de', 'de-DE', 'el', 'en-US', 'es', 'es-ES', 'es_ES', 'et',
119             'fi', 'fr', 'fr-FR', 'he_IL', 'hi-IN', 'hi_IN', 'hu', 'id', 'it',
120             'it-IT', 'it_IT', 'iw-IL', 'ja', 'ja-JP', 'kn_IN', 'ko', 'ko-KR',
121             'ko_KR', 'lt', 'nb', 'nb_NO', 'nl', 'nl-NL', 'no', 'pl', 'pl-PL',
122             'pl_PL', 'pt', 'pt-BR', 'pt-PT', 'pt_BR', 'ro', 'ro_RO', 'ru-RU',
123             'ru_RU', 'sv-SE', 'sv_SE', 'te', 'tr', 'tr-TR', 'uk', 'uk_UA', 'vi',
124             'vi_VN', 'zh-CN', 'zh_CN', 'zh_TW',
125         ]
126         locales = sorted(list(apps['org.fdroid.ci.test.app']['localized'].keys()))
127         self.assertEqual(correctlocales, locales)
128
129     def javagetsig(self, apkfile):
130         getsig_dir = os.path.join(os.path.dirname(__file__), 'getsig')
131         if not os.path.exists(getsig_dir + "/getsig.class"):
132             logging.critical("getsig.class not found. To fix: cd '%s' && ./make.sh" % getsig_dir)
133             sys.exit(1)
134         # FDroidPopen needs some config to work
135         config = dict()
136         fdroidserver.common.fill_config_defaults(config)
137         fdroidserver.common.config = config
138         p = FDroidPopen(['java', '-cp', os.path.join(os.path.dirname(__file__), 'getsig'),
139                          'getsig', os.path.join(os.getcwd(), apkfile)])
140         sig = None
141         for line in p.output.splitlines():
142             if line.startswith('Result:'):
143                 sig = line[7:].strip()
144                 break
145         if p.returncode == 0:
146             return sig
147         else:
148             return None
149
150     def testGoodGetsig(self):
151         # config needed to use jarsigner and keytool
152         config = dict()
153         fdroidserver.common.fill_config_defaults(config)
154         fdroidserver.update.config = config
155         apkfile = os.path.join(os.path.dirname(__file__), 'urzip.apk')
156         sig = self.javagetsig(apkfile)
157         self.assertIsNotNone(sig, "sig is None")
158         pysig = fdroidserver.update.getsig(apkfile)
159         self.assertIsNotNone(pysig, "pysig is None")
160         self.assertEqual(sig, fdroidserver.update.getsig(apkfile),
161                          "python sig not equal to java sig!")
162         self.assertEqual(len(sig), len(pysig),
163                          "the length of the two sigs are different!")
164         try:
165             self.assertEqual(unhexlify(sig), unhexlify(pysig),
166                              "the length of the two sigs are different!")
167         except TypeError as e:
168             print(e)
169             self.assertTrue(False, 'TypeError!')
170
171     def testBadGetsig(self):
172         """getsig() should still be able to fetch the fingerprint of bad signatures"""
173         # config needed to use jarsigner and keytool
174         config = dict()
175         fdroidserver.common.fill_config_defaults(config)
176         fdroidserver.update.config = config
177
178         apkfile = os.path.join(os.path.dirname(__file__), 'urzip-badsig.apk')
179         sig = fdroidserver.update.getsig(apkfile)
180         self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
181                          "python sig should be: " + str(sig))
182
183         apkfile = os.path.join(os.path.dirname(__file__), 'urzip-badcert.apk')
184         sig = fdroidserver.update.getsig(apkfile)
185         self.assertEqual(sig, 'e0ecb5fc2d63088e4a07ae410a127722',
186                          "python sig should be: " + str(sig))
187
188     def testScanApksAndObbs(self):
189         os.chdir(os.path.join(localmodule, 'tests'))
190         if os.path.basename(os.getcwd()) != 'tests':
191             raise Exception('This test must be run in the "tests/" subdir')
192
193         config = dict()
194         fdroidserver.common.fill_config_defaults(config)
195         config['ndk_paths'] = dict()
196         config['accepted_formats'] = ['json', 'txt', 'yml']
197         fdroidserver.common.config = config
198         fdroidserver.update.config = config
199
200         fdroidserver.update.options = type('', (), {})()
201         fdroidserver.update.options.clean = True
202         fdroidserver.update.options.delete_unknown = True
203         fdroidserver.update.options.rename_apks = False
204         fdroidserver.update.options.allow_disabled_algorithms = False
205
206         apps = fdroidserver.metadata.read_metadata(xref=True)
207         knownapks = fdroidserver.common.KnownApks()
208         apks, cachechanged = fdroidserver.update.scan_apks({}, 'repo', knownapks, False)
209         self.assertEqual(len(apks), 11)
210         apk = apks[0]
211         self.assertEqual(apk['packageName'], 'com.politedroid')
212         self.assertEqual(apk['versionCode'], 3)
213         self.assertEqual(apk['minSdkVersion'], '3')
214         self.assertEqual(apk['targetSdkVersion'], '3')
215         self.assertFalse('maxSdkVersion' in apk)
216         apk = apks[4]
217         self.assertEqual(apk['packageName'], 'obb.main.oldversion')
218         self.assertEqual(apk['versionCode'], 1444412523)
219         self.assertEqual(apk['minSdkVersion'], '4')
220         self.assertEqual(apk['targetSdkVersion'], '18')
221         self.assertFalse('maxSdkVersion' in apk)
222
223         fdroidserver.update.insert_obbs('repo', apps, apks)
224         for apk in apks:
225             if apk['packageName'] == 'obb.mainpatch.current':
226                 self.assertEqual(apk.get('obbMainFile'), 'main.1619.obb.mainpatch.current.obb')
227                 self.assertEqual(apk.get('obbPatchFile'), 'patch.1619.obb.mainpatch.current.obb')
228             elif apk['packageName'] == 'obb.main.oldversion':
229                 self.assertEqual(apk.get('obbMainFile'), 'main.1434483388.obb.main.oldversion.obb')
230                 self.assertIsNone(apk.get('obbPatchFile'))
231             elif apk['packageName'] == 'obb.main.twoversions':
232                 self.assertIsNone(apk.get('obbPatchFile'))
233                 if apk['versionCode'] == 1101613:
234                     self.assertEqual(apk.get('obbMainFile'), 'main.1101613.obb.main.twoversions.obb')
235                 elif apk['versionCode'] == 1101615:
236                     self.assertEqual(apk.get('obbMainFile'), 'main.1101615.obb.main.twoversions.obb')
237                 elif apk['versionCode'] == 1101617:
238                     self.assertEqual(apk.get('obbMainFile'), 'main.1101615.obb.main.twoversions.obb')
239                 else:
240                     self.assertTrue(False)
241             elif apk['packageName'] == 'info.guardianproject.urzip':
242                 self.assertIsNone(apk.get('obbMainFile'))
243                 self.assertIsNone(apk.get('obbPatchFile'))
244
245     def testScanApkMetadata(self):
246
247         def _build_yaml_representer(dumper, data):
248             '''Creates a YAML representation of a Build instance'''
249             return dumper.represent_dict(data)
250
251         config = dict()
252         fdroidserver.common.fill_config_defaults(config)
253         fdroidserver.update.config = config
254         os.chdir(os.path.join(localmodule, 'tests'))
255         if os.path.basename(os.getcwd()) != 'tests':
256             raise Exception('This test must be run in the "tests/" subdir')
257
258         config['ndk_paths'] = dict()
259         config['accepted_formats'] = ['json', 'txt', 'yml']
260         fdroidserver.common.config = config
261         fdroidserver.update.config = config
262
263         fdroidserver.update.options = type('', (), {})()
264         fdroidserver.update.options.clean = True
265         fdroidserver.update.options.rename_apks = False
266         fdroidserver.update.options.delete_unknown = True
267         fdroidserver.update.options.allow_disabled_algorithms = False
268
269         for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'):
270             if not os.path.exists(icon_dir):
271                 os.makedirs(icon_dir)
272
273         knownapks = fdroidserver.common.KnownApks()
274         apkList = ['../urzip.apk', '../org.dyndns.fules.ck_20.apk']
275
276         for apkName in apkList:
277             _, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, False)
278             # Don't care about the date added to the repo and relative apkName
279             del apk['added']
280             del apk['apkName']
281             # avoid AAPT application name bug
282             del apk['name']
283
284             savepath = os.path.join('metadata', 'apk', apk['packageName'] + '.yaml')
285             # Uncomment to save APK metadata
286             # with open(savepath, 'w') as f:
287             #     yaml.add_representer(fdroidserver.metadata.Build, _build_yaml_representer)
288             #     yaml.dump(apk, f, default_flow_style=False)
289
290             with open(savepath, 'r') as f:
291                 frompickle = yaml.load(f)
292             self.maxDiff = None
293             self.assertEqual(apk, frompickle)
294
295     def test_scan_apk_signed_by_disabled_algorithms(self):
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         config = dict()
301         fdroidserver.common.fill_config_defaults(config)
302         fdroidserver.update.config = config
303
304         config['ndk_paths'] = dict()
305         config['accepted_formats'] = ['json', 'txt', 'yml']
306         fdroidserver.common.config = config
307         fdroidserver.update.config = config
308
309         fdroidserver.update.options = type('', (), {})()
310         fdroidserver.update.options.clean = True
311         fdroidserver.update.options.verbose = True
312         fdroidserver.update.options.rename_apks = False
313         fdroidserver.update.options.delete_unknown = True
314         fdroidserver.update.options.allow_disabled_algorithms = False
315
316         knownapks = fdroidserver.common.KnownApks()
317         apksourcedir = os.getcwd()
318         tmpdir = os.path.join(localmodule, '.testfiles')
319         if not os.path.exists(tmpdir):
320             os.makedirs(tmpdir)
321         tmptestsdir = tempfile.mkdtemp(prefix='test_scan_apk_signed_by_disabled_algorithms-', dir=tmpdir)
322         print('tmptestsdir', tmptestsdir)
323         os.chdir(tmptestsdir)
324         os.mkdir('repo')
325         os.mkdir('archive')
326         # setup the repo, create icons dirs, etc.
327         fdroidserver.update.scan_apks({}, 'repo', knownapks)
328         fdroidserver.update.scan_apks({}, 'archive', knownapks)
329
330         disabledsigs = ['org.bitbucket.tickytacky.mirrormirror_2.apk', ]
331         for apkName in disabledsigs:
332             shutil.copy(os.path.join(apksourcedir, apkName),
333                         os.path.join(tmptestsdir, 'repo'))
334
335             skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
336                                                                    allow_disabled_algorithms=True,
337                                                                    archive_bad_sig=False)
338             self.assertFalse(skip)
339             self.assertIsNotNone(apk)
340             self.assertTrue(cachechanged)
341             self.assertFalse(os.path.exists(os.path.join('archive', apkName)))
342             self.assertTrue(os.path.exists(os.path.join('repo', apkName)))
343
344             # this test only works on systems with fully updated Java/jarsigner
345             # that has MD5 listed in jdk.jar.disabledAlgorithms in java.security
346             skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
347                                                                    allow_disabled_algorithms=False,
348                                                                    archive_bad_sig=True)
349             self.assertTrue(skip)
350             self.assertIsNone(apk)
351             self.assertFalse(cachechanged)
352             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
353             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
354
355             skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'archive', knownapks,
356                                                                    allow_disabled_algorithms=False,
357                                                                    archive_bad_sig=False)
358             self.assertFalse(skip)
359             self.assertIsNotNone(apk)
360             self.assertTrue(cachechanged)
361             self.assertTrue(os.path.exists(os.path.join('archive', apkName)))
362             self.assertFalse(os.path.exists(os.path.join('repo', apkName)))
363
364         badsigs = ['urzip-badcert.apk', 'urzip-badsig.apk', 'urzip-release-unsigned.apk', ]
365         for apkName in badsigs:
366             shutil.copy(os.path.join(apksourcedir, apkName),
367                         os.path.join(tmptestsdir, 'repo'))
368
369             skip, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks,
370                                                                    allow_disabled_algorithms=False,
371                                                                    archive_bad_sig=False)
372             self.assertTrue(skip)
373             self.assertIsNone(apk)
374             self.assertFalse(cachechanged)
375
376     def test_scan_invalid_apk(self):
377         os.chdir(os.path.join(localmodule, 'tests'))
378         if os.path.basename(os.getcwd()) != 'tests':
379             raise Exception('This test must be run in the "tests/" subdir')
380
381         config = dict()
382         fdroidserver.common.fill_config_defaults(config)
383         fdroidserver.common.config = config
384         fdroidserver.update.config = config
385         fdroidserver.update.options.delete_unknown = False
386
387         knownapks = fdroidserver.common.KnownApks()
388         apk = 'fake.ota.update_1234.zip'  # this is not an APK, scanning should fail
389         (skip, apk, cachechanged) = fdroidserver.update.scan_apk({}, apk, 'repo', knownapks, False)
390
391         self.assertTrue(skip)
392         self.assertIsNone(apk)
393         self.assertFalse(cachechanged)
394
395
396 if __name__ == "__main__":
397     parser = optparse.OptionParser()
398     parser.add_option("-v", "--verbose", action="store_true", default=False,
399                       help="Spew out even more information than normal")
400     (fdroidserver.common.options, args) = parser.parse_args(['--verbose'])
401
402     newSuite = unittest.TestSuite()
403     newSuite.addTest(unittest.makeSuite(UpdateTest))
404     unittest.main()