diff --git a/common/synchronizer.h b/common/synchronizer.h --- a/common/synchronizer.h +++ b/common/synchronizer.h @@ -79,6 +79,8 @@ virtual KAsync::Job replay(const Sink::ApplicationDomain::Addressbook &, Sink::Operation, const QByteArray &oldRemoteId, const QList &); virtual KAsync::Job replay(const Sink::ApplicationDomain::Mail &, Sink::Operation, const QByteArray &oldRemoteId, const QList &); virtual KAsync::Job replay(const Sink::ApplicationDomain::Folder &, Sink::Operation, const QByteArray &oldRemoteId, const QList &); + virtual KAsync::Job replay(const Sink::ApplicationDomain::Event &, Sink::Operation, const QByteArray &oldRemoteId, const QList &); + virtual KAsync::Job replay(const Sink::ApplicationDomain::Calendar &, Sink::Operation, const QByteArray &oldRemoteId, const QList &); protected: QString secret() const; diff --git a/common/synchronizer.cpp b/common/synchronizer.cpp --- a/common/synchronizer.cpp +++ b/common/synchronizer.cpp @@ -632,6 +632,10 @@ job = replay(store().readEntity(key), operation, oldRemoteId, modifiedProperties); } else if (type == ApplicationDomain::getTypeName()) { job = replay(store().readEntity(key), operation, oldRemoteId, modifiedProperties); + } else if (type == ApplicationDomain::getTypeName()) { + job = replay(store().readEntity(key), operation, oldRemoteId, modifiedProperties); + } else if (type == ApplicationDomain::getTypeName()) { + job = replay(store().readEntity(key), operation, oldRemoteId, modifiedProperties); } else { SinkErrorCtx(mLogCtx) << "Replayed unknown type: " << type; } @@ -688,6 +692,16 @@ return KAsync::null(); } +KAsync::Job Synchronizer::replay(const ApplicationDomain::Event &, Sink::Operation, const QByteArray &, const QList &) +{ + return KAsync::null(); +} + +KAsync::Job Synchronizer::replay(const ApplicationDomain::Calendar &, Sink::Operation, const QByteArray &, const QList &) +{ + return KAsync::null(); +} + bool Synchronizer::allChangesReplayed() { if (!mSyncRequestQueue.isEmpty()) { diff --git a/examples/caldavresource/caldavresource.cpp b/examples/caldavresource/caldavresource.cpp --- a/examples/caldavresource/caldavresource.cpp +++ b/examples/caldavresource/caldavresource.cpp @@ -107,6 +107,74 @@ { return syncStore().resolveRemoteId(ENTITY_TYPE_CALENDAR, resourceID(calendar)); } + + KAsync::Job replay(const Event &event, Sink::Operation operation, + const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE + { + SinkLog() << "Replaying event"; + + KDAV2::DavItem item; + + switch (operation) { + case Sink::Operation_Creation: { + auto rawIcal = event.getIcal(); + if(rawIcal == "") { + return KAsync::error("No ICal in event for creation replay"); + } + + auto collectionId = syncStore().resolveLocalId(ENTITY_TYPE_CALENDAR, event.getCalendar()); + + item.setData(rawIcal); + item.setContentType("text/calendar"); + item.setUrl(urlOf(collectionId, event.getUid())); + + SinkLog() << "Creating event:" << event.getSummary(); + return createItem(item).then([item] { return resourceID(item); }); + } + case Sink::Operation_Removal: { + // We only need the URL in the DAV item for removal + item.setUrl(urlOf(oldRemoteId)); + + SinkLog() << "Removing event:" << oldRemoteId; + return removeItem(item).then([] { return QByteArray{}; }); + } + case Sink::Operation_Modification: + auto rawIcal = event.getIcal(); + if(rawIcal == "") { + return KAsync::error("No ICal in event for modification replay"); + } + + item.setData(rawIcal); + item.setContentType("text/calendar"); + item.setUrl(urlOf(oldRemoteId)); + + SinkLog() << "Modifying event:" << event.getSummary(); + + // It would be nice to check that the URL of the item hasn't + // changed and move he item if it did, but since the URL is + // pretty much arbitrary, whoe does that anyway? + return modifyItem(item).then([oldRemoteId] { return oldRemoteId; }); + } + } + + KAsync::Job replay(const Calendar &calendar, Sink::Operation operation, + const QByteArray &oldRemoteId, const QList &changedProperties) Q_DECL_OVERRIDE + { + switch (operation) { + case Sink::Operation_Creation: + SinkWarning() << "Unimplemented replay of calendar creation"; + break; + case Sink::Operation_Removal: + SinkLog() << "Replaying calendar removal"; + removeCollection(urlOf(oldRemoteId)); + break; + case Sink::Operation_Modification: + SinkWarning() << "Unimplemented replay of calendar modification"; + break; + } + + return KAsync::null(); + } }; CalDavResource::CalDavResource(const Sink::ResourceContext &context) diff --git a/examples/caldavresource/tests/caldavtest.cpp b/examples/caldavresource/tests/caldavtest.cpp --- a/examples/caldavresource/tests/caldavtest.cpp +++ b/examples/caldavresource/tests/caldavtest.cpp @@ -1,13 +1,24 @@ #include +#include +#include +#include +#include +#include + +#include +#include + #include "../caldavresource.h" #include "common/resourcecontrol.h" #include "common/secretstore.h" #include "common/store.h" #include "common/test.h" #include "tests/testutils.h" +#include + using Sink::ApplicationDomain::Calendar; using Sink::ApplicationDomain::DummyResource; using Sink::ApplicationDomain::Event; @@ -17,19 +28,26 @@ { Q_OBJECT + // This test assumes a calendar MyCalendar with one event in it. + + const QString baseUrl = "http://localhost/dav/calendars/users/doe"; + const QString username = "doe"; + const QString password = "doe"; + SinkResource createResource() { auto resource = Sink::ApplicationDomain::CalDavResource::create("account1"); - resource.setProperty("server", "http://localhost/dav/calendars/users/doe"); - resource.setProperty("username", "doe"); - Sink::SecretStore::instance().insert(resource.identifier(), "doe"); + resource.setProperty("server", baseUrl); + resource.setProperty("username", username); + Sink::SecretStore::instance().insert(resource.identifier(), password); resource.setProperty("testmode", true); return resource; } - QByteArray mResourceInstanceIdentifier; + QString addedEventUid; + private slots: void initTestCase() @@ -51,34 +69,211 @@ VERIFYEXEC(Sink::ResourceControl::start(mResourceInstanceIdentifier)); } - // void testSyncCal() - // { - // VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); - // // Check in the logs that it doesn't synchronize events again because same CTag - // VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); - // } - - // void testSyncCalEmpty() - // { - // VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); - - // auto eventJob = - // Sink::Store::fetchAll(Sink::Query().request()).then([](const QList &events) { - // QCOMPARE(events.size(), 14); - // }); - // VERIFYEXEC(eventJob); - - // auto calendarJob = - // Sink::Store::fetchAll(Sink::Query().request()).then([](const QList &calendars) { - // QCOMPARE(calendars.size(), 2); - // for (const auto &calendar : calendars) { - // QVERIFY(calendar->getName() == "Calendar" || calendar->getName() == "Tasks"); - // } - // }); - // VERIFYEXEC(calendarJob); - - // SinkLog() << "Finished"; - // } + void testSyncCal() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + // Check in the logs that it doesn't synchronize events again because same CTag + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + } + + void testSyncCalEmpty() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto eventJob = Sink::Store::fetchAll(Sink::Query().request()) + .then([](const QList &events) { QCOMPARE(events.size(), 1); }); + VERIFYEXEC(eventJob); + + auto calendarJob = Sink::Store::fetchAll(Sink::Query().request()) + .then([](const QList &calendars) { + QCOMPARE(calendars.size(), 1); + for (const auto &calendar : calendars) { + QVERIFY(calendar->getName() == "MyCalendar"); + } + }); + VERIFYEXEC(calendarJob); + + SinkLog() << "Finished"; + } + + void testAddEvent() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto job = Sink::Store::fetchOne({}).exec(); + job.waitForFinished(); + QVERIFY2(!job.errorCode(), "Fetching Calendar failed"); + auto calendar = job.value(); + + auto event = QSharedPointer::create(); + event->setSummary("Hello"); + event->setDtStart(QDateTime::currentDateTime()); + event->setDtEnd(QDateTime::currentDateTime().addSecs(3600)); + event->setCreated(QDateTime::currentDateTime()); + addedEventUid = QUuid::createUuid().toString(); + event->setUid(addedEventUid); + + auto ical = KCalCore::ICalFormat().toICalString(event); + Event sinkEvent(mResourceInstanceIdentifier); + sinkEvent.setIcal(ical.toUtf8()); + sinkEvent.setCalendar(calendar); + + SinkLog() << "Adding event"; + VERIFYEXEC(Sink::Store::create(sinkEvent)); + VERIFYEXEC(Sink::ResourceControl::flushReplayQueue(mResourceInstanceIdentifier)); + + auto verifyEventCountJob = + Sink::Store::fetchAll(Sink::Query().request()).then([](const QList &events) { + QCOMPARE(events.size(), 2); + }); + VERIFYEXEC(verifyEventCountJob); + + auto verifyEventJob = + Sink::Store::fetchOne(Sink::Query().filter("uid", Sink::Query::Comparator(addedEventUid))) + .then([](const Event &event) { QCOMPARE(event.getSummary(), {"Hello"}); }); + VERIFYEXEC(verifyEventJob); + } + + void testModifyEvent() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto job = Sink::Store::fetchOne( + Sink::Query().filter("uid", Sink::Query::Comparator(addedEventUid))) + .exec(); + job.waitForFinished(); + QVERIFY2(!job.errorCode(), "Fetching Event failed"); + auto event = job.value(); + + auto incidence = KCalCore::ICalFormat().readIncidence(event.getIcal()); + auto calevent = incidence.dynamicCast(); + QVERIFY2(calevent, "Cannot convert to KCalCore event"); + + calevent->setSummary("Hello World!"); + auto dummy = QSharedPointer(calevent); + auto newical = KCalCore::ICalFormat().toICalString(dummy); + + event.setIcal(newical.toUtf8()); + + VERIFYEXEC(Sink::Store::modify(event)); + + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto verifyEventCountJob = Sink::Store::fetchAll({}).then( + [](const QList &events) { QCOMPARE(events.size(), 2); }); + VERIFYEXEC(verifyEventCountJob); + + auto verifyEventJob = + Sink::Store::fetchOne(Sink::Query().filter("uid", Sink::Query::Comparator(addedEventUid))) + .then([](const Event &event) { QCOMPARE(event.getSummary(), {"Hello World!"}); }); + VERIFYEXEC(verifyEventJob); + } + + void testSneakyModifyEvent() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + // Change the item without sink's knowledge + { + qWarning() << 1; + auto collection = ([this]() -> KDAV2::DavCollection { + QUrl url(baseUrl); + url.setUserName(username); + url.setPassword(password); + KDAV2::DavUrl davurl(url, KDAV2::CalDav); + KDAV2::DavCollectionsFetchJob collectionsJob(davurl); + collectionsJob.exec(); + Q_ASSERT(collectionsJob.error() == 0); + return collectionsJob.collections()[0]; + })(); + + auto itemList = ([&collection]() -> KDAV2::DavItem::List { + auto cache = std::make_shared(); + KDAV2::DavItemsListJob itemsListJob(collection.url(), cache); + itemsListJob.exec(); + Q_ASSERT(itemsListJob.error() == 0); + return itemsListJob.items(); + })(); + auto hollowDavItemIt = + std::find_if(itemList.begin(), itemList.end(), [this](const KDAV2::DavItem &item) { + return item.url().url().path().endsWith(addedEventUid); + }); + + auto davitem = ([this, &collection, &hollowDavItemIt]() -> KDAV2::DavItem { + QString itemUrl = collection.url().url().toEncoded() + addedEventUid; + KDAV2::DavItemFetchJob itemFetchJob(*hollowDavItemIt); + itemFetchJob.exec(); + Q_ASSERT(itemFetchJob.error() == 0); + return itemFetchJob.item(); + })(); + + qWarning() << 3; + auto incidence = KCalCore::ICalFormat().readIncidence(davitem.data()); + auto calevent = incidence.dynamicCast(); + QVERIFY2(calevent, "Cannot convert to KCalCore event"); + + qWarning() << 4; + calevent->setSummary("Manual Hello World!"); + auto newical = KCalCore::ICalFormat().toICalString(calevent); + + qWarning() << 5; + davitem.setData(newical.toUtf8()); + KDAV2::DavItemModifyJob itemModifyJob(davitem); + itemModifyJob.exec(); + QVERIFY2(itemModifyJob.error() == 0, "Cannot modify item"); + + qWarning() << 6; + } + + // Try to change the item with sink + { + auto job = Sink::Store::fetchOne( + Sink::Query().filter("uid", Sink::Query::Comparator(addedEventUid))) + .exec(); + job.waitForFinished(); + QVERIFY2(!job.errorCode(), "Fetching Event failed"); + auto event = job.value(); + + auto incidence = KCalCore::ICalFormat().readIncidence(event.getIcal()); + auto calevent = incidence.dynamicCast(); + QVERIFY2(calevent, "Cannot convert to KCalCore event"); + + calevent->setSummary("Sink Hello World!"); + auto dummy = QSharedPointer(calevent); + auto newical = KCalCore::ICalFormat().toICalString(dummy); + + event.setIcal(newical.toUtf8()); + + // TODO: make that fail + VERIFYEXEC(Sink::Store::modify(event)); + VERIFYEXEC(Sink::ResourceControl::flushReplayQueue(mResourceInstanceIdentifier)); + } + } + + void testRemoveEvent() + { + VERIFYEXEC(Sink::Store::synchronize(Sink::Query().resourceFilter(mResourceInstanceIdentifier))); + VERIFYEXEC(Sink::ResourceControl::flushMessageQueue(mResourceInstanceIdentifier)); + + auto job = Sink::Store::fetchOne( + Sink::Query().filter("uid", Sink::Query::Comparator(addedEventUid))) + .exec(); + job.waitForFinished(); + QVERIFY2(!job.errorCode(), "Fetching Event failed"); + auto event = job.value(); + + VERIFYEXEC(Sink::Store::remove(event)); + VERIFYEXEC(Sink::ResourceControl::flushReplayQueue(mResourceInstanceIdentifier)); + + auto verifyEventCountJob = Sink::Store::fetchAll({}).then( + [](const QList &events) { QCOMPARE(events.size(), 1); }); + VERIFYEXEC(verifyEventCountJob); + } }; QTEST_MAIN(CalDavTest) diff --git a/examples/webdavcommon/webdav.h b/examples/webdavcommon/webdav.h --- a/examples/webdavcommon/webdav.h +++ b/examples/webdavcommon/webdav.h @@ -35,6 +35,32 @@ KAsync::Job synchronizeWithSource(const Sink::QueryBase &query) Q_DECL_OVERRIDE; protected: + + /** + * Called in a child synchronizer, when replaying a creation of an item. + */ + KAsync::Job createItem(const KDAV2::DavItem &); + + /** + * Called in a child synchronizer, when replaying a removal of an item. + */ + KAsync::Job removeItem(const KDAV2::DavItem &); + + /** + * Called in a child synchronizer, when replaying a modification of an item. + * + * The item to modify is chosen according to the given item's URL. + * The job will fail if the ETag does not match. + */ + KAsync::Job modifyItem(const KDAV2::DavItem &); + + /** + * See comments of the *Item version above + */ + KAsync::Job createCollection(const KDAV2::DavUrl &); + KAsync::Job removeCollection(const KDAV2::DavUrl &); + KAsync::Job modifyCollection(const KDAV2::DavUrl &); + /** * Called with the list of discovered collections. It's purpose should be * adding the said collections to the store. @@ -63,6 +89,19 @@ static QByteArray resourceID(const KDAV2::DavCollection &); static QByteArray resourceID(const KDAV2::DavItem &); + /** + * Used to get the url of an item / collection with the given remote ID + */ + KDAV2::DavUrl urlOf(const QByteArray &remoteId); + + /** + * Used to get the url of an item / collection with the given remote ID, + * and append `itemPath` to the path of the URI. + * + * Useful when adding a new item to a collection + */ + KDAV2::DavUrl urlOf(const QByteArray &collectionRemoteId, const QString &itemPath); + bool unchanged(const KDAV2::DavCollection &); bool unchanged(const KDAV2::DavItem &); diff --git a/examples/webdavcommon/webdav.cpp b/examples/webdavcommon/webdav.cpp --- a/examples/webdavcommon/webdav.cpp +++ b/examples/webdavcommon/webdav.cpp @@ -22,8 +22,13 @@ #include "applicationdomaintype.h" #include "resourceconfig.h" +#include +#include #include +#include +#include #include +#include #include #include @@ -180,7 +185,6 @@ auto localRid = collectionLocalResourceID(collection); - // The ETag cache is useless here, since `sinkStore()` IS the cache. auto cache = std::make_shared(); auto davItemsListJob = new KDAV2::DavItemsListJob(collection.url(), std::move(cache)); @@ -234,14 +238,71 @@ }); } +KAsync::Job WebDavSynchronizer::createItem(const KDAV2::DavItem &item) +{ + auto job = new KDAV2::DavItemCreateJob(item); + return runJob(job).then([] { SinkTrace() << "Done creating item"; }); +} + +KAsync::Job WebDavSynchronizer::removeItem(const KDAV2::DavItem &item) +{ + auto job = new KDAV2::DavItemDeleteJob(item); + return runJob(job).then([] { SinkTrace() << "Done removing item"; }); +} + +KAsync::Job WebDavSynchronizer::modifyItem(const KDAV2::DavItem &item) +{ + auto job = new KDAV2::DavItemModifyJob(item); + return runJob(job).then([] { SinkTrace() << "Done modifying item"; }); +} + +// There is no "DavCollectionCreateJob" +/* +KAsync::Job WebDavSynchronizer::createCollection(const KDAV2::DavCollection &collection) +{ + auto job = new KDAV2::DavCollectionCreateJob(collection); + return runJob(job); +} +*/ + +KAsync::Job WebDavSynchronizer::removeCollection(const KDAV2::DavUrl &url) +{ + auto job = new KDAV2::DavCollectionDeleteJob(url); + return runJob(job).then([] { SinkLog() << "Done removing collection"; }); +} + +// Useless without using the `setProperty` method of DavCollectionModifyJob +/* +KAsync::Job WebDavSynchronizer::modifyCollection(const KDAV2::DavUrl &url) +{ + auto job = new KDAV2::DavCollectionModifyJob(url); + return runJob(job).then([] { SinkLog() << "Done modifying collection"; }); +} +*/ + QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavCollection &collection) { - return collection.url().toDisplayString().toUtf8(); + return collection.url().url().path().toUtf8(); } QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavItem &item) { - return item.url().toDisplayString().toUtf8(); + return item.url().url().path().toUtf8(); +} + +KDAV2::DavUrl WebDavSynchronizer::urlOf(const QByteArray &remoteId) +{ + auto davurl = serverUrl(); + auto url = davurl.url(); + url.setPath(remoteId); + SinkLog() << "Returning URL:" << url.toEncoded(); + davurl.setUrl(url); + return davurl; +} + +KDAV2::DavUrl WebDavSynchronizer::urlOf(const QByteArray &collectionRemoteId, const QString &itemPath) +{ + return urlOf(collectionRemoteId + "/" + itemPath.toUtf8()); } bool WebDavSynchronizer::unchanged(const KDAV2::DavCollection &collection)