File indexing completed on 2024-11-10 04:40:13

0001 /*
0002   SPDX-FileCopyrightText: 2006 Volker Krause <vkrause@kde.org>
0003   SPDX-FileCopyrightText: 2007 Robert Zwerus <arzie@dds.nl>
0004 
0005   SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "itemstoretest.h"
0009 
0010 #include "agentinstance.h"
0011 #include "agentmanager.h"
0012 #include "attributefactory.h"
0013 #include "collectionfetchjob.h"
0014 #include "config_p.h"
0015 #include "control.h"
0016 #include "itemcreatejob.h"
0017 #include "itemdeletejob.h"
0018 #include "itemfetchjob.h"
0019 #include "itemfetchscope.h"
0020 #include "itemmodifyjob.h"
0021 #include "itemmodifyjob_p.h"
0022 #include "qtest_akonadi.h"
0023 #include "resourceselectjob_p.h"
0024 #include "testattribute.h"
0025 
0026 using namespace Akonadi;
0027 
0028 QTEST_AKONADIMAIN(ItemStoreTest)
0029 
0030 static Collection res1_foo;
0031 static Collection res2;
0032 static Collection res3;
0033 
0034 void ItemStoreTest::initTestCase()
0035 {
0036     // The Item size tests expect the payload not to be compressed.
0037     QVERIFY(!Config::get().payloadCompression.enabled);
0038 
0039     AkonadiTest::checkTestIsIsolated();
0040     Control::start();
0041     AttributeFactory::registerAttribute<TestAttribute>();
0042 
0043     // get the collections we run the tests on
0044     res1_foo = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo")));
0045     QVERIFY(res1_foo.isValid());
0046     res2 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2")));
0047     QVERIFY(res2.isValid());
0048     res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3")));
0049     QVERIFY(res3.isValid());
0050 
0051     AkonadiTest::setAllResourcesOffline();
0052 }
0053 
0054 void ItemStoreTest::testFlagChange()
0055 {
0056     auto fjob = new ItemFetchJob(Item(1));
0057     AKVERIFYEXEC(fjob);
0058     QCOMPARE(fjob->items().count(), 1);
0059     Item item = fjob->items()[0];
0060 
0061     // add a flag
0062     Item::Flags origFlags = item.flags();
0063     Item::Flags expectedFlags = origFlags;
0064     expectedFlags.insert("added_test_flag_1");
0065     item.setFlag("added_test_flag_1");
0066     auto sjob = new ItemModifyJob(item, this);
0067     AKVERIFYEXEC(sjob);
0068 
0069     fjob = new ItemFetchJob(Item(1));
0070     AKVERIFYEXEC(fjob);
0071     QCOMPARE(fjob->items().count(), 1);
0072     item = fjob->items()[0];
0073     QCOMPARE(item.flags().count(), expectedFlags.count());
0074     Item::Flags diff = expectedFlags - item.flags();
0075     QVERIFY(diff.isEmpty());
0076 
0077     // set flags
0078     expectedFlags.insert("added_test_flag_2");
0079     item.setFlags(expectedFlags);
0080     sjob = new ItemModifyJob(item, this);
0081     AKVERIFYEXEC(sjob);
0082 
0083     fjob = new ItemFetchJob(Item(1));
0084     AKVERIFYEXEC(fjob);
0085     QCOMPARE(fjob->items().count(), 1);
0086     item = fjob->items()[0];
0087     QCOMPARE(item.flags().count(), expectedFlags.count());
0088     diff = expectedFlags - item.flags();
0089     QVERIFY(diff.isEmpty());
0090 
0091     // remove a flag
0092     item.clearFlag("added_test_flag_1");
0093     item.clearFlag("added_test_flag_2");
0094     sjob = new ItemModifyJob(item, this);
0095     AKVERIFYEXEC(sjob);
0096 
0097     fjob = new ItemFetchJob(Item(1));
0098     AKVERIFYEXEC(fjob);
0099     QCOMPARE(fjob->items().count(), 1);
0100     item = fjob->items()[0];
0101     QCOMPARE(item.flags().count(), origFlags.count());
0102     diff = origFlags - item.flags();
0103     QVERIFY(diff.isEmpty());
0104 }
0105 
0106 void ItemStoreTest::testDataChange_data()
0107 {
0108     QTest::addColumn<QByteArray>("data");
0109     QTest::addColumn<qint64>("expectedSize");
0110 
0111     QTest::newRow("simple") << QByteArray("testbody") << 8LL;
0112     QTest::newRow("null") << QByteArray() << 0LL;
0113     QTest::newRow("empty") << QByteArray("") << 0LL;
0114     QTest::newRow("nullbyte") << QByteArray("\0", 1) << 1LL;
0115     QTest::newRow("nullbyte2") << QByteArray("\0X", 2) << 2LL;
0116     QTest::newRow("linebreaks") << QByteArray("line1\nline2\n\rline3\rline4\r\n") << 26LL;
0117     QTest::newRow("linebreaks2") << QByteArray("line1\r\nline2\r\n\r\n") << 16LL;
0118     QTest::newRow("linebreaks3") << QByteArray("line1\nline2") << 11LL;
0119     QByteArray b;
0120     QTest::newRow("big") << b.fill('a', 1 << 20) << (1LL << 20);
0121     QTest::newRow("bignull") << b.fill('\0', 1 << 20) << (1LL << 20);
0122     QTest::newRow("bigcr") << b.fill('\r', 1 << 20) << (1LL << 20);
0123     QTest::newRow("biglf") << b.fill('\n', 1 << 20) << (1LL << 20);
0124 }
0125 
0126 void ItemStoreTest::testDataChange()
0127 {
0128     QFETCH(QByteArray, data);
0129     QFETCH(qint64, expectedSize);
0130 
0131     Item item;
0132     auto prefetchjob = new ItemFetchJob(Item(1));
0133     AKVERIFYEXEC(prefetchjob);
0134     item = prefetchjob->items()[0];
0135     item.setMimeType(QStringLiteral("application/octet-stream"));
0136     item.setPayload(data);
0137     QCOMPARE(item.payload<QByteArray>(), data);
0138 
0139     // modify data
0140     auto sjob = new ItemModifyJob(item);
0141     AKVERIFYEXEC(sjob);
0142 
0143     auto fjob = new ItemFetchJob(Item(1));
0144     fjob->fetchScope().fetchFullPayload();
0145     fjob->fetchScope().setCacheOnly(true);
0146     AKVERIFYEXEC(fjob);
0147     QCOMPARE(fjob->items().count(), 1);
0148     item = fjob->items()[0];
0149     QVERIFY(item.hasPayload<QByteArray>());
0150     QCOMPARE(item.payload<QByteArray>(), data);
0151     QEXPECT_FAIL("null", "STORE will not update item size on 0 sizes", Continue);
0152     QEXPECT_FAIL("empty", "STORE will not update item size on 0 sizes", Continue);
0153     // Cannot compare with data.size() due to payload compression
0154     QCOMPARE(item.size(), expectedSize);
0155 }
0156 
0157 void ItemStoreTest::testRemoteId_data()
0158 {
0159     QTest::addColumn<QString>("rid");
0160     QTest::addColumn<QString>("exprid");
0161 
0162     QTest::newRow("set") << QStringLiteral("A") << QStringLiteral("A");
0163     QTest::newRow("no-change") << QString() << QStringLiteral("A");
0164     QTest::newRow("clear") << QStringLiteral("") << QStringLiteral("");
0165     QTest::newRow("reset") << QStringLiteral("A") << QStringLiteral("A");
0166     QTest::newRow("utf8") << QStringLiteral("ä ö ü @") << QStringLiteral("ä ö ü @");
0167 }
0168 
0169 void ItemStoreTest::testRemoteId()
0170 {
0171     QFETCH(QString, rid);
0172     QFETCH(QString, exprid);
0173 
0174     // pretend to be a resource, we cannot change remote identifiers otherwise
0175     auto rsel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this);
0176     AKVERIFYEXEC(rsel);
0177 
0178     auto prefetchjob = new ItemFetchJob(Item(1));
0179     AKVERIFYEXEC(prefetchjob);
0180     Item item = prefetchjob->items()[0];
0181 
0182     item.setRemoteId(rid);
0183     auto store = new ItemModifyJob(item, this);
0184     store->disableRevisionCheck();
0185     store->setIgnorePayload(true); // we only want to update the remote id
0186     AKVERIFYEXEC(store);
0187 
0188     auto fetch = new ItemFetchJob(item, this);
0189     AKVERIFYEXEC(fetch);
0190     QCOMPARE(fetch->items().count(), 1);
0191     item = fetch->items().at(0);
0192     QEXPECT_FAIL("clear", "Clearing RID by clients is currently forbidden to avoid conflicts.", Continue);
0193     QCOMPARE(item.remoteId().toUtf8(), exprid.toUtf8());
0194 
0195     // no longer pretend to be a resource
0196     rsel = new ResourceSelectJob(QString(), this);
0197     AKVERIFYEXEC(rsel);
0198 }
0199 
0200 void ItemStoreTest::testMultiPart()
0201 {
0202     auto prefetchjob = new ItemFetchJob(Item(1));
0203     AKVERIFYEXEC(prefetchjob);
0204     QCOMPARE(prefetchjob->items().count(), 1);
0205     Item item = prefetchjob->items()[0];
0206     item.setMimeType(QStringLiteral("application/octet-stream"));
0207     item.setPayload<QByteArray>("testmailbody");
0208     item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra";
0209 
0210     // store item
0211     auto sjob = new ItemModifyJob(item);
0212     AKVERIFYEXEC(sjob);
0213 
0214     auto fjob = new ItemFetchJob(Item(1));
0215     fjob->fetchScope().fetchAttribute<TestAttribute>();
0216     fjob->fetchScope().fetchFullPayload();
0217     AKVERIFYEXEC(fjob);
0218     QCOMPARE(fjob->items().count(), 1);
0219     item = fjob->items()[0];
0220     QVERIFY(item.hasPayload<QByteArray>());
0221     QCOMPARE(item.payload<QByteArray>(), QByteArray("testmailbody"));
0222     QVERIFY(item.hasAttribute<TestAttribute>());
0223     QCOMPARE(item.attribute<TestAttribute>()->data, QByteArray("extra"));
0224 
0225     // clean up
0226     item.removeAttribute("EXTRA");
0227     sjob = new ItemModifyJob(item);
0228     AKVERIFYEXEC(sjob);
0229 }
0230 
0231 void ItemStoreTest::testPartRemove()
0232 {
0233     auto prefetchjob = new ItemFetchJob(Item(2));
0234     AKVERIFYEXEC(prefetchjob);
0235     Item item = prefetchjob->items()[0];
0236     item.setMimeType(QStringLiteral("application/octet-stream"));
0237     item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra";
0238 
0239     // store item
0240     auto sjob = new ItemModifyJob(item);
0241     AKVERIFYEXEC(sjob);
0242 
0243     // fetch item and its parts (should be RFC822, HEAD and EXTRA)
0244     auto fjob = new ItemFetchJob(Item(2));
0245     fjob->fetchScope().fetchFullPayload();
0246     fjob->fetchScope().fetchAllAttributes();
0247     fjob->fetchScope().setCacheOnly(true);
0248     AKVERIFYEXEC(fjob);
0249     QCOMPARE(fjob->items().count(), 1);
0250     item = fjob->items()[0];
0251     QCOMPARE(item.attributes().count(), 2);
0252     QVERIFY(item.hasAttribute<TestAttribute>());
0253 
0254     // remove a part
0255     item.removeAttribute<TestAttribute>();
0256     sjob = new ItemModifyJob(item);
0257     AKVERIFYEXEC(sjob);
0258 
0259     // fetch item again (should only have RFC822 and HEAD left)
0260     auto fjob2 = new ItemFetchJob(Item(2));
0261     fjob2->fetchScope().fetchFullPayload();
0262     fjob2->fetchScope().fetchAllAttributes();
0263     fjob2->fetchScope().setCacheOnly(true);
0264     AKVERIFYEXEC(fjob2);
0265     QCOMPARE(fjob2->items().count(), 1);
0266     item = fjob2->items()[0];
0267     QCOMPARE(item.attributes().count(), 1);
0268     QVERIFY(!item.hasAttribute<TestAttribute>());
0269 }
0270 
0271 void ItemStoreTest::testRevisionCheck()
0272 {
0273     // fetch same item twice
0274     Item ref(2);
0275     auto prefetchjob = new ItemFetchJob(ref);
0276     AKVERIFYEXEC(prefetchjob);
0277     QCOMPARE(prefetchjob->items().count(), 1);
0278     Item item1 = prefetchjob->items()[0];
0279     Item item2 = prefetchjob->items()[0];
0280 
0281     // store first item unmodified
0282     auto sjob = new ItemModifyJob(item1);
0283     AKVERIFYEXEC(sjob);
0284 
0285     // store the first item with modifications (should work)
0286     item1.attribute<TestAttribute>(Item::AddIfMissing)->data = "random stuff 1";
0287     sjob = new ItemModifyJob(item1, this);
0288     AKVERIFYEXEC(sjob);
0289 
0290     // try to store second item with modifications (should be detected as a conflict)
0291     item2.attribute<TestAttribute>(Item::AddIfMissing)->data = "random stuff 2";
0292     auto sjob2 = new ItemModifyJob(item2);
0293     sjob2->disableAutomaticConflictHandling();
0294     QVERIFY(!sjob2->exec());
0295 
0296     // fetch same again
0297     prefetchjob = new ItemFetchJob(ref);
0298     AKVERIFYEXEC(prefetchjob);
0299     item1 = prefetchjob->items()[0];
0300 
0301     // delete item
0302     auto djob = new ItemDeleteJob(ref, this);
0303     AKVERIFYEXEC(djob);
0304 
0305     // try to store it
0306     sjob = new ItemModifyJob(item1);
0307     QVERIFY(!sjob->exec());
0308 }
0309 
0310 void ItemStoreTest::testModificationTime()
0311 {
0312     Item item;
0313     item.setMimeType(QStringLiteral("text/directory"));
0314     QVERIFY(item.modificationTime().isNull());
0315 
0316     auto job = new ItemCreateJob(item, res1_foo);
0317     AKVERIFYEXEC(job);
0318 
0319     // The item should have a datetime set now.
0320     item = job->item();
0321     QVERIFY(!item.modificationTime().isNull());
0322     QDateTime initialDateTime = item.modificationTime();
0323 
0324     // Fetch the same item again.
0325     Item item2(item.id());
0326     auto fjob = new ItemFetchJob(item2, this);
0327     AKVERIFYEXEC(fjob);
0328     item2 = fjob->items().first();
0329     QCOMPARE(initialDateTime, item2.modificationTime());
0330 
0331     // Lets wait at least a second, which is the resolution of mtime
0332     QTest::qWait(1000);
0333 
0334     // Modify the item
0335     item.attribute<TestAttribute>(Item::AddIfMissing)->data = "extra";
0336     auto mjob = new ItemModifyJob(item);
0337     AKVERIFYEXEC(mjob);
0338 
0339     // The item should still have a datetime set and that date should be somewhere
0340     // after the initialDateTime.
0341     item = mjob->item();
0342     QVERIFY(!item.modificationTime().isNull());
0343     QVERIFY(initialDateTime < item.modificationTime());
0344 
0345     // Fetch the item after modification.
0346     Item item3(item.id());
0347     auto fjob2 = new ItemFetchJob(item3, this);
0348     AKVERIFYEXEC(fjob2);
0349 
0350     // item3 should have the same modification time as item.
0351     item3 = fjob2->items().first();
0352     QCOMPARE(item3.modificationTime(), item.modificationTime());
0353 
0354     // Clean up
0355     auto idjob = new ItemDeleteJob(item, this);
0356     AKVERIFYEXEC(idjob);
0357 }
0358 
0359 void ItemStoreTest::testRemoteIdRace()
0360 {
0361     // Create an item and store it
0362     Item item;
0363     item.setMimeType(QStringLiteral("text/directory"));
0364     auto job = new ItemCreateJob(item, res1_foo);
0365     AKVERIFYEXEC(job);
0366 
0367     // Fetch the same item again. It should not have a remote Id yet, as the resource
0368     // is offline.
0369     // The remote id should be null, not only empty, so that item modify jobs with this
0370     // item don't overwrite the remote id.
0371     Item item2(job->item().id());
0372     auto fetchJob = new ItemFetchJob(item2);
0373     AKVERIFYEXEC(fetchJob);
0374     QCOMPARE(fetchJob->items().size(), 1);
0375     QVERIFY(fetchJob->items().first().remoteId().isEmpty());
0376 }
0377 
0378 void ItemStoreTest::itemModifyJobShouldOnlySendModifiedAttributes()
0379 {
0380     // Given an item with an attribute (created on the server)
0381     Item item;
0382     item.setMimeType(QStringLiteral("text/directory"));
0383     item.attribute<TestAttribute>(Item::AddIfMissing)->data = "initial";
0384     auto job = new ItemCreateJob(item, res1_foo);
0385     AKVERIFYEXEC(job);
0386     item = job->item();
0387     QCOMPARE(item.attributes().count(), 1);
0388 
0389     // When one job modifies this attribute, and another one does an unrelated change
0390     Item item1(item.id());
0391     item1.attribute<TestAttribute>(Item::AddIfMissing)->data = "modified";
0392     auto mjob = new ItemModifyJob(item1);
0393     mjob->disableRevisionCheck();
0394     AKVERIFYEXEC(mjob);
0395 
0396     item.setFlag("added_test_flag_1");
0397     // this job shouldn't send the old attribute again
0398     auto mjob2 = new ItemModifyJob(item);
0399     mjob2->disableRevisionCheck();
0400     AKVERIFYEXEC(mjob2);
0401 
0402     // Then the item has the new value for the attribute (the other one didn't send the old attribute value)
0403     {
0404         auto fetchJob = new ItemFetchJob(Item(item.id()));
0405         ItemFetchScope fetchScope;
0406         fetchScope.fetchAllAttributes(true);
0407         fetchJob->setFetchScope(fetchScope);
0408         AKVERIFYEXEC(fetchJob);
0409         QCOMPARE(fetchJob->items().size(), 1);
0410         const Item fetchedItem = fetchJob->items().first();
0411         QCOMPARE(fetchedItem.flags().count(), 1);
0412         QCOMPARE(fetchedItem.attributes().count(), 1);
0413         QCOMPARE(fetchedItem.attribute<TestAttribute>()->data, "modified");
0414     }
0415 }
0416 
0417 class ParallelJobsRunner
0418 {
0419 public:
0420     explicit ParallelJobsRunner(int count)
0421         : numSessions(count)
0422     {
0423         sessions.reserve(numSessions);
0424         modifyJobs.reserve(numSessions);
0425         for (int i = 0; i < numSessions; ++i) {
0426             auto session = new Session(QByteArray::number(i));
0427             sessions.push_back(session);
0428         }
0429     }
0430 
0431     ~ParallelJobsRunner()
0432     {
0433         qDeleteAll(sessions);
0434     }
0435 
0436     void addJob(ItemModifyJob *mjob)
0437     {
0438         modifyJobs.push_back(mjob);
0439         QObject::connect(mjob, &KJob::result, mjob, [mjob, this]() {
0440             if (mjob->error()) {
0441                 errors.append(mjob->errorString());
0442             }
0443             doneJobs.push_back(mjob);
0444         });
0445     }
0446 
0447     void waitForAllJobs()
0448     {
0449         for (int i = 0; i < modifyJobs.count(); ++i) {
0450             ItemModifyJob *mjob = modifyJobs.at(i);
0451             if (!doneJobs.contains(mjob)) {
0452                 QSignalSpy spy(mjob, &ItemModifyJob::result);
0453                 QVERIFY(spy.wait());
0454                 if (mjob->error()) {
0455                     qWarning() << mjob->errorString();
0456                 }
0457                 QCOMPARE(mjob->error(), KJob::NoError);
0458             }
0459         }
0460         QVERIFY2(errors.isEmpty(), qPrintable(errors.join(QLatin1StringView("; "))));
0461     }
0462 
0463     const int numSessions;
0464     std::vector<Session *> sessions;
0465     QList<ItemModifyJob *> modifyJobs, doneJobs;
0466     QStringList errors;
0467 };
0468 
0469 void ItemStoreTest::testParallelJobsAddingAttributes()
0470 {
0471     // Given an item (created on the server)
0472     Item::Id itemId;
0473     {
0474         Item item;
0475         item.setMimeType(QStringLiteral("text/directory"));
0476         auto job = new ItemCreateJob(item, res1_foo);
0477         AKVERIFYEXEC(job);
0478         itemId = job->item().id();
0479         QVERIFY(itemId >= 0);
0480     }
0481 
0482     // When adding N attributes from N different sessions (e.g. threads or processes)
0483     ParallelJobsRunner runner(10);
0484     for (int i = 0; i < runner.numSessions; ++i) {
0485         Item item(itemId);
0486         Attribute *attr = AttributeFactory::createAttribute("type" + QByteArray::number(i));
0487         QVERIFY(attr);
0488         attr->deserialize("attr" + QByteArray::number(i));
0489         item.addAttribute(attr);
0490         auto mjob = new ItemModifyJob(item, runner.sessions.at(i));
0491         runner.addJob(mjob);
0492     }
0493     runner.waitForAllJobs();
0494 
0495     // Then the item should have all attributes
0496     auto fetchJob = new ItemFetchJob(Item(itemId));
0497     ItemFetchScope fetchScope;
0498     fetchScope.fetchAllAttributes(true);
0499     fetchJob->setFetchScope(fetchScope);
0500     AKVERIFYEXEC(fetchJob);
0501     QCOMPARE(fetchJob->items().size(), 1);
0502     const Item fetchedItem = fetchJob->items().first();
0503     QCOMPARE(fetchedItem.attributes().count(), runner.numSessions);
0504 }
0505 
0506 #include "moc_itemstoretest.cpp"