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"