diff --git a/autotests/libs/tagtest.cpp b/autotests/libs/tagtest.cpp index 7f3bd3728..ef1ea15f3 100644 --- a/autotests/libs/tagtest.cpp +++ b/autotests/libs/tagtest.cpp @@ -1,839 +1,895 @@ /* Copyright (c) 2014 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "test_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; class TagTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testTag(); void testCreateFetch(); void testRID(); void testDelete(); void testDeleteRIDIsolation(); void testModify(); void testModifyFromResource(); void testCreateMerge(); void testAttributes(); void testTagItem(); void testCreateItem(); void testRIDIsolation(); void testFetchTagIdWithItem(); void testFetchFullTagWithItem(); void testModifyItemWithTagByGID(); void testModifyItemWithTagByRID(); void testMonitor(); + void testTagAttributeConfusionBug(); void testFetchItemsByTag(); }; void TagTest::initTestCase() { AkonadiTest::checkTestIsIsolated(); AkonadiTest::setAllResourcesOffline(); AttributeFactory::registerAttribute(); qRegisterMetaType(); qRegisterMetaType >(); qRegisterMetaType(); // Delete the default Knut tag - it's interfering with this test TagFetchJob *fetchJob = new TagFetchJob(this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); AKVERIFYEXEC(deleteJob); } void TagTest::testTag() { Tag tag1; Tag tag2; // Invalid tags are equal QVERIFY(tag1 == tag2); // Invalid tags with different GIDs are not equal tag1.setGid("GID1"); QVERIFY(tag1 != tag2); tag2.setGid("GID2"); QVERIFY(tag1 != tag2); // Invalid tags with equal GIDs are equal tag1.setGid("GID2"); QVERIFY(tag1 == tag2); // Valid tags with different IDs are not equal tag1 = Tag(1); tag2 = Tag(2); QVERIFY(tag1 != tag2); // Valid tags with different IDs and equal GIDs are still not equal tag1.setGid("GID1"); tag2.setGid("GID1"); QVERIFY(tag1 != tag2); // Valid tags with equal ID are equal regardless of GIDs tag2 = Tag(1); tag2.setGid("GID2"); QVERIFY(tag1 == tag2); } void TagTest::testCreateFetch() { Tag tag; tag.setGid("gid"); tag.setType("mytype"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); { TagFetchJob *fetchJob = new TagFetchJob(this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); AKVERIFYEXEC(deleteJob); } { TagFetchJob *fetchJob = new TagFetchJob(this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 0); } } void TagTest::testRID() { { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); AKVERIFYEXEC(select); } Tag tag; tag.setGid("gid"); tag.setType("mytype"); tag.setRemoteId("rid"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); { TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().setFetchRemoteId(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid")); TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); AKVERIFYEXEC(deleteJob); } { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); AKVERIFYEXEC(select); } } void TagTest::testRIDIsolation() { { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); AKVERIFYEXEC(select); } Tag tag; tag.setGid("gid"); tag.setType("mytype"); tag.setRemoteId("rid_0"); TagCreateJob *createJob = new TagCreateJob(tag, this); AKVERIFYEXEC(createJob); QVERIFY(createJob->tag().isValid()); qint64 tagId; { TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().setFetchRemoteId(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().count(), 1); QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_0")); tagId = fetchJob->tags().first().id(); } { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); AKVERIFYEXEC(select); } tag.setRemoteId("rid_1"); createJob = new TagCreateJob(tag, this); createJob->setMergeIfExisting(true); AKVERIFYEXEC(createJob); QVERIFY(createJob->tag().isValid()); { TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().setFetchRemoteId(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().count(), 1); QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_1")); QCOMPARE(fetchJob->tags().first().id(), tagId); } TagDeleteJob *deleteJob = new TagDeleteJob(Tag(tagId), this); AKVERIFYEXEC(deleteJob); { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); AKVERIFYEXEC(select); } } void TagTest::testDelete() { Akonadi::Monitor monitor; monitor.setTypeMonitored(Monitor::Tags); QSignalSpy spy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); Tag tag1; { tag1.setGid("tag1"); TagCreateJob *createjob = new TagCreateJob(tag1, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag1 = createjob->tag(); } Tag tag2; { tag2.setGid("tag2"); TagCreateJob *createjob = new TagCreateJob(tag2, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag2 = createjob->tag(); } { TagDeleteJob *deleteJob = new TagDeleteJob(tag1, this); AKVERIFYEXEC(deleteJob); } { TagFetchJob *fetchJob = new TagFetchJob(this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QCOMPARE(fetchJob->tags().first().gid(), tag2.gid()); } { TagDeleteJob *deleteJob = new TagDeleteJob(tag2, this); AKVERIFYEXEC(deleteJob); } // Collect Remove notification, so that they don't interfere with testDeleteRIDIsolation QTRY_VERIFY(!spy.isEmpty()); } void TagTest::testDeleteRIDIsolation() { Tag tag; tag.setGid("gid"); tag.setType("mytype"); tag.setRemoteId("rid_0"); { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); AKVERIFYEXEC(select); TagCreateJob *createJob = new TagCreateJob(tag, this); AKVERIFYEXEC(createJob); QVERIFY(createJob->tag().isValid()); tag.setId(createJob->tag().id()); } tag.setRemoteId("rid_1"); { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); AKVERIFYEXEC(select); TagCreateJob *createJob = new TagCreateJob(tag, this); createJob->setMergeIfExisting(true); AKVERIFYEXEC(createJob); QVERIFY(createJob->tag().isValid()); } Akonadi::Monitor monitor; monitor.setTypeMonitored(Akonadi::Monitor::Tags); QSignalSpy signalSpy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); // Other tests notifications might interfere due to notification compression on server QTRY_VERIFY(signalSpy.count() >= 1); Tag removedTag; while (!signalSpy.isEmpty()) { const Tag t = signalSpy.takeFirst().takeFirst().value(); if (t.id() == tag.id()) { removedTag = t; break; } } QVERIFY(removedTag.isValid()); QVERIFY(removedTag.remoteId().isEmpty()); { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral(""), this); AKVERIFYEXEC(select); } } void TagTest::testModify() { Tag tag; { tag.setGid("gid"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag = createjob->tag(); } //We can add an attribute { Akonadi::TagAttribute *attr = tag.attribute(Tag::AddIfMissing); attr->setDisplayName(QStringLiteral("display name")); tag.addAttribute(attr); tag.setParent(Tag(0)); tag.setType("mytype"); TagModifyJob *modJob = new TagModifyJob(tag, this); AKVERIFYEXEC(modJob); TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().fetchAttribute(); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QVERIFY(fetchJob->tags().first().hasAttribute()); } //We can update an attribute { Akonadi::TagAttribute *attr = tag.attribute(Tag::AddIfMissing); attr->setDisplayName(QStringLiteral("display name2")); TagModifyJob *modJob = new TagModifyJob(tag, this); AKVERIFYEXEC(modJob); TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().fetchAttribute(); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QVERIFY(fetchJob->tags().first().hasAttribute()); QCOMPARE(fetchJob->tags().first().attribute()->displayName(), attr->displayName()); } //We can clear an attribute { tag.removeAttribute(); TagModifyJob *modJob = new TagModifyJob(tag, this); AKVERIFYEXEC(modJob); TagFetchJob *fetchJob = new TagFetchJob(this); fetchJob->fetchScope().fetchAttribute(); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QVERIFY(!fetchJob->tags().first().hasAttribute()); } TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testModifyFromResource() { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); AKVERIFYEXEC(select); Tag tag; { tag.setGid("gid"); tag.setRemoteId("rid"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag = createjob->tag(); } { tag.setRemoteId(QByteArray("")); TagModifyJob *modJob = new TagModifyJob(tag, this); AKVERIFYEXEC(modJob); // The tag is removed on the server, because we just removed the last // RemoteID TagFetchJob *fetchJob = new TagFetchJob(this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 0); } } void TagTest::testCreateMerge() { Tag tag; { tag.setGid("gid"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag = createjob->tag(); } { Tag tag2; tag2.setGid("gid"); TagCreateJob *createjob = new TagCreateJob(tag2, this); createjob->setMergeIfExisting(true); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); QCOMPARE(createjob->tag().id(), tag.id()); } TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testAttributes() { Tag tag; { tag.setGid("gid2"); TagAttribute *attr = tag.attribute(Tag::AddIfMissing); attr->setDisplayName(QStringLiteral("name")); attr->setInToolbar(true); tag.addAttribute(attr); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag = createjob->tag(); { TagFetchJob *fetchJob = new TagFetchJob(createjob->tag(), this); fetchJob->fetchScope().fetchAttribute(); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 1); QVERIFY(fetchJob->tags().first().hasAttribute()); //we need to clone because the returned attribute is just a reference and destroyed on the next line //FIXME we should find a better solution for this (like returning a smart pointer or value object) QScopedPointer tagAttr(fetchJob->tags().first().attribute()->clone()); QVERIFY(tagAttr); QCOMPARE(tagAttr->displayName(), QStringLiteral("name")); QCOMPARE(tagAttr->inToolbar(), true); } } //Try fetching multiple items Tag tag2; { tag2.setGid("gid22"); TagAttribute *attr = tag.attribute(Tag::AddIfMissing)->clone(); attr->setDisplayName(QStringLiteral("name2")); attr->setInToolbar(true); tag2.addAttribute(attr); TagCreateJob *createjob = new TagCreateJob(tag2, this); AKVERIFYEXEC(createjob); QVERIFY(createjob->tag().isValid()); tag2 = createjob->tag(); { TagFetchJob *fetchJob = new TagFetchJob(Tag::List() << tag << tag2, this); fetchJob->fetchScope().fetchAttribute(); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->tags().size(), 2); QVERIFY(fetchJob->tags().at(0).hasAttribute()); QVERIFY(fetchJob->tags().at(1).hasAttribute()); } } TagDeleteJob *deleteJob = new TagDeleteJob(Tag::List() << tag << tag2, this); AKVERIFYEXEC(deleteJob); } void TagTest::testTagItem() { Akonadi::Monitor monitor; monitor.itemFetchScope().setFetchTags(true); monitor.setAllMonitored(true); const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag; { TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); AKVERIFYEXEC(createjob); tag = createjob->tag(); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); } item1.setTag(tag); QSignalSpy tagsSpy(&monitor, SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))); QVERIFY(tagsSpy.isValid()); ItemModifyJob *modJob = new ItemModifyJob(item1, this); AKVERIFYEXEC(modJob); QTRY_VERIFY(tagsSpy.count() >= 1); QTRY_COMPARE(tagsSpy.last().first().value().first().id(), item1.id()); QTRY_COMPARE(tagsSpy.last().at(1).value< QSet >().size(), 1); //1 added tag ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testCreateItem() { // Akonadi::Monitor monitor; // monitor.itemFetchScope().setFetchTags(true); // monitor.setAllMonitored(true); const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag; { TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); AKVERIFYEXEC(createjob); tag = createjob->tag(); } // QSignalSpy tagsSpy(&monitor, SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet,QSet))); // QVERIFY(tagsSpy.isValid()); Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); item1.setTag(tag); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); } // QTRY_VERIFY(tagsSpy.count() >= 1); // QTest::qWait(10); // kDebug() << tagsSpy.count(); // QTRY_COMPARE(tagsSpy.last().first().value().first().id(), item1.id()); // QTRY_COMPARE(tagsSpy.last().at(1).value< QSet >().size(), 1); //1 added tag ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testFetchTagIdWithItem() { const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag; { TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); AKVERIFYEXEC(createjob); tag = createjob->tag(); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); item1.setTag(tag); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); } ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); Tag t = fetchJob->items().first().tags().first(); QCOMPARE(t.id(), tag.id()); QVERIFY(t.gid().isEmpty()); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testFetchFullTagWithItem() { const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag; { TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); AKVERIFYEXEC(createjob); tag = createjob->tag(); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); //FIXME This should also be possible with create, but isn't item1.setTag(tag); } ItemModifyJob *modJob = new ItemModifyJob(item1, this); AKVERIFYEXEC(modJob); ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(false); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); Tag t = fetchJob->items().first().tags().first(); QCOMPARE(t, tag); QVERIFY(!t.gid().isEmpty()); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } void TagTest::testModifyItemWithTagByGID() { const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); { Tag tag; tag.setGid("gid2"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); } Tag tag; tag.setGid("gid2"); item1.setTag(tag); ItemModifyJob *modJob = new ItemModifyJob(item1, this); AKVERIFYEXEC(modJob); ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); AKVERIFYEXEC(deleteJob); } void TagTest::testModifyItemWithTagByRID() { { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); AKVERIFYEXEC(select); } const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag3; { tag3.setGid("gid3"); tag3.setRemoteId("rid3"); TagCreateJob *createjob = new TagCreateJob(tag3, this); AKVERIFYEXEC(createjob); tag3 = createjob->tag(); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); } Tag tag; tag.setRemoteId("rid2"); item1.setTag(tag); ItemModifyJob *modJob = new ItemModifyJob(item1, this); AKVERIFYEXEC(modJob); ItemFetchJob *fetchJob = new ItemFetchJob(item1, this); fetchJob->fetchScope().setFetchTags(true); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().first().tags().size(), 1); { TagDeleteJob *deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); AKVERIFYEXEC(deleteJob); } { TagDeleteJob *deleteJob = new TagDeleteJob(tag3, this); AKVERIFYEXEC(deleteJob); } { ResourceSelectJob *select = new ResourceSelectJob(QStringLiteral("")); AKVERIFYEXEC(select); } } void TagTest::testMonitor() { Akonadi::Monitor monitor; monitor.setTypeMonitored(Akonadi::Monitor::Tags); monitor.tagFetchScope().fetchAttribute(); QTest::qWait(10); // give Monitor time to upload settings Akonadi::Tag createdTag; { QSignalSpy addedSpy(&monitor, SIGNAL(tagAdded(Akonadi::Tag))); QVERIFY(addedSpy.isValid()); Tag tag; tag.setGid("gid2"); tag.setName(QStringLiteral("name2")); tag.setType("type2"); TagCreateJob *createjob = new TagCreateJob(tag, this); AKVERIFYEXEC(createjob); createdTag = createjob->tag(); QCOMPARE(createdTag.type(), tag.type()); QCOMPARE(createdTag.name(), tag.name()); QCOMPARE(createdTag.gid(), tag.gid()); //We usually pick up signals from the previous tests as well (due to server-side notification caching) QTRY_VERIFY(addedSpy.count() >= 1); QTRY_COMPARE(addedSpy.last().first().value().id(), createdTag.id()); const Akonadi::Tag notifiedTag = addedSpy.last().first().value(); QCOMPARE(notifiedTag.type(), createdTag.type()); QCOMPARE(notifiedTag.gid(), createdTag.gid()); QVERIFY(notifiedTag.hasAttribute()); QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute } { QSignalSpy modifiedSpy(&monitor, SIGNAL(tagChanged(Akonadi::Tag))); QVERIFY(modifiedSpy.isValid()); createdTag.setName(QStringLiteral("name3")); TagModifyJob *modJob = new TagModifyJob(createdTag, this); AKVERIFYEXEC(modJob); //We usually pick up signals from the previous tests as well (due to server-side notification caching) QTRY_VERIFY(modifiedSpy.count() >= 1); QTRY_COMPARE(modifiedSpy.last().first().value().id(), createdTag.id()); const Akonadi::Tag notifiedTag = modifiedSpy.last().first().value(); QCOMPARE(notifiedTag.type(), createdTag.type()); QCOMPARE(notifiedTag.gid(), createdTag.gid()); QVERIFY(notifiedTag.hasAttribute()); QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute } { QSignalSpy removedSpy(&monitor, SIGNAL(tagRemoved(Akonadi::Tag))); QVERIFY(removedSpy.isValid()); TagDeleteJob *deletejob = new TagDeleteJob(createdTag, this); AKVERIFYEXEC(deletejob); QTRY_VERIFY(removedSpy.count() >= 1); QTRY_COMPARE(removedSpy.last().first().value().id(), createdTag.id()); const Akonadi::Tag notifiedTag = removedSpy.last().first().value(); QCOMPARE(notifiedTag.type(), createdTag.type()); QCOMPARE(notifiedTag.gid(), createdTag.gid()); QVERIFY(notifiedTag.hasAttribute()); QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute } } +void TagTest::testTagAttributeConfusionBug() +{ + // Create two tags + Tag firstTag; + { + firstTag.setGid("gid"); + firstTag.setName(QStringLiteral("display name")); + TagCreateJob *createjob = new TagCreateJob(firstTag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + firstTag = createjob->tag(); + } + Tag secondTag; + { + secondTag.setGid("AnotherGID"); + secondTag.setName(QStringLiteral("another name")); + TagCreateJob *createjob = new TagCreateJob(secondTag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + secondTag = createjob->tag(); + } + + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Tags); + + const QList firstTagIdList{ firstTag.id() }; + + // Modify attribute on the first tag + // and check the notification + { + QSignalSpy modifiedSpy(&monitor, &Akonadi::Monitor::tagChanged); + + firstTag.setName(QStringLiteral("renamed")); + TagModifyJob *modJob = new TagModifyJob(firstTag, this); + AKVERIFYEXEC(modJob); + + TagFetchJob *fetchJob = new TagFetchJob(firstTagIdList, this); + QVERIFY(fetchJob->fetchScope().fetchAllAttributes()); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().name(), firstTag.name()); + + QTRY_VERIFY(modifiedSpy.count() >= 1); + QTRY_COMPARE(modifiedSpy.last().first().value().id(), firstTag.id()); + const Akonadi::Tag notifiedTag = modifiedSpy.last().first().value(); + QCOMPARE(notifiedTag.name(), firstTag.name()); + } + + // Cleanup + TagDeleteJob *deleteJob = new TagDeleteJob(firstTag, this); + AKVERIFYEXEC(deleteJob); + TagDeleteJob *anotherDeleteJob = new TagDeleteJob(secondTag, this); + AKVERIFYEXEC(anotherDeleteJob); +} + void TagTest::testFetchItemsByTag() { const Collection res3 = Collection(collectionIdFromPath(QStringLiteral("res3"))); Tag tag; { TagCreateJob *createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); AKVERIFYEXEC(createjob); tag = createjob->tag(); } Item item1; { item1.setMimeType(QStringLiteral("application/octet-stream")); ItemCreateJob *append = new ItemCreateJob(item1, res3, this); AKVERIFYEXEC(append); item1 = append->item(); //FIXME This should also be possible with create, but isn't item1.setTag(tag); } ItemModifyJob *modJob = new ItemModifyJob(item1, this); AKVERIFYEXEC(modJob); ItemFetchJob *fetchJob = new ItemFetchJob(tag, this); AKVERIFYEXEC(fetchJob); QCOMPARE(fetchJob->items().size(), 1); Item i = fetchJob->items().first(); QCOMPARE(i, item1); TagDeleteJob *deleteJob = new TagDeleteJob(tag, this); AKVERIFYEXEC(deleteJob); } #include "tagtest.moc" QTEST_AKONADIMAIN(TagTest) diff --git a/src/server/handlerhelper.cpp b/src/server/handlerhelper.cpp index e3c3e4dc6..6610fba61 100644 --- a/src/server/handlerhelper.cpp +++ b/src/server/handlerhelper.cpp @@ -1,442 +1,443 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "handlerhelper.h" #include "storage/countquerybuilder.h" #include "storage/datastore.h" #include "storage/selectquerybuilder.h" #include "storage/collectionstatistics.h" #include "storage/queryhelper.h" #include "storage/collectionqueryhelper.h" #include "commandcontext.h" #include "handler.h" #include "connection.h" #include "utils.h" #include #include #include using namespace Akonadi; using namespace Akonadi::Server; Collection HandlerHelper::collectionFromIdOrName(const QByteArray &id) { // id is a number bool ok = false; qint64 collectionId = id.toLongLong(&ok); if (ok) { return Collection::retrieveById(collectionId); } // id is a path QString path = QString::fromUtf8(id); // ### should be UTF-7 for real IMAP compatibility const QStringList pathParts = path.split(QLatin1Char('/'), QString::SkipEmptyParts); Collection col; for (const QString &part : pathParts) { SelectQueryBuilder qb; qb.addValueCondition(Collection::nameColumn(), Query::Equals, part); if (col.isValid()) { qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, col.id()); } else { qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); } if (!qb.exec()) { return Collection(); } Collection::List list = qb.result(); if (list.count() != 1) { return Collection(); } col = list.first(); } return col; } QString HandlerHelper::pathForCollection(const Collection &col) { QStringList parts; Collection current = col; while (current.isValid()) { parts.prepend(current.name()); current = current.parent(); } return parts.join(QLatin1Char('/')); } Protocol::CachePolicy HandlerHelper::cachePolicyResponse(const Collection &col) { Protocol::CachePolicy cachePolicy; cachePolicy.setInherit(col.cachePolicyInherit()); cachePolicy.setCacheTimeout(col.cachePolicyCacheTimeout()); cachePolicy.setCheckInterval(col.cachePolicyCheckInterval()); if (!col.cachePolicyLocalParts().isEmpty()) { cachePolicy.setLocalParts(col.cachePolicyLocalParts().split(QLatin1Char(' '))); } cachePolicy.setSyncOnDemand(col.cachePolicySyncOnDemand()); return cachePolicy; } Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(const Collection &col) { QStringList mimeTypes; mimeTypes.reserve(col.mimeTypes().size()); Q_FOREACH (const MimeType &mt, col.mimeTypes()) { mimeTypes << mt.name(); } return fetchCollectionsResponse(col, col.attributes(), false, 0, QStack(), QStack(), false, mimeTypes); } Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(const Collection &col, const CollectionAttribute::List &attrs, bool includeStatistics, int ancestorDepth, const QStack &ancestors, const QStack &ancestorAttributes, bool isReferenced, const QStringList &mimeTypes) { Protocol::FetchCollectionsResponse response; response.setId(col.id()); response.setParentId(col.parentId()); response.setName(col.name()); response.setMimeTypes(mimeTypes); response.setRemoteId(col.remoteId()); response.setRemoteRevision(col.remoteRevision()); response.setResource(col.resource().name()); response.setIsVirtual(col.isVirtual()); if (includeStatistics) { const CollectionStatistics::Statistics stats = CollectionStatistics::self()->statistics(col); if (stats.count > -1) { Protocol::FetchCollectionStatsResponse statsResponse; statsResponse.setCount(stats.count); statsResponse.setUnseen(stats.count - stats.read); statsResponse.setSize(stats.size); response.setStatistics(statsResponse); } } if (!col.queryString().isEmpty()) { response.setSearchQuery(col.queryString()); QVector searchCols; const QStringList searchColIds = col.queryCollections().split(QLatin1Char(' ')); searchCols.reserve(searchColIds.size()); for (const QString &searchColId : searchColIds) { searchCols << searchColId.toLongLong(); } response.setSearchCollections(searchCols); } Protocol::CachePolicy cachePolicy = cachePolicyResponse(col); response.setCachePolicy(cachePolicy); if (ancestorDepth) { QVector ancestorList = HandlerHelper::ancestorsResponse(ancestorDepth, ancestors, ancestorAttributes); response.setAncestors(ancestorList); } response.setReferenced(isReferenced); response.setEnabled(col.enabled()); response.setDisplayPref(static_cast(col.displayPref())); response.setSyncPref(static_cast(col.syncPref())); response.setIndexPref(static_cast(col.indexPref())); QMap ra; for (const CollectionAttribute &attr : attrs) { ra.insert(attr.type(), attr.value()); } response.setAttributes(ra); return response; } QVector HandlerHelper::ancestorsResponse(int ancestorDepth, const QStack &_ancestors, const QStack &_ancestorsAttributes) { QVector rv; if (ancestorDepth > 0) { QStack ancestors(_ancestors); QStack ancestorAttributes(_ancestorsAttributes); for (int i = 0; i < ancestorDepth; ++i) { if (ancestors.isEmpty()) { Protocol::Ancestor ancestor; ancestor.setId(0); rv << ancestor; break; } const Collection c = ancestors.pop(); Protocol::Ancestor a; a.setId(c.id()); a.setRemoteId(c.remoteId()); a.setName(c.name()); if (!ancestorAttributes.isEmpty()) { QMap attrs; Q_FOREACH (const CollectionAttribute &attr, ancestorAttributes.pop()) { attrs.insert(attr.type(), attr.value()); } a.setAttributes(attrs); } rv << a; } } return rv; } Protocol::FetchTagsResponse HandlerHelper::fetchTagsResponse(const Tag &tag, const Protocol::TagFetchScope &tagFetchScope, Connection *connection) { Protocol::FetchTagsResponse response; response.setId(tag.id()); qCDebug(AKONADISERVER_LOG) << "TAGFETCH IDONLY" << tagFetchScope.fetchIdOnly(); if (tagFetchScope.fetchIdOnly()) { return response; } response.setType(tag.tagType().name().toUtf8()); response.setParentId(tag.parentId()); response.setGid(tag.gid().toUtf8()); qCDebug(AKONADISERVER_LOG) << "TAGFETCH" << tagFetchScope.fetchRemoteID() << connection; if (tagFetchScope.fetchRemoteID() && connection) { qCDebug(AKONADISERVER_LOG) << connection->context()->resource().name(); // Fail silently if retrieving tag RID is not allowed in current context if (connection->context()->resource().isValid()) { QueryBuilder qb(TagRemoteIdResourceRelation::tableName()); qb.addColumn(TagRemoteIdResourceRelation::remoteIdColumn()); qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, connection->context()->resource().id()); qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tag.id()); if (!qb.exec()) { throw HandlerException("Unable to query Tag Remote ID"); } QSqlQuery query = qb.query(); // RID may not be available if (query.next()) { response.setRemoteId(Utils::variantToByteArray(query.value(0))); } } } if (tagFetchScope.fetchAllAttributes() || !tagFetchScope.attributes().isEmpty()) { QueryBuilder qb(TagAttribute::tableName()); qb.addColumns({ TagAttribute::typeFullColumnName(), TagAttribute::valueFullColumnName() }); Query::Condition cond(Query::And); cond.addValueCondition(TagAttribute::tagIdFullColumnName(), Query::Equals, tag.id()); if (!tagFetchScope.fetchAllAttributes() && !tagFetchScope.attributes().isEmpty()) { QVariantList types; const auto scope = tagFetchScope.attributes(); std::transform(scope.cbegin(), scope.cend(), std::back_inserter(types), [](const QByteArray &ba) { return QVariant(ba); }); cond.addValueCondition(TagAttribute::typeFullColumnName(), Query::In, types); } + qb.addCondition(cond); if (!qb.exec()) { throw HandlerException("Unable to query Tag Attributes"); } QSqlQuery query = qb.query(); Protocol::Attributes attributes; while (query.next()) { attributes.insert(Utils::variantToByteArray(query.value(0)), Utils::variantToByteArray(query.value(1))); } response.setAttributes(attributes); } return response; } Protocol::FetchRelationsResponse HandlerHelper::fetchRelationsResponse(const Relation &relation) { Protocol::FetchRelationsResponse resp; resp.setLeft(relation.leftId()); resp.setLeftMimeType(relation.left().mimeType().name().toUtf8()); resp.setRight(relation.rightId()); resp.setRightMimeType(relation.right().mimeType().name().toUtf8()); resp.setType(relation.relationType().name().toUtf8()); return resp; } Flag::List HandlerHelper::resolveFlags(const QSet &flagNames) { Flag::List flagList; flagList.reserve(flagNames.size()); for (const QByteArray &flagName : flagNames) { const Flag flag = Flag::retrieveByNameOrCreate(QString::fromUtf8(flagName)); if (!flag.isValid()) { throw HandlerException("Unable to create flag"); } flagList.append(flag); } return flagList; } Tag::List HandlerHelper::resolveTagsByUID(const ImapSet &tags) { if (tags.isEmpty()) { return Tag::List(); } SelectQueryBuilder qb; QueryHelper::setToQuery(tags, Tag::idFullColumnName(), qb); if (!qb.exec()) { throw HandlerException("Unable to resolve tags"); } const Tag::List result = qb.result(); if (result.isEmpty()) { throw HandlerException("No tags found"); } return result; } Tag::List HandlerHelper::resolveTagsByGID(const QStringList &tagsGIDs) { Tag::List tagList; if (tagsGIDs.isEmpty()) { return tagList; } for (const QString &tagGID : tagsGIDs) { Tag::List tags = Tag::retrieveFiltered(Tag::gidColumn(), tagGID); Tag tag; if (tags.isEmpty()) { tag.setGid(tagGID); tag.setParentId(0); const TagType type = TagType::retrieveByNameOrCreate(QStringLiteral("PLAIN")); if (!type.isValid()) { throw HandlerException("Unable to create tag type"); } tag.setTagType(type); if (!tag.insert()) { throw HandlerException("Unable to create tag"); } } else if (tags.count() == 1) { tag = tags[0]; } else { // Should not happen throw HandlerException("Tag GID is not unique"); } tagList.append(tag); } return tagList; } Tag::List HandlerHelper::resolveTagsByRID(const QStringList &tagsRIDs, CommandContext *context) { Tag::List tags; if (tagsRIDs.isEmpty()) { return tags; } if (!context->resource().isValid()) { throw HandlerException("Tags can be resolved by their RID only in resource context"); } tags.reserve(tagsRIDs.size()); for (const QString &tagRID : tagsRIDs) { SelectQueryBuilder qb; Query::Condition cond; cond.addColumnCondition(Tag::idFullColumnName(), Query::Equals, TagRemoteIdResourceRelation::tagIdFullColumnName()); cond.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context->resource().id()); qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), cond); qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::Equals, tagRID); if (!qb.exec()) { throw HandlerException("Unable to resolve tags"); } Tag tag; Tag::List results = qb.result(); if (results.isEmpty()) { // If the tag does not exist, we create a new one with GID matching RID Tag::List tags = resolveTagsByGID(QStringList() << tagRID); if (tags.count() != 1) { throw HandlerException("Unable to resolve tag"); } tag = tags[0]; TagRemoteIdResourceRelation rel; rel.setRemoteId(tagRID); rel.setTagId(tag.id()); rel.setResourceId(context->resource().id()); if (!rel.insert()) { throw HandlerException("Unable to create tag"); } } else if (results.count() == 1) { tag = results[0]; } else { throw HandlerException("Tag RID is not unique within this resource context"); } tags.append(tag); } return tags; } Collection HandlerHelper::collectionFromScope(const Scope &scope, Connection *connection) { if (scope.scope() == Scope::Invalid || scope.scope() == Scope::Gid) { throw HandlerException("Invalid collection scope"); } SelectQueryBuilder qb; CollectionQueryHelper::scopeToQuery(scope, connection, qb); if (!qb.exec()) { throw HandlerException("Failed to execute SQL query"); } const Collection::List c = qb.result(); if (c.isEmpty()) { return Collection(); } else if (c.count() == 1) { return c.at(0); } else { throw HandlerException("Query returned more than one reslut"); } } Tag::List HandlerHelper::tagsFromScope(const Scope &scope, Connection *connection) { if (scope.scope() == Scope::Invalid || scope.scope() == Scope::HierarchicalRid) { throw HandlerException("Invalid tag scope"); } if (scope.scope() == Scope::Uid) { return resolveTagsByUID(scope.uidSet()); } else if (scope.scope() == Scope::Gid) { return resolveTagsByGID(scope.gidSet()); } else if (scope.scope() == Scope::Rid) { return resolveTagsByRID(scope.ridSet(), connection->context()); } Q_ASSERT(false); return Tag::List(); }