File indexing completed on 2024-05-12 05:10:37

0001 /*
0002     SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "collectioncalendartest.h"
0008 #include "collectioncalendar.h"
0009 
0010 #include <QSignalSpy>
0011 #include <QTest>
0012 #include <QUuid>
0013 
0014 #include <akonadi/qtest_akonadi.h>
0015 
0016 #include <Akonadi/AgentInstanceCreateJob>
0017 #include <Akonadi/CollectionFetchJob>
0018 #include <Akonadi/EntityTreeModel>
0019 #include <Akonadi/ItemCreateJob>
0020 #include <Akonadi/ItemDeleteJob>
0021 #include <Akonadi/ItemModifyJob>
0022 #include <Akonadi/ItemMoveJob>
0023 #include <Akonadi/Monitor>
0024 #include <akonadi/private/dbus_p.h>
0025 
0026 #include <KCalendarCore/Event>
0027 
0028 namespace
0029 {
0030 
0031 class EphemeralItem
0032 {
0033 public:
0034     EphemeralItem(const Akonadi::Item &item)
0035         : mItem(item)
0036     {
0037     }
0038     EphemeralItem &operator=(const Akonadi::Item &item)
0039     {
0040         deleteItem();
0041         mItem = item;
0042         return *this;
0043     }
0044 
0045     EphemeralItem(const EphemeralItem &) = delete;
0046     EphemeralItem(EphemeralItem &&) = delete;
0047     EphemeralItem &operator=(const EphemeralItem &) = delete;
0048     EphemeralItem &operator=(EphemeralItem &&) = delete;
0049 
0050     ~EphemeralItem()
0051     {
0052         deleteItem();
0053     }
0054 
0055     const Akonadi::Item *operator->() const
0056     {
0057         return &mItem;
0058     }
0059 
0060     const Akonadi::Item &operator*() const
0061     {
0062         return mItem;
0063     }
0064 
0065     Akonadi::Item &operator*()
0066     {
0067         return mItem;
0068     }
0069 
0070 private:
0071     void deleteItem()
0072     {
0073         if (mItem.isValid()) {
0074             auto *job = new Akonadi::ItemDeleteJob(mItem);
0075             job->exec();
0076             mItem = {};
0077         }
0078     }
0079     Akonadi::Item mItem;
0080 };
0081 
0082 class Observer : public QObject, public KCalendarCore::Calendar::CalendarObserver
0083 {
0084     Q_OBJECT
0085 public:
0086     Observer(Akonadi::CollectionCalendar &calendar)
0087         : mCalendar(calendar)
0088     {
0089         mCalendar.registerObserver(this);
0090     }
0091 
0092     ~Observer()
0093     {
0094         mCalendar.unregisterObserver(this);
0095     }
0096 
0097     QSignalSpy incidenceAddedSpy{this, &Observer::incidenceAdded};
0098     QSignalSpy incidenceChangedSpy{this, &Observer::incidenceChanged};
0099     QSignalSpy incidenceRemovedSpy{this, &Observer::incidenceRemoved};
0100 
0101 Q_SIGNALS:
0102     void incidenceAdded(const Akonadi::Item &item);
0103     void incidenceChanged(const Akonadi::Item &item);
0104     void incidenceRemoved(const Akonadi::Item &item);
0105 
0106 private:
0107     void calendarIncidenceAdded(const KCalendarCore::Incidence::Ptr &incidence) override
0108     {
0109         Q_EMIT incidenceAdded(mCalendar.item(incidence));
0110     }
0111 
0112     void calendarIncidenceChanged(const KCalendarCore::Incidence::Ptr &incidence) override
0113     {
0114         Q_EMIT incidenceChanged(mCalendar.item(incidence));
0115     }
0116 
0117     void calendarIncidenceDeleted(const KCalendarCore::Incidence::Ptr &incidence, const KCalendarCore::Calendar *) override
0118     {
0119         Akonadi::Item item(incidence->customProperty("VOLATILE", "AKONADI-ID").toLongLong());
0120         item.setParentCollection(Akonadi::Collection(incidence->customProperty("VOLATILE", "COLLECTION-ID").toLongLong()));
0121 
0122         Q_EMIT incidenceRemoved(item);
0123     }
0124 
0125     Akonadi::CollectionCalendar &mCalendar;
0126 };
0127 
0128 Akonadi::Collection findResourceCollection(const QString &resource)
0129 {
0130     auto *fetch = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive);
0131     fetch->fetchScope().setResource(resource);
0132     if (!fetch->exec()) {
0133         qWarning() << "Failed to fetch collection tree";
0134         return {};
0135     }
0136     for (const auto &col : fetch->collections()) {
0137         if (col.resource() == resource) {
0138             return col;
0139         }
0140     }
0141 
0142     qWarning() << "Failed to find a suitable parent collection for our test";
0143     return {};
0144 }
0145 
0146 Akonadi::Item createIncidence(const Akonadi::Collection &parent, const QString &summary)
0147 {
0148     auto event = KCalendarCore::Event::Ptr::create();
0149     event->setSummary(summary);
0150     event->setDtStart(QDateTime(QDate::currentDate(), QTime(10, 0, 0), QTimeZone::utc()));
0151     event->setDtEnd(QDateTime(QDate::currentDate(), QTime(11, 0, 0), QTimeZone::utc()));
0152     event->setUid(QUuid::createUuid().toString());
0153 
0154     Akonadi::Item item;
0155     item.setMimeType(event->mimeType());
0156     item.setParentCollection(parent);
0157     item.setPayload(event);
0158     item.setGid(event->uid());
0159 
0160     auto *job = new Akonadi::ItemCreateJob(item, parent);
0161     if (!job->exec()) {
0162         qWarning() << "Failed to create test incidence" << summary << ":" << job->errorString();
0163         return {};
0164     }
0165     return job->item();
0166 }
0167 
0168 std::unique_ptr<Akonadi::Monitor> createMonitor()
0169 {
0170     auto monitor = std::make_unique<Akonadi::Monitor>();
0171     monitor->setAllMonitored(true);
0172     monitor->itemFetchScope().fetchFullPayload(true);
0173     monitor->itemFetchScope().setCacheOnly(true);
0174 
0175     return monitor;
0176 }
0177 
0178 bool waitForEtmToPopulate(Akonadi::EntityTreeModel *etm)
0179 {
0180     return QTest::qWaitFor([etm]() {
0181         return etm->isFullyPopulated();
0182     });
0183 }
0184 
0185 bool populateCollection(const Akonadi::Collection &collection, int count)
0186 {
0187     for (int i = 0; i < count; ++i) {
0188         const auto item = createIncidence(collection, QStringLiteral("Test Incidence %1").arg(i));
0189         if (!item.isValid()) {
0190             return false;
0191         }
0192     }
0193 
0194     return true;
0195 }
0196 
0197 void configureResource(const Akonadi::AgentInstance &instance, const QString &file)
0198 {
0199     QDBusInterface iface(Akonadi::DBus::agentServiceName(instance.identifier(), Akonadi::DBus::Resource),
0200                          QStringLiteral("/Settings"),
0201                          QStringLiteral("org.kde.Akonadi.ICal.Settings"));
0202     iface.call(QStringLiteral("setPath"), file);
0203     iface.call(QStringLiteral("save"));
0204     instance.reconfigure();
0205 }
0206 }
0207 
0208 CollectionCalendarTest::CollectionCalendarTest(QObject *parent)
0209     : QObject(parent)
0210 {
0211     qRegisterMetaType<Akonadi::Collection::Id>();
0212 }
0213 
0214 void CollectionCalendarTest::initTestCase()
0215 {
0216     AkonadiTest::checkTestIsIsolated();
0217 
0218     otherResourceConfig.open();
0219 
0220     // We need two agents to test that the calendar properly ignores events in other collections
0221     // However we can't just add it to unittestenv, as having multiple resources breaks other
0222     // more complex tests that expect exactly one calendar agent.
0223     auto *agentJob = new Akonadi::AgentInstanceCreateJob(QStringLiteral("akonadi_ical_resource"));
0224     QVERIFY(agentJob->exec());
0225     configureResource(agentJob->instance(), otherResourceConfig.fileName());
0226 
0227     // Populate the ETM
0228     testCollection = findResourceCollection(QStringLiteral("akonadi_ical_resource_0"));
0229     QVERIFY(testCollection.isValid());
0230     QVERIFY(populateCollection(testCollection, 10));
0231 
0232     otherCollection = findResourceCollection(agentJob->instance().identifier());
0233     QVERIFY(otherCollection.isValid());
0234     QVERIFY(populateCollection(otherCollection, 5));
0235 }
0236 
0237 void CollectionCalendarTest::testPopulateFromReadyETM()
0238 {
0239     auto monitor = createMonitor();
0240     Akonadi::EntityTreeModel etm(monitor.get());
0241     QVERIFY(waitForEtmToPopulate(&etm));
0242 
0243     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0244 
0245     // Should populate right away
0246     QCOMPARE(calendar.incidences().size(), 10);
0247 }
0248 
0249 void CollectionCalendarTest::testPopulateWhenETMLoads()
0250 {
0251     auto monitor = createMonitor();
0252     Akonadi::EntityTreeModel etm(monitor.get());
0253     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0254     QVERIFY(!etm.isFullyPopulated());
0255     QVERIFY(!etm.isCollectionPopulated(testCollection.id()));
0256     QVERIFY(calendar.incidences().empty());
0257 
0258     QVERIFY(waitForEtmToPopulate(&etm));
0259 
0260     QCOMPARE(calendar.incidences().size(), 10);
0261 }
0262 
0263 void CollectionCalendarTest::testItemAdded()
0264 {
0265     auto monitor = createMonitor();
0266     Akonadi::EntityTreeModel etm(monitor.get());
0267     QVERIFY(waitForEtmToPopulate(&etm));
0268 
0269     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0270     Observer observer(calendar);
0271 
0272     QCOMPARE(calendar.incidences().size(), 10);
0273 
0274     EphemeralItem item = createIncidence(testCollection, QStringLiteral("New Test Item"));
0275     QVERIFY(observer.incidenceAddedSpy.wait());
0276 
0277     const auto newItem = std::as_const(observer.incidenceAddedSpy)[0][0].value<Akonadi::Item>();
0278     QCOMPARE(newItem.id(), item->id());
0279     QVERIFY(newItem.hasPayload<KCalendarCore::Event::Ptr>());
0280     QCOMPARE(*newItem.payload<KCalendarCore::Event::Ptr>(), *item->payload<KCalendarCore::Event::Ptr>());
0281 
0282     // Also make sure that the incidence is actually obtainable from the calendar
0283     auto newEvent = calendar.event(item->gid());
0284     QVERIFY(newEvent);
0285     QCOMPARE(*newItem.payload<KCalendarCore::Event::Ptr>(), *newEvent);
0286 
0287     QCOMPARE(calendar.incidences().size(), 11);
0288 }
0289 
0290 void CollectionCalendarTest::testItemRemoved()
0291 {
0292     auto monitor = createMonitor();
0293     Akonadi::EntityTreeModel etm(monitor.get());
0294     QVERIFY(waitForEtmToPopulate(&etm));
0295 
0296     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0297     Observer observer(calendar);
0298 
0299     QCOMPARE(calendar.incidences().size(), 10);
0300 
0301     Akonadi::Item newItem;
0302     {
0303         EphemeralItem item = createIncidence(testCollection, QStringLiteral("Will be deleted"));
0304         newItem = *item;
0305         QVERIFY(observer.incidenceAddedSpy.wait());
0306         QCOMPARE(calendar.incidences().size(), 11);
0307         // EphemeralItem will delete the Item here
0308     }
0309 
0310     QVERIFY(observer.incidenceRemovedSpy.wait());
0311     const auto removedItem = std::as_const(observer.incidenceRemovedSpy)[0][0].value<Akonadi::Item>();
0312     QCOMPARE(removedItem.id(), newItem.id());
0313 
0314     QCOMPARE(calendar.incidences().size(), 10);
0315 }
0316 
0317 void CollectionCalendarTest::testItemChanged()
0318 {
0319     auto monitor = createMonitor();
0320     Akonadi::EntityTreeModel etm(monitor.get());
0321     QVERIFY(waitForEtmToPopulate(&etm));
0322 
0323     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0324     Observer observer(calendar);
0325 
0326     QCOMPARE(calendar.incidences().size(), 10);
0327 
0328     {
0329         EphemeralItem item = createIncidence(testCollection, QStringLiteral("Will be changed"));
0330         QVERIFY(observer.incidenceAddedSpy.wait());
0331         QCOMPARE(calendar.incidences().size(), 11);
0332 
0333         KCalendarCore::Event::Ptr copy(item->payload<KCalendarCore::Event::Ptr>()->clone());
0334         copy->setSummary(QStringLiteral("Changed"));
0335         Akonadi::Item newItem = *item;
0336         newItem.setPayload(copy);
0337 
0338         auto *modifyJob = new Akonadi::ItemModifyJob(newItem);
0339         QVERIFY(modifyJob->exec());
0340 
0341         QVERIFY(observer.incidenceChangedSpy.wait());
0342         const auto changedItem = std::as_const(observer.incidenceChangedSpy)[0][0].value<Akonadi::Item>();
0343         QCOMPARE(changedItem.id(), newItem.id());
0344         QVERIFY(changedItem.hasPayload<KCalendarCore::Event::Ptr>());
0345         QCOMPARE(*changedItem.payload<KCalendarCore::Event::Ptr>(), *copy);
0346     }
0347 }
0348 
0349 void CollectionCalendarTest::testUnrelatedItemIsNotSeen()
0350 {
0351     auto monitor = createMonitor();
0352     Akonadi::EntityTreeModel etm(monitor.get());
0353     QVERIFY(waitForEtmToPopulate(&etm));
0354     QSignalSpy etmAddedSpy(&etm, &Akonadi::EntityTreeModel::rowsInserted);
0355     QSignalSpy etmChangedSpy(&etm, &Akonadi::EntityTreeModel::dataChanged);
0356     QSignalSpy etmRemovedSpy(&etm, &Akonadi::EntityTreeModel::rowsRemoved);
0357 
0358     Akonadi::CollectionCalendar calendar(&etm, testCollection);
0359     Observer observer(calendar);
0360 
0361     {
0362         // Create new incidence in the other collection
0363         EphemeralItem item = createIncidence(otherCollection, QStringLiteral("Invisible"));
0364         // Wait for it to appear in the ETM
0365         QVERIFY(etmAddedSpy.wait());
0366 
0367         // Our calendar should remain unaffected
0368         QVERIFY(observer.incidenceAddedSpy.empty());
0369         QVERIFY(observer.incidenceChangedSpy.empty());
0370         QVERIFY(observer.incidenceRemovedSpy.empty());
0371         QCOMPARE(calendar.incidences().size(), 10);
0372 
0373         // Now we modify the incidence in the other collection
0374         item->payload<KCalendarCore::Event::Ptr>()->setSummary(QStringLiteral("Invisible and Changed"));
0375         auto *modifyJob = new Akonadi::ItemModifyJob(*item);
0376         QVERIFY(modifyJob->exec());
0377         // Wait for the change to appear in the ETM
0378         QVERIFY(etmChangedSpy.wait());
0379 
0380         // Our calendar should still remain unaffected
0381         QVERIFY(observer.incidenceAddedSpy.empty());
0382         QVERIFY(observer.incidenceChangedSpy.empty());
0383         QVERIFY(observer.incidenceRemovedSpy.empty());
0384         QCOMPARE(calendar.incidences().size(), 10);
0385 
0386         // The item will be deleted when EphemeralItem leaves this scope
0387     }
0388 
0389     // Wait for ETM to notice the Item has been removed
0390     QVERIFY(etmRemovedSpy.wait());
0391 
0392     // Our calendar still remains unaffected
0393     QVERIFY(observer.incidenceAddedSpy.empty());
0394     QVERIFY(observer.incidenceChangedSpy.empty());
0395     QVERIFY(observer.incidenceRemovedSpy.empty());
0396     QCOMPARE(calendar.incidences().size(), 10);
0397 }
0398 
0399 QTEST_AKONADIMAIN(CollectionCalendarTest)
0400 
0401 #include "collectioncalendartest.moc"
0402 #include "moc_collectioncalendartest.cpp"