File indexing completed on 2025-02-16 04:50:16

0001 /*
0002    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003    SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0004 
0005    SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "imaptestbase.h"
0009 
0010 #include "noinferiorsattribute.h"
0011 #include "noselectattribute.h"
0012 #include "retrievecollectionstask.h"
0013 
0014 #include <Akonadi/MessageParts>
0015 
0016 #include <Akonadi/CachePolicy>
0017 #include <Akonadi/EntityDisplayAttribute>
0018 
0019 #include <QTest>
0020 
0021 class TestRetrieveCollectionsTask : public ImapTestBase
0022 {
0023     Q_OBJECT
0024 public:
0025     TestRetrieveCollectionsTask(QObject *parent = nullptr)
0026         : ImapTestBase(parent)
0027         , m_nextCollectionId(1)
0028     {
0029     }
0030 
0031 private Q_SLOTS:
0032     void shouldListCollections_data()
0033     {
0034         QTest::addColumn<Akonadi::Collection::List>("expectedCollections");
0035         QTest::addColumn<QList<QByteArray>>("scenario");
0036         QTest::addColumn<QStringList>("callNames");
0037         QTest::addColumn<bool>("isSubscriptionEnabled");
0038         QTest::addColumn<bool>("isDisconnectedModeEnabled");
0039         QTest::addColumn<int>("intervalCheckTime");
0040         QTest::addColumn<char>("separator");
0041 
0042         Akonadi::Collection collection;
0043 
0044         Akonadi::Collection::List expectedCollections;
0045         QList<QByteArray> scenario;
0046         QStringList callNames;
0047         bool isSubscriptionEnabled;
0048         bool isDisconnectedModeEnabled;
0049         int intervalCheckTime;
0050 
0051         expectedCollections.clear();
0052         expectedCollections << createRootCollection() << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
0053                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
0054                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
0055                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
0056 
0057         scenario.clear();
0058         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0059                  << "S: * LIST ( \\HasChildren ) / INBOX"
0060                  << "S: * LIST ( \\HasChildren ) / INBOX/Calendar"
0061                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0062                  << "S: * LIST ( ) / INBOX/Archives"
0063                  << "S: A000003 OK list done";
0064 
0065         callNames.clear();
0066         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0067 
0068         isSubscriptionEnabled = false;
0069         isDisconnectedModeEnabled = false;
0070         intervalCheckTime = -1;
0071 
0072         QTest::newRow("first listing, connected IMAP")
0073             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0074 
0075         expectedCollections.clear();
0076         expectedCollections << createRootCollection(true, 5) << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
0077                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
0078                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
0079                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
0080 
0081         scenario.clear();
0082         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0083                  << "S: * LIST ( \\HasChildren ) / INBOX"
0084                  << "S: * LIST ( \\HasChildren ) / INBOX/Calendar"
0085                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0086                  << "S: * LIST ( ) / INBOX/Archives"
0087                  << "S: A000003 OK list done";
0088 
0089         callNames.clear();
0090         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0091 
0092         isSubscriptionEnabled = false;
0093         isDisconnectedModeEnabled = true;
0094         intervalCheckTime = 5;
0095 
0096         QTest::newRow("first listing, disconnected IMAP")
0097             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0098 
0099         expectedCollections.clear();
0100         expectedCollections << createRootCollection(true, 5) << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
0101                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
0102 
0103         scenario.clear();
0104         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0105                  << "S: * LIST ( \\HasChildren ) / INBOX"
0106                  << "S: * LIST ( \\HasChildren ) / INBOX/"
0107                  << "S: * LIST ( \\HasChildren ) / INBOX/Archives"
0108                  << "S: A000003 OK list done";
0109 
0110         callNames.clear();
0111         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0112 
0113         isSubscriptionEnabled = false;
0114         isDisconnectedModeEnabled = true;
0115         intervalCheckTime = 5;
0116 
0117         QTest::newRow("first listing, spurious INBOX/ (BR: 25342)")
0118             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0119 
0120         expectedCollections.clear();
0121         expectedCollections << createRootCollection() << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
0122                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"), true)
0123                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
0124                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
0125 
0126         scenario.clear();
0127         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0128                  << "S: * LIST ( \\HasChildren ) / INBOX"
0129                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0130                  << "S: * LIST ( ) / INBOX/Archives"
0131                  << "S: A000003 OK list done";
0132 
0133         callNames.clear();
0134         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0135 
0136         isSubscriptionEnabled = false;
0137         isDisconnectedModeEnabled = false;
0138         intervalCheckTime = -1;
0139 
0140         QTest::newRow("auto-insert missing nodes in the tree")
0141             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0142 
0143         scenario.clear();
0144         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0145                  << "S: * LIST ( ) / INBOX/Archives"
0146                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0147                  << "S: * LIST ( \\HasChildren ) / INBOX"
0148                  << "S: A000003 OK list done";
0149 
0150         callNames.clear();
0151         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0152 
0153         isSubscriptionEnabled = false;
0154         isDisconnectedModeEnabled = false;
0155         intervalCheckTime = -1;
0156 
0157         QTest::newRow("auto-insert missing nodes in the tree (reverse order)")
0158             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0159 
0160         expectedCollections.clear();
0161         expectedCollections << createRootCollection() << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
0162                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
0163                             << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"));
0164 
0165         scenario.clear();
0166         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0167                  << "S: * LIST ( ) / INBOX/Unsubscribed"
0168                  << "S: * LIST ( ) / INBOX/Calendar"
0169                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0170                  << "S: * LIST ( \\HasChildren ) / INBOX"
0171                  << "S: A000003 OK list done"
0172                  << "C: A000004 LSUB \"\" *"
0173                  << "S: * LSUB ( \\HasChildren ) / INBOX"
0174                  << "S: * LSUB ( ) / INBOX/SubscribedButNotExisting"
0175                  << "S: * LSUB ( ) / INBOX/Calendar"
0176                  << "S: * LSUB ( ) / INBOX/Calendar/Private"
0177                  << "S: A000004 OK list done";
0178 
0179         callNames.clear();
0180         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0181 
0182         isSubscriptionEnabled = true;
0183         isDisconnectedModeEnabled = false;
0184         intervalCheckTime = -1;
0185 
0186         QTest::newRow("subscription enabled") << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled
0187                                               << intervalCheckTime << '/';
0188         scenario.clear();
0189         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0190                  << "S: * LIST ( ) / INBOX/Unsubscribed"
0191                  << "S: * LIST ( ) / INBOX/Calendar"
0192                  << "S: * LIST ( ) / INBOX/Calendar/Private"
0193                  << "S: * LIST ( \\HasChildren ) / INBOX"
0194                  << "S: A000003 OK list done"
0195                  << "C: A000004 LSUB \"\" *"
0196                  << "S: * LSUB ( \\HasChildren ) / Inbox"
0197                  << "S: * LSUB ( ) / Inbox/SubscribedButNotExisting"
0198                  << "S: * LSUB ( ) / Inbox/Calendar"
0199                  << "S: * LSUB ( ) / Inbox/Calendar/Private"
0200                  << "S: A000004 OK list done";
0201 
0202         callNames.clear();
0203         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0204 
0205         isSubscriptionEnabled = true;
0206         isDisconnectedModeEnabled = false;
0207         intervalCheckTime = -1;
0208 
0209         QTest::newRow("subscription enabled, case insensitive inbox")
0210             << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime << '/';
0211 
0212         expectedCollections.clear();
0213         expectedCollections << createRootCollection() << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"), false, true)
0214                             << createCollection(QStringLiteral("/"), QStringLiteral("Archive"));
0215 
0216         scenario.clear();
0217         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0218                  << "S: * LIST ( \\Noinferiors ) / INBOX"
0219                  << "S: * LIST ( ) / Archive"
0220                  << "S: A000003 OK list done";
0221 
0222         callNames.clear();
0223         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0224 
0225         isSubscriptionEnabled = false;
0226         isDisconnectedModeEnabled = false;
0227         intervalCheckTime = -1;
0228 
0229         QTest::newRow("Noinferiors") << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
0230                                      << '/';
0231 
0232         scenario.clear();
0233         scenario << defaultPoolConnectionScenario() << "C: A000003 LIST \"\" *"
0234                  << "S: * LIST ( ) . INBOX"
0235                  << "S: * LIST ( ) . INBOX.Foo"
0236                  << "S: * LIST ( ) . INBOX.Bar"
0237                  << "S: A000003 OK list done";
0238         callNames.clear();
0239         callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
0240 
0241         expectedCollections.clear();
0242         expectedCollections << createRootCollection() << createCollection(QStringLiteral("."), QStringLiteral("INBOX"))
0243                             << createCollection(QStringLiteral("."), QStringLiteral("INBOX.Foo"))
0244                             << createCollection(QStringLiteral("."), QStringLiteral("INBOX.Bar"));
0245         isSubscriptionEnabled = false;
0246         isDisconnectedModeEnabled = false;
0247         intervalCheckTime = -1;
0248 
0249         QTest::newRow("non-standard separators") << expectedCollections << scenario << callNames << isSubscriptionEnabled << isDisconnectedModeEnabled
0250                                                  << intervalCheckTime << '.';
0251     }
0252 
0253     void shouldListCollections()
0254     {
0255         QFETCH(Akonadi::Collection::List, expectedCollections);
0256         QFETCH(QList<QByteArray>, scenario);
0257         QFETCH(QStringList, callNames);
0258         QFETCH(bool, isSubscriptionEnabled);
0259         QFETCH(bool, isDisconnectedModeEnabled);
0260         QFETCH(int, intervalCheckTime);
0261         QFETCH(char, separator);
0262 
0263         FakeServer server;
0264         server.setScenario(scenario);
0265         server.startAndWait();
0266 
0267         SessionPool pool(1);
0268 
0269         pool.setPasswordRequester(createDefaultRequester());
0270         QVERIFY(pool.connect(createDefaultAccount()));
0271         QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int, QString))));
0272 
0273         DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
0274         state->setResourceName(QStringLiteral("resource"));
0275         state->setSubscriptionEnabled(isSubscriptionEnabled);
0276         state->setDisconnectedModeEnabled(isDisconnectedModeEnabled);
0277         state->setIntervalCheckTime(intervalCheckTime);
0278 
0279         auto task = new RetrieveCollectionsTask(state);
0280         task->start(&pool);
0281 
0282         Akonadi::Collection::List collections;
0283 
0284         QTRY_COMPARE(state->calls().count(), callNames.size());
0285         for (int i = 0; i < callNames.size(); i++) {
0286             QString command = QString::fromUtf8(state->calls().at(i).first);
0287             QVariant parameter = state->calls().at(i).second;
0288 
0289             if (command == QLatin1StringView("cancelTask") && callNames[i] != QLatin1StringView("cancelTask")) {
0290                 qDebug() << "Got a cancel:" << parameter.toString();
0291             }
0292 
0293             QCOMPARE(command, callNames[i]);
0294 
0295             if (command == QLatin1StringView("cancelTask")) {
0296                 QVERIFY(!parameter.toString().isEmpty());
0297             } else if (command == QLatin1StringView("collectionsRetrieved")) {
0298                 collections += parameter.value<Akonadi::Collection::List>();
0299             }
0300         }
0301 
0302         QCOMPARE(state->separatorCharacter(), QChar::fromLatin1(separator));
0303 
0304         QVERIFY(server.isAllScenarioDone());
0305         compareCollectionLists(collections, expectedCollections);
0306 
0307         server.quit();
0308     }
0309 
0310 private:
0311     qint64 m_nextCollectionId;
0312 
0313     Akonadi::Collection createRootCollection(bool isDisconnectedImap = false, int intervalCheck = -1)
0314     {
0315         // Root
0316         Akonadi::Collection collection = Akonadi::Collection(m_nextCollectionId++);
0317         collection.setName(QStringLiteral("resource"));
0318         collection.setRemoteId(QStringLiteral("root-id"));
0319         collection.setContentMimeTypes(QStringList(Akonadi::Collection::mimeType()));
0320         collection.setParentCollection(Akonadi::Collection::root());
0321         collection.addAttribute(new NoSelectAttribute(true));
0322         collection.setRights(Akonadi::Collection::CanCreateCollection);
0323 
0324         Akonadi::CachePolicy policy;
0325         policy.setInheritFromParent(false);
0326         policy.setSyncOnDemand(true);
0327 
0328         if (isDisconnectedImap) {
0329             policy.setLocalParts(QStringList() << QLatin1StringView(Akonadi::MessagePart::Envelope) << QLatin1StringView(Akonadi::MessagePart::Header)
0330                                                << QLatin1StringView(Akonadi::MessagePart::Body));
0331             policy.setCacheTimeout(-1);
0332         } else {
0333             policy.setLocalParts(QStringList() << QLatin1StringView(Akonadi::MessagePart::Envelope) << QLatin1StringView(Akonadi::MessagePart::Header));
0334             policy.setCacheTimeout(60);
0335         }
0336 
0337         policy.setIntervalCheckTime(intervalCheck);
0338 
0339         collection.setCachePolicy(policy);
0340 
0341         return collection;
0342     }
0343 
0344     Akonadi::Collection createCollection(const QString &separator, const QString &path, bool isNoSelect = false, bool isNoInferiors = false)
0345     {
0346         // No path? That's the root of this resource then
0347         if (path.isEmpty()) {
0348             return createRootCollection();
0349         }
0350 
0351         QStringList pathParts = path.split(separator);
0352 
0353         const QString pathPart = pathParts.takeLast();
0354         const QString parentPath = pathParts.join(separator);
0355 
0356         // Here we should likely reuse already produced collections if possible to be 100% accurate
0357         // but in the tests we check only a limited amount of properties (namely remote id and name).
0358         const Akonadi::Collection parentCollection = createCollection(separator, parentPath);
0359 
0360         Akonadi::Collection collection(m_nextCollectionId++);
0361         collection.setName(pathPart);
0362         collection.setRemoteId(separator + pathPart);
0363 
0364         collection.setParentCollection(parentCollection);
0365         collection.setContentMimeTypes(QStringList() << QStringLiteral("message/rfc822") << Akonadi::Collection::mimeType());
0366 
0367         // If the folder is the Inbox, make some special settings.
0368         if (pathPart.compare(QLatin1StringView("INBOX"), Qt::CaseInsensitive) == 0) {
0369             auto attr = new Akonadi::EntityDisplayAttribute;
0370             attr->setDisplayName(QStringLiteral("Inbox"));
0371             attr->setIconName(QStringLiteral("mail-folder-inbox"));
0372             collection.addAttribute(attr);
0373         }
0374 
0375         // If the folder is the user top-level folder, mark it as well, even although it is not officially noted in the RFC
0376         if ((pathPart.compare(QLatin1StringView("user"), Qt::CaseInsensitive) == 0) && isNoSelect) {
0377             auto attr = new Akonadi::EntityDisplayAttribute;
0378             attr->setDisplayName(QStringLiteral("Shared Folders"));
0379             attr->setIconName(QStringLiteral("x-mail-distribution-list"));
0380             collection.addAttribute(attr);
0381         }
0382 
0383         // If this folder is a noselect folder, make some special settings.
0384         if (isNoSelect) {
0385             collection.addAttribute(new NoSelectAttribute(true));
0386             collection.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
0387             collection.setRights(Akonadi::Collection::ReadOnly);
0388         }
0389 
0390         if (isNoInferiors) {
0391             collection.addAttribute(new NoInferiorsAttribute(true));
0392             collection.setRights(collection.rights() & ~Akonadi::Collection::CanCreateCollection);
0393         }
0394 
0395         return collection;
0396     }
0397 
0398     void compareCollectionLists(const Akonadi::Collection::List &resultList, const Akonadi::Collection::List &expectedList)
0399     {
0400         for (int i = 0; i < expectedList.size(); i++) {
0401             Akonadi::Collection expected = expectedList[i];
0402             bool found = false;
0403 
0404             for (int j = 0; j < resultList.size(); j++) {
0405                 Akonadi::Collection result = resultList[j];
0406 
0407                 if (result.remoteId() == expected.remoteId()) {
0408                     found = true;
0409 
0410                     QVERIFY(!result.name().isEmpty());
0411 
0412                     QCOMPARE(result.name(), expected.name());
0413                     QCOMPARE(result.contentMimeTypes(), expected.contentMimeTypes());
0414                     QCOMPARE(result.rights(), expected.rights());
0415                     if (expected.parentCollection() == Akonadi::Collection::root()) {
0416                         QCOMPARE(result.parentCollection(), expected.parentCollection());
0417                     } else {
0418                         QCOMPARE(result.parentCollection().remoteId(), expected.parentCollection().remoteId());
0419                     }
0420 
0421                     QCOMPARE(result.cachePolicy().inheritFromParent(), expected.cachePolicy().inheritFromParent());
0422                     QCOMPARE(result.cachePolicy().syncOnDemand(), expected.cachePolicy().syncOnDemand());
0423                     QCOMPARE(result.cachePolicy().localParts(), expected.cachePolicy().localParts());
0424                     QCOMPARE(result.cachePolicy().cacheTimeout(), expected.cachePolicy().cacheTimeout());
0425                     QCOMPARE(result.cachePolicy().intervalCheckTime(), expected.cachePolicy().intervalCheckTime());
0426 
0427                     QCOMPARE(result.hasAttribute<NoSelectAttribute>(), expected.hasAttribute<NoSelectAttribute>());
0428                     QCOMPARE(result.hasAttribute<Akonadi::EntityDisplayAttribute>(), expected.hasAttribute<Akonadi::EntityDisplayAttribute>());
0429 
0430                     break;
0431                 }
0432             }
0433 
0434             QVERIFY2(found, QString::fromLatin1("%1 not found!").arg(expected.remoteId()).toUtf8().constData());
0435         }
0436 
0437         QCOMPARE(resultList.size(), expectedList.size());
0438     }
0439 };
0440 
0441 QTEST_GUILESS_MAIN(TestRetrieveCollectionsTask)
0442 
0443 #include "testretrievecollectionstask.moc"