Merge lp:~mardy/ubuntu-system-settings-online-accounts/simplify-hooks into lp:ubuntu-system-settings-online-accounts

Proposed by Alberto Mardegan on 2015-08-10
Status: Merged
Merged at revision: 297
Proposed branch: lp:~mardy/ubuntu-system-settings-online-accounts/simplify-hooks
Merge into: lp:ubuntu-system-settings-online-accounts
Diff against target: 2165 lines (+1979/-4)
16 files modified
.bzrignore (+3/-0)
click-hooks/accounts.cpp (+722/-0)
click-hooks/accounts.hook.in (+3/-0)
click-hooks/acl-updater.cpp (+121/-0)
click-hooks/acl-updater.h (+40/-0)
click-hooks/click-hooks.pro (+4/-0)
click-hooks/main.cpp (+24/-4)
click-hooks/online-accounts-hooks2.pro (+38/-0)
debian/control (+3/-0)
debian/ubuntu-system-settings-online-accounts.install (+1/-0)
tests/click-hooks/click-hooks.pro (+4/-0)
tests/click-hooks/fake_signond.h (+52/-0)
tests/click-hooks/signond.py (+101/-0)
tests/click-hooks/tst_online_accounts_hooks.cpp (+18/-0)
tests/click-hooks/tst_online_accounts_hooks2.cpp (+813/-0)
tests/click-hooks/tst_online_accounts_hooks2.pro (+32/-0)
To merge this branch: bzr merge lp:~mardy/ubuntu-system-settings-online-accounts/simplify-hooks
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve on 2015-09-28
David Barth (community) 2015-08-10 Approve on 2015-08-28
Review via email: mp+267525@code.launchpad.net

Commit Message

Add a simplified accounts hook

Add a new account hook ("accounts") which just takes a single json file as input. The hook program generates .application and .service files as needed.

Description of the Change

Add a simplified accounts hook

Add a new account hook ("accounts") which just takes a single json file as input. The hook program generates .application and .service files as needed.

To post a comment you must log in.
David Barth (dbarth) wrote :

I would add a comment in the generated XML, stating that files were auto-generated (same as makefiles), and discouraging the use of the old application/service files.

291. By Alberto Mardegan on 2015-08-11

Add comment

Alberto Mardegan (mardy) wrote :

I added a comment to say that the files are auto-generated.
However, explanations on how to generate them belong to a separate programming guide (a newer version of https://developer.ubuntu.com/en/start/platform/guides/online-accounts-developer-guide/ ). Once we have it ready, we can put its link into the comment.

292. By Alberto Mardegan on 2015-08-12

Fix hooks path

293. By Alberto Mardegan on 2015-08-13

Disable app when uninstalling

294. By Alberto Mardegan on 2015-08-13

disable peer bus in signond

295. By Alberto Mardegan on 2015-08-21

Handle missing "name" service key

296. By Alberto Mardegan on 2015-08-21

Add support for account plugins

David Barth (dbarth) wrote :

Clean code, unit tests and documentation update: +1 !

review: Approve
297. By Alberto Mardegan on 2015-08-28

Don't delete each other's files

298. By Alberto Mardegan on 2015-09-15

Plugins don't have services

299. By Alberto Mardegan on 2015-09-16

Use a specific timestamp file

300. By Alberto Mardegan on 2015-09-16

Removing existing symlinks

301. By Alberto Mardegan on 2015-09-16

Don't write .application file for plugins

302. By Alberto Mardegan on 2015-09-18

Add packagedir, remove stale timestamps

303. By Alberto Mardegan on 2015-09-21

Don't set empty name and description

libaccounts-glib won't parse the .service file if the element text is just " ".

304. By Alberto Mardegan on 2015-09-23

Hooks: write settings

305. By Alberto Mardegan on 2015-09-28

Support symbolic icon names

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-06-23 14:37:36 +0000
3+++ .bzrignore 2015-09-28 14:35:45 +0000
4@@ -12,7 +12,9 @@
5 /click-hooks/account-application.hook
6 /click-hooks/account-provider.hook
7 /click-hooks/account-service.hook
8+/click-hooks/accounts.hook
9 /click-hooks/online-accounts-hooks
10+/click-hooks/online-accounts-hooks2
11 /client/OnlineAccountsClient/OnlineAccountsClient.pc
12 /client/OnlineAccountsClient/libonline-accounts-client.so*
13 /client/doc/html/*
14@@ -44,6 +46,7 @@
15 /plugins/module/libOnlineAccountsPlugin.so*
16 /plugins/module/qmldir
17 /tests/click-hooks/tst_online_accounts_hooks
18+/tests/click-hooks/tst_online_accounts_hooks2
19 /tests/client/tst_client
20 /tests/client/tst_qml_client
21 /tests/online-accounts-service/tst_inactivity_timer
22
23=== added file 'click-hooks/accounts.cpp'
24--- click-hooks/accounts.cpp 1970-01-01 00:00:00 +0000
25+++ click-hooks/accounts.cpp 2015-09-28 14:35:45 +0000
26@@ -0,0 +1,722 @@
27+/*
28+ * Copyright (C) 2015 Canonical Ltd.
29+ *
30+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
31+ *
32+ * This file is part of online-accounts-ui
33+ *
34+ * This program is free software: you can redistribute it and/or modify it
35+ * under the terms of the GNU General Public License version 3, as published
36+ * by the Free Software Foundation.
37+ *
38+ * This program is distributed in the hope that it will be useful, but
39+ * WITHOUT ANY WARRANTY; without even the implied warranties of
40+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
41+ * PURPOSE. See the GNU General Public License for more details.
42+ *
43+ * You should have received a copy of the GNU General Public License along
44+ * with this program. If not, see <http://www.gnu.org/licenses/>.
45+ */
46+
47+#include <Accounts/Account>
48+#include <Accounts/Manager>
49+#include <Accounts/Service>
50+#include <QCoreApplication>
51+#include <QDateTime>
52+#include <QDebug>
53+#include <QDir>
54+#include <QDomDocument>
55+#include <QDomElement>
56+#include <QFile>
57+#include <QFileInfo>
58+#include <QJsonArray>
59+#include <QJsonDocument>
60+#include <QJsonObject>
61+#include <QStandardPaths>
62+#include <QStringList>
63+#include <click.h>
64+#include "acl-updater.h"
65+
66+static QString findPackageDir(const QString &appId)
67+{
68+ /* For testing */
69+ QByteArray packageDirEnv = qgetenv("OAH_CLICK_DIR");
70+ if (!packageDirEnv.isEmpty()) {
71+ return QString::fromUtf8(packageDirEnv);
72+ }
73+
74+ QStringList components = appId.split('_');
75+ QByteArray package = components.first().toUtf8();
76+
77+ GError *error = NULL;
78+ ClickUser *user = click_user_new_for_user(NULL, NULL, &error);
79+ if (Q_UNLIKELY(!user)) {
80+ qWarning() << "Unable to read Click database:" << error->message;
81+ g_error_free(error);
82+ return QString();
83+ }
84+
85+ gchar *pkgDir = click_user_get_path(user, package.constData(), &error);
86+ if (Q_UNLIKELY(!pkgDir)) {
87+ qWarning() << "Unable to get the Click package directory for" <<
88+ package << ":" << error->message;
89+ g_error_free(error);
90+ g_object_unref(user);
91+ return QString();
92+ }
93+
94+ QString ret = QString::fromUtf8(pkgDir);
95+ g_object_unref(user);
96+ g_free(pkgDir);
97+ return ret;
98+}
99+
100+static QString stripVersion(const QString &appId)
101+{
102+ QStringList components = appId.split('_');
103+ if (components.count() != 3) return appId;
104+
105+ /* Click packages have a profile of the form
106+ * $name_$application_$version
107+ * (see https://wiki.ubuntu.com/SecurityTeam/Specifications/ApplicationConfinement/Manifest#Click)
108+ *
109+ * So we just need to strip out the last part.
110+ */
111+ components.removeLast();
112+ return components.join('_');
113+}
114+
115+class ManifestFile {
116+public:
117+ ManifestFile(const QFileInfo &hookFileInfo,
118+ const QString &appId, const QString &shortAppId);
119+
120+ bool writeFiles(const QDir &accountsDir);
121+ bool writeServiceFile(const QDir &accountsDir, const QString &id,
122+ const QString &provider, const QJsonObject &json);
123+ bool writePlugins(const QDir &accountsDir);
124+ bool writeProviderFile(const QDir &accountsDir, const QString &id,
125+ const QJsonObject &json);
126+ QDomDocument createDocument() const;
127+ QDomElement createGroup(QDomDocument &doc, const QString &name);
128+ void addTemplate(QDomDocument &doc, const QJsonObject &json);
129+ void addSetting(QDomElement &parent, const QString &name,
130+ const QString &value, const QString &type = QString());
131+ void addSettings(QDomElement &parent, const QJsonObject &json);
132+ void parseManifest(const QJsonObject &mainObject);
133+ void checkId(const QString &shortAppId);
134+ void addProfile(QDomDocument &doc);
135+ void addPackageDir(QDomDocument &doc);
136+ void addTranslations(QDomDocument &doc);
137+ QString profile() const;
138+ void addDesktopFile(QDomDocument &doc);
139+ bool writeXmlFile(const QDomDocument &doc, const QString &fileName) const;
140+ bool isValid() const { return m_isValid; }
141+
142+private:
143+ QFileInfo m_hookFileInfo;
144+ QString m_packageDir;
145+ QJsonArray m_services;
146+ QJsonArray m_plugins;
147+ QString m_appId;
148+ QString m_shortAppId;
149+ QString m_trDomain;
150+ bool m_isValid;
151+};
152+
153+ManifestFile::ManifestFile(const QFileInfo &hookFileInfo,
154+ const QString &appId,
155+ const QString &shortAppId):
156+ m_hookFileInfo(hookFileInfo),
157+ m_appId(appId),
158+ m_shortAppId(shortAppId),
159+ m_isValid(false)
160+{
161+ QFile file(hookFileInfo.filePath());
162+ if (file.open(QIODevice::ReadOnly)) {
163+ QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
164+ QJsonObject mainObject = doc.object();
165+ file.close();
166+
167+ m_services = mainObject.value("services").toArray();
168+ m_trDomain = mainObject.value("translations").toString();
169+ m_plugins = mainObject.value("plugins").toArray();
170+ m_isValid = !m_services.isEmpty() || !m_plugins.isEmpty();
171+
172+ m_packageDir = findPackageDir(appId);
173+ }
174+}
175+
176+bool ManifestFile::writeFiles(const QDir &accountsDir)
177+{
178+ bool ok = true;
179+ QDomDocument doc = createDocument();
180+ QDomElement root = doc.createElement(QStringLiteral("application"));
181+ root.setAttribute(QStringLiteral("id"), m_shortAppId);
182+ doc.appendChild(root);
183+
184+ addProfile(doc);
185+ addPackageDir(doc);
186+ addDesktopFile(doc);
187+ addTranslations(doc);
188+
189+ QDomElement servicesElem = doc.createElement(QStringLiteral("services"));
190+ Q_FOREACH(const QJsonValue &v, m_services) {
191+ QJsonObject o = v.toObject();
192+ QString provider = o.value("provider").toString();
193+ QString id = QString("%1_%2").arg(m_shortAppId).arg(provider);
194+ QString description = o.value("description").toString();
195+ if (description.isEmpty()) description = ".";
196+
197+ QDomElement serviceElem = doc.createElement(QStringLiteral("service"));
198+ serviceElem.setAttribute(QStringLiteral("id"), id);
199+ QDomElement elem = doc.createElement(QStringLiteral("description"));
200+ elem.appendChild(doc.createTextNode(description));
201+ serviceElem.appendChild(elem);
202+ servicesElem.appendChild(serviceElem);
203+
204+ if (!writeServiceFile(accountsDir, id, provider, o)) {
205+ qWarning() << "Writing service file failed" << id;
206+ ok = false;
207+ break;
208+ }
209+ }
210+ root.appendChild(servicesElem);
211+
212+ if (!writePlugins(accountsDir)) {
213+ ok = false;
214+ }
215+
216+ if (ok && !m_services.isEmpty()) {
217+ QString applicationFile =
218+ QString("applications/%1.application").arg(m_shortAppId);
219+ if (!writeXmlFile(doc, accountsDir.filePath(applicationFile))) {
220+ qWarning() << "Writing application file failed" << applicationFile;
221+ ok = false;
222+ }
223+ }
224+
225+ return ok;
226+}
227+
228+bool ManifestFile::writeServiceFile(const QDir &accountsDir, const QString &id,
229+ const QString &provider,
230+ const QJsonObject &json)
231+{
232+ QDomDocument doc = createDocument();
233+ QDomElement root = doc.createElement(QStringLiteral("service"));
234+ root.setAttribute(QStringLiteral("id"), id);
235+ doc.appendChild(root);
236+
237+ // type
238+ QDomElement elem = doc.createElement(QStringLiteral("type"));
239+ elem.appendChild(doc.createTextNode(m_shortAppId));
240+ root.appendChild(elem);
241+
242+ // provider
243+ elem = doc.createElement(QStringLiteral("provider"));
244+ elem.appendChild(doc.createTextNode(provider));
245+ root.appendChild(elem);
246+
247+ // name
248+ QString name = json.value(QStringLiteral("name")).toString();
249+ if (name.isEmpty()) {
250+ name = ".";
251+ }
252+ elem = doc.createElement(QStringLiteral("name"));
253+ elem.appendChild(doc.createTextNode(name));
254+ root.appendChild(elem);
255+
256+ addProfile(doc);
257+ addTranslations(doc);
258+ addTemplate(doc, json);
259+
260+ return writeXmlFile(doc, accountsDir.filePath(QString("services/%1.service").arg(id)));
261+}
262+
263+bool ManifestFile::writePlugins(const QDir &accountsDir)
264+{
265+ bool ok = true;
266+
267+ Q_FOREACH(const QJsonValue &v, m_plugins) {
268+ QJsonObject o = v.toObject();
269+ QString provider = o.value("provider").toString();
270+ if (Q_UNLIKELY(provider.isEmpty())) {
271+ qWarning() << "Plugin is missing 'provider' key";
272+ ok = false;
273+ break;
274+ }
275+
276+ QString id = QString("%1_%2").arg(m_shortAppId).arg(provider);
277+
278+ QString qmlPlugin = o.value("qml").toString();
279+ if (Q_UNLIKELY(qmlPlugin.isEmpty())) {
280+ qWarning() << "Plugin is missing 'qml' key";
281+ ok = false;
282+ break;
283+ }
284+
285+ if (!QDir::isAbsolutePath(qmlPlugin)) {
286+ qmlPlugin = m_packageDir + "/" + qmlPlugin;
287+ }
288+
289+ if (!QFile::exists(qmlPlugin)) {
290+ qWarning() << "Can't find QML files in" << qmlPlugin;
291+ ok = false;
292+ break;
293+ }
294+
295+ QString qmlDestination = QString("qml-plugins/%1").arg(id);
296+ QFile::remove(accountsDir.filePath(qmlDestination));
297+ if (!QFile::link(qmlPlugin, accountsDir.filePath(qmlDestination))) {
298+ qWarning() << "Cannot symlink QML files" << qmlPlugin;
299+ ok = false;
300+ break;
301+ }
302+
303+ if (!writeProviderFile(accountsDir, id, o)) {
304+ qWarning() << "Writing provider file failed" << id;
305+ ok = false;
306+ break;
307+ }
308+ }
309+ return ok;
310+}
311+
312+bool ManifestFile::writeProviderFile(const QDir &accountsDir,
313+ const QString &id,
314+ const QJsonObject &json)
315+{
316+ QDomDocument doc = createDocument();
317+ QDomElement root = doc.createElement(QStringLiteral("provider"));
318+ root.setAttribute(QStringLiteral("id"), id);
319+ doc.appendChild(root);
320+
321+ // name
322+ QString name = json.value(QStringLiteral("name")).toString();
323+ if (Q_UNLIKELY(name.isEmpty())) {
324+ qWarning() << "Provider name is required";
325+ return false;
326+ }
327+ QDomElement elem = doc.createElement(QStringLiteral("name"));
328+ elem.appendChild(doc.createTextNode(name));
329+ root.appendChild(elem);
330+
331+ // icon
332+ QString icon = json.value(QStringLiteral("icon")).toString();
333+ if (Q_UNLIKELY(icon.isEmpty())) {
334+ qWarning() << "Provider icon is required";
335+ return false;
336+ }
337+ if (!QDir::isAbsolutePath(icon)) {
338+ QString test = m_packageDir + "/" + icon;
339+ if (QFile::exists(test)) {
340+ icon = test;
341+ }
342+ }
343+ elem = doc.createElement(QStringLiteral("icon"));
344+ elem.appendChild(doc.createTextNode(icon));
345+ root.appendChild(elem);
346+
347+ addProfile(doc);
348+ addTranslations(doc);
349+ addTemplate(doc, json);
350+ addPackageDir(doc);
351+
352+ return writeXmlFile(doc, accountsDir.filePath(QString("providers/%1.provider").arg(id)));
353+}
354+
355+QDomDocument ManifestFile::createDocument() const
356+{
357+ QDomDocument doc;
358+ doc.appendChild(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""));
359+ QString comment = QString("this file is auto-generated by %1; do not modify").
360+ arg(QCoreApplication::applicationName());
361+ doc.appendChild(doc.createComment(comment));
362+ return doc;
363+}
364+
365+QDomElement ManifestFile::createGroup(QDomDocument &doc, const QString &name)
366+{
367+ QDomElement group = doc.createElement(QStringLiteral("group"));
368+ group.setAttribute(QStringLiteral("name"), name);
369+ return group;
370+}
371+
372+void ManifestFile::addTemplate(QDomDocument &doc, const QJsonObject &json)
373+{
374+ QDomElement root = doc.documentElement();
375+ QDomElement templateElem = doc.createElement(QStringLiteral("template"));
376+
377+ // auth
378+ QDomElement authElem = createGroup(doc, QStringLiteral("auth"));
379+ QJsonObject auth = json.value(QStringLiteral("auth")).toObject();
380+ for (QJsonObject::const_iterator i = auth.begin(); i != auth.end(); i++) {
381+ QStringList authParts = i.key().split("/");
382+ if (authParts.count() != 2) {
383+ qWarning() << "auth key must contain exactly one '/'!";
384+ continue;
385+ }
386+ QString method = authParts[0];
387+ QString mechanism = authParts[1];
388+
389+ addSetting(authElem, QStringLiteral("method"), method);
390+ addSetting(authElem, QStringLiteral("mechanism"), mechanism);
391+
392+ QDomElement methodGroup = createGroup(doc, method);
393+ authElem.appendChild(methodGroup);
394+ QDomElement mechanismGroup = createGroup(doc, mechanism);
395+
396+ addSettings(mechanismGroup, i.value().toObject());
397+ methodGroup.appendChild(mechanismGroup);
398+ }
399+
400+ if (!auth.isEmpty()) templateElem.appendChild(authElem);
401+
402+ // settings
403+ QJsonObject settings = json.value(QStringLiteral("settings")).toObject();
404+ addSettings(templateElem, settings);
405+
406+ if (templateElem.hasChildNodes()) {
407+ root.appendChild(templateElem);
408+ }
409+}
410+
411+void ManifestFile::addSetting(QDomElement &parent, const QString &name,
412+ const QString &value, const QString &type)
413+{
414+ QDomDocument doc = parent.ownerDocument();
415+ QDomElement setting = doc.createElement(QStringLiteral("setting"));
416+ setting.setAttribute(QStringLiteral("name"), name);
417+ if (!type.isEmpty()) {
418+ setting.setAttribute(QStringLiteral("type"), type);
419+ }
420+ setting.appendChild(doc.createTextNode(value));
421+ parent.appendChild(setting);
422+}
423+
424+void ManifestFile::addSettings(QDomElement &parent, const QJsonObject &json)
425+{
426+ QDomDocument doc = parent.ownerDocument();
427+ for (QJsonObject::const_iterator i = json.begin(); i != json.end(); i++) {
428+ QDomElement elem = parent;
429+ QStringList parts = i.key().split('/');
430+ QString key = parts.takeLast();
431+ Q_FOREACH(const QString &groupName, parts) {
432+ QDomElement subGroup = createGroup(doc, groupName);
433+ elem.appendChild(subGroup);
434+ elem = subGroup;
435+ }
436+ QString value;
437+ QString type;
438+ switch (i.value().type()) {
439+ case QJsonValue::Bool:
440+ type = "b";
441+ value = i.value().toBool() ? "true" : "false";
442+ break;
443+ case QJsonValue::String:
444+ value = i.value().toString();
445+ break;
446+ case QJsonValue::Array:
447+ {
448+ QJsonArray array = i.value().toArray();
449+ QStringList values;
450+ Q_FOREACH(const QJsonValue &v, array) {
451+ if (Q_UNLIKELY(v.type() != QJsonValue::String)) {
452+ qWarning() << "Only arrays of strings are supported!";
453+ continue;
454+ }
455+ values.append(QString("'%1'").arg(v.toString()));
456+ }
457+ type = "as";
458+ value = QString("[%1]").arg(values.join(','));
459+ }
460+ break;
461+ case QJsonValue::Object:
462+ {
463+ QDomElement subGroup = createGroup(doc, key);
464+ QJsonObject object = i.value().toObject();
465+ addSettings(subGroup, object);
466+ elem.appendChild(subGroup);
467+ }
468+ break;
469+ default:
470+ qWarning() << "Unsupported setting type:" << i.value();
471+ }
472+ if (value.isEmpty()) continue;
473+ addSetting(elem, key, value, type);
474+ }
475+}
476+
477+void ManifestFile::addProfile(QDomDocument &doc)
478+{
479+ QDomElement root = doc.documentElement();
480+ QDomElement elem = doc.createElement(QStringLiteral("profile"));
481+ elem.appendChild(doc.createTextNode(m_appId));
482+ root.appendChild(elem);
483+}
484+
485+void ManifestFile::addTranslations(QDomDocument &doc)
486+{
487+ if (m_trDomain.isEmpty()) return;
488+ QDomElement root = doc.documentElement();
489+ QDomElement elem = doc.createElement(QStringLiteral("translations"));
490+ elem.appendChild(doc.createTextNode(m_trDomain));
491+ root.appendChild(elem);
492+}
493+
494+void ManifestFile::addPackageDir(QDomDocument &doc)
495+{
496+ if (Q_UNLIKELY(m_packageDir.isEmpty())) return;
497+
498+ QDomElement root = doc.documentElement();
499+ QDomElement elem = doc.createElement(QStringLiteral("package-dir"));
500+ elem.appendChild(doc.createTextNode(m_packageDir));
501+ root.appendChild(elem);
502+}
503+
504+void ManifestFile::addDesktopFile(QDomDocument &doc)
505+{
506+ QDomElement root = doc.documentElement();
507+ QDomElement elem = doc.createElement(QStringLiteral("desktop-entry"));
508+ elem.appendChild(doc.createTextNode(m_appId));
509+ root.appendChild(elem);
510+}
511+
512+bool ManifestFile::writeXmlFile(const QDomDocument &doc, const QString &fileName) const
513+{
514+ QFile file(fileName);
515+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return false;
516+
517+ bool ok = (file.write(doc.toByteArray(2)) > 0);
518+ file.close();
519+
520+ if (ok) {
521+ return true;
522+ } else {
523+ QFile::remove(fileName);
524+ return false;
525+ }
526+}
527+
528+class LibAccountsFile: public QDomDocument {
529+public:
530+ LibAccountsFile(const QFileInfo &fileInfo);
531+
532+ QString profile() const;
533+ bool createdByUs() const;
534+ bool isValid() const { return m_isValid; }
535+
536+private:
537+ QFileInfo m_fileInfo;
538+ bool m_isValid;
539+};
540+
541+LibAccountsFile::LibAccountsFile(const QFileInfo &fileInfo):
542+ QDomDocument(),
543+ m_fileInfo(fileInfo),
544+ m_isValid(false)
545+{
546+ QFile file(fileInfo.filePath());
547+ if (file.open(QIODevice::ReadOnly)) {
548+ if (setContent(&file)) {
549+ m_isValid = true;
550+ }
551+ file.close();
552+ }
553+}
554+
555+QString LibAccountsFile::profile() const
556+{
557+ QDomElement root = documentElement();
558+ return root.firstChildElement("profile").text();
559+}
560+
561+bool LibAccountsFile::createdByUs() const
562+{
563+ QString creatorMark = QCoreApplication::applicationName() + ";";
564+ for (QDomNode n = firstChild(); !n.isNull(); n = n.nextSibling()) {
565+ if (n.isComment() && n.nodeValue().contains(creatorMark)) {
566+ return true;
567+ }
568+ }
569+ return false;
570+}
571+
572+static void disableService(Accounts::Manager *manager,
573+ const QString &serviceId,
574+ const QString &profile)
575+{
576+ Accounts::Service service = manager->service(serviceId);
577+ if (Q_UNLIKELY(!service.isValid())) return;
578+
579+ AclUpdater aclUpdater;
580+ Q_FOREACH(Accounts::AccountId accountId, manager->accountListEnabled()) {
581+ Accounts::Account *account = manager->account(accountId);
582+ if (Q_UNLIKELY(!account)) continue;
583+
584+ if (account->providerName() != service.provider()) continue;
585+
586+ uint credentialsId = account->credentialsId();
587+ account->selectService(service);
588+ if (account->isEnabled()) {
589+ account->setEnabled(false);
590+ account->sync();
591+ aclUpdater.removeApp(stripVersion(profile), credentialsId);
592+ }
593+ }
594+}
595+
596+static void removeStaleAccounts(Accounts::Manager *manager,
597+ const QString &providerName)
598+{
599+ Q_FOREACH(Accounts::AccountId id, manager->accountList()) {
600+ Accounts::Account *account = manager->account(id);
601+ if (account->providerName() == providerName) {
602+ account->remove();
603+ account->syncAndBlock();
604+ }
605+ }
606+}
607+
608+static void removeStaleFiles(Accounts::Manager *manager,
609+ const QStringList &fileTypes,
610+ const QDir &accountsDir,
611+ const QDir &hooksDirIn)
612+{
613+ /* Walk through all of
614+ * ~/.local/share/accounts/{providers,services,applications}/
615+ * and remove files which are no longer present in hooksDirIn.
616+ */
617+ Q_FOREACH(const QString &fileType, fileTypes) {
618+ QDir dir(accountsDir.filePath(fileType + "s"));
619+ dir.setFilter(QDir::Files | QDir::Readable);
620+ QStringList fileTypeFilter;
621+ fileTypeFilter << "*." + fileType;
622+ dir.setNameFilters(fileTypeFilter);
623+
624+ Q_FOREACH(const QFileInfo &fileInfo, dir.entryInfoList()) {
625+ LibAccountsFile file(fileInfo.filePath());
626+
627+ /* If this file was not created by our hook let's ignore it. */
628+ if (!file.createdByUs()) continue;
629+
630+ QString profile = file.profile();
631+
632+ /* Check that the hook file is still there; if it isn't, then it
633+ * means that the click package was removed, and we must remove our
634+ * copy as well. */
635+ QString hookFileName = stripVersion(profile) + "_*.accounts";
636+ QStringList nameFilters = QStringList() << hookFileName;
637+ if (!hooksDirIn.entryList(nameFilters).isEmpty()) continue;
638+
639+ if (fileType == "service") {
640+ /* Make sure services get disabled. See also:
641+ * https://bugs.launchpad.net/bugs/1417261 */
642+ disableService(manager, fileInfo.completeBaseName(), profile);
643+ } else if (fileType == "provider") {
644+ /* If this is a provider, we must also remove any accounts
645+ * associated with it */
646+ removeStaleAccounts(manager, fileInfo.completeBaseName());
647+ }
648+ QFile::remove(fileInfo.filePath());
649+ }
650+ }
651+}
652+
653+static void removeStaleTimestampFiles(const QDir &hooksDirIn)
654+{
655+ Q_FOREACH(const QFileInfo &fileInfo, hooksDirIn.entryInfoList()) {
656+ if (fileInfo.suffix() != "processed") continue;
657+
658+ /* Find the corresponding hook file */
659+ QFileInfo hookInfo(fileInfo.absolutePath() + "/" + fileInfo.completeBaseName());
660+
661+ if (!hookInfo.exists()) {
662+ QFile::remove(fileInfo.filePath());
663+ }
664+ }
665+}
666+
667+int main(int argc, char **argv)
668+{
669+ QCoreApplication app(argc, argv);
670+
671+ Accounts::Manager::Options managerOptions;
672+ if (qgetenv("DBUS_SESSION_BUS_ADDRESS").isEmpty()) {
673+ managerOptions |= Accounts::Manager::DisableNotifications;
674+ }
675+ Accounts::Manager *manager = new Accounts::Manager(managerOptions);
676+
677+ /* Go through the hook files in ~/.local/share/online-accounts-hooks2/ and
678+ * check if they have already been processed into a file under
679+ * ~/.local/share/accounts/{services,applications}/;
680+ * if not, open the hook file, write the APP_ID somewhere in it, and
681+ * save the result in the location where libaccounts expects to find it.
682+ */
683+
684+ QStringList fileTypes;
685+ fileTypes <<
686+ QStringLiteral("service") <<
687+ QStringLiteral("provider") <<
688+ QStringLiteral("application");
689+
690+ // This is ~/.local/share/
691+ const QString localShare =
692+ QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
693+ QDir hooksDirIn(localShare + "/" HOOK_FILES_SUBDIR);
694+
695+ /* Make sure the directories exist */
696+ QDir accountsDir(localShare + "/accounts");
697+ accountsDir.mkpath("applications");
698+ accountsDir.mkpath("services");
699+ accountsDir.mkpath("providers");
700+ accountsDir.mkpath("qml-plugins");
701+
702+ removeStaleFiles(manager, fileTypes, accountsDir, hooksDirIn);
703+ removeStaleTimestampFiles(hooksDirIn);
704+
705+ Q_FOREACH(const QFileInfo &fileInfo, hooksDirIn.entryInfoList()) {
706+ if (fileInfo.suffix() != "accounts") continue;
707+
708+ // Our click hook sets the base name to the APP_ID
709+ QString appId = fileInfo.completeBaseName();
710+
711+ /* When publishing this file for libaccounts, we want to strip
712+ * the version number out. */
713+ QString shortAppId = stripVersion(appId);
714+
715+ /* We create an empty file whenever we succesfully process a hook file.
716+ * The name of this file is the same as the hook file, with the
717+ * .processed suffix appended.
718+ */
719+ QFileInfo processedInfo(fileInfo.filePath() + ".processed");
720+ if (processedInfo.exists() &&
721+ processedInfo.lastModified() >= fileInfo.lastModified()) {
722+ continue;
723+ }
724+
725+ ManifestFile manifest = ManifestFile(fileInfo, appId, shortAppId);
726+ if (!manifest.isValid()) {
727+ qWarning() << "Invalid file" << fileInfo.filePath();
728+ continue;
729+ }
730+
731+ if (manifest.writeFiles(accountsDir)) {
732+ QFile file(processedInfo.filePath());
733+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
734+ qWarning() << "Could not create timestamp file" <<
735+ processedInfo.filePath();
736+ }
737+ }
738+ }
739+
740+ /* To ensure that all the installed services are parsed into
741+ * libaccounts' DB, we enumerate them now.
742+ */
743+ manager->serviceList();
744+ delete manager;
745+
746+ return EXIT_SUCCESS;
747+}
748+
749
750=== added file 'click-hooks/accounts.hook.in'
751--- click-hooks/accounts.hook.in 1970-01-01 00:00:00 +0000
752+++ click-hooks/accounts.hook.in 2015-09-28 14:35:45 +0000
753@@ -0,0 +1,3 @@
754+Pattern: ${home}/.local/share/$${TARGET}/${id}.accounts
755+Exec: $${target.path}/$$TARGET
756+User-Level: yes
757
758=== added file 'click-hooks/acl-updater.cpp'
759--- click-hooks/acl-updater.cpp 1970-01-01 00:00:00 +0000
760+++ click-hooks/acl-updater.cpp 2015-09-28 14:35:45 +0000
761@@ -0,0 +1,121 @@
762+/*
763+ * Copyright (C) 2015 Canonical Ltd.
764+ *
765+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
766+ *
767+ * This file is part of online-accounts-ui
768+ *
769+ * This program is free software: you can redistribute it and/or modify it
770+ * under the terms of the GNU General Public License version 3, as published
771+ * by the Free Software Foundation.
772+ *
773+ * This program is distributed in the hope that it will be useful, but
774+ * WITHOUT ANY WARRANTY; without even the implied warranties of
775+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
776+ * PURPOSE. See the GNU General Public License for more details.
777+ *
778+ * You should have received a copy of the GNU General Public License along
779+ * with this program. If not, see <http://www.gnu.org/licenses/>.
780+ */
781+
782+#include "acl-updater.h"
783+
784+#include <QCoreApplication>
785+#include <QDebug>
786+#include <QEventLoop>
787+#include <QObject>
788+#include <SignOn/Identity>
789+#include <SignOn/IdentityInfo>
790+
791+class AclUpdaterPrivate: public QObject
792+{
793+ Q_OBJECT
794+
795+public:
796+ AclUpdaterPrivate();
797+
798+public Q_SLOTS:
799+ void onInfo(const SignOn::IdentityInfo &info);
800+ void onStored(const quint32 id);
801+ void onError(const SignOn::Error &error);
802+
803+private:
804+ friend class AclUpdater;
805+ QEventLoop m_loop;
806+};
807+
808+AclUpdaterPrivate::AclUpdaterPrivate():
809+ QObject()
810+{
811+}
812+
813+void AclUpdaterPrivate::onInfo(const SignOn::IdentityInfo &info)
814+{
815+ SignOn::Identity *identity = qobject_cast<SignOn::Identity*>(sender());
816+
817+ QString shortAppId = identity->property("appToBeRemoved").toString();
818+ QStringList acl;
819+ Q_FOREACH(const QString &token, info.accessControlList()) {
820+ if (!token.startsWith(shortAppId)) {
821+ acl.append(token);
822+ }
823+ }
824+
825+ if (acl != info.accessControlList()) {
826+ SignOn::IdentityInfo newInfo(info);
827+ newInfo.setAccessControlList(acl);
828+ identity->storeCredentials(newInfo);
829+ } else {
830+ qDebug() << shortAppId << "was not in ACL of" << info.id();
831+ m_loop.exit(0);
832+ }
833+}
834+
835+void AclUpdaterPrivate::onStored(const quint32 id)
836+{
837+ Q_UNUSED(id);
838+ m_loop.exit(0);
839+}
840+
841+void AclUpdaterPrivate::onError(const SignOn::Error &err)
842+{
843+ qWarning() << "Error occurred updating ACL" << err.message();
844+ m_loop.exit(1);
845+}
846+
847+AclUpdater::AclUpdater():
848+ d_ptr(new AclUpdaterPrivate)
849+{
850+}
851+
852+AclUpdater::~AclUpdater()
853+{
854+ delete d_ptr;
855+}
856+
857+bool AclUpdater::removeApp(const QString &shortAppId, uint credentialsId)
858+{
859+ Q_D(AclUpdater);
860+
861+ if (credentialsId == 0 ||
862+ !shortAppId.contains('_')) return false;
863+
864+ SignOn::Identity *identity =
865+ SignOn::Identity::existingIdentity(credentialsId, d);
866+ if (Q_UNLIKELY(!identity)) return false;
867+
868+ identity->setProperty("appToBeRemoved", shortAppId);
869+ QObject::connect(identity, SIGNAL(info(const SignOn::IdentityInfo&)),
870+ d, SLOT(onInfo(const SignOn::IdentityInfo&)));
871+ QObject::connect(identity, SIGNAL(credentialsStored(const quint32)),
872+ d, SLOT(onStored(const quint32)));
873+ QObject::connect(identity, SIGNAL(error(const SignOn::Error &)),
874+ d, SLOT(onError(const SignOn::Error &)));
875+ identity->queryInfo();
876+
877+ bool ok = (d->m_loop.exec() == 0);
878+ delete identity;
879+ return ok;
880+}
881+
882+#include "acl-updater.moc"
883
884=== added file 'click-hooks/acl-updater.h'
885--- click-hooks/acl-updater.h 1970-01-01 00:00:00 +0000
886+++ click-hooks/acl-updater.h 2015-09-28 14:35:45 +0000
887@@ -0,0 +1,40 @@
888+/*
889+ * Copyright (C) 2015 Canonical Ltd.
890+ *
891+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
892+ *
893+ * This file is part of online-accounts-ui
894+ *
895+ * This program is free software: you can redistribute it and/or modify it
896+ * under the terms of the GNU General Public License version 3, as published
897+ * by the Free Software Foundation.
898+ *
899+ * This program is distributed in the hope that it will be useful, but
900+ * WITHOUT ANY WARRANTY; without even the implied warranties of
901+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
902+ * PURPOSE. See the GNU General Public License for more details.
903+ *
904+ * You should have received a copy of the GNU General Public License along
905+ * with this program. If not, see <http://www.gnu.org/licenses/>.
906+ */
907+
908+#ifndef ACCOUNTS_HOOK_ACL_UPDATER
909+#define ACCOUNTS_HOOK_ACL_UPDATER
910+
911+#include <QString>
912+
913+class AclUpdaterPrivate;
914+class AclUpdater
915+{
916+public:
917+ AclUpdater();
918+ virtual ~AclUpdater();
919+
920+ bool removeApp(const QString &shortAppId, uint credentialsId);
921+
922+private:
923+ Q_DECLARE_PRIVATE(AclUpdater);
924+ AclUpdaterPrivate *d_ptr;
925+};
926+
927+#endif // ACCOUNTS_HOOK_ACL_UPDATER
928
929=== added file 'click-hooks/click-hooks.pro'
930--- click-hooks/click-hooks.pro 1970-01-01 00:00:00 +0000
931+++ click-hooks/click-hooks.pro 2015-09-28 14:35:45 +0000
932@@ -0,0 +1,4 @@
933+TEMPLATE = subdirs
934+SUBDIRS = \
935+ online-accounts-hooks.pro \
936+ online-accounts-hooks2.pro
937
938=== modified file 'click-hooks/main.cpp'
939--- click-hooks/main.cpp 2015-06-18 11:32:51 +0000
940+++ click-hooks/main.cpp 2015-09-28 14:35:45 +0000
941@@ -92,6 +92,8 @@
942 void addServiceType(const QString &shortAppId);
943 void checkIconPath(const QString &appId);
944 bool writeTo(const QString &fileName) const;
945+ void addCreatorMark();
946+ bool createdByUs() const;
947 bool isValid() const { return m_isValid; }
948
949 private:
950@@ -217,6 +219,24 @@
951 }
952 }
953
954+void LibAccountsFile::addCreatorMark()
955+{
956+ QString comment = QString("this file is auto-generated by %1; do not modify").
957+ arg(QCoreApplication::applicationName());
958+ appendChild(createComment(comment));
959+}
960+
961+bool LibAccountsFile::createdByUs() const
962+{
963+ QString creatorMark = QCoreApplication::applicationName() + ";";
964+ for (QDomNode n = firstChild(); !n.isNull(); n = n.nextSibling()) {
965+ if (n.isComment() && n.nodeValue().contains(creatorMark)) {
966+ return true;
967+ }
968+ }
969+ return false;
970+}
971+
972 static void removeStaleAccounts(Accounts::Manager *manager,
973 const QString &providerName)
974 {
975@@ -248,12 +268,11 @@
976 Q_FOREACH(const QFileInfo &fileInfo, dir.entryInfoList()) {
977 LibAccountsFile file(fileInfo.filePath());
978
979+ /* If this file was not created by our hook let's ignore it. */
980+ if (!file.createdByUs()) continue;
981+
982 QString profile = file.profile();
983
984- /* If there is no <profile> element, then this file was not created
985- * by our hook; let's ignore it. */
986- if (profile.isEmpty()) continue;
987-
988 /* Check that the hook file is still there; if it isn't, then it
989 * means that the click package was removed, and we must remove our
990 * copy as well. */
991@@ -330,6 +349,7 @@
992 LibAccountsFile xml = LibAccountsFile(fileInfo);
993 if (!xml.isValid()) continue;
994
995+ xml.addCreatorMark();
996 xml.checkId(shortAppId);
997 xml.addProfile(appId);
998 xml.addPackageDir(appId);
999
1000=== renamed file 'click-hooks/click-hooks.pro' => 'click-hooks/online-accounts-hooks.pro'
1001=== added file 'click-hooks/online-accounts-hooks2.pro'
1002--- click-hooks/online-accounts-hooks2.pro 1970-01-01 00:00:00 +0000
1003+++ click-hooks/online-accounts-hooks2.pro 2015-09-28 14:35:45 +0000
1004@@ -0,0 +1,38 @@
1005+include(../common-project-config.pri)
1006+
1007+TEMPLATE = app
1008+TARGET = online-accounts-hooks2
1009+
1010+CONFIG += \
1011+ link_pkgconfig \
1012+ qt
1013+
1014+QT += \
1015+ xml
1016+
1017+PKGCONFIG += \
1018+ accounts-qt5 \
1019+ click-0.4 \
1020+ gobject-2.0 \
1021+ libsignon-qt5
1022+
1023+SOURCES += \
1024+ accounts.cpp \
1025+ acl-updater.cpp
1026+
1027+HEADERS += \
1028+ acl-updater.h
1029+
1030+DEFINES += \
1031+ HOOK_FILES_SUBDIR=\\\"$${TARGET}\\\" \
1032+ QT_NO_KEYWORDS
1033+
1034+QMAKE_SUBSTITUTES += \
1035+ accounts.hook.in
1036+
1037+hooks.files = \
1038+ accounts.hook
1039+hooks.path = $${INSTALL_PREFIX}/share/click/hooks
1040+INSTALLS += hooks
1041+
1042+include($${TOP_SRC_DIR}/common-installs-config.pri)
1043
1044=== modified file 'debian/control'
1045--- debian/control 2015-07-16 18:34:58 +0000
1046+++ debian/control 2015-09-28 14:35:45 +0000
1047@@ -10,6 +10,8 @@
1048 libclick-0.4-dev,
1049 libmirclient-dev (>= 0.14.0),
1050 libnotify-dev,
1051+ libqtdbusmock1-dev,
1052+ libqtdbustest1-dev,
1053 libsignon-qt5-dev,
1054 libsystemsettings-dev (>= 0.1+13.10.20130806),
1055 qt5-default,
1056@@ -21,6 +23,7 @@
1057 qtdeclarative5-ubuntu-ui-toolkit-plugin,
1058 signon-plugins-dev,
1059 dbus-test-runner,
1060+ xmldiff,
1061 xvfb,
1062 Standards-Version: 3.9.4
1063 Homepage: https://launchpad.net/ubuntu-system-settings-online-accounts
1064
1065=== modified file 'debian/ubuntu-system-settings-online-accounts.install'
1066--- debian/ubuntu-system-settings-online-accounts.install 2014-10-06 08:44:37 +0000
1067+++ debian/ubuntu-system-settings-online-accounts.install 2015-09-28 14:35:45 +0000
1068@@ -1,4 +1,5 @@
1069 usr/bin/online-accounts-hooks
1070+usr/bin/online-accounts-hooks2
1071 usr/bin/online-accounts-service
1072 usr/bin/online-accounts-ui
1073 usr/lib/*/libonline-accounts-plugin.so.*
1074
1075=== added file 'tests/click-hooks/click-hooks.pro'
1076--- tests/click-hooks/click-hooks.pro 1970-01-01 00:00:00 +0000
1077+++ tests/click-hooks/click-hooks.pro 2015-09-28 14:35:45 +0000
1078@@ -0,0 +1,4 @@
1079+TEMPLATE = subdirs
1080+SUBDIRS = \
1081+ tst_online_accounts_hooks.pro \
1082+ tst_online_accounts_hooks2.pro
1083
1084=== added file 'tests/click-hooks/fake_signond.h'
1085--- tests/click-hooks/fake_signond.h 1970-01-01 00:00:00 +0000
1086+++ tests/click-hooks/fake_signond.h 2015-09-28 14:35:45 +0000
1087@@ -0,0 +1,52 @@
1088+/*
1089+ * This file is part of libOnlineAccounts
1090+ *
1091+ * Copyright (C) 2015 Canonical Ltd.
1092+ *
1093+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1094+ *
1095+ * This program is free software: you can redistribute it and/or modify it
1096+ * under the terms of the GNU Lesser General Public License version 3, as
1097+ * published by the Free Software Foundation.
1098+ *
1099+ * This program is distributed in the hope that it will be useful, but
1100+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1101+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1102+ * PURPOSE. See the GNU Lesser General Public License for more details.
1103+ *
1104+ * You should have received a copy of the GNU Lesser General Public License
1105+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1106+ */
1107+
1108+#ifndef OAD_FAKE_SIGNOND_H
1109+#define OAD_FAKE_SIGNOND_H
1110+
1111+#include <QVariantMap>
1112+#include <libqtdbusmock/DBusMock.h>
1113+
1114+class FakeSignond
1115+{
1116+public:
1117+ FakeSignond(QtDBusMock::DBusMock *mock): m_mock(mock) {
1118+ m_mock->registerTemplate("com.google.code.AccountsSSO.SingleSignOn",
1119+ SIGNOND_MOCK_TEMPLATE,
1120+ QDBusConnection::SessionBus);
1121+ }
1122+
1123+ void addIdentity(uint id, const QVariantMap &info) {
1124+ mockedAuthService().call("AddIdentity", id, info);
1125+ }
1126+
1127+private:
1128+ OrgFreedesktopDBusMockInterface &mockedAuthService() {
1129+ return m_mock->mockInterface("com.google.code.AccountsSSO.SingleSignOn",
1130+ "/com/google/code/AccountsSSO/SingleSignOn",
1131+ "com.google.code.AccountsSSO.SingleSignOn.AuthService",
1132+ QDBusConnection::SessionBus);
1133+ }
1134+
1135+private:
1136+ QtDBusMock::DBusMock *m_mock;
1137+};
1138+
1139+#endif // OAD_FAKE_SIGNOND_H
1140
1141=== added file 'tests/click-hooks/signond.py'
1142--- tests/click-hooks/signond.py 1970-01-01 00:00:00 +0000
1143+++ tests/click-hooks/signond.py 2015-09-28 14:35:45 +0000
1144@@ -0,0 +1,101 @@
1145+'''signond mock template
1146+
1147+This creates the expected methods and properties of the
1148+com.google.code.AccountsSSO.SingleSignOn service.
1149+'''
1150+
1151+# This program is free software; you can redistribute it and/or modify it under
1152+# the terms of the GNU Lesser General Public License as published by the Free
1153+# Software Foundation; either version 3 of the License, or (at your option) any
1154+# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text
1155+# of the license.
1156+
1157+__author__ = 'Alberto Mardegan'
1158+__email__ = 'alberto.mardegan@canonical.com'
1159+__copyright__ = '(c) 2015 Canonical Ltd.'
1160+__license__ = 'LGPL 3+'
1161+
1162+import dbus
1163+import time
1164+
1165+from dbusmock import MOCK_IFACE
1166+import dbusmock
1167+
1168+BUS_NAME = 'com.google.code.AccountsSSO.SingleSignOn'
1169+MAIN_OBJ = '/com/google/code/AccountsSSO/SingleSignOn'
1170+AUTH_SERVICE_IFACE = 'com.google.code.AccountsSSO.SingleSignOn.AuthService'
1171+MAIN_IFACE = AUTH_SERVICE_IFACE
1172+IDENTITY_IFACE = 'com.google.code.AccountsSSO.SingleSignOn.Identity'
1173+AUTH_SESSION_IFACE = 'com.google.code.AccountsSSO.SingleSignOn.AuthSession'
1174+SYSTEM_BUS = False
1175+
1176+ERROR_PREFIX = 'com.google.code.AccountsSSO.SingleSignOn.Error.'
1177+ERROR_IDENTITY_NOT_FOUND = ERROR_PREFIX + 'IdentityNotFound'
1178+ERROR_PERMISSION_DENIED = ERROR_PREFIX + 'PermissionDenied'
1179+ERROR_USER_INTERACTION= ERROR_PREFIX + 'UserInteraction'
1180+
1181+
1182+def identity_store(self, new_info):
1183+ self.info.update(new_info)
1184+ return self.info['Id']
1185+
1186+def get_identity(self, identity):
1187+ if identity not in self.identities:
1188+ raise dbus.exceptions.DBusException('Identity not found',
1189+ name=ERROR_IDENTITY_NOT_FOUND)
1190+ path = '/Identity%s' % identity
1191+ if not path in dbusmock.get_objects():
1192+ self.AddObject(path, IDENTITY_IFACE, {}, [
1193+ ('getInfo', '', 'a{sv}', 'ret = self.parent.identities[%s]' % identity),
1194+ ('store', 'a{sv}', 'u', 'ret = self.store(self, args[0])'),
1195+ ])
1196+ identity_obj = dbusmock.get_object(path)
1197+ identity_obj.parent = self
1198+ identity_obj.store = identity_store
1199+ identity_obj.info = self.identities[identity]
1200+ return (path, self.identities[identity])
1201+
1202+
1203+def auth_session_process(identity, params, method):
1204+ if 'errorName' in params:
1205+ raise dbus.exceptions.DBusException('Authentication error',
1206+ name=params['errorName'])
1207+ if 'delay' in params:
1208+ time.sleep(params['delay'])
1209+ return params
1210+
1211+def get_auth_session_object_path(self, identity, method):
1212+ if identity != 0 and (identity not in self.identities):
1213+ raise dbus.exceptions.DBusException('Identity not found',
1214+ name=ERROR_IDENTITY_NOT_FOUND)
1215+ path = '/AuthSession%s' % self.sessions_counter
1216+ self.sessions_counter += 1
1217+ self.AddObject(path, AUTH_SESSION_IFACE, {}, [
1218+ ('process', 'a{sv}s', 'a{sv}', 'ret = self.auth_session_process(self.identity, args[0], args[1])'),
1219+ ])
1220+
1221+ auth_session = dbusmock.get_object(path)
1222+ auth_session.auth_session_process = auth_session_process
1223+ auth_session.identity = identity
1224+ auth_session.method = method
1225+
1226+ return path
1227+
1228+
1229+def load(mock, parameters):
1230+ mock.get_identity = get_identity
1231+ mock.get_auth_session_object_path = get_auth_session_object_path
1232+ mock.AddMethods(AUTH_SERVICE_IFACE, [
1233+ ('getIdentity', 'u', 'oa{sv}', 'ret = self.get_identity(self, args[0])'),
1234+ ('getAuthSessionObjectPath', 'us', 's', 'ret = self.get_auth_session_object_path(self, args[0], args[1])'),
1235+ ])
1236+
1237+ mock.sessions_counter = 1
1238+ mock.identities = {}
1239+ mock.auth_sessions = {}
1240+ mock.auth_replies = {}
1241+
1242+@dbus.service.method(MOCK_IFACE, in_signature='ua{sv}', out_signature='')
1243+def AddIdentity(self, identity, data):
1244+ self.identities[identity] = data
1245+
1246
1247=== modified file 'tests/click-hooks/tst_online_accounts_hooks.cpp'
1248--- tests/click-hooks/tst_online_accounts_hooks.cpp 2015-06-18 11:32:51 +0000
1249+++ tests/click-hooks/tst_online_accounts_hooks.cpp 2015-09-28 14:35:45 +0000
1250@@ -334,6 +334,7 @@
1251 QString stillInstalled("applications/com.ubuntu.test_StillInstalled.application");
1252 writeInstalledFile(stillInstalled,
1253 "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1254+ "<!--this file is auto-generated by online-accounts-hooks; do not modify-->\n"
1255 "<application>\n"
1256 " <description>My application</description>\n"
1257 " <service-types>\n"
1258@@ -346,6 +347,7 @@
1259 QString myApp("applications/com.ubuntu.test_MyApp.application");
1260 writeInstalledFile(myApp,
1261 "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1262+ "<!--this file is auto-generated by online-accounts-hooks; do not modify-->\n"
1263 "<application>\n"
1264 " <description>My application</description>\n"
1265 " <service-types>\n"
1266@@ -366,6 +368,19 @@
1267 "</application>");
1268 QVERIFY(m_installDir.exists(noProfile));
1269
1270+ QString notOurs("applications/com.ubuntu.test_NotOurs.application");
1271+ writeInstalledFile(notOurs,
1272+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1273+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1274+ "<application>\n"
1275+ " <description>My application</description>\n"
1276+ " <service-types>\n"
1277+ " <service-type>some type</service-type>\n"
1278+ " </service-types>\n"
1279+ " <profile>com-ubuntu.test_NotOurs_0.1.0</profile>\n"
1280+ "</application>");
1281+ QVERIFY(m_installDir.exists(notOurs));
1282+
1283 writeHookFile("com-ubuntu.test_StillInstalled_2.0.application",
1284 "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1285 "<application>\n"
1286@@ -380,6 +395,7 @@
1287 QVERIFY(m_installDir.exists(stillInstalled));
1288 QVERIFY(!m_installDir.exists(myApp));
1289 QVERIFY(m_installDir.exists(noProfile));
1290+ QVERIFY(m_installDir.exists(noProfile));
1291 }
1292
1293 void OnlineAccountsHooksTest::testAccountRemoval()
1294@@ -393,6 +409,7 @@
1295 QString stillInstalled("providers/com.ubuntu.test_StillInstalled.provider");
1296 writeInstalledFile(stillInstalled,
1297 "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1298+ "<!--this file is auto-generated by online-accounts-hooks; do not modify-->\n"
1299 "<provider>\n"
1300 " <profile>com-ubuntu.test_StillInstalled_2.0</profile>\n"
1301 "</provider>");
1302@@ -415,6 +432,7 @@
1303 QString myProvider("providers/com.ubuntu.test_MyProvider.provider");
1304 writeInstalledFile(myProvider,
1305 "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1306+ "<!--this file is auto-generated by online-accounts-hooks; do not modify-->\n"
1307 "<provider>\n"
1308 " <profile>com-ubuntu.test_MyProvider_3.0</profile>\n"
1309 "</provider>");
1310
1311=== renamed file 'tests/click-hooks/click-hooks.pro' => 'tests/click-hooks/tst_online_accounts_hooks.pro'
1312=== added file 'tests/click-hooks/tst_online_accounts_hooks2.cpp'
1313--- tests/click-hooks/tst_online_accounts_hooks2.cpp 1970-01-01 00:00:00 +0000
1314+++ tests/click-hooks/tst_online_accounts_hooks2.cpp 2015-09-28 14:35:45 +0000
1315@@ -0,0 +1,813 @@
1316+/*
1317+ * Copyright (C) 2014 Canonical Ltd.
1318+ *
1319+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1320+ *
1321+ * This file is part of online-accounts-ui
1322+ *
1323+ * This program is free software: you can redistribute it and/or modify it
1324+ * under the terms of the GNU General Public License version 3, as published
1325+ * by the Free Software Foundation.
1326+ *
1327+ * This program is distributed in the hope that it will be useful, but
1328+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1329+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1330+ * PURPOSE. See the GNU General Public License for more details.
1331+ *
1332+ * You should have received a copy of the GNU General Public License along
1333+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1334+ */
1335+
1336+#include <Accounts/Account>
1337+#include <Accounts/Manager>
1338+#include <QDebug>
1339+#include <QDir>
1340+#include <QDomDocument>
1341+#include <QDomElement>
1342+#include <QFile>
1343+#include <QProcess>
1344+#include <QRegularExpression>
1345+#include <QSignalSpy>
1346+#include <QTest>
1347+#include <QTextStream>
1348+#include <SignOn/Identity>
1349+#include <SignOn/IdentityInfo>
1350+#include <libqtdbusmock/DBusMock.h>
1351+#include "fake_signond.h"
1352+
1353+#define TEST_DIR "/tmp/hooks-test2"
1354+
1355+namespace QTest {
1356+template<>
1357+char *toString(const QSet<QString> &set)
1358+{
1359+ QByteArray ba = "QSet<QString>(";
1360+ QStringList list = set.toList();
1361+ ba += list.join(", ");
1362+ ba += ")";
1363+ return qstrdup(ba.data());
1364+}
1365+} // QTest namespace
1366+
1367+// map file name -> contents
1368+typedef QHash<QString,QString> FileData;
1369+
1370+class OnlineAccountsHooksTest: public QObject
1371+{
1372+ Q_OBJECT
1373+
1374+public:
1375+ OnlineAccountsHooksTest();
1376+
1377+private Q_SLOTS:
1378+ void initTestCase();
1379+ void cleanup() {
1380+ if (!QTest::currentTestFailed()) {
1381+ clearHooksDir();
1382+ clearInstallDir();
1383+ clearPackageDir();
1384+ }
1385+ }
1386+ void testNoFiles();
1387+ void testValidHooks_data();
1388+ void testValidHooks();
1389+ void testRemoval();
1390+ void testRemovalWithAcl();
1391+ void testTimestampRemoval();
1392+
1393+private:
1394+ void clearHooksDir();
1395+ void clearInstallDir();
1396+ void clearPackageDir();
1397+ bool runHookProcess();
1398+ bool runXmlDiff(const QString &generated, const QString &expected);
1399+ void writeHookFile(const QString &name, const QString &contents);
1400+ void writeInstalledFile(const QString &name, const QString &contents);
1401+ void writePackageFile(const QString &name,
1402+ const QString &contents = QString());
1403+ QStringList findGeneratedFiles() const;
1404+
1405+private:
1406+ QDir m_testDir;
1407+ QDir m_hooksDir;
1408+ QDir m_installDir;
1409+ QDir m_packageDir;
1410+};
1411+
1412+OnlineAccountsHooksTest::OnlineAccountsHooksTest():
1413+ QObject(0),
1414+ m_testDir(TEST_DIR),
1415+ m_hooksDir(TEST_DIR "/online-accounts-hooks2"),
1416+ m_installDir(TEST_DIR "/accounts"),
1417+ m_packageDir(TEST_DIR "/package")
1418+{
1419+}
1420+
1421+void OnlineAccountsHooksTest::clearHooksDir()
1422+{
1423+ m_hooksDir.removeRecursively();
1424+ m_hooksDir.mkpath(".");
1425+}
1426+
1427+void OnlineAccountsHooksTest::clearInstallDir()
1428+{
1429+ m_installDir.removeRecursively();
1430+ m_installDir.mkpath(".");
1431+}
1432+
1433+void OnlineAccountsHooksTest::clearPackageDir()
1434+{
1435+ m_packageDir.removeRecursively();
1436+ m_packageDir.mkpath(".");
1437+}
1438+
1439+bool OnlineAccountsHooksTest::runHookProcess()
1440+{
1441+ QProcess process;
1442+ process.setProcessChannelMode(QProcess::ForwardedChannels);
1443+ process.start(HOOK_PROCESS);
1444+ if (!process.waitForFinished()) return false;
1445+
1446+ return process.exitCode() == EXIT_SUCCESS;
1447+}
1448+
1449+bool OnlineAccountsHooksTest::runXmlDiff(const QString &generated,
1450+ const QString &expected)
1451+{
1452+ QProcess process;
1453+ process.setProcessChannelMode(QProcess::ForwardedChannels);
1454+ QStringList args;
1455+ args.append(generated);
1456+ args.append(expected);
1457+ process.start("xmldiff", args);
1458+ if (!process.waitForFinished()) return false;
1459+
1460+ int exitCode = process.exitCode();
1461+ if (exitCode != EXIT_SUCCESS) {
1462+ process.start("cat", args);
1463+ process.waitForFinished();
1464+ }
1465+ return exitCode == EXIT_SUCCESS;
1466+}
1467+
1468+void OnlineAccountsHooksTest::writeHookFile(const QString &name,
1469+ const QString &contents)
1470+{
1471+ QFile file(m_hooksDir.filePath(name));
1472+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1473+ qWarning() << "Could not write file" << name;
1474+ return;
1475+ }
1476+
1477+ file.write(contents.toUtf8());
1478+}
1479+
1480+void OnlineAccountsHooksTest::writeInstalledFile(const QString &name,
1481+ const QString &contents)
1482+{
1483+ QFileInfo fileInfo(name);
1484+ m_installDir.mkpath(fileInfo.path());
1485+ QFile file(m_installDir.filePath(name));
1486+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1487+ qWarning() << "Could not write file" << name;
1488+ return;
1489+ }
1490+
1491+ file.write(contents.toUtf8());
1492+}
1493+
1494+void OnlineAccountsHooksTest::writePackageFile(const QString &name,
1495+ const QString &contents)
1496+{
1497+ QFileInfo fileInfo(name);
1498+ m_packageDir.mkpath(fileInfo.path());
1499+ QFile file(m_packageDir.filePath(name));
1500+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1501+ qWarning() << "Could not write file" << name;
1502+ return;
1503+ }
1504+
1505+ file.write(contents.toUtf8());
1506+}
1507+
1508+static QStringList findFiles(const QDir &dir)
1509+{
1510+ QStringList files = dir.entryList(QDir::Files);
1511+
1512+ QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
1513+ Q_FOREACH(const QString &d, dirs) {
1514+ QStringList dirFiles = findFiles(QDir(dir.filePath(d)));
1515+ Q_FOREACH(const QString &file, dirFiles) {
1516+ files.append(dir.relativeFilePath(d + "/" + file));
1517+ }
1518+ }
1519+
1520+ return files;
1521+}
1522+
1523+QStringList OnlineAccountsHooksTest::findGeneratedFiles() const
1524+{
1525+ return findFiles(m_installDir);
1526+}
1527+
1528+void OnlineAccountsHooksTest::initTestCase()
1529+{
1530+ qputenv("XDG_DATA_HOME", TEST_DIR);
1531+ qputenv("OAH_CLICK_DIR", m_packageDir.path().toUtf8());
1532+ qputenv("ACCOUNTS", TEST_DIR);
1533+ qputenv("SSO_USE_PEER_BUS", "0");
1534+
1535+ // The hook must be able to run without a D-Bus session
1536+ qunsetenv("DBUS_SESSION_BUS_ADDRESS");
1537+
1538+ clearHooksDir();
1539+ clearInstallDir();
1540+ clearPackageDir();
1541+}
1542+
1543+void OnlineAccountsHooksTest::testNoFiles()
1544+{
1545+ QVERIFY(runHookProcess());
1546+ QVERIFY(m_hooksDir.entryList(QDir::Files).isEmpty());
1547+ QVERIFY(findGeneratedFiles().isEmpty());
1548+}
1549+
1550+void OnlineAccountsHooksTest::testValidHooks_data()
1551+{
1552+ QTest::addColumn<QString>("hookName");
1553+ QTest::addColumn<QString>("contents");
1554+ QTest::addColumn<FileData>("packageFiles");
1555+ QTest::addColumn<FileData>("expectedFiles");
1556+
1557+ FileData files;
1558+ FileData package;
1559+ QTest::newRow("empty file") <<
1560+ "com.ubuntu.test_MyApp_0.1.accounts" <<
1561+ "" <<
1562+ package <<
1563+ files;
1564+
1565+ QTest::newRow("empty dictionary") <<
1566+ "com.ubuntu.test_MyApp_0.1.accounts" <<
1567+ "{}" <<
1568+ package <<
1569+ files;
1570+
1571+ files["applications/com.ubuntu.test_MyApp.application"] =
1572+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1573+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1574+ "<application id=\"com.ubuntu.test_MyApp\">\n"
1575+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1576+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1577+ " <desktop-entry>com.ubuntu.test_MyApp_0.2</desktop-entry>\n"
1578+ " <services>\n"
1579+ " <service id=\"com.ubuntu.test_MyApp_google\">\n"
1580+ " <description>Publish to Picasa</description>\n"
1581+ " </service>\n"
1582+ " </services>\n"
1583+ "</application>\n";
1584+ files["services/com.ubuntu.test_MyApp_google.service"] =
1585+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1586+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1587+ "<service id=\"com.ubuntu.test_MyApp_google\">\n"
1588+ " <type>com.ubuntu.test_MyApp</type>\n"
1589+ " <provider>google</provider>\n"
1590+ " <name>Picasa</name>\n"
1591+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1592+ " <template>\n"
1593+ " <setting name=\"Key\">value</setting>\n"
1594+ " <group name=\"a\">\n"
1595+ " <group name=\"b\">\n"
1596+ " <group name=\"c\">\n"
1597+ " <setting name=\"Key3\">value3</setting>\n"
1598+ " <setting name=\"Key4\">value4</setting>\n"
1599+ " </group>\n"
1600+ " </group>\n"
1601+ " </group>\n"
1602+ " <group name=\"other\">\n"
1603+ " <setting name=\"key\">value5</setting>\n"
1604+ " </group>\n"
1605+ " <group name=\"subgroup\">\n"
1606+ " <setting name=\"Key2\">value2</setting>\n"
1607+ " </group>\n"
1608+ " </template>\n"
1609+ "</service>\n";
1610+ QTest::newRow("one service") <<
1611+ "com.ubuntu.test_MyApp_0.2.accounts" <<
1612+ "{"
1613+ " \"services\": ["
1614+ " {"
1615+ " \"provider\": \"google\","
1616+ " \"description\": \"Publish to Picasa\","
1617+ " \"name\": \"Picasa\","
1618+ " \"settings\": {"
1619+ " \"Key\": \"value\","
1620+ " \"subgroup\": {"
1621+ " \"Key2\": \"value2\""
1622+ " },"
1623+ " \"a/b/c\": {"
1624+ " \"Key3\": \"value3\","
1625+ " \"Key4\": \"value4\""
1626+ " },"
1627+ " \"other/key\": \"value5\""
1628+ " }"
1629+ " }"
1630+ " ]"
1631+ "}" <<
1632+ package <<
1633+ files;
1634+
1635+ files["applications/com.ubuntu.test_MyApp.application"] =
1636+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1637+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1638+ "<application id=\"com.ubuntu.test_MyApp\">\n"
1639+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1640+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1641+ " <desktop-entry>com.ubuntu.test_MyApp_0.2</desktop-entry>\n"
1642+ " <services>\n"
1643+ " <service id=\"com.ubuntu.test_MyApp_google\">\n"
1644+ " <description>.</description>\n"
1645+ " </service>\n"
1646+ " </services>\n"
1647+ "</application>\n";
1648+ files["services/com.ubuntu.test_MyApp_google.service"] =
1649+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1650+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1651+ "<service id=\"com.ubuntu.test_MyApp_google\">\n"
1652+ " <type>com.ubuntu.test_MyApp</type>\n"
1653+ " <provider>google</provider>\n"
1654+ " <name>.</name>\n"
1655+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1656+ "</service>\n";
1657+ QTest::newRow("minimal") <<
1658+ "com.ubuntu.test_MyApp_0.2.accounts" <<
1659+ "{"
1660+ " \"services\": ["
1661+ " {"
1662+ " \"provider\": \"google\""
1663+ " }"
1664+ " ]"
1665+ "}" <<
1666+ package <<
1667+ files;
1668+
1669+ files.clear();
1670+ files["applications/com.ubuntu.test_MyApp.application"] =
1671+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1672+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1673+ "<application id=\"com.ubuntu.test_MyApp\">\n"
1674+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1675+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1676+ " <desktop-entry>com.ubuntu.test_MyApp_0.3</desktop-entry>\n"
1677+ " <translations>my-app</translations>\n"
1678+ " <services>\n"
1679+ " <service id=\"com.ubuntu.test_MyApp_facebook\">\n"
1680+ " <description>Publish to Facebook</description>\n"
1681+ " </service>\n"
1682+ " <service id=\"com.ubuntu.test_MyApp_google\">\n"
1683+ " <description>Publish to Picasa</description>\n"
1684+ " </service>\n"
1685+ " </services>\n"
1686+ "</application>\n";
1687+ files["services/com.ubuntu.test_MyApp_google.service"] =
1688+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1689+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1690+ "<service id=\"com.ubuntu.test_MyApp_google\">\n"
1691+ " <type>com.ubuntu.test_MyApp</type>\n"
1692+ " <provider>google</provider>\n"
1693+ " <name>Picasa</name>\n"
1694+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1695+ " <translations>my-app</translations>\n"
1696+ "</service>\n";
1697+ files["services/com.ubuntu.test_MyApp_facebook.service"] =
1698+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1699+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1700+ "<service id=\"com.ubuntu.test_MyApp_facebook\">\n"
1701+ " <type>com.ubuntu.test_MyApp</type>\n"
1702+ " <provider>facebook</provider>\n"
1703+ " <name>Facebook photos</name>\n"
1704+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1705+ " <translations>my-app</translations>\n"
1706+ "</service>\n";
1707+ QTest::newRow("two services") <<
1708+ "com.ubuntu.test_MyApp_0.3.accounts" <<
1709+ "{"
1710+ " \"translations\": \"my-app\","
1711+ " \"services\": ["
1712+ " {"
1713+ " \"provider\": \"facebook\","
1714+ " \"description\": \"Publish to Facebook\","
1715+ " \"name\": \"Facebook photos\""
1716+ " },"
1717+ " {"
1718+ " \"provider\": \"google\","
1719+ " \"description\": \"Publish to Picasa\","
1720+ " \"name\": \"Picasa\""
1721+ " }"
1722+ " ]"
1723+ "}" <<
1724+ package <<
1725+ files;
1726+
1727+ files.clear();
1728+ files["applications/com.ubuntu.test_MyApp2.application"] =
1729+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1730+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1731+ "<application id=\"com.ubuntu.test_MyApp2\">\n"
1732+ " <profile>com.ubuntu.test_MyApp2_0.2</profile>\n"
1733+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1734+ " <desktop-entry>com.ubuntu.test_MyApp2_0.2</desktop-entry>\n"
1735+ " <services>\n"
1736+ " <service id=\"com.ubuntu.test_MyApp2_google\">\n"
1737+ " <description>Publish to Picasa</description>\n"
1738+ " </service>\n"
1739+ " </services>\n"
1740+ "</application>\n";
1741+ files["services/com.ubuntu.test_MyApp2_google.service"] =
1742+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1743+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1744+ "<service id=\"com.ubuntu.test_MyApp2_google\">\n"
1745+ " <type>com.ubuntu.test_MyApp2</type>\n"
1746+ " <provider>google</provider>\n"
1747+ " <name>Picasa</name>\n"
1748+ " <profile>com.ubuntu.test_MyApp2_0.2</profile>\n"
1749+ " <template>\n"
1750+ " <group name=\"auth\">\n"
1751+ " <setting name=\"method\">oauth2</setting>\n"
1752+ " <setting name=\"mechanism\">web_server</setting>\n"
1753+ " <group name=\"oauth2\">\n"
1754+ " <group name=\"web_server\">\n"
1755+ " <setting name=\"ClientId\">foo</setting>\n"
1756+ " <setting name=\"ClientSecret\">bar</setting>\n"
1757+ " <setting name=\"Scopes\" type=\"as\">['one scope','and another']</setting>\n"
1758+ " <setting name=\"UseSSL\" type=\"b\">false</setting>\n"
1759+ " </group>\n"
1760+ " </group>\n"
1761+ " </group>\n"
1762+ " </template>\n"
1763+ "</service>\n";
1764+ QTest::newRow("one service, with auth data") <<
1765+ "com.ubuntu.test_MyApp2_0.2.accounts" <<
1766+ "{"
1767+ " \"services\": ["
1768+ " {"
1769+ " \"provider\": \"google\","
1770+ " \"description\": \"Publish to Picasa\","
1771+ " \"name\": \"Picasa\","
1772+ " \"auth\": {"
1773+ " \"oauth2/web_server\": {"
1774+ " \"ClientId\": \"foo\","
1775+ " \"ClientSecret\": \"bar\","
1776+ " \"UseSSL\": false,"
1777+ " \"Scopes\": [\"one scope\",\"and another\"]"
1778+ " }"
1779+ " }"
1780+ " }"
1781+ " ]"
1782+ "}" <<
1783+ package <<
1784+ files;
1785+
1786+ package["myapp/Main.qml"] = "Something here";
1787+ package["google.svg"] = "icon contents";
1788+ files.clear();
1789+ files["applications/com.ubuntu.test_MyApp.application"] =
1790+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1791+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1792+ "<application id=\"com.ubuntu.test_MyApp\">\n"
1793+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1794+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1795+ " <desktop-entry>com.ubuntu.test_MyApp_0.2</desktop-entry>\n"
1796+ " <services>\n"
1797+ " <service id=\"com.ubuntu.test_MyApp_google\">\n"
1798+ " <description>.</description>\n"
1799+ " </service>\n"
1800+ " </services>\n"
1801+ "</application>\n";
1802+ files["services/com.ubuntu.test_MyApp_google.service"] =
1803+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1804+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1805+ "<service id=\"com.ubuntu.test_MyApp_google\">\n"
1806+ " <type>com.ubuntu.test_MyApp</type>\n"
1807+ " <provider>google</provider>\n"
1808+ " <name>.</name>\n"
1809+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1810+ "</service>\n";
1811+ files["providers/com.ubuntu.test_MyApp_google.provider"] =
1812+ QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1813+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1814+ "<provider id=\"com.ubuntu.test_MyApp_google\">\n"
1815+ " <name>Google</name>\n"
1816+ " <icon>%1/google.svg</icon>\n"
1817+ " <profile>com.ubuntu.test_MyApp_0.2</profile>\n"
1818+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1819+ "</provider>\n").arg(m_packageDir.path());
1820+ files["qml-plugins/com.ubuntu.test_MyApp_google/Main.qml"] =
1821+ "Something here";
1822+ QTest::newRow("account plugin, icon file") <<
1823+ "com.ubuntu.test_MyApp_0.2.accounts" <<
1824+ "{"
1825+ " \"services\": ["
1826+ " {"
1827+ " \"provider\": \"google\""
1828+ " }"
1829+ " ],"
1830+ " \"plugins\": ["
1831+ " {"
1832+ " \"provider\": \"google\","
1833+ " \"name\": \"Google\","
1834+ " \"icon\": \"google.svg\","
1835+ " \"qml\": \"myapp\""
1836+ " }"
1837+ " ]"
1838+ "}" <<
1839+ package <<
1840+ files;
1841+
1842+ files.clear();
1843+ package.clear();
1844+ package["myapp/Main.qml"] = "Something herez";
1845+ files["applications/com.ubuntu.test_MyApp.application"] =
1846+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1847+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1848+ "<application id=\"com.ubuntu.test_MyApp\">\n"
1849+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1850+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1851+ " <desktop-entry>com.ubuntu.test_MyApp_0.3</desktop-entry>\n"
1852+ " <services>\n"
1853+ " <service id=\"com.ubuntu.test_MyApp_abc\">\n"
1854+ " <description>.</description>\n"
1855+ " </service>\n"
1856+ " </services>\n"
1857+ "</application>\n";
1858+ files["services/com.ubuntu.test_MyApp_abc.service"] =
1859+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1860+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1861+ "<service id=\"com.ubuntu.test_MyApp_abc\">\n"
1862+ " <type>com.ubuntu.test_MyApp</type>\n"
1863+ " <provider>abc</provider>\n"
1864+ " <name>.</name>\n"
1865+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1866+ "</service>\n";
1867+ files["providers/com.ubuntu.test_MyApp_abc.provider"] =
1868+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1869+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1870+ "<provider id=\"com.ubuntu.test_MyApp_abc\">\n"
1871+ " <name>A B C</name>\n"
1872+ " <icon>abc.svg</icon>\n"
1873+ " <profile>com.ubuntu.test_MyApp_0.3</profile>\n"
1874+ " <package-dir>/tmp/hooks-test2/package</package-dir>\n"
1875+ "</provider>\n";
1876+ files["qml-plugins/com.ubuntu.test_MyApp_abc/Main.qml"] =
1877+ "Something herez";
1878+ QTest::newRow("account plugin, stock icon") <<
1879+ "com.ubuntu.test_MyApp_0.3.accounts" <<
1880+ "{"
1881+ " \"services\": ["
1882+ " {"
1883+ " \"provider\": \"abc\""
1884+ " }"
1885+ " ],"
1886+ " \"plugins\": ["
1887+ " {"
1888+ " \"provider\": \"abc\","
1889+ " \"name\": \"A B C\","
1890+ " \"icon\": \"abc.svg\","
1891+ " \"qml\": \"myapp\""
1892+ " }"
1893+ " ]"
1894+ "}" <<
1895+ package <<
1896+ files;
1897+}
1898+
1899+void OnlineAccountsHooksTest::testValidHooks()
1900+{
1901+ QFETCH(QString, hookName);
1902+ QFETCH(QString, contents);
1903+ QFETCH(FileData, packageFiles);
1904+ QFETCH(FileData, expectedFiles);
1905+
1906+ writeHookFile(hookName, contents);
1907+ for (FileData::const_iterator i = packageFiles.constBegin();
1908+ i != packageFiles.constEnd(); i++) {
1909+ writePackageFile(i.key(), i.value());
1910+ }
1911+ QVERIFY(runHookProcess());
1912+
1913+ QCOMPARE(findGeneratedFiles().toSet(), expectedFiles.keys().toSet());
1914+
1915+ QStringList xmlSuffixes;
1916+ xmlSuffixes << "provider" << "service" << "application";
1917+ for (FileData::const_iterator i = expectedFiles.constBegin();
1918+ i != expectedFiles.constEnd(); i++) {
1919+ QFileInfo generatedFile(m_installDir.filePath(i.key()));
1920+ if (xmlSuffixes.contains(generatedFile.suffix())) {
1921+ /* Dump the expected XML into a file */
1922+ QString expectedXml = generatedFile.filePath() + ".expected";
1923+ QFile file(expectedXml);
1924+ QVERIFY(file.open(QIODevice::WriteOnly));
1925+ QByteArray expectedContents = i.value().toUtf8();
1926+ QCOMPARE(file.write(expectedContents), expectedContents.length());
1927+ file.close();
1928+
1929+ QVERIFY(runXmlDiff(generatedFile.filePath(), expectedXml));
1930+ } else {
1931+ // direct comparison
1932+ QFile file(generatedFile.filePath());
1933+ QVERIFY(file.open(QIODevice::ReadOnly));
1934+ QByteArray expectedContents = i.value().toUtf8();
1935+ QCOMPARE(file.readAll(), expectedContents);
1936+ }
1937+ }
1938+}
1939+
1940+void OnlineAccountsHooksTest::testRemoval()
1941+{
1942+ QString stillInstalled("applications/com.ubuntu.test_StillInstalled.application");
1943+ writeInstalledFile(stillInstalled,
1944+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1945+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1946+ "<application>\n"
1947+ " <description>My application</description>\n"
1948+ " <service-types>\n"
1949+ " <service-type>some type</service-type>\n"
1950+ " </service-types>\n"
1951+ " <profile>com-ubuntu.test_StillInstalled_2.0</profile>\n"
1952+ "</application>");
1953+ QVERIFY(m_installDir.exists(stillInstalled));
1954+
1955+ QString myApp("applications/com.ubuntu.test_MyApp.application");
1956+ writeInstalledFile(myApp,
1957+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1958+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1959+ "<application>\n"
1960+ " <description>My application</description>\n"
1961+ " <services>\n"
1962+ " <service id=\"com-ubuntu.test_MyApp_example\">\n"
1963+ " <description>Publish somewhere</description>\n"
1964+ " </service>\n"
1965+ " </services>\n"
1966+ " <profile>com-ubuntu.test_MyApp_3.0</profile>\n"
1967+ "</application>");
1968+ QVERIFY(m_installDir.exists(myApp));
1969+
1970+ QString myService("services/com.ubuntu.test_MyApp_example.service");
1971+ writeInstalledFile(myService,
1972+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1973+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
1974+ "<service>\n"
1975+ " <name>Hello world</name>\n"
1976+ " <provider>example</provider>\n"
1977+ " <description>My application</description>\n"
1978+ " <profile>com-ubuntu.test_MyApp_3.0</profile>\n"
1979+ "</service>");
1980+ QVERIFY(m_installDir.exists(myService));
1981+
1982+ QString noProfile("applications/com.ubuntu.test_NoProfile.application");
1983+ writeInstalledFile(noProfile,
1984+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1985+ "<application>\n"
1986+ " <description>My application</description>\n"
1987+ " <service-types>\n"
1988+ " <service-type>some type</service-type>\n"
1989+ " </service-types>\n"
1990+ "</application>");
1991+ QVERIFY(m_installDir.exists(noProfile));
1992+
1993+ QString notOurs("applications/com.ubuntu.test_NotOurs.application");
1994+ writeInstalledFile(notOurs,
1995+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
1996+ "<!--this file is auto-generated by online-accounts-hooks; do not modify-->\n"
1997+ "<application>\n"
1998+ " <description>My application</description>\n"
1999+ " <service-types>\n"
2000+ " <service-type>some type</service-type>\n"
2001+ " </service-types>\n"
2002+ " <profile>com-ubuntu.test_NotOurs_0.1.0</profile>\n"
2003+ "</application>");
2004+ QVERIFY(m_installDir.exists(notOurs));
2005+
2006+ writeHookFile("com-ubuntu.test_StillInstalled_2.0.accounts",
2007+ "{"
2008+ " \"services\": ["
2009+ " {"
2010+ " \"provider\": \"example\","
2011+ " \"description\": \"Publish somewhere\","
2012+ " \"name\": \"Hello world\""
2013+ " }"
2014+ " ]"
2015+ "}");
2016+
2017+ QVERIFY(runHookProcess());
2018+
2019+ QVERIFY(m_installDir.exists(stillInstalled));
2020+ QVERIFY(!m_installDir.exists(myApp));
2021+ QVERIFY(!m_installDir.exists(myService));
2022+ QVERIFY(m_installDir.exists(noProfile));
2023+ QVERIFY(m_installDir.exists(notOurs));
2024+}
2025+
2026+void OnlineAccountsHooksTest::testRemovalWithAcl()
2027+{
2028+ QtDBusTest::DBusTestRunner dbus;
2029+ QtDBusMock::DBusMock mock(dbus);
2030+ FakeSignond signond(&mock);
2031+
2032+ dbus.startServices();
2033+
2034+ QString myApp("applications/com.ubuntu.test_MyAcl.application");
2035+ writeInstalledFile(myApp,
2036+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
2037+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
2038+ "<application id=\"com-ubuntu.test_MyAcl\">\n"
2039+ " <description>My application</description>\n"
2040+ " <services>\n"
2041+ " <service id=\"com-ubuntu.test_MyAcl_example\">\n"
2042+ " <description>Publish somewhere</description>\n"
2043+ " </service>\n"
2044+ " </services>\n"
2045+ " <profile>com-ubuntu.test_MyAcl_3.0</profile>\n"
2046+ "</application>");
2047+ QVERIFY(m_installDir.exists(myApp));
2048+
2049+ QString myService("services/com.ubuntu.test_MyAcl_example.service");
2050+ writeInstalledFile(myService,
2051+ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
2052+ "<!--this file is auto-generated by online-accounts-hooks2; do not modify-->\n"
2053+ "<service id=\"com-ubuntu.test_MyAcl_example\">\n"
2054+ " <name>Hello world</name>\n"
2055+ " <type>com-ubuntu.test_MyAcl</type>\n"
2056+ " <provider>example</provider>\n"
2057+ " <description>My application</description>\n"
2058+ " <profile>com-ubuntu.test_MyAcl_3.0</profile>\n"
2059+ "</service>");
2060+ QVERIFY(m_installDir.exists(myService));
2061+
2062+ /* Create an account, enable the app and add it to the ACL */
2063+ Accounts::Manager manager;
2064+ Accounts::Service service =
2065+ manager.service("com.ubuntu.test_MyAcl_example");
2066+ QVERIFY(service.isValid());
2067+ Accounts::Account *account = manager.createAccount("example");
2068+ account->setDisplayName("Example account");
2069+ account->setEnabled(true);
2070+ account->setCredentialsId(25);
2071+ account->selectService(service);
2072+ account->setEnabled(true);
2073+ account->syncAndBlock();
2074+ Accounts::AccountId accountId = account->id();
2075+ QVERIFY(accountId > 0);
2076+
2077+ QVariantMap initialInfo;
2078+ QStringList initialAcl;
2079+ initialAcl << "one" << "com-ubuntu.test_MyAcl_0.1" << "two_click";
2080+ initialInfo["ACL"] = initialAcl;
2081+ initialInfo["Id"] = 25;
2082+ signond.addIdentity(25, initialInfo);
2083+
2084+ /* Now run the hook process; it should delete the .service and .application
2085+ * files, and also disable the service and remove the app from the ACL */
2086+ QVERIFY(runHookProcess());
2087+
2088+ QVERIFY(!m_installDir.exists(myApp));
2089+ QVERIFY(!m_installDir.exists(myService));
2090+ QTRY_COMPARE(account->isEnabled(), false);
2091+
2092+ SignOn::Identity *identity = SignOn::Identity::existingIdentity(25, this);
2093+ QSignalSpy gotInfo(identity, SIGNAL(info(const SignOn::IdentityInfo&)));
2094+ identity->queryInfo();
2095+ QTRY_COMPARE(gotInfo.count(), 1);
2096+
2097+ SignOn::IdentityInfo info =
2098+ gotInfo.at(0).at(0).value<SignOn::IdentityInfo>();
2099+ QStringList expectedAcl;
2100+ expectedAcl << "one" << "two_click";
2101+ QCOMPARE(info.accessControlList().toSet(), expectedAcl.toSet());
2102+}
2103+
2104+void OnlineAccountsHooksTest::testTimestampRemoval()
2105+{
2106+ QString stillInstalled("com-ubuntu.test_MyApp_2.0.accounts");
2107+ writeHookFile(stillInstalled, "{}");
2108+ QString stillInstalledTimestamp("com-ubuntu.test_MyApp_2.0.accounts.processed");
2109+ writeHookFile(stillInstalledTimestamp, "");
2110+ QString oldTimestamp("com-ubuntu.test_MyApp_1.0.accounts.processed");
2111+ writeHookFile(oldTimestamp, "");
2112+ QString staleTimestamp1("com-ubuntu.app_MyOtherApp_2.0.accounts.processed");
2113+ writeHookFile(staleTimestamp1, "");
2114+ QString staleTimestamp2("com-ubuntu.app_MyOtherApp_1.0.accounts.processed");
2115+ writeHookFile(staleTimestamp2, "");
2116+
2117+ QVERIFY(runHookProcess());
2118+
2119+ QVERIFY(m_hooksDir.exists(stillInstalled));
2120+ QVERIFY(m_hooksDir.exists(stillInstalledTimestamp));
2121+ QVERIFY(!m_hooksDir.exists(oldTimestamp));
2122+ QVERIFY(!m_hooksDir.exists(staleTimestamp1));
2123+ QVERIFY(!m_hooksDir.exists(staleTimestamp2));
2124+}
2125+
2126+QTEST_MAIN(OnlineAccountsHooksTest);
2127+
2128+#include "tst_online_accounts_hooks2.moc"
2129
2130=== added file 'tests/click-hooks/tst_online_accounts_hooks2.pro'
2131--- tests/click-hooks/tst_online_accounts_hooks2.pro 1970-01-01 00:00:00 +0000
2132+++ tests/click-hooks/tst_online_accounts_hooks2.pro 2015-09-28 14:35:45 +0000
2133@@ -0,0 +1,32 @@
2134+include(../../common-project-config.pri)
2135+
2136+TARGET = tst_online_accounts_hooks2
2137+
2138+CONFIG += \
2139+ c++11 \
2140+ debug \
2141+ link_pkgconfig
2142+
2143+QT += \
2144+ core \
2145+ dbus \
2146+ testlib \
2147+ xml
2148+
2149+PKGCONFIG += \
2150+ accounts-qt5 \
2151+ libqtdbusmock-1 \
2152+ libqtdbustest-1 \
2153+ libsignon-qt5
2154+
2155+DEFINES += \
2156+ DEBUG_ENABLED \
2157+ HOOK_PROCESS=\\\"../../click-hooks/online-accounts-hooks2\\\" \
2158+ SIGNOND_MOCK_TEMPLATE=\\\"$${PWD}/signond.py\\\"
2159+
2160+SOURCES += \
2161+ tst_online_accounts_hooks2.cpp
2162+
2163+check.commands = "xvfb-run -s '-screen 0 640x480x24' -a ./$${TARGET}"
2164+check.depends = $${TARGET}
2165+QMAKE_EXTRA_TARGETS += check

Subscribers

People subscribed via source and target branches