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"