diff --git a/components/kube/main.qml b/components/kube/main.qml new file mode 100644 index 00000000..2e1459ea --- /dev/null +++ b/components/kube/main.qml @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2018 Christian Mollekopf, + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Window 2.0 + +import org.kube.components.kube 1.0 as KubeComponent +import org.kube.framework 1.0 as Kube +import org.kube.test 1.0 +import "qml" + + +KubeComponent.Kube { + id: app + height: Screen.desktopAvailableHeight * 0.8 + width: Screen.desktopAvailableWidth * 0.8 + + function setHour(date, hour) { + var d = date + return new Date(d.setHours(hour)) + } + + function addDays(date, days) { + return new Date(date.getTime() + (24*60*60*1000 * days)) + } + + function getFirstDayOfWeek(date) { + //This works with negative days to get to the previous month + //Date.getDate() is the day of the month, Date.getDay() is the day of the week + return new Date(date.getFullYear(), date.getMonth(), date.getDate() - (date.getDay() - Qt.locale().firstDayOfWeek)) + } + + function dayOfWeek(date, day) { + //This works with negative days to get to the previous month + //Date.getDate() is the day of the month, Date.getDay() is the day of the week + return new Date(date.getFullYear(), date.getMonth(), date.getDate() - (date.getDay() - day)) + } + + Component.onCompleted: { + var now = new Date() + var firstDayOfWeek = getFirstDayOfWeek(now) + var initialState = { + accounts: [ + { + id: "account1", + name: "Work" + }, + { + id: "account2", + name: "Private" + }, + ], + identities: [{ + account: "account1", + name: "John Doe", + address: "john@example.org" + }], + resources: [ + { + id: "resource1", + account: "account1", + type: "dummy", + }, + { + id: "resource2", + account: "account2", + type: "dummy", + } + ], + folders: [{ + id: "folder1", + resource: "resource1", + name: "Inbox", + specialpurpose: ["inbox"], + mails: [ + { + messageId: "", + date: setHour(now, 17), + to: ["\"John Doe\"", "\"Andrea Mueller\""], + from: "\"Jane Doe\"", + subject: "Kube 0.8 is out!", + body: "Hey,\n\nDid you see? Kube 0.8 is finally out!\n\nGet it now on www.kube-project.com\n\nCheers, Jane", + unread: false, + important: true + }, + { + inReplyTo: "", + date: setHour(now, 18), + from: "\"Andrea Mueller\"", + subject: "RE: Kube 0.8 is out!", + body: "Looking good indeed =)", + to: ["\"Jane Doe\"", "\"John Doe\""], + important: true, + unread: false + }, + { + messageId: "", + date: setHour(now, 15), + subject: "subject1", + body: "body", + to: ["to@example.org"], + from: "\"Jane Doe\"", + unread: true + }, + { + inReplyTo: "", + date: setHour(now, 16), + subject: "RE: Meeting report", + body: "body2", + to: ["to@example.org"], + from: "\"Jane Doe\"", + unread: true + }, + { + date: setHour(now, 14), + subject: " ❆ ❆ ❆ Winter is coming ❆ ❆ ❆", + body: "UTF-8 Madness Umlauts:öüä Snowflake:❆ Heart:♥", + to: ["öüä@example.org"], + from: "\"Jane Doe\"", + unread: true + }, + { + date: addDays(now, -1), + subject: "Great Post!", + to: ["öüä@example.org"], + from: "\"Jane Doe\"", + unread: true + }, + { + date: addDays(now, -3), + subject: "Last weeks meeting", + to: ["öüä@example.org"], + from: "\"Evan Smith\"", + unread: false + }, + { + date: addDays(now, -4), + subject: "Tennis tomorrow?", + to: ["öüä@example.org"], + from: "\"Anna Cole\"", + unread: false + }, + { + date: addDays(now, -4), + subject: "Project report", + to: ["öüä@example.org"], + from: "\"Phil Barry\"", + unread: false + }, + { + date: addDays(now, -5), + subject: "Dummy", + from: "test@example.org", + }, + { + date: addDays(now, -5), + subject: "Dummy", + from: "test@example.org", + }, + { + date: addDays(now, -5), + subject: "Dummy", + from: "test@example.org", + }, + { + date: addDays(now, -5), + subject: "Dummy", + from: "test@example.org", + }, + ] + }, + { + id: "folder2", + resource: "resource1", + specialpurpose: ["drafts"], + name: "Drafts", + mails: [ + { + date: setHour(now, 17), + to: ["\"Jane Doe\"", "\"Andrea Mueller\""], + from: "\"John Doe\"", + subject: "Kube 0.8 is out!", + body: "Hey,\n\nDid you see? Kube 0.8 is finally out!\n\nGet it now on www.kube-project.com\n\nCheers, Jane", + unread: false, + important: true + }, + ] + }, + { + id: "folder3", + resource: "resource1", + specialpurpose: ["sent"], + name: "Sent" + }, + { + id: "folder4", + resource: "resource1", + specialpurpose: ["trash"], + name: "Trash" + }, + { + id: "folder5", + resource: "resource1", + name: "Spam" + } + ], + calendars: [{ + id: "calendar1", + resource: "resource1", + name: "Calendar", + color: "#af1a6a", + events: [ + { + summary: "Monday morning standup", + starts: setHour(firstDayOfWeek, 9), + }, + { + summary: "Lunch with Claire", + starts: setHour(firstDayOfWeek, 12), + ends: setHour(firstDayOfWeek, 13), + }, + { + summary: "Meeting with Nik", + starts: setHour(firstDayOfWeek, 14), + ends: setHour(firstDayOfWeek, 16), + }, + { + summary: "Tennis?", + description: "This is test event #2", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 1), 13), + ends: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 1), 15), + }, + { + summary: "Dinner with Julia", + description: "This is test event #3", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 1), 18), + ends: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 1), 20), + }, + { + summary: "Mischa in Zurich", + description: "This is test event #4", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 2), 8), + ends: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 4), 16), + }, + { + summary: "Draft Report", + description: "This is test event #3", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 4), 13), + ends: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 4), 17), + }, + { + summary: "Send Report", + description: "This is test event #3", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 5), 12), + }, + ], + }, + { + id: "calendar2", + resource: "resource1", + name: "Vacations", + color: "#00cc4b", + events: [ + { + summary: "Sandra is off", + description: "This is test day-long event #1", + starts: dayOfWeek(now, Qt.locale().firstDayOfWeek), + ends: dayOfWeek(now, Qt.locale().firstDayOfWeek + 7), + allDay: true, + }, + { + summary: "Jeremy is off", + description: "This is test day-long event #2", + starts: dayOfWeek(now, Qt.locale().firstDayOfWeek + 4), + allDay: true, + }, + { + summary: "John's early weekend", + description: "This is test event #3", + starts: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 5), 14), + ends: setHour(dayOfWeek(now, Qt.locale().firstDayOfWeek + 5), 24), + }, + ] + }, + { + id: "account2calendar", + resource: "resource2", + name: "Account2Calendar", + color: "#f67400" + }], + } + TestStore.setup(initialState) + } +} diff --git a/tests/teststore.cpp b/tests/teststore.cpp index eaf03cd5..e54bd12c 100644 --- a/tests/teststore.cpp +++ b/tests/teststore.cpp @@ -1,408 +1,420 @@ /* Copyright (c) 2018 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "teststore.h" #include #include #include #include #include #include #include #include #include #include #include #include "framework/src/domain/mime/mailtemplates.h" +#include "framework/src/keyring.h" using namespace Kube; static void iterateOverObjects(const QVariantList &list, std::function callback) { for (const auto &entry : list) { auto object = entry.toMap(); callback(object); } } static QStringList toStringList(const QVariantList &list) { QStringList s; for (const auto &e : list) { s << e.toString(); } return s; } static QByteArrayList toByteArrayList(const QVariantList &list) { QByteArrayList s; for (const auto &e : list) { s << e.toByteArray(); } return s; } -static void createMail(const QVariantMap &object, const QByteArray &folder = {}) +static void createMail(const QVariantMap &object, const QByteArray &folder = {}, const QByteArray &resourceId = {}) { using namespace Sink::ApplicationDomain; auto toAddresses = toStringList(object["to"].toList()); auto ccAddresses = toStringList(object["cc"].toList()); auto bccAddresses = toStringList(object["bcc"].toList()); QList attachments = {}; if (object.contains("attachments")) { auto attachmentSpecs = object["attachments"].toList(); for (int i = 0; i < attachmentSpecs.size(); ++i) { auto const &spec = attachmentSpecs.at(i).toMap(); attachments << Attachment{spec["name"].toString(), spec["name"].toString(), spec["mimeType"].toByteArray(), false, spec["data"].toByteArray()}; } } - KMime::Types::Mailbox mb; - mb.fromUnicodeString("identity@example.org"); + KMime::Types::Mailbox from; + if (object.contains("from")) { + from.fromUnicodeString(object["from"].toString()); + } else { + from.fromUnicodeString("identity@example.org"); + } auto msg = MailTemplates::createMessage({}, toAddresses, ccAddresses, bccAddresses, - mb, + from, object["subject"].toString(), object["body"].toString(), object["bodyIsHtml"].toBool(), attachments, {}, {}); Q_ASSERT(msg); if (object.contains("messageId")) { msg->messageID(true)->from7BitString(object["messageId"].toByteArray()); } if (object.contains("inReplyTo")) { msg->inReplyTo(true)->from7BitString(object["inReplyTo"].toByteArray()); } if (object.contains("date")) { msg->date(true)->setDateTime(QDateTime::fromString(object["date"].toString(), Qt::ISODate)); } msg->assemble(); - auto mail = ApplicationDomainType::createEntity(object["resource"].toByteArray()); + auto res = resourceId; + if (res.isEmpty()) { + res = object["resource"].toByteArray(); + } + auto mail = ApplicationDomainType::createEntity(res); mail.setMimeMessage(msg->encodedContent(true)); mail.setUnread(object["unread"].toBool()); mail.setDraft(object["draft"].toBool()); if (!folder.isEmpty()) { mail.setFolder(folder); } Sink::Store::create(mail).exec().waitForFinished(); } static void createFolder(const QVariantMap &object) { using namespace Sink::ApplicationDomain; - auto folder = ApplicationDomainType::createEntity(object["resource"].toByteArray()); + auto resourceId = object["resource"].toByteArray(); + auto folder = ApplicationDomainType::createEntity(resourceId); folder.setName(object["name"].toString()); folder.setSpecialPurpose(toByteArrayList(object["specialpurpose"].toList())); Sink::Store::create(folder).exec().waitForFinished(); iterateOverObjects(object.value("mails").toList(), [=](const QVariantMap &object) { - createMail(object, folder.identifier()); + createMail(object, folder.identifier(), resourceId); }); } -static void createEvent(const QVariantMap &object, const QByteArray &calendarId = {}) +static void createEvent(const QVariantMap &object, const QByteArray &calendarId, const QByteArray &resourceId) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Event; - auto sinkEvent = ApplicationDomainType::createEntity(object["resource"].toByteArray()); + auto sinkEvent = ApplicationDomainType::createEntity(resourceId); auto calcoreEvent = QSharedPointer::create(); QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } calcoreEvent->setUid(uid); auto summary = object["summary"].toString(); calcoreEvent->setSummary(summary); if (object.contains("description")) { auto description = object["description"].toString(); calcoreEvent->setDescription(description); } auto startTime = object["starts"].toDateTime(); auto endTime = object["ends"].toDateTime(); calcoreEvent->setDtStart(startTime); calcoreEvent->setDtEnd(endTime); if (object.contains("allDay")) { calcoreEvent->setAllDay(object["allDay"].toBool()); } auto ical = KCalCore::ICalFormat().toICalString(calcoreEvent); sinkEvent.setIcal(ical.toUtf8()); sinkEvent.setCalendar(calendarId); Sink::Store::create(sinkEvent).exec().waitForFinished(); } -static void createTodo(const QVariantMap &object, const QByteArray &calendarId = {}) +static void createTodo(const QVariantMap &object, const QByteArray &calendarId, const QByteArray &resourceId) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Todo; - auto sinkEvent = ApplicationDomainType::createEntity(object["resource"].toByteArray()); + auto sinkEvent = ApplicationDomainType::createEntity(resourceId); auto calcoreEvent = QSharedPointer::create(); QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } calcoreEvent->setUid(uid); auto summary = object["summary"].toString(); calcoreEvent->setSummary(summary); if (object.contains("description")) { calcoreEvent->setDescription(object["description"].toString()); } calcoreEvent->setDtStart(object["starts"].toDateTime()); calcoreEvent->setDtDue(object["due"].toDateTime()); sinkEvent.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); sinkEvent.setCalendar(calendarId); Sink::Store::create(sinkEvent).exec().waitForFinished(); } static void createCalendar(const QVariantMap &object) { using Sink::ApplicationDomain::Calendar; using Sink::ApplicationDomain::ApplicationDomainType; - auto calendar = ApplicationDomainType::createEntity(object["resource"].toByteArray()); + auto resourceId = object["resource"].toByteArray(); + auto calendar = ApplicationDomainType::createEntity(resourceId); calendar.setName(object["name"].toString()); calendar.setColor(object["color"].toByteArray()); calendar.setContentTypes({"event", "todo"}); Sink::Store::create(calendar).exec().waitForFinished(); auto calendarId = calendar.identifier(); iterateOverObjects(object.value("events").toList(), - [calendarId](const QVariantMap &object) { createEvent(object, calendarId); }); + [calendarId, resourceId](const QVariantMap &object) { createEvent(object, calendarId, resourceId); }); iterateOverObjects(object.value("todos").toList(), - [calendarId](const QVariantMap &object) { createTodo(object, calendarId); }); + [calendarId, resourceId](const QVariantMap &object) { createTodo(object, calendarId, resourceId); }); } static void createContact(const QVariantMap &object, const QByteArray &addressbookId = {}) { using Sink::ApplicationDomain::ApplicationDomainType; using Sink::ApplicationDomain::Contact; QString uid; if (object.contains("uid")) { uid = object["uid"].toString(); } else { uid = QUuid::createUuid().toString(); } auto sinkContact = ApplicationDomainType::createEntity(object["resource"].toByteArray()); KContacts::Addressee addressee; addressee.setUid(uid); addressee.setGivenName(object["givenname"].toString()); addressee.setFamilyName(object["familyname"].toString()); addressee.setFormattedName(object["givenname"].toString() + " " + object["familyname"].toString()); addressee.setEmails(object["email"].toStringList()); KContacts::VCardConverter converter; auto contact = ApplicationDomainType::createEntity(object["resource"].toByteArray()); contact.setVcard(converter.createVCard(addressee, KContacts::VCardConverter::v3_0)); contact.setAddressbook(addressbookId); Sink::Store::create(contact).exec().waitForFinished(); } static void createAddressbook(const QVariantMap &object) { using namespace Sink::ApplicationDomain; auto addressbook = ApplicationDomainType::createEntity(object["resource"].toByteArray()); addressbook.setName(object["name"].toString()); Sink::Store::create(addressbook).exec().waitForFinished(); iterateOverObjects(object.value("contacts").toList(), [=](const QVariantMap &object) { createContact(object, addressbook.identifier()); }); } void TestStore::setup(const QVariantMap &map) { using namespace Sink::ApplicationDomain; //Cleanup any old data const auto accounts = Sink::Store::read({}); for (const auto &account : accounts) { Sink::Store::remove(account).exec().waitForFinished(); } iterateOverObjects(map.value("accounts").toList(), [&] (const QVariantMap &object) { auto account = ApplicationDomainType::createEntity("", object["id"].toByteArray()); account.setName(object["name"].toString()); + Kube::Keyring::instance()->unlock(account.identifier()); Sink::Store::create(account).exec().waitForFinished(); }); QByteArrayList resources; iterateOverObjects(map.value("resources").toList(), [&] (const QVariantMap &object) { resources << object["id"].toByteArray(); auto resource = [&] { using namespace Sink::ApplicationDomain; auto resource = ApplicationDomainType::createEntity("", object["id"].toByteArray()); if (object["type"] == "dummy") { resource.setResourceType("sink.dummy"); } else if (object["type"] == "mailtransport") { resource.setResourceType("sink.mailtransport"); resource.setProperty("testmode", true); } else if (object["type"] == "caldav") { resource.setResourceType("sink.caldav"); resource.setProperty("testmode", true); } else if (object["type"] == "carddav") { resource.setResourceType("sink.carddav"); resource.setProperty("testmode", true); } else { Q_ASSERT(false); } return resource; }(); resource.setAccount(object["account"].toByteArray()); Sink::Store::create(resource).exec().waitForFinished(); Sink::SecretStore::instance().insert(resource.identifier(), "secret"); }); iterateOverObjects(map.value("identities").toList(), [] (const QVariantMap &object) { auto identity = Sink::ApplicationDomain::Identity{}; identity.setAccount(object["account"].toByteArray()); identity.setAddress(object["address"].toString()); identity.setName(object["name"].toString()); Sink::Store::create(identity).exec().waitForFinished(); }); iterateOverObjects(map.value("folders").toList(), createFolder); iterateOverObjects(map.value("mails").toList(), [] (const QVariantMap &map) { createMail(map); }); iterateOverObjects(map.value("calendars").toList(), createCalendar); iterateOverObjects(map.value("addressbooks").toList(), createAddressbook); Sink::ResourceControl::flushMessageQueue(resources).exec().waitForFinished(); } void TestStore::shutdownResources() { const auto resources = Sink::Store::read({}); for (const auto &resource : resources) { Sink::ResourceControl::shutdown(resource.identifier()).exec().waitForFinished(); } } QVariant TestStore::load(const QByteArray &type, const QVariantMap &filter) { using namespace Sink::ApplicationDomain; const auto list = loadList(type, filter); if (!list.isEmpty()) { if (list.size() > 1) { qWarning() << "While loading" << type << "with filter" << filter << "; got multiple elements, but returning the first one."; } return list.first(); } return {}; } template QVariantList toVariantList(const QList &list) { QVariantList result; std::transform(list.constBegin(), list.constEnd(), std::back_inserter(result), [] (const T &m) { return QVariant::fromValue(T::Ptr::create(m)); }); Q_ASSERT(list.size() == result.size()); return result; } QVariantList TestStore::loadList(const QByteArray &type, const QVariantMap &_filter) { auto filter = _filter; using namespace Sink::ApplicationDomain; Sink::Query query; if (filter.contains("resource")) { query.resourceFilter(filter.take("resource").toByteArray()); } for (auto it = filter.begin(); it != filter.end(); ++it) { query.filter(it.key().toUtf8(), {it.value()}); } if (type == "mail") { return toVariantList(Sink::Store::read(query)); } if (type == "folder") { return toVariantList(Sink::Store::read(query)); } if (type == "resource") { return toVariantList(Sink::Store::read(query)); } if (type == "account") { return toVariantList(Sink::Store::read(query)); } Q_ASSERT(false); return {}; } QVariantMap TestStore::read(const QVariant &object) { using namespace Sink::ApplicationDomain; QVariantMap map; if (auto mail = object.value()) { map.insert("uid", mail->identifier()); map.insert("subject", mail->getSubject()); map.insert("draft", mail->getDraft()); return map; } Q_ASSERT(false); return {}; }