Merge lp:~mardy/click-reviewers-tools/new-account-hook into lp:click-reviewers-tools

Proposed by Alberto Mardegan on 2015-08-19
Status: Merged
Merged at revision: 516
Proposed branch: lp:~mardy/click-reviewers-tools/new-account-hook
Merge into: lp:click-reviewers-tools
Diff against target: 538 lines (+367/-8)
4 files modified
clickreviews/cr_common.py (+2/-0)
clickreviews/cr_online_accounts.py (+159/-4)
clickreviews/cr_tests.py (+26/-4)
clickreviews/tests/test_cr_online_accounts.py (+180/-0)
To merge this branch: bzr merge lp:~mardy/click-reviewers-tools/new-account-hook
Reviewer Review Type Date Requested Status
Jamie Strandboge 2015-08-19 Approve on 2015-09-10
Review via email: mp+268445@code.launchpad.net

Commit Message

Support the new "accounts" hook

A new hook for Online Accounts is being added, and it's intended to replace both the "account-application" and "account-service" hooks. It takes a JSON file, which acts as a manifest for how the app uses OA, and the hook program will then generate the needed .application and .service files.

With this change we check that the new "accounts" hook becomes the mandatory OA hook starting from the 15.10 framework.

Description of the Change

Support the new "accounts" hook

A new hook for Online Accounts is being added, and it's intended to replace both the "account-application" and "account-service" hooks. It takes a JSON file, which acts as a manifest for how the app uses OA, and the hook program will then generate the needed .application and .service files.

With this change we check that the new "accounts" hook becomes the mandatory OA hook starting from the 15.10 framework.

To post a comment you must log in.
511. By Alberto Mardegan on 2015-08-21

OA: the name and description keys should not be mandatory

512. By Alberto Mardegan on 2015-08-24

Support plugins in new OA hook

513. By Alberto Mardegan on 2015-08-26

Add "auth" key to plugins

Jamie Strandboge (jdstrand) wrote :

Thanks for the MP! I've got a few comments inline.

review: Needs Fixing
514. By Alberto Mardegan on 2015-09-10

Address review comments, turn 15.10 errors into warnings

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'clickreviews/cr_common.py'
2--- clickreviews/cr_common.py 2015-08-20 20:54:51 +0000
3+++ clickreviews/cr_common.py 2015-09-10 08:33:02 +0000
4@@ -66,6 +66,7 @@
5 # click service)
6 app_allowed_peer_hooks = ["account-application",
7 "account-service",
8+ "accounts",
9 "account-provider",
10 "account-qml-plugin",
11 "apparmor",
12@@ -76,6 +77,7 @@
13 ]
14 scope_allowed_peer_hooks = ["account-application",
15 "account-service",
16+ "accounts",
17 "apparmor",
18 "scope",
19 ]
20
21=== modified file 'clickreviews/cr_online_accounts.py'
22--- clickreviews/cr_online_accounts.py 2015-08-13 21:07:13 +0000
23+++ clickreviews/cr_online_accounts.py 2015-09-10 08:33:02 +0000
24@@ -16,8 +16,10 @@
25
26 from __future__ import print_function
27
28-from clickreviews.cr_common import ClickReview, error
29+from clickreviews.cr_common import ClickReview, error, open_file_read
30+import json
31 import os
32+import re
33 # http://lxml.de/tutorial.html
34 import lxml.etree as etree
35
36@@ -40,6 +42,13 @@
37 ClickReview.app_allowed_peer_hooks + \
38 ClickReview.scope_allowed_peer_hooks
39
40+ peer_hooks['accounts'] = dict()
41+ peer_hooks['accounts']['allowed'] = \
42+ [h for h in (ClickReview.app_allowed_peer_hooks +
43+ ClickReview.scope_allowed_peer_hooks)
44+ if not h.startswith('account-')]
45+ peer_hooks['accounts']['required'] = ['apparmor']
46+
47 peer_hooks['account-provider'] = dict()
48 peer_hooks['account-provider']['required'] = ['account-qml-plugin',
49 'apparmor'
50@@ -64,7 +73,8 @@
51 self.accounts_files = dict()
52 self.accounts = dict()
53
54- self.account_hooks = ['account-application',
55+ self.account_hooks = ['accounts',
56+ 'account-application',
57 'account-provider',
58 'account-qml-plugin',
59 'account-service']
60@@ -77,7 +87,7 @@
61 error("manifest malformed: hooks/%s/%s is not a str" % (
62 app, h))
63
64- (full_fn, xml) = self._extract_account(app, h)
65+ (full_fn, parsed) = self._extract_account(app, h)
66
67 if app not in self.accounts_files:
68 self.accounts_files[app] = dict()
69@@ -85,7 +95,28 @@
70
71 if app not in self.accounts:
72 self.accounts[app] = dict()
73- self.accounts[app][h] = xml
74+ self.accounts[app][h] = parsed
75+
76+ self.required_keys = dict()
77+ self.allowed_keys = dict()
78+ self.required_keys["service"] = [
79+ ('provider', str),
80+ ]
81+ self.allowed_keys["service"] = [
82+ ('auth', dict),
83+ ('name', str),
84+ ('description', str),
85+ ]
86+ self.required_keys["plugin"] = [
87+ ('provider', str),
88+ ('name', str),
89+ ('icon', str),
90+ ('qml', str),
91+ ]
92+ self.allowed_keys["plugin"] = [
93+ ('auth', dict),
94+ ]
95+ self.provider_re = re.compile('^[a-zA-Z0-9_.-]+$')
96
97 def _extract_account(self, app, account_type):
98 '''Extract accounts'''
99@@ -100,6 +131,23 @@
100 # the hook present for now
101 if account_type == "account-qml-plugin":
102 return (fn, True)
103+ elif account_type == "accounts":
104+ fh = open_file_read(fn)
105+ contents = ""
106+ for line in fh.readlines():
107+ contents += line
108+ fh.close()
109+
110+ try:
111+ jd = json.loads(contents)
112+ except Exception as e:
113+ error("accounts json unparseable: %s (%s):\n%s" % (bn,
114+ str(e), contents))
115+
116+ if not isinstance(jd, dict):
117+ error("accounts json is malformed: %s:\n%s" % (bn, contents))
118+
119+ return (fn, jd)
120 else:
121 try:
122 tree = etree.parse(fn)
123@@ -108,6 +156,113 @@
124 error("accounts xml unparseable: %s (%s)" % (bn, str(e)))
125 return (fn, xml)
126
127+ def check_hooks_versions(self):
128+ '''Check hooks versions'''
129+ framework = self.manifest['framework']
130+ if not framework.startswith("ubuntu-sdk"):
131+ return
132+ t = "error"
133+ if framework < "ubuntu-sdk-15.10":
134+ for app in sorted(self.accounts.keys()):
135+ for hook in self.accounts[app].keys():
136+ if hook == "accounts":
137+ n = self._get_check_name('%s_hook' % hook, app=app)
138+ s = "'accounts' hook is not available in '%s' (must be 15.10 or later)" % \
139+ (framework)
140+ self._add_result(t, n, s)
141+ return
142+ hook_state = "disallowed"
143+ if framework < "ubuntu-sdk-16.04":
144+ t = "warn"
145+ hook_state = "deprecated"
146+ for app in sorted(self.accounts.keys()):
147+ for hook in self.accounts[app].keys():
148+ if hook.startswith("account-"):
149+ n = self._get_check_name('%s_hook' % hook, app=app)
150+ s = "'%s' is %s in %s: use 'accounts' hook instead" % \
151+ (hook, hook_state, framework)
152+ self._add_result(t, n, s)
153+
154+ def _check_object(self, obj_type, obj, n):
155+ t = "info"
156+ s = "OK"
157+ if not isinstance(obj, dict):
158+ t = "error"
159+ s = "%s is not an object" % obj_type
160+ self._add_result(t, n, s)
161+ return
162+
163+ for (k, vt) in self.required_keys[obj_type]:
164+ if k not in obj.keys():
165+ t = "error"
166+ s = "required key '%s' is missing" % k
167+ self._add_result(t, n, s)
168+ if t == "error":
169+ return
170+
171+ known_keys = self.required_keys[obj_type] + self.allowed_keys[obj_type]
172+ for (k, v) in obj.items():
173+ type_list = [kk[1] for kk in known_keys if kk[0] == k]
174+ if len(type_list) < 1:
175+ t = "error"
176+ s = "unrecognized key '%s'" % k
177+ self._add_result(t, n, s)
178+ continue
179+ if not isinstance(v, type_list[0]):
180+ t = "error"
181+ s = "value for '%s' must be of type %s" % (k, type_list[0])
182+ self._add_result(t, n, s)
183+ continue
184+ if k == 'provider' and not self.provider_re.match(v):
185+ t = "error"
186+ s = "'provider' must only consist of alphanumeric characters"
187+ self._add_result(t, n, s)
188+ self._add_result(t, n, s)
189+
190+ def _check_object_list(self, app, key, obj_type, obj_list):
191+ t = 'info'
192+ n = self._get_check_name('accounts_%s' % key, app=app)
193+ if not isinstance(obj_list, list):
194+ t = "error"
195+ s = "'%s' is not a list" % key
196+ elif len(obj_list) < 1:
197+ t = "error"
198+ s = "'%s' is empty" % key
199+ if t == "error":
200+ self._add_result(t, n, s)
201+ return
202+
203+ for (i, obj) in enumerate(obj_list):
204+ n = self._get_check_name('accounts_%s' % (obj_type), app=app, extra=str(i))
205+ self._check_object(obj_type, obj, n)
206+
207+ def check_manifest(self):
208+ '''Check manifest'''
209+ for app in sorted(self.accounts.keys()):
210+ account_type = "accounts"
211+
212+ t = 'info'
213+ n = self._get_check_name('%s_root' % account_type, app=app)
214+ s = "OK"
215+ if account_type not in self.accounts[app]:
216+ s = "OK (missing)"
217+ self._add_result(t, n, s)
218+ continue
219+
220+ n = self._get_check_name('%s_services' % account_type, app=app)
221+ if 'services' not in self.accounts[app][account_type]:
222+ t = "error"
223+ s = "'services' key is missing"
224+ self._add_result(t, n, s)
225+ continue
226+ services = self.accounts[app][account_type]['services']
227+ self._check_object_list(app, "services", "service", services)
228+
229+ n = self._get_check_name('%s_plugins' % account_type, app=app)
230+ if 'plugins' in self.accounts[app][account_type]:
231+ plugins = self.accounts[app][account_type]['plugins']
232+ self._check_object_list(app, "plugins", "plugin", plugins)
233+
234 def check_application(self):
235 '''Check application'''
236 for app in sorted(self.accounts.keys()):
237
238=== modified file 'clickreviews/cr_tests.py'
239--- clickreviews/cr_tests.py 2015-07-07 19:42:41 +0000
240+++ clickreviews/cr_tests.py 2015-09-10 08:33:02 +0000
241@@ -40,6 +40,7 @@
242 TEST_URLS = dict()
243 TEST_SCOPES = dict()
244 TEST_CONTENT_HUB = dict()
245+TEST_ACCOUNTS_MANIFEST = dict()
246 TEST_ACCOUNTS_APPLICATION = dict()
247 TEST_ACCOUNTS_PROVIDER = dict()
248 TEST_ACCOUNTS_QML_PLUGIN = dict()
249@@ -190,7 +191,10 @@
250 '''Pretend we read the accounts file'''
251 f = app
252 val = None
253- if account_type == "account-application":
254+ if account_type == "accounts":
255+ f += ".accounts"
256+ val = TEST_ACCOUNTS_MANIFEST[app]
257+ elif account_type == "account-application":
258 f += ".application"
259 val = TEST_ACCOUNTS_APPLICATION[app]
260 elif account_type == "account-provider":
261@@ -449,6 +453,7 @@
262 self.test_url_dispatcher = dict()
263 self.test_scopes = dict()
264 self.test_content_hub = dict()
265+ self.test_accounts_manifest = dict()
266 self.test_accounts_application = dict()
267 self.test_accounts_provider = dict()
268 self.test_accounts_qml_plugin = dict()
269@@ -488,6 +493,7 @@
270 self.set_test_content_hub(app, None, None)
271
272 # Reset to no accounts entries in manifest
273+ self.set_test_account(app, "accounts", None)
274 self.set_test_account(app, "account-application", None)
275 self.set_test_account(app, "account-provider", None)
276 self.set_test_account(app, "account-qml-plugin", None)
277@@ -515,6 +521,7 @@
278 self._update_test_url_dispatcher()
279 self._update_test_scopes()
280 self._update_test_content_hub()
281+ self._update_test_accounts_manifest()
282 self._update_test_accounts_application()
283 self._update_test_accounts_provider()
284 self._update_test_accounts_qml_plugin()
285@@ -618,6 +625,15 @@
286 "%s.content.json" % app
287 self._update_test_manifest()
288
289+ def _update_test_accounts_manifest(self):
290+ global TEST_ACCOUNTS_MANIFEST
291+ TEST_ACCOUNTS_MANIFEST = dict()
292+ for app in self.test_accounts_manifest.keys():
293+ TEST_ACCOUNTS_MANIFEST[app] = self.test_accounts_manifest[app]
294+ self.test_manifest["hooks"][app]["accounts"] = \
295+ "%s.accounts" % app
296+ self._update_test_manifest()
297+
298 def _update_test_accounts_application(self):
299 global TEST_ACCOUNTS_APPLICATION
300 TEST_ACCOUNTS_APPLICATION = dict()
301@@ -923,7 +939,9 @@
302
303 def set_test_account(self, app, account_type, value):
304 '''Set accounts XML. If value is None, remove from manifest'''
305- if account_type == "account-application":
306+ if account_type == "accounts":
307+ d = self.test_accounts_manifest
308+ elif account_type == "account-application":
309 d = self.test_accounts_application
310 elif account_type == "account-provider":
311 d = self.test_accounts_provider
312@@ -940,7 +958,9 @@
313 else:
314 d[app] = value
315
316- if account_type == "account-application":
317+ if account_type == "accounts":
318+ self._update_test_accounts_manifest()
319+ elif account_type == "account-application":
320 self._update_test_accounts_application()
321 elif account_type == "account-provider":
322 self._update_test_accounts_provider()
323@@ -1115,6 +1135,8 @@
324 TEST_SCOPES = dict()
325 global TEST_CONTENT_HUB
326 TEST_CONTENT_HUB = dict()
327+ global TEST_ACCOUNTS_MANIFEST
328+ TEST_ACCOUNTS_MANIFEST = dict()
329 global TEST_ACCOUNTS_APPLICATION
330 TEST_ACCOUNTS_APPLICATION = dict()
331 global TEST_ACCOUNTS_PROVIDER
332@@ -1122,7 +1144,7 @@
333 global TEST_ACCOUNTS_QML_PLUGIN
334 TEST_ACCOUNTS_QML_PLUGIN = dict()
335 global TEST_ACCOUNTS_SERVICE
336- TEST_ACCOUNTS_APPLICATION = dict()
337+ TEST_ACCOUNTS_SERVICE = dict()
338 global TEST_PUSH_HELPER
339 TEST_PUSH_HELPER = dict()
340 global TEST_BIN_PATH
341
342=== modified file 'clickreviews/tests/test_cr_online_accounts.py'
343--- clickreviews/tests/test_cr_online_accounts.py 2015-07-22 16:30:36 +0000
344+++ clickreviews/tests/test_cr_online_accounts.py 2015-09-10 08:33:02 +0000
345@@ -16,6 +16,7 @@
346
347 from clickreviews.cr_online_accounts import ClickReviewAccounts
348 import clickreviews.cr_tests as cr_tests
349+import json
350 import lxml.etree as etree
351
352
353@@ -84,6 +85,185 @@
354 # More can go here, see /usr/share/accounts/providers/*
355 return xml
356
357+ def test_check_hooks_versions_new(self):
358+ '''Test check_hooks_versions() - new hook'''
359+ self.set_test_manifest("framework", "ubuntu-sdk-15.10")
360+ self.set_test_account(self.default_appname, "accounts", dict())
361+ c = ClickReviewAccounts(self.test_name)
362+ c.check_hooks_versions()
363+ r = c.click_report
364+ expected_counts = {'info': 0, 'warn': 0, 'error': 0}
365+ self.check_results(r, expected_counts)
366+
367+ def test_check_hooks_versions_deprecated_service(self):
368+ '''Test check_hooks_versions() - deprecated -service hook'''
369+ self.set_test_manifest("framework", "ubuntu-sdk-15.10")
370+ self.set_test_account(self.default_appname, "account-service", dict())
371+ c = ClickReviewAccounts(self.test_name)
372+ c.check_hooks_versions()
373+ r = c.click_report
374+ expected_counts = {'info': 0, 'warn': 1, 'error': 0}
375+ self.check_results(r, expected_counts)
376+
377+ def test_check_hooks_versions_disallowed_service(self):
378+ '''Test check_hooks_versions() - deprecated -service hook'''
379+ self.set_test_manifest("framework", "ubuntu-sdk-16.04")
380+ self.set_test_account(self.default_appname, "account-service", dict())
381+ c = ClickReviewAccounts(self.test_name)
382+ c.check_hooks_versions()
383+ r = c.click_report
384+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
385+ self.check_results(r, expected_counts)
386+
387+ def test_check_hooks_versions_deprecated_application(self):
388+ '''Test check_hooks_versions() - deprecated -application hook'''
389+ self.set_test_manifest("framework", "ubuntu-sdk-15.10")
390+ self.set_test_account(self.default_appname, "account-application", dict())
391+ c = ClickReviewAccounts(self.test_name)
392+ c.check_hooks_versions()
393+ r = c.click_report
394+ expected_counts = {'info': 0, 'warn': 1, 'error': 0}
395+ self.check_results(r, expected_counts)
396+
397+ def test_check_hooks_versions_old_framework(self):
398+ '''Test check_hooks_versions() - deprecated -application hook'''
399+ self.set_test_manifest("framework", "ubuntu-sdk-15.04")
400+ self.set_test_account(self.default_appname, "account-application", dict())
401+ self.set_test_account(self.default_appname, "account-service", dict())
402+ c = ClickReviewAccounts(self.test_name)
403+ c.check_hooks_versions()
404+ r = c.click_report
405+ expected_counts = {'info': 0, 'warn': 0, 'error': 0}
406+ self.check_results(r, expected_counts)
407+
408+ def test_check_manifest(self):
409+ '''Test check_manifest()'''
410+ data = json.loads('''{
411+ "translations": "my-app",
412+ "services": [
413+ {
414+ "name": "Example",
415+ "provider": "myapp.com_example",
416+ "description": "publish my photos in example.com",
417+ "auth": {
418+ "oauth2/web_server": {
419+ "ClientId": "foo",
420+ "ClientSecret": "bar",
421+ "UseSSL": false,
422+ "Scopes": ["one scope","and another"]
423+ }
424+ }
425+ },
426+ {
427+ "provider": "becool"
428+ }
429+ ],
430+ "plugins": [
431+ {
432+ "provider": "example",
433+ "name": "Example site",
434+ "icon": "example.png",
435+ "qml": "qml_files"
436+ }
437+ ]
438+ }''')
439+ self.set_test_account(self.default_appname, "accounts", data)
440+ c = ClickReviewAccounts(self.test_name)
441+ c.check_manifest()
442+ r = c.click_report
443+ expected_counts = {'info': 3, 'warn': 0, 'error': 0}
444+ self.check_results(r, expected_counts)
445+
446+ def test_check_manifest_not_specified(self):
447+ '''Test check_manifest() - not specified'''
448+ c = ClickReviewAccounts(self.test_name)
449+ c.check_manifest()
450+ r = c.click_report
451+ expected_counts = {'info': 0, 'warn': 0, 'error': 0}
452+ self.check_results(r, expected_counts)
453+
454+ def test_check_manifest_missing_services(self):
455+ '''Test check_manifest() - missing services'''
456+ data = json.loads('''{ "translations": "my-app" }''')
457+ self.set_test_account(self.default_appname, "accounts", data)
458+ c = ClickReviewAccounts(self.test_name)
459+ c.check_manifest()
460+ r = c.click_report
461+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
462+ self.check_results(r, expected_counts)
463+
464+ def test_check_manifest_invalid_services(self):
465+ '''Test check_manifest() - invalid services'''
466+ data = json.loads('''{ "services": 12 }''')
467+ self.set_test_account(self.default_appname, "accounts", data)
468+ c = ClickReviewAccounts(self.test_name)
469+ c.check_manifest()
470+ r = c.click_report
471+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
472+ self.check_results(r, expected_counts)
473+
474+ def test_check_manifest_empty_services(self):
475+ '''Test check_manifest() - empty services'''
476+ data = json.loads('''{ "services": [] }''')
477+ self.set_test_account(self.default_appname, "accounts", data)
478+ c = ClickReviewAccounts(self.test_name)
479+ c.check_manifest()
480+ r = c.click_report
481+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
482+ self.check_results(r, expected_counts)
483+
484+ def test_check_manifest_empty_service(self):
485+ '''Test check_manifest() - empty services'''
486+ data = json.loads('''{ "services": [{}] }''')
487+ self.set_test_account(self.default_appname, "accounts", data)
488+ c = ClickReviewAccounts(self.test_name)
489+ c.check_manifest()
490+ r = c.click_report
491+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
492+ self.check_results(r, expected_counts)
493+
494+ def test_check_manifest_no_provider(self):
495+ '''Test check_manifest() - no provider'''
496+ data = json.loads('''{ "services": [{
497+ "name": "Example",
498+ "description": "Hello world"
499+ }] }''')
500+ self.set_test_account(self.default_appname, "accounts", data)
501+ c = ClickReviewAccounts(self.test_name)
502+ c.check_manifest()
503+ r = c.click_report
504+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
505+ self.check_results(r, expected_counts)
506+
507+ def test_check_manifest_invalid_provider(self):
508+ '''Test check_manifest() - invalid provider'''
509+ data = json.loads('''{ "services": [{
510+ "name": "Example",
511+ "provider": "no/slashes.please",
512+ "description": "Hello world"
513+ }] }''')
514+ self.set_test_account(self.default_appname, "accounts", data)
515+ c = ClickReviewAccounts(self.test_name)
516+ c.check_manifest()
517+ r = c.click_report
518+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
519+ self.check_results(r, expected_counts)
520+
521+ def test_check_manifest_unknown_key(self):
522+ '''Test check_manifest() - unknown key'''
523+ data = json.loads('''{ "services": [{
524+ "name": "Example",
525+ "provider": "example",
526+ "description": "Hello world",
527+ "intruder": "Who, me?"
528+ }] }''')
529+ self.set_test_account(self.default_appname, "accounts", data)
530+ c = ClickReviewAccounts(self.test_name)
531+ c.check_manifest()
532+ r = c.click_report
533+ expected_counts = {'info': 0, 'warn': 0, 'error': 1}
534+ self.check_results(r, expected_counts)
535+
536 def test_check_application(self):
537 '''Test check_application()'''
538 xml = self._stub_application()

Subscribers

People subscribed via source and target branches