File indexing completed on 2025-01-05 04:55:03
0001 #include <QTest> 0002 #include <QDebug> 0003 #include <sink/test.h> 0004 #include <sink/store.h> 0005 #include <sink/resourcecontrol.h> 0006 0007 #include <QDateTime> 0008 #include <KCalendarCore/ICalFormat> 0009 #include <KCalendarCore/ScheduleMessage> 0010 #include <KCalendarCore/Event> 0011 #include <KCalendarCore/Attendee> 0012 #include <KMime/Message> 0013 #include "invitationcontroller.h" 0014 0015 using namespace Sink::ApplicationDomain; 0016 0017 0018 class InvitationControllerTest : public QObject 0019 { 0020 Q_OBJECT 0021 0022 QByteArray resourceId; 0023 QByteArray mailtransportResourceId; 0024 0025 struct Invitation { 0026 QByteArray uid; 0027 QString summary; 0028 int revision; 0029 QDateTime dtStart{QDateTime::currentDateTime()}; 0030 bool recurring = false; 0031 QDateTime recurrenceId = {}; 0032 bool cancelled = false; 0033 KCalendarCore::iTIPMethod method = KCalendarCore::iTIPRequest; 0034 }; 0035 0036 QString createInvitation(const Invitation &invitation) 0037 { 0038 auto calcoreEvent = QSharedPointer<KCalendarCore::Event>::create(); 0039 calcoreEvent->setUid(invitation.uid); 0040 calcoreEvent->setSummary(invitation.summary); 0041 calcoreEvent->setDescription("description"); 0042 calcoreEvent->setLocation("location"); 0043 calcoreEvent->setDtStart(invitation.dtStart); 0044 calcoreEvent->setOrganizer("organizer@test.com"); 0045 calcoreEvent->addAttendee(KCalendarCore::Attendee("John Doe", "attendee1@test.com", true, KCalendarCore::Attendee::NeedsAction)); 0046 calcoreEvent->setRevision(invitation.revision); 0047 if (invitation.cancelled) { 0048 calcoreEvent->setStatus(KCalendarCore::Incidence::StatusCanceled); 0049 } 0050 0051 if (invitation.recurring) { 0052 calcoreEvent->recurrence()->setDaily(1); 0053 } 0054 if (invitation.recurrenceId.isValid()) { 0055 calcoreEvent->setRecurrenceId(invitation.recurrenceId); 0056 } 0057 0058 return KCalendarCore::ICalFormat{}.createScheduleMessage(calcoreEvent, invitation.method); 0059 } 0060 0061 private slots: 0062 void initTestCase() 0063 { 0064 Sink::Test::initTest(); 0065 0066 auto account = ApplicationDomainType::createEntity<SinkAccount>(); 0067 Sink::Store::create(account).exec().waitForFinished(); 0068 0069 auto identity = ApplicationDomainType::createEntity<Identity>(); 0070 identity.setAccount(account); 0071 identity.setAddress("attendee1@test.com"); 0072 identity.setName("John Doe"); 0073 0074 Sink::Store::create(identity).exec().waitForFinished(); 0075 0076 auto resource = DummyResource::create(account.identifier()); 0077 Sink::Store::create(resource).exec().waitForFinished(); 0078 resourceId = resource.identifier(); 0079 0080 auto mailtransport = MailtransportResource::create(account.identifier()); 0081 Sink::Store::create(mailtransport).exec().waitForFinished(); 0082 mailtransportResourceId = mailtransport.identifier(); 0083 } 0084 0085 void testAccept() 0086 { 0087 auto calendar = ApplicationDomainType::createEntity<Calendar>(resourceId); 0088 Sink::Store::create(calendar).exec().waitForFinished(); 0089 0090 const QByteArray uid{"uid1"}; 0091 const auto ical = createInvitation({uid, "summary", 0}); 0092 0093 { 0094 InvitationController controller; 0095 controller.loadICal(ical); 0096 0097 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0098 0099 QTRY_COMPARE(controller.getMethod(), InvitationController::Request); 0100 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0101 QTRY_COMPARE(controller.getEventState(), InvitationController::New); 0102 0103 controller.acceptAction()->execute(); 0104 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0105 QCOMPARE(controller.getState(), InvitationController::Accepted); 0106 0107 //Ensure the event is stored 0108 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0109 0110 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0111 QCOMPARE(list.size(), 1); 0112 0113 auto event = KCalendarCore::ICalFormat().readIncidence(list.first().getIcal()).dynamicCast<KCalendarCore::Event>(); 0114 QVERIFY(event); 0115 QCOMPARE(event->uid().toUtf8(), uid); 0116 QCOMPARE(event->organizer().fullName(), QLatin1String{"organizer@test.com"}); 0117 0118 const auto attendee = event->attendeeByMail("attendee1@test.com"); 0119 QCOMPARE(attendee.status(), KCalendarCore::Attendee::Accepted); 0120 0121 //Ensure the mail is sent to the organizer 0122 QTRY_COMPARE(Sink::Store::read<Mail>(Sink::Query{}.resourceFilter(mailtransportResourceId)).size(), 1); 0123 auto mail = Sink::Store::read<Mail>(Sink::Query{}.resourceFilter(mailtransportResourceId)).first(); 0124 auto msg = KMime::Message::Ptr(new KMime::Message); 0125 msg->setContent(mail.getMimeMessage()); 0126 msg->parse(); 0127 0128 QCOMPARE(msg->to()->asUnicodeString(), QLatin1String{"organizer@test.com"}); 0129 QCOMPARE(msg->from()->asUnicodeString(), QLatin1String{"attendee1@test.com"}); 0130 } 0131 0132 //Reload the event 0133 { 0134 InvitationController controller; 0135 controller.loadICal(ical); 0136 QTRY_COMPARE(controller.getState(), InvitationController::Accepted); 0137 QTRY_COMPARE(controller.getUid(), uid); 0138 } 0139 0140 const auto updatedIcal = createInvitation({uid, "summary2", 1}); 0141 //Load an update and accept it 0142 { 0143 InvitationController controller; 0144 controller.loadICal(updatedIcal); 0145 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0146 QTRY_COMPARE(controller.getUid(), uid); 0147 0148 //Accept the update 0149 controller.acceptAction()->execute(); 0150 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0151 0152 QCOMPARE(controller.getState(), InvitationController::Accepted); 0153 0154 //Ensure the event is stored 0155 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0156 0157 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0158 QCOMPARE(list.size(), 1); 0159 0160 auto event = KCalendarCore::ICalFormat().readIncidence(list.first().getIcal()).dynamicCast<KCalendarCore::Event>(); 0161 QVERIFY(event); 0162 QCOMPARE(event->uid().toUtf8(), uid); 0163 QCOMPARE(event->summary(), QLatin1String{"summary2"}); 0164 } 0165 0166 //Reload the event 0167 { 0168 InvitationController controller; 0169 controller.loadICal(updatedIcal); 0170 QTRY_COMPARE(controller.getState(), InvitationController::Accepted); 0171 QTRY_COMPARE(controller.getUid(), uid); 0172 QCOMPARE(controller.getEventState(), InvitationController::Existing); 0173 } 0174 } 0175 0176 void testAcceptRecurrenceException() 0177 { 0178 auto calendar = ApplicationDomainType::createEntity<Calendar>(resourceId); 0179 Sink::Store::create(calendar).exec().waitForFinished(); 0180 0181 const QByteArray uid{"uid2"}; 0182 auto dtstart = QDateTime{{2020, 1, 1}, {14, 0, 0}, Qt::UTC}; 0183 const auto ical = createInvitation({uid, "summary", 0, dtstart, true}); 0184 0185 { 0186 InvitationController controller; 0187 controller.loadICal(ical); 0188 0189 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0190 0191 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0192 QTRY_COMPARE(controller.getEventState(), InvitationController::New); 0193 0194 controller.acceptAction()->execute(); 0195 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0196 QCOMPARE(controller.getState(), InvitationController::Accepted); 0197 0198 //Ensure the event is stored 0199 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0200 0201 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0202 QCOMPARE(list.size(), 1); 0203 0204 auto event = KCalendarCore::ICalFormat().readIncidence(list.first().getIcal()).dynamicCast<KCalendarCore::Event>(); 0205 QVERIFY(event); 0206 QCOMPARE(event->uid().toUtf8(), uid); 0207 QCOMPARE(event->organizer().fullName(), QLatin1String{"organizer@test.com"}); 0208 } 0209 0210 //Reload the event 0211 { 0212 InvitationController controller; 0213 controller.loadICal(ical); 0214 QTRY_COMPARE(controller.getState(), InvitationController::Accepted); 0215 QTRY_COMPARE(controller.getUid(), uid); 0216 QVERIFY(!controller.getRecurrenceId().isValid()); 0217 } 0218 0219 //Load an exception and accept it 0220 { 0221 InvitationController controller; 0222 //TODO I suppose the revision of the exception can also be 0? 0223 controller.loadICal(createInvitation({uid, "exceptionSummary", 1, dtstart.addSecs(3600), false, dtstart})); 0224 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0225 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0226 QTRY_COMPARE(controller.getUid(), uid); 0227 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0228 QTRY_COMPARE(controller.getRecurrenceId(), dtstart); 0229 0230 //Accept the update 0231 controller.acceptAction()->execute(); 0232 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0233 0234 QCOMPARE(controller.getState(), InvitationController::Accepted); 0235 0236 //Ensure the event is stored 0237 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 2); 0238 0239 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0240 QCOMPARE(list.size(), 2); 0241 0242 for (const auto &entry : list) { 0243 auto event = KCalendarCore::ICalFormat().readIncidence(entry.getIcal()).dynamicCast<KCalendarCore::Event>(); 0244 QVERIFY(event); 0245 QCOMPARE(event->uid().toUtf8(), uid); 0246 if (event->recurrenceId().isValid()) { 0247 QCOMPARE(event->summary(), QLatin1String{"exceptionSummary"}); 0248 } else { 0249 QCOMPARE(event->summary(), QLatin1String{"summary"}); 0250 } 0251 } 0252 } 0253 0254 //Update the exception and accept it 0255 { 0256 InvitationController controller; 0257 controller.loadICal(createInvitation({uid, "exceptionSummary2", 3, dtstart.addSecs(3600), false, dtstart})); 0258 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0259 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0260 QTRY_COMPARE(controller.getUid(), uid); 0261 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0262 QTRY_COMPARE(controller.getRecurrenceId(), dtstart); 0263 0264 //Accept the update 0265 controller.acceptAction()->execute(); 0266 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0267 0268 QTRY_COMPARE(controller.getState(), InvitationController::Accepted); 0269 0270 //Ensure the event is stored 0271 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 2); 0272 0273 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0274 QCOMPARE(list.size(), 2); 0275 0276 for (const auto &entry : list) { 0277 auto event = KCalendarCore::ICalFormat().readIncidence(entry.getIcal()).dynamicCast<KCalendarCore::Event>(); 0278 QVERIFY(event); 0279 QCOMPARE(event->uid().toUtf8(), uid); 0280 if (event->recurrenceId().isValid()) { 0281 QCOMPARE(event->summary(), QLatin1String{"exceptionSummary2"}); 0282 } else { 0283 QCOMPARE(event->summary(), QLatin1String{"summary"}); 0284 } 0285 } 0286 } 0287 0288 //Update the main event and accept it 0289 { 0290 InvitationController controller; 0291 controller.loadICal(createInvitation({.uid = uid, .summary = "summary2", .revision = 4, .dtStart = dtstart, .recurring = true})); 0292 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0293 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0294 QTRY_COMPARE(controller.getUid(), uid); 0295 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0296 QVERIFY(!controller.getRecurrenceId().isValid()); 0297 0298 //Accept the update 0299 controller.acceptAction()->execute(); 0300 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0301 0302 QTRY_COMPARE(controller.getState(), InvitationController::Accepted); 0303 0304 //Ensure the event is stored 0305 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 2); 0306 0307 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0308 QCOMPARE(list.size(), 2); 0309 0310 for (const auto &entry : list) { 0311 auto event = KCalendarCore::ICalFormat().readIncidence(entry.getIcal()).dynamicCast<KCalendarCore::Event>(); 0312 QVERIFY(event); 0313 QCOMPARE(event->uid().toUtf8(), uid); 0314 if (event->recurrenceId().isValid()) { 0315 QCOMPARE(event->summary(), QLatin1String{"exceptionSummary2"}); 0316 } else { 0317 QCOMPARE(event->summary(), QLatin1String{"summary2"}); 0318 } 0319 } 0320 } 0321 0322 //Cancel an exception of the event 0323 { 0324 InvitationController controller; 0325 const auto recurrenceId = dtstart; 0326 controller.loadICal(createInvitation({.uid = uid, .summary = "exceptionSummary2", .revision = 5, .recurrenceId = recurrenceId, .cancelled = true})); 0327 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0328 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0329 QTRY_COMPARE(controller.getUid(), uid); 0330 //TODO should this still be unknown until we accept? 0331 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0332 QTRY_COMPARE(controller.getRecurrenceId(), recurrenceId); 0333 0334 //Accept the update 0335 controller.acceptAction()->execute(); 0336 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0337 0338 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0339 0340 //Ensure the event is stored 0341 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 2); 0342 0343 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0344 QCOMPARE(list.size(), 2); 0345 0346 for (const auto &entry : list) { 0347 auto event = KCalendarCore::ICalFormat().readIncidence(entry.getIcal()).dynamicCast<KCalendarCore::Event>(); 0348 QVERIFY(event); 0349 QCOMPARE(event->uid().toUtf8(), uid); 0350 if (event->recurrenceId().isValid()) { 0351 QCOMPARE(event->summary(), QLatin1String{"exceptionSummary2"}); 0352 QCOMPARE(event->status(), KCalendarCore::Incidence::StatusCanceled); 0353 } else { 0354 QCOMPARE(event->summary(), QLatin1String{"summary2"}); 0355 QCOMPARE(event->status(), KCalendarCore::Incidence::StatusNone); 0356 } 0357 } 0358 } 0359 0360 //Cancel the entire event 0361 { 0362 InvitationController controller; 0363 const auto recurrenceId = dtstart; 0364 controller.loadICal(createInvitation({.uid = uid, .summary = "summary2", .revision = 6, .cancelled = true})); 0365 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0366 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0367 QTRY_COMPARE(controller.getUid(), uid); 0368 //TODO should this still be unknown until we accept? 0369 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0370 QVERIFY(!controller.getRecurrenceId().isValid()); 0371 0372 //Accept the update 0373 controller.acceptAction()->execute(); 0374 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0375 0376 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0377 0378 //Ensure the event is stored 0379 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 2); 0380 0381 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0382 QCOMPARE(list.size(), 2); 0383 0384 for (const auto &entry : list) { 0385 auto event = KCalendarCore::ICalFormat().readIncidence(entry.getIcal()).dynamicCast<KCalendarCore::Event>(); 0386 QVERIFY(event); 0387 QCOMPARE(event->uid().toUtf8(), uid); 0388 if (event->recurrenceId().isValid()) { 0389 QCOMPARE(event->summary(), QLatin1String{"exceptionSummary2"}); 0390 QCOMPARE(event->status(), KCalendarCore::Incidence::StatusCanceled); 0391 } else { 0392 QCOMPARE(event->summary(), QLatin1String{"summary2"}); 0393 QCOMPARE(event->status(), KCalendarCore::Incidence::StatusCanceled); 0394 } 0395 } 0396 } 0397 0398 } 0399 0400 void testCancellation() 0401 { 0402 auto calendar = ApplicationDomainType::createEntity<Calendar>(resourceId); 0403 Sink::Store::create(calendar).exec().waitForFinished(); 0404 0405 const QByteArray uid{"uid3"}; 0406 0407 //Create event 0408 { 0409 InvitationController controller; 0410 const auto ical = createInvitation({uid, "summary", 0}); 0411 controller.loadICal(ical); 0412 QTRY_COMPARE(controller.getMethod(), InvitationController::Request); 0413 QTRY_COMPARE(controller.getEventState(), InvitationController::New); 0414 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0415 controller.acceptAction()->execute(); 0416 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0417 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0418 } 0419 0420 //Cancellation via status update, like roundcube does 0421 { 0422 InvitationController controller; 0423 const auto ical = createInvitation({.uid = uid, .summary = "summary", .revision = 1, .cancelled = true}); 0424 controller.loadICal(ical); 0425 0426 QTRY_COMPARE(controller.getMethod(), InvitationController::Cancel); 0427 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0428 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0429 } 0430 0431 //Cancellation per rfc 0432 { 0433 InvitationController controller; 0434 const auto ical = createInvitation({.uid = uid, .summary = "summary", .revision = 1, .method = KCalendarCore::iTIPCancel}); 0435 controller.loadICal(ical); 0436 0437 QTRY_COMPARE(controller.getMethod(), InvitationController::Cancel); 0438 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0439 QTRY_COMPARE(controller.getEventState(), InvitationController::Update); 0440 0441 controller.acceptAction()->execute(); 0442 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0443 QTRY_COMPARE(controller.getState(), InvitationController::Cancelled); 0444 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0445 } 0446 } 0447 0448 void testReply() 0449 { 0450 auto calendar = ApplicationDomainType::createEntity<Calendar>(resourceId); 0451 Sink::Store::create(calendar).exec().waitForFinished(); 0452 0453 const QByteArray uid{"uid1"}; 0454 const auto ical = createInvitation({.uid = uid, .summary = "summary", .method = KCalendarCore::iTIPReply}); 0455 0456 { 0457 InvitationController controller; 0458 controller.loadICal(ical); 0459 0460 controller.setCalendar(ApplicationDomainType::Ptr::create(calendar)); 0461 0462 QTRY_COMPARE(controller.getMethod(), InvitationController::Reply); 0463 QTRY_COMPARE(controller.getState(), InvitationController::Unknown); 0464 0465 controller.acceptAction()->execute(); 0466 Sink::ResourceControl::flushMessageQueue(resourceId).exec().waitForFinished(); 0467 QCOMPARE(controller.getState(), InvitationController::Accepted); 0468 0469 //Ensure the event is stored 0470 QCOMPARE(Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)).size(), 1); 0471 0472 auto list = Sink::Store::read<Event>(Sink::Query{}.filter<Event::Calendar>(calendar)); 0473 QCOMPARE(list.size(), 1); 0474 } 0475 } 0476 0477 }; 0478 0479 QTEST_MAIN(InvitationControllerTest) 0480 #include "invitationcontrollertest.moc"