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"