diff --git a/agents/migration/autotests/schedulertest.cpp b/agents/migration/autotests/schedulertest.cpp index 3438df3e0..30de7c3f5 100644 --- a/agents/migration/autotests/schedulertest.cpp +++ b/agents/migration/autotests/schedulertest.cpp @@ -1,295 +1,295 @@ /* * Copyright 2013 Christian Mollekopf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License * along with this program. If not, see . * */ #include #include #include #include #include "../migrationscheduler.h" #include Q_DECLARE_METATYPE(QModelIndex) class Testmigrator : public MigratorBase { Q_OBJECT public: explicit Testmigrator(const QString &identifier, QObject *parent = nullptr) : MigratorBase(QLatin1String("testmigrator") + identifier, QString(), QString(), parent) , mAutostart(false) { } QString displayName() const override { return QStringLiteral("name"); } void startWork() override { } void abort() override { setMigrationState(Aborted); } virtual void complete() { setMigrationState(Complete); } bool shouldAutostart() const override { return mAutostart; } void pause() override { setMigrationState(Paused); } void resume() override { setMigrationState(InProgress); } bool mAutostart; }; class TestJobTracker : public KJobTrackerInterface { public: TestJobTracker() : mPercent(0) { } void registerJob(KJob *job) override { KJobTrackerInterface::registerJob(job); mJobs << job; } void unregisterJob(KJob *job) override { mJobs.removeAll(job); } void finished(KJob *job) override { mJobs.removeAll(job); } void percent(KJob *job, long unsigned int percent) override { Q_UNUSED(job); mPercent = percent; } QList mJobs; int mPercent; }; class SchedulerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestcase() { qRegisterMetaType(); } void testInsertRow() { MigrationScheduler scheduler; QAbstractItemModel &model(scheduler.model()); QCOMPARE(model.rowCount(), 0); - QSignalSpy rowsInsertedSpy(&model, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy rowsInsertedSpy(&model, &QAbstractItemModel::rowsInserted); QVERIFY(rowsInsertedSpy.isValid()); scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id")))); QCOMPARE(model.rowCount(), 1); QCOMPARE(rowsInsertedSpy.count(), 1); QVERIFY(model.index(0, 0).isValid()); QVERIFY(!model.index(1, 0).isValid()); scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id2")))); QCOMPARE(model.rowCount(), 2); QCOMPARE(rowsInsertedSpy.count(), 2); } void testDisplayName() { MigrationScheduler scheduler; scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id")))); QAbstractItemModel &model(scheduler.model()); QCOMPARE(model.data(model.index(0, 0)).toString(), QStringLiteral("name")); } void testStartStop() { MigrationScheduler scheduler; QSharedPointer migrator(new Testmigrator(QStringLiteral("id"))); scheduler.addMigrator(migrator); scheduler.start(migrator->identifier()); QCOMPARE(migrator->migrationState(), MigratorBase::InProgress); scheduler.abort(migrator->identifier()); QCOMPARE(migrator->migrationState(), MigratorBase::Aborted); } void testNoDuplicates() { MigrationScheduler scheduler; scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id")))); scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id")))); QAbstractItemModel &model(scheduler.model()); QCOMPARE(model.rowCount(), 1); } void testMigrationStateChanged() { MigrationScheduler scheduler; scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id1")))); QSharedPointer migrator(new Testmigrator(QStringLiteral("id2"))); scheduler.addMigrator(migrator); scheduler.addMigrator(QSharedPointer(new Testmigrator(QStringLiteral("id3")))); QAbstractItemModel &model(scheduler.model()); - QSignalSpy spy(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex))); + QSignalSpy spy(&model, &QAbstractItemModel::dataChanged); QVERIFY(spy.isValid()); migrator->start(); QCOMPARE(spy.count(), 1); const QVariantList args = spy.takeFirst(); QCOMPARE(args.at(0).value().row(), 1); QCOMPARE(args.at(1).value().row(), 1); } void testRunMultiple() { MigrationScheduler scheduler; QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); scheduler.addMigrator(m1); QSharedPointer m2(new Testmigrator(QStringLiteral("id2"))); scheduler.addMigrator(m2); scheduler.start(m1->identifier()); scheduler.start(m2->identifier()); QCOMPARE(m1->migrationState(), MigratorBase::InProgress); QCOMPARE(m2->migrationState(), MigratorBase::InProgress); } void testRunAutostart() { MigrationScheduler scheduler; QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); m1->mAutostart = true; scheduler.addMigrator(m1); QSharedPointer m2(new Testmigrator(QStringLiteral("id2"))); m2->mAutostart = true; scheduler.addMigrator(m2); QCOMPARE(m1->migrationState(), MigratorBase::InProgress); qDebug() << m2->migrationState(); QCOMPARE(m2->migrationState(), MigratorBase::None); m1->complete(); QCOMPARE(m2->migrationState(), MigratorBase::InProgress); } void testJobTracker() { TestJobTracker jobTracker; MigrationScheduler scheduler(&jobTracker); QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); m1->mAutostart = true; scheduler.addMigrator(m1); QCOMPARE(jobTracker.mJobs.size(), 1); m1->complete(); //Give the job some time to delete itself QTest::qWait(500); QCOMPARE(jobTracker.mJobs.size(), 0); } void testSuspend() { TestJobTracker jobTracker; MigrationScheduler scheduler(&jobTracker); QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); m1->mAutostart = true; scheduler.addMigrator(m1); jobTracker.mJobs.first()->suspend(); QCOMPARE(m1->migrationState(), MigratorBase::Paused); jobTracker.mJobs.first()->resume(); QCOMPARE(m1->migrationState(), MigratorBase::InProgress); } /* * Even if the migrator doesn't implement suspend, the executor suspends after completing the current job and waits with starting the second job. */ void testJobFinishesDuringSuspend() { TestJobTracker jobTracker; MigrationScheduler scheduler(&jobTracker); QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); m1->mAutostart = true; scheduler.addMigrator(m1); QSharedPointer m2(new Testmigrator(QStringLiteral("id2"))); m2->mAutostart = true; scheduler.addMigrator(m2); jobTracker.mJobs.first()->suspend(); m1->complete(); QCOMPARE(m2->migrationState(), MigratorBase::None); jobTracker.mJobs.first()->resume(); QCOMPARE(m2->migrationState(), MigratorBase::InProgress); } void testProgressReporting() { TestJobTracker jobTracker; MigrationScheduler scheduler(&jobTracker); QSharedPointer m1(new Testmigrator(QStringLiteral("id1"))); m1->mAutostart = true; scheduler.addMigrator(m1); QCOMPARE(jobTracker.mPercent, 0); m1->complete(); QCOMPARE(jobTracker.mPercent, 100); } }; QTEST_MAIN(SchedulerTest) #include "schedulertest.moc" diff --git a/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp index 143fed0d5..bf8b19a86 100644 --- a/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp +++ b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp @@ -1,288 +1,288 @@ /* Copyright (C) 2015-2017 Krzysztof Nowicki 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 "ewspoxautodiscoverrequest.h" #include #include #include #include #include "ewsclient_debug.h" static const QString poxAdOuReqNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"); static const QString poxAdRespNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"); static const QString poxAdOuRespNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"); EwsPoxAutodiscoverRequest::EwsPoxAutodiscoverRequest(const QUrl &url, const QString &email, const QString &userAgent, bool useNTLMv2, QObject *parent) : EwsJob(parent) , mUrl(url) , mEmail(email) , mUserAgent(userAgent) , mUseNTLMv2(useNTLMv2) , mServerVersion(EwsServerVersion::ewsVersion2007Sp1) , mAction(Settings) { } EwsPoxAutodiscoverRequest::~EwsPoxAutodiscoverRequest() { } void EwsPoxAutodiscoverRequest::doSend() { Q_FOREACH (KJob *job, subjobs()) { job->start(); } } -void EwsPoxAutodiscoverRequest::prepare(const QString body) +void EwsPoxAutodiscoverRequest::prepare(const QString &body) { mBody = body; mLastUrl = mUrl; KIO::TransferJob *job = KIO::http_post(mUrl, body.toUtf8(), KIO::HideProgressInfo); job->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/xml")); job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); if (mUseNTLMv2) { job->addMetaData(QStringLiteral("EnableNTLMv2Auth"), QStringLiteral("true")); } if (!mUserAgent.isEmpty()) { job->addMetaData(QStringLiteral("UserAgent"), mUserAgent); } //config->readEntry("no-spoof-check", false) connect(job, &KIO::TransferJob::result, this, &EwsPoxAutodiscoverRequest::requestResult); connect(job, &KIO::TransferJob::data, this, &EwsPoxAutodiscoverRequest::requestData); connect(job, &KIO::TransferJob::redirection, this, &EwsPoxAutodiscoverRequest::requestRedirect); addSubjob(job); } void EwsPoxAutodiscoverRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); writer.writeStartDocument(); writer.writeDefaultNamespace(poxAdOuReqNsUri); writer.writeStartElement(poxAdOuReqNsUri, QStringLiteral("Autodiscover")); writer.writeStartElement(poxAdOuReqNsUri, QStringLiteral("Request")); writer.writeTextElement(poxAdOuReqNsUri, QStringLiteral("EMailAddress"), mEmail); writer.writeTextElement(poxAdOuReqNsUri, QStringLiteral("AcceptableResponseSchema"), poxAdOuRespNsUri); writer.writeEndElement(); // Request writer.writeEndElement(); // Autodiscover writer.writeEndDocument(); qCDebug(EWSCLI_PROTO_LOG) << reqString; qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting POX Autodiscovery request (url: ") << mUrl << QStringLiteral(", email: ") << mEmail; prepare(reqString); doSend(); } void EwsPoxAutodiscoverRequest::requestData(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job); qCDebug(EWSCLI_PROTO_LOG) << "data" << job << data; mResponseData += QString::fromUtf8(data); } void EwsPoxAutodiscoverRequest::requestResult(KJob *job) { if (EWSCLI_PROTO_LOG().isDebugEnabled()) { ewsLogDir.setAutoRemove(false); if (ewsLogDir.isValid()) { QTemporaryFile dumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmldump_XXXXXXX.xml")); dumpFile.open(); dumpFile.setAutoRemove(false); dumpFile.write(mResponseData.toUtf8()); qCDebug(EWSCLI_PROTO_LOG) << "response dumped to" << dumpFile.fileName(); dumpFile.close(); } } KIO::TransferJob *trJob = qobject_cast(job); int resp = trJob->metaData()[QStringLiteral("responsecode")].toUInt(); if (job->error() != 0) { setErrorMsg(QStringLiteral("Failed to process EWS request: ") + job->errorString()); setError(job->error()); } else if (resp >= 300) { setErrorMsg(QStringLiteral("Failed to process EWS request - HTTP code %1").arg(resp)); setError(resp); } else { QXmlStreamReader reader(mResponseData); readResponse(reader); } emitResult(); } bool EwsPoxAutodiscoverRequest::readResponse(QXmlStreamReader &reader) { if (!reader.readNextStartElement()) { return setErrorMsg(QStringLiteral("Failed to read POX response XML")); } if ((reader.name() != QStringLiteral("Autodiscover")) || (reader.namespaceUri() != poxAdRespNsUri)) { return setErrorMsg(QStringLiteral("Failed to read POX response - not an Autodiscover response")); } if (!reader.readNextStartElement()) { return setErrorMsg(QStringLiteral("Failed to read POX response - expected %1 element") .arg(QStringLiteral("Response"))); } if ((reader.name() != QStringLiteral("Response")) || (reader.namespaceUri() != poxAdOuRespNsUri)) { return setErrorMsg(QStringLiteral("Failed to read POX response - expected %1 element, found %2") .arg(QStringLiteral("Response").arg(reader.name().toString()))); } while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } if (reader.name() == QStringLiteral("User")) { reader.skipCurrentElement(); } else if (reader.name() == QStringLiteral("Account")) { if (!readAccount(reader)) { return false; } } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown element '%1' inside '%2'") .arg(reader.name().toString()).arg(QStringLiteral("Response"))); } } return true; } bool EwsPoxAutodiscoverRequest::readAccount(QXmlStreamReader &reader) { while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } const QStringRef readerName = reader.name(); if (readerName == QStringLiteral("Action")) { QString action = reader.readElementText(); if (action == QStringLiteral("settings")) { mAction = Settings; } else if (action == QStringLiteral("redirectUrl")) { mAction = RedirectUrl; } else if (action == QStringLiteral("redirectAddr")) { mAction = RedirectAddr; } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown action '%1'") .arg(action)); } } else if (readerName == QStringLiteral("RedirectUrl")) { mRedirectUrl = reader.readElementText(); } else if (readerName == QStringLiteral("RedirectAddr")) { mRedirectAddr = reader.readElementText(); } else if (readerName == QStringLiteral("RedirectAddr")) { mRedirectAddr = reader.readElementText(); } else if (readerName == QStringLiteral("Protocol")) { if (!readProtocol(reader)) { return false; } } else { reader.skipCurrentElement(); } } return true; } bool EwsPoxAutodiscoverRequest::readProtocol(QXmlStreamReader &reader) { Protocol proto; while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } const QStringRef readerName = reader.name(); if (readerName == QStringLiteral("Type")) { QString type = reader.readElementText(); if (type == QStringLiteral("EXCH")) { proto.mType = ExchangeProto; } else if (type == QStringLiteral("EXPR")) { proto.mType = ExchangeProxyProto; } else if (type == QStringLiteral("WEB")) { proto.mType = ExchangeWebProto; } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown protocol '%1'") .arg(type)); } } else if (readerName == QStringLiteral("EwsUrl")) { proto.mEwsUrl = reader.readElementText(); } else if (readerName == QStringLiteral("OabUrl")) { proto.mOabUrl = reader.readElementText(); } else { reader.skipCurrentElement(); } } qCDebug(EWSCLI_LOG) << "Adding proto type" << proto.mType << proto.isValid(); mProtocols[proto.mType] = proto; return true; } void EwsPoxAutodiscoverRequest::requestRedirect(KIO::Job *job, const QUrl &url) { Q_UNUSED(job); qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got HTTP redirect to: ") << mUrl; mLastUrl = url; } void EwsPoxAutodiscoverRequest::dump() const { ewsLogDir.setAutoRemove(false); if (ewsLogDir.isValid()) { QTemporaryFile reqDumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmlreqdump_XXXXXXX.xml")); reqDumpFile.open(); reqDumpFile.setAutoRemove(false); reqDumpFile.write(mBody.toUtf8()); reqDumpFile.close(); QTemporaryFile resDumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmlresdump_XXXXXXX.xml")); resDumpFile.open(); resDumpFile.setAutoRemove(false); resDumpFile.write(mResponseData.toUtf8()); resDumpFile.close(); qCDebug(EWSCLI_LOG) << "request dumped to" << reqDumpFile.fileName(); qCDebug(EWSCLI_LOG) << "response dumped to" << resDumpFile.fileName(); } else { qCWarning(EWSCLI_LOG) << "failed to dump request and response"; } } diff --git a/resources/ews/ewsclient/ewspoxautodiscoverrequest.h b/resources/ews/ewsclient/ewspoxautodiscoverrequest.h index 04d9d7bc2..b2ba38ae0 100644 --- a/resources/ews/ewsclient/ewspoxautodiscoverrequest.h +++ b/resources/ews/ewsclient/ewspoxautodiscoverrequest.h @@ -1,150 +1,150 @@ /* Copyright (C) 2015-2017 Krzysztof Nowicki 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. */ #ifndef EWSPOXAUTODISCOVERREQUEST_H #define EWSPOXAUTODISCOVERREQUEST_H #include #include #include #include "ewsjob.h" #include "ewsserverversion.h" #include "ewstypes.h" namespace KIO { class Job; } class EwsPoxAutodiscoverRequest : public EwsJob { Q_OBJECT public: enum Action { RedirectUrl = 0, RedirectAddr, Settings, }; enum ProtocolType { ExchangeProto, ExchangeProxyProto, ExchangeWebProto, UnknownProto }; class Protocol { public: Protocol() : mType(UnknownProto) { } bool isValid() const { return mType != UnknownProto; } ProtocolType type() const { return mType; } const QString &ewsUrl() const { return mEwsUrl; } const QString &oabUrl() const { return mOabUrl; } private: ProtocolType mType; QString mEwsUrl; QString mOabUrl; friend class EwsPoxAutodiscoverRequest; }; EwsPoxAutodiscoverRequest(const QUrl &url, const QString &email, const QString &userAgent, bool useNTLMv2, QObject *parent); ~EwsPoxAutodiscoverRequest() override; const EwsServerVersion &serverVersion() const { return mServerVersion; } void dump() const; void start() override; Action action() const { return mAction; } const Protocol protocol(ProtocolType type) const { return mProtocols.value(type); } const QString &redirectAddr() const { return mRedirectAddr; } const QString &redirectUrl() const { return mRedirectUrl; } const QUrl &lastHttpUrl() const { return mLastUrl; } protected: void doSend(); - void prepare(const QString body); + void prepare(const QString &body); bool readResponse(QXmlStreamReader &reader); protected Q_SLOTS: void requestResult(KJob *job); void requestData(KIO::Job *job, const QByteArray &data); void requestRedirect(KIO::Job *job, const QUrl &url); private: bool readAccount(QXmlStreamReader &reader); bool readProtocol(QXmlStreamReader &reader); QString mResponseData; QString mBody; QUrl mUrl; QString mEmail; QString mUserAgent; bool mUseNTLMv2; EwsServerVersion mServerVersion; Action mAction; QString mRedirectUrl; QString mRedirectAddr; QHash mProtocols; QUrl mLastUrl; }; #endif diff --git a/resources/ews/ewsresource.cpp b/resources/ews/ewsresource.cpp index d4fac8a13..c245d0c55 100644 --- a/resources/ews/ewsresource.cpp +++ b/resources/ews/ewsresource.cpp @@ -1,1403 +1,1403 @@ /* Copyright (C) 2015-2018 Krzysztof Nowicki 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 "ewsresource.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "ewsfetchitemsjob.h" #include "ewsfetchfoldersjob.h" #include "ewsfetchfoldersincrjob.h" #include "ewsgetitemrequest.h" #include "ewsupdateitemrequest.h" #include "ewsmodifyitemflagsjob.h" #include "ewsmoveitemrequest.h" #include "ewsdeleteitemrequest.h" #include "ewscreatefolderrequest.h" #include "ewsmovefolderrequest.h" #include "ewsupdatefolderrequest.h" #include "ewsdeletefolderrequest.h" #include "ewssubscriptionmanager.h" #include "ewsgetfolderrequest.h" #include "ewsitemhandler.h" #include "ewsmodifyitemjob.h" #include "ewscreateitemjob.h" #include "ewsconfigdialog.h" #include "ewssettings.h" #include "auth/ewsabstractauth.h" #ifdef HAVE_SEPARATE_MTA_RESOURCE #include "ewscreateitemrequest.h" #endif #include "tags/ewstagstore.h" #include "tags/ewsupdateitemstagsjob.h" #include "tags/ewsglobaltagswritejob.h" #include "tags/ewsglobaltagsreadjob.h" #include "ewsresource_debug.h" #include "ewsresourceadaptor.h" #include "ewssettingsadaptor.h" #include "ewswalletadaptor.h" using namespace Akonadi; struct SpecialFolders { EwsDistinguishedId did; SpecialMailCollections::Type type; QString iconName; }; static const QVector specialFolderList = { {EwsDIdInbox, SpecialMailCollections::Inbox, QStringLiteral("mail-folder-inbox")}, {EwsDIdOutbox, SpecialMailCollections::Outbox, QStringLiteral("mail-folder-outbox")}, {EwsDIdSentItems, SpecialMailCollections::SentMail, QStringLiteral("mail-folder-sent")}, {EwsDIdDeletedItems, SpecialMailCollections::Trash, QStringLiteral("user-trash")}, {EwsDIdDrafts, SpecialMailCollections::Drafts, QStringLiteral("document-properties")} }; const QString EwsResource::akonadiEwsPropsetUuid = QStringLiteral("9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28"); const EwsPropertyField EwsResource::globalTagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTags"), EwsPropTypeStringArray); const EwsPropertyField EwsResource::globalTagsVersionProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTagsVersion"), EwsPropTypeInteger); const EwsPropertyField EwsResource::tagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Tags"), EwsPropTypeStringArray); const EwsPropertyField EwsResource::flagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Flags"), EwsPropTypeStringArray); static Q_CONSTEXPR int InitialReconnectTimeout = 60; static Q_CONSTEXPR int ReconnectTimeout = 300; EwsResource::EwsResource(const QString &id) : Akonadi::ResourceBase(id) , mAuthStage(0) , mTagsRetrieved(false) , mReconnectTimeout(InitialReconnectTimeout) , mSettings(new EwsSettings(winIdForDialogs())) { mEwsClient.setUserAgent(mSettings->userAgent()); mEwsClient.setEnableNTLMv2(mSettings->enableNTLMv2()); changeRecorder()->fetchCollection(true); changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->itemFetchScope().setFetchTags(true); mRootCollection.setParentCollection(Collection::root()); mRootCollection.setName(name()); mRootCollection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); mRootCollection.setRights(Collection::ReadOnly); setScheduleAttributeSyncBeforeItemSync(true); // Load the sync state QByteArray data = QByteArray::fromBase64(mSettings->syncState().toLatin1()); if (!data.isEmpty()) { data = qUncompress(data); if (!data.isEmpty()) { QDataStream stream(data); stream >> mSyncState; } } data = QByteArray::fromBase64(mSettings->folderSyncState().toLatin1()); if (!data.isEmpty()) { data = qUncompress(data); if (!data.isEmpty()) { mFolderSyncState = QString::fromLatin1(data); } } setHierarchicalRemoteIdentifiersEnabled(true); mTagStore = new EwsTagStore(this); QMetaObject::invokeMethod(this, &EwsResource::delayedInit, Qt::QueuedConnection); connect(this, &AgentBase::reloadConfiguration, this, &EwsResource::reloadConfig); } EwsResource::~EwsResource() { } void EwsResource::delayedInit() { new EwsResourceAdaptor(this); new EwsSettingsAdaptor(mSettings.data()); new EwsWalletAdaptor(mSettings.data()); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings.data(), QDBusConnection::ExportAdaptors); } void EwsResource::resetUrl() { Q_EMIT status(Running, i18nc("@info:status", "Connecting to Exchange server")); EwsGetFolderRequest *req = new EwsGetFolderRequest(mEwsClient, this); const EwsId::List folders {EwsId(EwsDIdMsgFolderRoot), EwsId(EwsDIdInbox)}; req->setFolderIds(folders); EwsFolderShape shape(EwsShapeIdOnly); shape << EwsPropertyField(QStringLiteral("folder:DisplayName")); // Use the opportunity of reading the root folder to read the tag data. shape << globalTagsProperty << globalTagsVersionProperty; req->setFolderShape(shape); connect(req, &EwsRequest::result, this, &EwsResource::rootFolderFetchFinished); req->start(); } void EwsResource::rootFolderFetchFinished(KJob *job) { EwsGetFolderRequest *req = qobject_cast(job); if (!req) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object"); return; } if (req->error()) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qWarning() << "ERROR" << req->errorString(); return; } if (req->responses().size() != 2) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qCWarning(EWSRES_LOG) << QStringLiteral("Invalid number of responses received"); return; } EwsFolder folder = req->responses()[1].folder(); EwsId id = folder[EwsFolderFieldFolderId].value(); if (id.type() == EwsId::Real) { /* Since KDE PIM is heavily based on IMAP philosophy it would only consider for filtering * folders with the remote identifier set to "INBOX". While this is true for IMAP/POP3, Exchange * uses Base64-encoded strings with data private to the server. In order for mail filtering to work * the EWS resource has pretended that the inbox folder's remote name is "INBOX". Since KDE Applications * 17.12 this workaround is no longer needed, however in order to clean-up after old Akonadi EWS * installations the code below sets the correct Exchange id for the Inbox folder. * * At some day in the future this part of code can be removed too. */ Collection c; c.setRemoteId(QStringLiteral("INBOX")); CollectionFetchJob *job = new CollectionFetchJob(c, CollectionFetchJob::Base, this); job->setFetchScope(changeRecorder()->collectionFetchScope()); job->fetchScope().setResource(identifier()); job->fetchScope().setListFilter(CollectionFetchScope::Sync); job->setProperty("inboxId", id.id()); connect(job, &CollectionFetchJob::result, this, &EwsResource::adjustInboxRemoteIdFetchFinished); int inboxIdx = mSettings->serverSubscriptionList().indexOf(QStringLiteral("INBOX")); if (inboxIdx >= 0) { QStringList subList = mSettings->serverSubscriptionList(); subList[inboxIdx] = id.id(); mSettings->setServerSubscriptionList(subList); } } folder = req->responses().first().folder(); id = folder[EwsFolderFieldFolderId].value(); if (id.type() == EwsId::Real) { mRootCollection.setRemoteId(id.id()); mRootCollection.setRemoteRevision(id.changeKey()); qCDebug(EWSRES_LOG) << "Root folder is " << id; Q_EMIT status(Idle, i18nc("@info:status Resource is ready", "Ready")); if (mSettings->serverSubscription()) { mSubManager.reset(new EwsSubscriptionManager(mEwsClient, id, mSettings.data(), this)); connect(mSubManager.data(), &EwsSubscriptionManager::foldersModified, this, &EwsResource::foldersModifiedEvent); connect(mSubManager.data(), &EwsSubscriptionManager::folderTreeModified, this, &EwsResource::folderTreeModifiedEvent); connect(mSubManager.data(), &EwsSubscriptionManager::fullSyncRequested, this, &EwsResource::fullSyncRequestedEvent); /* Use a queued connection here as the connectionError() method will actually destroy the subscription manager. If this * was done with a direct connection this would have ended up with destroying the caller object followed by a crash. */ connect(mSubManager.data(), &EwsSubscriptionManager::connectionError, this, &EwsResource::connectionError, Qt::QueuedConnection); mSubManager->start(); } synchronizeCollectionTree(); mTagStore->readTags(folder[globalTagsProperty].toStringList(), folder[globalTagsVersionProperty].toInt()); } } void EwsResource::adjustInboxRemoteIdFetchFinished(KJob *job) { if (!job->error()) { CollectionFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); if (!fetchJob->collections().isEmpty()) { Collection c = fetchJob->collections()[0]; c.setRemoteId(fetchJob->property("inboxId").toString()); CollectionModifyJob *modifyJob = new CollectionModifyJob(c, this); modifyJob->start(); } } } void EwsResource::retrieveCollections() { if (mRootCollection.remoteId().isNull()) { cancelTask(i18nc("@info:status", "Root folder id not known.")); return; } Q_EMIT status(Running, i18nc("@info:status", "Retrieving collection tree")); if (!mFolderSyncState.isEmpty() && !mRootCollection.isValid()) { /* When doing an incremental sync the real Akonadi identifier of the root collection must * be known, because the retrieved list of changes needs to include all parent folders up * to the root. None of the child collections are required to be valid, but the root must * be, as it needs to be the anchor point. */ CollectionFetchJob *fetchJob = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Base); connect(fetchJob, &CollectionFetchJob::result, this, &EwsResource::rootCollectionFetched); fetchJob->start(); } else { doRetrieveCollections(); } synchronizeTags(); } void EwsResource::rootCollectionFetched(KJob *job) { if (job->error()) { qCWarning(EWSRES_LOG) << "ERROR" << job->errorString(); } else { CollectionFetchJob *fetchJob = qobject_cast(job); if (fetchJob && !fetchJob->collections().isEmpty()) { mRootCollection = fetchJob->collections().first(); qCDebugNC(EWSRES_LOG) << QStringLiteral("Root collection fetched: ") << mRootCollection; } } /* If the fetch failed for whatever reason force a full sync, which doesn't require the root * collection to be valid. */ if (!mRootCollection.isValid()) { mFolderSyncState.clear(); } doRetrieveCollections(); } void EwsResource::doRetrieveCollections() { if (mFolderSyncState.isEmpty()) { EwsFetchFoldersJob *job = new EwsFetchFoldersJob(mEwsClient, mRootCollection, this); connect(job, &EwsFetchFoldersJob::result, this, &EwsResource::fetchFoldersJobFinished); job->start(); } else { EwsFetchFoldersIncrJob *job = new EwsFetchFoldersIncrJob(mEwsClient, mFolderSyncState, mRootCollection, this); connect(job, &EwsFetchFoldersIncrJob::result, this, &EwsResource::fetchFoldersIncrJobFinished); job->start(); } } void EwsResource::connectionError() { Q_EMIT status(Broken, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); } void EwsResource::retrieveItems(const Collection &collection) { Q_EMIT status(1, QStringLiteral("Retrieving item list")); Q_EMIT status(Running, i18nc("@info:status", "Retrieving %1 items", collection.name())); QString rid = collection.remoteId(); EwsFetchItemsJob *job = new EwsFetchItemsJob(collection, mEwsClient, mSyncState.value(rid), mItemsToCheck.value(rid), mTagStore, this); job->setQueuedUpdates(mQueuedUpdates.value(collection.remoteId())); mQueuedUpdates.remove(collection.remoteId()); connect(job, &EwsFetchItemsJob::result, this, &EwsResource::itemFetchJobFinished); connect(job, &EwsFetchItemsJob::status, this, [this](int s, const QString &message) { Q_EMIT status(s, message); }); connect(job, &EwsFetchItemsJob::percent, this, [this](int p) { Q_EMIT percent(p); }); job->start(); } bool EwsResource::retrieveItems(const Item::List &items, const QSet &parts) { qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: start " << items << parts; EwsGetItemRequest *req = new EwsGetItemRequest(mEwsClient, this); EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { ids << EwsId(item.remoteId(), item.remoteRevision()); } req->setItemIds(ids); EwsItemShape shape(EwsShapeIdOnly); shape << EwsPropertyField(QStringLiteral("item:MimeContent")); req->setItemShape(shape); req->setProperty("items", QVariant::fromValue(items)); connect(req, &EwsGetItemRequest::result, this, &EwsResource::getItemsRequestFinished); req->start(); return true; } void EwsResource::getItemsRequestFinished(KJob *job) { if (job->error()) { qWarning() << "ERROR" << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process items retrieval request")); return; } EwsGetItemRequest *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetItemRequest job object"); cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error")); return; } const Item::List items = req->property("items").value(); QHash itemHash; itemHash.reserve(items.count()); for (const Item &item : items) { itemHash.insert(item.remoteId(), item); } const EwsGetItemRequest::Response &resp = req->responses()[0]; if (!resp.isSuccess()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Item fetch failed."); cancelTask(i18nc("@info:status", "Failed to retrieve items")); return; } if (items.size() != req->responses().size()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: incorrect number of responses."); cancelTask(i18nc("@info:status", "Failed to retrieve items - incorrect number of responses")); return; } Q_FOREACH (const EwsGetItemRequest::Response &resp, req->responses()) { const EwsItem &ewsItem = resp.item(); EwsId id = ewsItem[EwsItemFieldItemId].value(); auto it = itemHash.find(id.id()); if (it == itemHash.end()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Akonadi item not found for item %1.").arg(id.id()); cancelTask(i18nc("@info:status", "Failed to retrieve items - Akonadi item not found for item %1", id.id())); return; } EwsItemType type = ewsItem.internalType(); if (type == EwsItemTypeUnknown) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Unknown item type for item %1!").arg(id.id()); cancelTask(i18nc("@info:status", "Failed to retrieve items - Unknown item type for item %1", id.id())); return; } if (!EwsItemHandler::itemHandler(type)->setItemPayload(*it, ewsItem)) { qCWarningNC(EWSRES_AGENTIF_LOG) << "retrieveItems: Failed to fetch item payload"; cancelTask(i18nc("@info:status", "Failed to fetch item payload")); return; } } qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: done"; itemsRetrieved(itemHash.values().toVector()); } void EwsResource::reloadConfig() { mSubManager.reset(nullptr); mEwsClient.setUrl(mSettings->baseUrl()); setUpAuth(); mEwsClient.setAuth(mAuth.data()); } void EwsResource::configure(WId windowId) { QPointer dlg = new EwsConfigDialog(this, mEwsClient, windowId, mSettings.data()); if (dlg->exec()) { reloadConfig(); Q_EMIT configurationDialogAccepted(); } else { Q_EMIT configurationDialogRejected(); } delete dlg; } void EwsResource::fetchFoldersJobFinished(KJob *job) { Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); EwsFetchFoldersJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersJob job object"); cancelTask(i18nc("@info:status", "Failed to retrieve folders - internal error")); return; } if (req->error()) { qWarning() << "ERROR" << req->errorString(); cancelTask(i18nc("@info:status", "Failed to process folders retrieval request")); return; } mFolderSyncState = req->syncState(); saveState(); collectionsRetrieved(req->folders()); fetchSpecialFolders(); } void EwsResource::fetchFoldersIncrJobFinished(KJob *job) { Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); EwsFetchFoldersIncrJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersIncrJob job object"); cancelTask(i18nc("@info:status", "Invalid incremental folders retrieval request job object")); return; } if (req->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("ERROR") << req->errorString(); /* Retry with a full sync. */ qCWarningNC(EWSRES_LOG) << QStringLiteral("Retrying with a full sync."); mFolderSyncState.clear(); doRetrieveCollections(); return; } mFolderSyncState = req->syncState(); saveState(); collectionsRetrievedIncremental(req->changedFolders(), req->deletedFolders()); fetchSpecialFolders(); } void EwsResource::itemFetchJobFinished(KJob *job) { EwsFetchItemsJob *fetchJob = qobject_cast(job); if (!fetchJob) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchItemsJobjob object"); cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error")); return; } if (job->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Item fetch error:") << job->errorString(); if (mSyncState.contains(fetchJob->collection().remoteId())) { qCDebugNC(EWSRES_LOG) << QStringLiteral("Retrying with empty state."); // Retry with a clear sync state. mSyncState.remove(fetchJob->collection().remoteId()); retrieveItems(fetchJob->collection()); } else { qCDebugNC(EWSRES_LOG) << QStringLiteral("Clean sync failed."); // No more hope cancelTask(i18nc("@info:status", "Failed to retrieve items")); return; } } else { mSyncState[fetchJob->collection().remoteId()] = fetchJob->syncState(); itemsRetrievedIncremental(fetchJob->changedItems(), fetchJob->deletedItems()); } saveState(); mItemsToCheck.remove(fetchJob->collection().remoteId()); Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); } void EwsResource::itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) { qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: start " << item << partIdentifiers; EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Item type not supported for changing"; cancelTask(i18nc("@info:status", "Item type not supported for changing")); } else { EwsModifyItemJob *job = EwsItemHandler::itemHandler(type)->modifyItemJob(mEwsClient, Item::List() << item, partIdentifiers, this); connect(job, &KJob::result, this, &EwsResource::itemChangeRequestFinished); job->start(); } } void EwsResource::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) { qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: start" << items << addedFlags << removedFlags; EwsModifyItemFlagsJob *job = new EwsModifyItemFlagsJob(mEwsClient, this, items, addedFlags, removedFlags); connect(job, &EwsModifyItemFlagsJob::result, this, &EwsResource::itemModifyFlagsRequestFinished); job->start(); } void EwsResource::itemModifyFlagsRequestFinished(KJob *job) { if (job->error()) { qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged:" << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item flags update request")); return; } EwsModifyItemFlagsJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: Invalid EwsModifyItemFlagsJob job object"; cancelTask(i18nc("@info:status", "Failed to update item flags - internal error")); return; } qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: done"; changesCommitted(req->items()); } void EwsResource::itemChangeRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item update request")); return; } EwsModifyItemJob *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Invalid EwsModifyItemJob job object"; cancelTask(i18nc("@info:status", "Failed to update item - internal error")); return; } qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: done"; changesCommitted(req->items()); } void EwsResource::itemsMoved(const Item::List &items, const Collection &sourceCollection, const Collection &destinationCollection) { qCDebug(EWSRES_AGENTIF_LOG) << "itemsMoved: start" << items << sourceCollection << destinationCollection; EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { EwsId id(item.remoteId(), item.remoteRevision()); ids.append(id); } EwsMoveItemRequest *req = new EwsMoveItemRequest(mEwsClient, this); req->setItemIds(ids); EwsId destId(destinationCollection.remoteId(), QString()); req->setDestinationFolderId(destId); req->setProperty("items", QVariant::fromValue(items)); req->setProperty("sourceCollection", QVariant::fromValue(sourceCollection)); req->setProperty("destinationCollection", QVariant::fromValue(destinationCollection)); connect(req, &KJob::result, this, &EwsResource::itemMoveRequestFinished); req->start(); } void EwsResource::itemMoveRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item move request")); return; } EwsMoveItemRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid EwsMoveItemRequest job object"; cancelTask(i18nc("@info:status", "Failed to move item - internal error")); return; } Item::List items = job->property("items").value(); if (items.count() != req->responses().count()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid number of responses received from server"; cancelTask(i18nc("@info:status", "Failed to move item - invalid number of responses received from server")); return; } /* When moving a batch of items it is possible that the operation will fail for some of them. * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order * to work around this in case of partial failure the source and destination folders will be * resynchronised. In order to avoid doing a full sync a hint will be provided in order to * indicate the item(s) to check. */ Item::List movedItems; EwsId::List failedIds; Collection srcCol = req->property("sourceCollection").value(); Collection dstCol = req->property("destinationCollection").value(); Item::List::iterator it = items.begin(); Q_FOREACH (const EwsMoveItemRequest::Response &resp, req->responses()) { Item &item = *it; if (resp.isSuccess()) { qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsMoved: succeeded for item %1 (new id: %2)") .arg(ewsHash(item.remoteId())).arg(ewsHash(resp.itemId().id())); if (item.isValid()) { /* Log item deletion in the source folder so that the next sync doesn't trip over * non-existent items. Use old remote ids for that. */ if (mSubManager) { mSubManager->queueUpdate(EwsDeletedEvent, item.remoteId(), QString()); } mQueuedUpdates[srcCol.remoteId()].append({item.remoteId(), QString(), EwsDeletedEvent}); item.setRemoteId(resp.itemId().id()); item.setRemoteRevision(resp.itemId().changeKey()); movedItems.append(item); } } else { Q_EMIT warning(QStringLiteral("Move failed for item %1").arg(item.remoteId())); qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsMoved: failed for item %1").arg(ewsHash(item.remoteId())); failedIds.append(EwsId(item.remoteId(), QString())); } ++it; } if (!failedIds.isEmpty()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to move %1 items. Forcing src & dst folder sync.") .arg(failedIds.size()); mItemsToCheck[srcCol.remoteId()] += failedIds; foldersModifiedEvent(EwsId::List({EwsId(srcCol.remoteId(), QString())})); mItemsToCheck[dstCol.remoteId()] += failedIds; foldersModifiedEvent(EwsId::List({EwsId(dstCol.remoteId(), QString())})); } qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsMoved: done"; changesCommitted(movedItems); } void EwsResource::itemsRemoved(const Item::List &items) { qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: start" << items; EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { EwsId id(item.remoteId(), item.remoteRevision()); ids.append(id); } EwsDeleteItemRequest *req = new EwsDeleteItemRequest(mEwsClient, this); req->setItemIds(ids); req->setProperty("items", QVariant::fromValue(items)); connect(req, &EwsDeleteItemRequest::result, this, &EwsResource::itemDeleteRequestFinished); req->start(); } void EwsResource::itemDeleteRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item delete request")); return; } EwsDeleteItemRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid EwsDeleteItemRequest job object"; cancelTask(i18nc("@info:status", "Failed to delete item - internal error")); return; } Item::List items = job->property("items").value(); if (items.count() != req->responses().count()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid number of responses received from server"; cancelTask(i18nc("@info:status", "Failed to delete item - invalid number of responses received from server")); return; } /* When removing a batch of items it is possible that the operation will fail for some of them. * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order * to work around this in case of partial failure the original folder(s) will be resynchronised. * In order to avoid doing a full sync a hint will be provided in order to indicate the item(s) * to check. */ EwsId::List foldersToSync; Item::List::iterator it = items.begin(); Q_FOREACH (const EwsDeleteItemRequest::Response &resp, req->responses()) { Item &item = *it; if (resp.isSuccess()) { qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: succeeded for item %1").arg(ewsHash(item.remoteId())); if (mSubManager) { mSubManager->queueUpdate(EwsDeletedEvent, item.remoteId(), QString()); } mQueuedUpdates[item.parentCollection().remoteId()].append({item.remoteId(), QString(), EwsDeletedEvent}); } else { Q_EMIT warning(QStringLiteral("Delete failed for item %1").arg(item.remoteId())); qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: failed for item %1").arg(ewsHash(item.remoteId())); EwsId colId = EwsId(item.parentCollection().remoteId(), QString()); mItemsToCheck[colId.id()].append(EwsId(item.remoteId(), QString())); if (!foldersToSync.contains(colId)) { foldersToSync.append(colId); } } ++it; } if (!foldersToSync.isEmpty()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Need to force sync for %1 folders.") .arg(foldersToSync.size()); foldersModifiedEvent(foldersToSync); } qCDebug(EWSRES_AGENTIF_LOG) << "itemsRemoved: done"; changeProcessed(); } void EwsResource::itemAdded(const Item &item, const Collection &collection) { EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { cancelTask(i18nc("@info:status", "Item type not supported for creation")); } else { EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, collection, mTagStore, this); connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemCreateRequestFinished); job->start(); } } void EwsResource::itemCreateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process item create request")); return; } EwsCreateItemJob *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to create item - internal error")); return; } changeCommitted(req->item()); } void EwsResource::collectionAdded(const Collection &collection, const Collection &parent) { EwsFolderType type; QStringList mimeTypes = collection.contentMimeTypes(); if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeCalendarItem)->mimeType())) { type = EwsFolderTypeCalendar; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeContact)->mimeType())) { type = EwsFolderTypeContacts; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeTask)->mimeType())) { type = EwsFolderTypeTasks; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeMessage)->mimeType())) { type = EwsFolderTypeMail; } else { qCWarningNC(EWSRES_LOG) << QStringLiteral("Cannot determine EWS folder type."); cancelTask(i18nc("@info:status", "Failed to add collection - cannot determine EWS folder type")); return; } EwsFolder folder; folder.setType(type); folder.setField(EwsFolderFieldDisplayName, collection.name()); EwsCreateFolderRequest *req = new EwsCreateFolderRequest(mEwsClient, this); req->setParentFolderId(EwsId(parent.remoteId())); req->setFolders(EwsFolder::List() << folder); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsCreateFolderRequest::result, this, &EwsResource::folderCreateRequestFinished); req->start(); } void EwsResource::folderCreateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder create request")); return; } EwsCreateFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to create folder - internal error")); return; } Collection col = job->property("collection").value(); EwsCreateFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to create folder")); } } void EwsResource::collectionMoved(const Collection &collection, const Collection &collectionSource, const Collection &collectionDestination) { Q_UNUSED(collectionSource) EwsId::List ids; ids.append(EwsId(collection.remoteId(), collection.remoteRevision())); EwsMoveFolderRequest *req = new EwsMoveFolderRequest(mEwsClient, this); req->setFolderIds(ids); EwsId destId(collectionDestination.remoteId()); req->setDestinationFolderId(destId); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsMoveFolderRequest::result, this, &EwsResource::folderMoveRequestFinished); req->start(); } void EwsResource::folderMoveRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder move request")); return; } EwsMoveFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to move folder - internal error")); return; } Collection col = job->property("collection").value(); if (req->responses().count() != 1) { cancelTask(i18nc("@info:status", "Failed to move folder - invalid number of responses received from server")); return; } EwsMoveFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to move folder")); } } void EwsResource::collectionChanged(const Collection &collection, const QSet &changedAttributes) { if (changedAttributes.contains("NAME")) { EwsUpdateFolderRequest *req = new EwsUpdateFolderRequest(mEwsClient, this); EwsUpdateFolderRequest::FolderChange fc(EwsId(collection.remoteId(), collection.remoteRevision()), EwsFolderTypeMail); EwsUpdateFolderRequest::Update *upd = new EwsUpdateFolderRequest::SetUpdate(EwsPropertyField(QStringLiteral("folder:DisplayName")), collection.name()); fc.addUpdate(upd); req->addFolderChange(fc); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsUpdateFolderRequest::finished, this, &EwsResource::folderUpdateRequestFinished); req->start(); } else { changeCommitted(collection); } } void EwsResource::collectionChanged(const Akonadi::Collection &collection) { Q_UNUSED(collection) } void EwsResource::folderUpdateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder update request")); return; } EwsUpdateFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to update folder - internal error")); return; } Collection col = job->property("collection").value(); if (req->responses().count() != 1) { cancelTask(i18nc("@info:status", "Failed to update folder - invalid number of responses received from server")); return; } EwsUpdateFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to update folder")); } } void EwsResource::collectionRemoved(const Collection &collection) { EwsDeleteFolderRequest *req = new EwsDeleteFolderRequest(mEwsClient, this); EwsId::List ids; ids.append(EwsId(collection.remoteId(), collection.remoteRevision())); req->setFolderIds(ids); connect(req, &EwsDeleteFolderRequest::result, this, &EwsResource::folderDeleteRequestFinished); req->start(); } void EwsResource::folderDeleteRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder delete request")); return; } EwsDeleteFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to delete folder - internal error")); return; } EwsDeleteFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { changeProcessed(); } else { cancelTask(i18nc("@info:status", "Failed to delete folder")); mFolderSyncState.clear(); synchronizeCollectionTree(); } } void EwsResource::sendItem(const Akonadi::Item &item) { EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { itemSent(item, TransportFailed, i18nc("@info:status", "Item type not supported for creation")); } else { EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, Collection(), mTagStore, this); job->setSend(true); job->setProperty("item", QVariant::fromValue(item)); connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemSendRequestFinished); job->start(); } } void EwsResource::itemSendRequestFinished(KJob *job) { Item item = job->property("item").value(); if (job->error()) { itemSent(item, TransportFailed, i18nc("@info:status", "Failed to process item send request")); return; } EwsCreateItemJob *req = qobject_cast(job); if (!req) { itemSent(item, TransportFailed, i18nc("@info:status", "Failed to send item - internal error")); return; } itemSent(item, TransportSucceeded); } void EwsResource::sendMessage(const QString &id, const QByteArray &content) { #ifdef HAVE_SEPARATE_MTA_RESOURCE EwsCreateItemRequest *req = new EwsCreateItemRequest(mEwsClient, this); EwsItem item; item.setType(EwsItemTypeMessage); item.setField(EwsItemFieldMimeContent, content); req->setItems(EwsItem::List() << item); req->setMessageDisposition(EwsDispSendOnly); req->setProperty("requestId", id); connect(req, &EwsCreateItemRequest::finished, this, &EwsResource::messageSendRequestFinished); req->start(); #endif } #ifdef HAVE_SEPARATE_MTA_RESOURCE void EwsResource::messageSendRequestFinished(KJob *job) { QString id = job->property("requestId").toString(); if (job->error()) { Q_EMIT messageSent(id, i18nc("@info:status", "Failed to process item send request")); return; } EwsCreateItemRequest *req = qobject_cast(job); if (!req) { Q_EMIT messageSent(id, i18nc("@info:status", "Failed to send item - internal error")); return; } if (req->responses().count() != 1) { Q_EMIT messageSent(id, i18nc("@info:status", "Invalid number of responses received from server")); return; } EwsCreateItemRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { Q_EMIT messageSent(id, QString()); } else { Q_EMIT messageSent(id, resp.responseMessage()); } } #endif void EwsResource::foldersModifiedEvent(const EwsId::List &folders) { for (const EwsId &id : folders) { Collection c; c.setRemoteId(id.id()); CollectionFetchJob *job = new CollectionFetchJob(c, CollectionFetchJob::Base); job->setFetchScope(changeRecorder()->collectionFetchScope()); job->fetchScope().setResource(identifier()); job->fetchScope().setListFilter(CollectionFetchScope::Sync); connect(job, &KJob::result, this, &EwsResource::foldersModifiedCollectionSyncFinished); } } void EwsResource::foldersModifiedCollectionSyncFinished(KJob *job) { if (job->error()) { qCDebug(EWSRES_LOG) << QStringLiteral("Failed to fetch collection tree for sync."); return; } CollectionFetchJob *fetchJob = qobject_cast(job); synchronizeCollection(fetchJob->collections()[0].id()); } void EwsResource::folderTreeModifiedEvent() { synchronizeCollectionTree(); } void EwsResource::fullSyncRequestedEvent() { synchronize(); } void EwsResource::clearFolderSyncState() { mSyncState.clear(); saveState(); } void EwsResource::clearFolderSyncState(const QString &folderId) { mSyncState.remove(folderId); saveState(); } void EwsResource::clearFolderTreeSyncState() { mFolderSyncState.clear(); saveState(); } void EwsResource::fetchSpecialFolders() { CollectionFetchJob *job = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Recursive, this); connect(job, &CollectionFetchJob::collectionsReceived, this, &EwsResource::specialFoldersCollectionsRetrieved); - connect(job, &CollectionFetchJob::result, this, [this](KJob *job) { + connect(job, &CollectionFetchJob::result, this, [](KJob *job) { if (job->error()) { qCWarningNC(EWSRES_LOG) << "Special folders fetch failed:" << job->errorString(); } }); job->start(); } void EwsResource::specialFoldersCollectionsRetrieved(const Collection::List &folders) { EwsId::List queryItems; queryItems.reserve(specialFolderList.count()); for (const SpecialFolders &sf : qAsConst(specialFolderList)) { queryItems.append(EwsId(sf.did)); } if (!queryItems.isEmpty()) { EwsGetFolderRequest *req = new EwsGetFolderRequest(mEwsClient, this); req->setFolderShape(EwsFolderShape(EwsShapeIdOnly)); req->setFolderIds(queryItems); req->setProperty("collections", QVariant::fromValue(folders)); connect(req, &EwsGetFolderRequest::finished, this, &EwsResource::specialFoldersFetchFinished); req->start(); } } void EwsResource::specialFoldersFetchFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << job->errorString(); return; } EwsGetFolderRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid EwsGetFolderRequest job object"); return; } const Collection::List collections = req->property("collections").value(); if (req->responses().size() != specialFolderList.size()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid number of responses received"); return; } QMap map; for (const Collection &col : collections) { map.insert(col.remoteId(), col); } auto it = specialFolderList.cbegin(); Q_FOREACH (const EwsGetFolderRequest::Response &resp, req->responses()) { if (resp.isSuccess()) { EwsId fid = resp.folder()[EwsFolderFieldFolderId].value(); QMap::iterator mapIt = map.find(fid.id()); if (mapIt != map.end()) { qCDebugNC(EWSRES_LOG) << QStringLiteral("Registering folder %1(%2) as special collection %3") .arg(ewsHash(mapIt->remoteId())).arg(mapIt->id()).arg(it->type); SpecialMailCollections::self()->registerCollection(it->type, *mapIt); if (!mapIt->hasAttribute()) { EntityDisplayAttribute *attr = mapIt->attribute(Collection::AddIfMissing); attr->setIconName(it->iconName); CollectionModifyJob *modJob = new CollectionModifyJob(*mapIt, this); modJob->start(); } } } it++; } } void EwsResource::saveState() { QByteArray str; QDataStream dataStream(&str, QIODevice::WriteOnly); dataStream << mSyncState; mSettings->setSyncState(QString::fromLatin1(qCompress(str, 9).toBase64())); mSettings->setFolderSyncState(QString::fromLatin1(qCompress(mFolderSyncState.toLatin1(), 9).toBase64())); mSettings->save(); } void EwsResource::doSetOnline(bool online) { if (online) { reloadConfig(); } else { mSubManager.reset(nullptr); } } int EwsResource::reconnectTimeout() { // Return InitialReconnectTimeout for the first time, then ReconnectTimeout. int timeout = mReconnectTimeout; mReconnectTimeout = ReconnectTimeout; return timeout; } void EwsResource::itemsTagsChanged(const Item::List &items, const QSet &addedTags, const QSet &removedTags) { Q_UNUSED(addedTags) Q_UNUSED(removedTags) EwsUpdateItemsTagsJob *job = new EwsUpdateItemsTagsJob(items, mTagStore, mEwsClient, this); connect(job, &EwsUpdateItemsTagsJob::result, this, &EwsResource::itemsTagChangeFinished); job->start(); } void EwsResource::itemsTagChangeFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process item tags update request")); return; } EwsUpdateItemsTagsJob *updJob = qobject_cast(job); if (!updJob) { cancelTask(i18nc("@info:status", "Failed to update item tags - internal error")); return; } changesCommitted(updJob->items()); } void EwsResource::tagAdded(const Tag &tag) { mTagStore->addTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::tagChanged(const Tag &tag) { mTagStore->addTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::tagRemoved(const Tag &tag) { mTagStore->removeTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::globalTagChangeFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process global tag update request")); } else { changeProcessed(); } } void EwsResource::retrieveTags() { EwsGlobalTagsReadJob *job = new EwsGlobalTagsReadJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsReadJob::result, this, &EwsResource::globalTagsRetrievalFinished); job->start(); } void EwsResource::globalTagsRetrievalFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process global tags retrieval request")); } else { EwsGlobalTagsReadJob *readJob = qobject_cast(job); Q_ASSERT(readJob); tagsRetrieved(readJob->tags(), QHash()); } } void EwsResource::setUpAuth() { EwsAbstractAuth *auth = mSettings->loadAuth(this); /* Use queued connections here to avoid stack overflow when the reauthentication proceeds through all stages. */ connect(auth, &EwsAbstractAuth::authSucceeded, this, &EwsResource::authSucceeded, Qt::QueuedConnection); connect(auth, &EwsAbstractAuth::authFailed, this, &EwsResource::authFailed, Qt::QueuedConnection); connect(auth, &EwsAbstractAuth::requestAuthFailed, this, &EwsResource::requestAuthFailed, Qt::QueuedConnection); qCDebugNC(EWSRES_LOG) << QStringLiteral("Initializing authentication"); mAuth.reset(auth); auth->init(); } void EwsResource::authSucceeded() { mAuthStage = 0; resetUrl(); } void EwsResource::reauthNotificationDismissed(bool accepted) { if (mReauthNotification) { mReauthNotification.clear(); if (accepted) { mAuth->authenticate(true); } else { Q_EMIT authFailed(QStringLiteral("Interactive authentication request denied")); } } } void EwsResource::authFailed(const QString &error) { qCWarningNC(EWSRES_LOG) << "Authentication failed: " << error; reauthenticate(); } void EwsResource::reauthenticate() { switch (mAuthStage) { case 0: if (mAuth->authenticate(false)) { break; } else { ++mAuthStage; } /* fall through */ case 1: { const auto reauthPrompt = mAuth->reauthPrompt(); if (!reauthPrompt.isNull()) { mReauthNotification = new KNotification(QStringLiteral("auth-expired"), KNotification::Persistent, this); mReauthNotification->setText(reauthPrompt.arg(name())); mReauthNotification->setActions(QStringList(i18nc("@action:button", "Authenticate"))); mReauthNotification->setComponentName(QStringLiteral("akonadi_ews_resource")); auto acceptedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, true); auto rejectedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, false); connect(mReauthNotification.data(), &KNotification::action1Activated, this, acceptedFn); connect(mReauthNotification.data(), &KNotification::closed, this, rejectedFn); connect(mReauthNotification.data(), &KNotification::ignored, this, rejectedFn); mReauthNotification->sendEvent(); break; } } /* fall through */ case 2: Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed")); break; } ++mAuthStage; } void EwsResource::requestAuthFailed() { qCWarningNC(EWSRES_LOG) << "requestAuthFailed - going offline"; setTemporaryOffline(reconnectTimeout()); Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed")); reauthenticate(); } AKONADI_RESOURCE_MAIN(EwsResource) diff --git a/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp b/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp index 5fdb930b7..d1985861c 100644 --- a/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp +++ b/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp @@ -1,878 +1,878 @@ /* Copyright (C) 2015-2017 Krzysztof Nowicki 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 #include #include #include #include #include #include #include #include "fakeewsserver.h" #include "fakeewsserverthread.h" class UtEwsFakeSrvTest : public QObject { Q_OBJECT private Q_SLOTS: void emptyDialog(); void invalidURL(); void invalidMethod(); void emptyRequest(); void defaultCallback(); void simpleResponse(); void callbackResponse(); void multipleResponses(); void emptyResponse(); void getEventsRequest(); void getEventsRequest_data(); void getStreamingEventsRequest(); void serverThread(); void delayedContentSize(); void notAuthenticated(); void badAuthentication(); void xqueryResultsInCallback(); private: QPair synchronousHttpReq(const QString &content, ushort port, std::function chunkFn = nullptr); }; void UtEwsFakeSrvTest::emptyDialog() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); url.setUserName(QStringLiteral("test")); url.setPassword(QStringLiteral("test")); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); QNetworkReply *reply = nam.post(req, "test"); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { resp += QString::fromUtf8(reply->readAll()); }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); QCOMPARE(respCode, static_cast(500)); } void UtEwsFakeSrvTest::invalidURL() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/some/url").arg(srv->portNumber())); url.setUserName(QStringLiteral("test")); url.setPassword(QStringLiteral("test")); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); QNetworkReply *reply = nam.post(req, "test"); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { resp += QString::fromUtf8(reply->readAll()); }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); QCOMPARE(respCode, static_cast(500)); } void UtEwsFakeSrvTest::invalidMethod() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); url.setUserName(QStringLiteral("test")); url.setPassword(QStringLiteral("test")); QNetworkRequest req(url); QNetworkReply *reply = nam.get(req); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { resp += QString::fromUtf8(reply->readAll()); }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); QCOMPARE(respCode, static_cast(500)); } void UtEwsFakeSrvTest::emptyRequest() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.second, static_cast(500)); } void UtEwsFakeSrvTest::defaultCallback() { QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDefaultReplyCallback([&receivedReq](const QString &req, QXmlResultItems &, const QXmlNamePool &) { receivedReq = req; return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral("testresp"), 200); }); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral("testreq"), srv->portNumber()); QCOMPARE(receivedReq, QStringLiteral("testreq")); QCOMPARE(resp.first, QStringLiteral("testresp")); QCOMPARE(resp.second, static_cast(200)); } void UtEwsFakeSrvTest::simpleResponse() { const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then () else ()"), FakeEwsServer::DialogEntry::ReplyCallback(), QStringLiteral("Sample request 1") } }; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDialog(dialog); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); } void UtEwsFakeSrvTest::callbackResponse() { const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then () else ()"), [](const QString &, QXmlResultItems &, const QXmlNamePool &) { return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral(""), 200); }, QStringLiteral("Sample request 1") } }; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDialog(dialog); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); } void UtEwsFakeSrvTest::multipleResponses() { const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = or //test1/b = ) then () else ()"), FakeEwsServer::DialogEntry::ReplyCallback(), QStringLiteral("Sample request 1") }, { QStringLiteral("if (//test1/b = or //test1/c = ) then () else ()"), FakeEwsServer::DialogEntry::ReplyCallback(), QStringLiteral("Sample request 2") } }; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDialog(dialog); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.second, static_cast(500)); } void UtEwsFakeSrvTest::emptyResponse() { bool callbackCalled = false; const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then () else ()"), [&callbackCalled](const QString &, QXmlResultItems &, const QXmlNamePool &) { callbackCalled = true; return FakeEwsServer::EmptyResponse; }, QStringLiteral("Sample request 1") } }; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDialog(dialog); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.second, static_cast(500)); QCOMPARE(callbackCalled, true); } void UtEwsFakeSrvTest::getEventsRequest() { const FakeEwsServer::DialogEntry::List emptyDialog; const QFETCH(QString, request); const QFETCH(QStringList, events); const QFETCH(ushort, responseCode); const QFETCH(QString, response); QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); srv->queueEventsXml(events); auto resp = synchronousHttpReq(request, srv->portNumber()); QCOMPARE(resp.second, responseCode); if (responseCode == 200) { QCOMPARE(resp.first, response); } } void UtEwsFakeSrvTest::getEventsRequest_data() { QTest::addColumn("request"); QTest::addColumn("events"); QTest::addColumn("responseCode"); QTest::addColumn("response"); QTest::newRow("valid request (MSDN)") << QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "" "" "") << (QStringList() << QStringLiteral("AAAAAM4GAAAAAAAAAQ==" "2006-08-22T00:36:29Z" "" "" "") << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "")) << static_cast(200) << QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "false" "" "AAAAAM4GAAAAAAAAAQ==" "2006-08-22T00:36:29Z" "" "" "" "" "AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "" "" "" "" "" "" ""); QTest::newRow("valid request (namespaced)") << QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "" "" "") << (QStringList() << QStringLiteral("AAAAAM4GAAAAAAAAAQ==" "2006-08-22T00:36:29Z" "" "" "") << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "")) << static_cast(200) << QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "false" "" "AAAAAM4GAAAAAAAAAQ==" "2006-08-22T00:36:29Z" "" "" "" "" "AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "" "" "" "" "" "" ""); QTest::newRow("invalid request (missing subscription id)") << QStringLiteral("" "" "" "" "" "AAAAAMAGAAAAAAAAAQ==" "" "" "") << (QStringList() << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "")) << static_cast(200) << QStringLiteral("" "" "" "" "" "" "" "" "" "Missing subscription id or watermark." "ErrorInvalidPullSubscriptionId" "0" "" "" "" "" ""); QTest::newRow("invalid request (missing watermark)") << QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "" "" "" "") << (QStringList() << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" "2006-08-22T01:00:50Z" "" "" "")) << static_cast(200) << QStringLiteral("" "" "" "" "" "" "" "" "" "Missing subscription id or watermark." "ErrorInvalidPullSubscriptionId" "0" "" "" "" "" ""); QTest::newRow("valid request (no events)") << QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "" "" "") << QStringList() << static_cast(200) << QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" "f6bc657d-dde1-4f94-952d-143b95d6483d" "AAAAAMAGAAAAAAAAAQ==" "false" "" "" "" "" "" ""); QTest::newRow("invalid request (not a GetEvents request)") << QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "30" "" "" "") << QStringList() << static_cast(500) << QString(); } void UtEwsFakeSrvTest::getStreamingEventsRequest() { bool callbackCalled = false; const FakeEwsServer::DialogEntry::List emptyDialog; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QDateTime startTime = QDateTime::currentDateTime(); const QString event = QStringLiteral("" "2006-08-22T01:00:10Z" "" "" ""); const QString content = QStringLiteral("" "" "" "" "f6bc657d-dde1-4f94-952d-143b95d6483d" "1" "" "" ""); srv->queueEventsXml(QStringList() << event); bool unknownRequestEncountered = false; srv->setDefaultReplyCallback([&](const QString &req, QXmlResultItems &, const QXmlNamePool &) { qDebug() << "Unknown EWS request encountered." << req; unknownRequestEncountered = true; return FakeEwsServer::EmptyResponse; }); bool callbackError = false; int responseNo = 0; QDateTime pushEventTime; auto resp = synchronousHttpReq(content, srv->portNumber(), [&](const QString &chunk) { const QString respHead = QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" "OK"); const QString respTail = QStringLiteral("" "" "" "" ""); const QString eventHead = QStringLiteral("" "" "f6bc657d-dde1-4f94-952d-143b95d6483d"); const QString eventTail = QStringLiteral(""); const QString event2 = QStringLiteral("" "2006-08-22T01:00:50Z" "" "" ""); callbackCalled = true; QString expResp = respHead; if (responseNo == 0) { expResp += eventHead + event + eventTail; } else if (responseNo == 2) { expResp += eventHead + event2 + eventTail; } expResp += respTail; if (chunk != expResp) { qWarning() << "Unexpected GetStreamingEventsResponse"; callbackError = true; return false; } if (responseNo == 1) { srv->queueEventsXml(QStringList() << event2); pushEventTime = QDateTime::currentDateTime(); } else if (responseNo == 2) { /* Check if the response to the above event came "immediately" */ QDateTime now = QDateTime::currentDateTime(); if (pushEventTime.msecsTo(now) > 200) { qWarning() << "Push event maximum roundtrip time exceeded"; callbackError = true; return false; } } responseNo++; return true; }); QCOMPARE(callbackCalled, true); QCOMPARE(resp.second, static_cast(200)); QCOMPARE(callbackError, false); QCOMPARE(unknownRequestEncountered, false); QDateTime now = QDateTime::currentDateTime(); qint64 elapsed = startTime.msecsTo(now); qDebug() << elapsed; QVERIFY(elapsed >= 1 * 60 * 1000 - 600); QVERIFY(elapsed <= 1 * 60 * 1000 + 600); } void UtEwsFakeSrvTest::serverThread() { const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then () else ()"), FakeEwsServer::DialogEntry::ReplyCallback(), QStringLiteral("Sample request 1") } }; QString receivedReq; FakeEwsServerThread thread; thread.start(); QVERIFY(thread.waitServerStarted()); thread.setDialog(dialog); auto resp = synchronousHttpReq(QStringLiteral(""), thread.portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); thread.exit(); thread.wait(); } void UtEwsFakeSrvTest::delayedContentSize() { /* This test case simulates the behaviour of KIO HTTP, which sends the data in three chunks: * - initial headers * - Content-Length header + end of headers * - content */ const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then () else ()"), FakeEwsServer::DialogEntry::ReplyCallback(), QStringLiteral("Sample request 1") } }; QString receivedReq; FakeEwsServerThread thread; thread.start(); QVERIFY(thread.waitServerStarted()); thread.setDialog(dialog); QTcpSocket sock; sock.connectToHost(QHostAddress(QHostAddress::LocalHost), thread.portNumber()); QVERIFY(sock.waitForConnected(1000)); sock.write(QStringLiteral("POST /EWS/Exchange.asmx HTTP/1.1\r\n" "Host: 127.0.0.1:%1\r\n" "Connection: keep-alive\r\n" "User-Agent: Mozilla/5.0 (X11; Linux x86_64) KHTML/5.26.0 (like Gecko) Konqueror/5.26\r\n" "Pragma: no-cache\r\n" "Cache-control: no-cache\r\n" "Accept: text/html, text/*;q=0.9, image/jpeg;q=0.9, image/png;q=0.9, image/*;q=0.9, */*;q=0.8\r\n" "Accept-Charset: utf-8,*;q=0.5\r\n" "Accept-Language: pl-PL,en;q=0.9\r\n" "Authorization: Basic dGVzdDp0ZXN0\r\n" "Content-Type: text/xml\r\n").arg(thread.portNumber()).toLatin1()); sock.waitForBytesWritten(100); QThread::msleep(100); sock.write("Content-Length: 20\r\n\r\n"); sock.waitForBytesWritten(100); QThread::msleep(100); sock.write(""); sock.waitForBytesWritten(100); sock.waitForReadyRead(300); QByteArray data = sock.readAll(); thread.exit(); thread.wait(); QCOMPARE(data, QByteArray("HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: Keep-Alive\n\r\n")); } void UtEwsFakeSrvTest::notAuthenticated() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); QNetworkReply *reply = nam.post(req, "test"); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { resp += QString::fromUtf8(reply->readAll()); }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); QCOMPARE(respCode, static_cast(401)); } void UtEwsFakeSrvTest::badAuthentication() { QScopedPointer srv(new FakeEwsServer(this)); QVERIFY(srv->start()); QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); url.setUserName(QStringLiteral("foo")); url.setPassword(QStringLiteral("bar")); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); QNetworkReply *reply = nam.post(req, "test"); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { resp += QString::fromUtf8(reply->readAll()); }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); QCOMPARE(respCode, static_cast(401)); } void UtEwsFakeSrvTest::xqueryResultsInCallback() { bool callbackOk = false; const FakeEwsServer::DialogEntry::List dialog = { { QStringLiteral("if (//test1/a = ) then (test) else ()"), - [&callbackOk](const QString &, QXmlResultItems &ri, const QXmlNamePool &) + [](const QString &, QXmlResultItems &ri, const QXmlNamePool &) { if (ri.hasError()) { qDebug() << "XQuery result has errors."; return FakeEwsServer::EmptyResponse; } QXmlItem item = ri.next(); if (item.isNull()) { qDebug() << "XQuery result has no items."; return FakeEwsServer::EmptyResponse; } if (!item.isNode()) { qDebug() << "XQuery result is not a XML node."; return FakeEwsServer::EmptyResponse; } QXmlNodeModelIndex index = item.toNodeModelIndex(); if (index.isNull()) { qDebug() << "XQuery XML node is NULL."; return FakeEwsServer::EmptyResponse; } if (index.model()->stringValue(index) != QStringLiteral("test")) { qDebug() << "Unexpected item value:" << index.model()->stringValue(index); return FakeEwsServer::EmptyResponse; } return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral(""), 200); }, QStringLiteral("Sample request 1") } }; QString receivedReq; QScopedPointer srv(new FakeEwsServer(this)); srv->setDialog(dialog); QVERIFY(srv->start()); auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); QCOMPARE(resp.first, QStringLiteral("")); QCOMPARE(resp.second, static_cast(200)); } QPair UtEwsFakeSrvTest::synchronousHttpReq(const QString &content, ushort port, std::function chunkFn) { QNetworkAccessManager nam(this); QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(port)); url.setUserName(QStringLiteral("test")); url.setPassword(QStringLiteral("test")); QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); QNetworkReply *reply = nam.post(req, content.toUtf8()); QEventLoop loop; QString resp; ushort respCode = 0; connect(reply, &QNetworkReply::readyRead, this, [reply, &resp, &chunkFn, &loop]() { QString chunk = QString::fromUtf8(reply->readAll()); if (chunkFn) { bool cont = chunkFn(chunk); if (!cont) { reply->close(); loop.exit(200); return; } } else { resp += chunk; } }); connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); }); respCode = loop.exec(); return {resp, respCode}; } QTEST_MAIN(UtEwsFakeSrvTest) #include "ewsfakesrv_test.moc" diff --git a/resources/ews/test/isolatedtestbase.cpp b/resources/ews/test/isolatedtestbase.cpp index 60bcd17f3..97b62bea8 100644 --- a/resources/ews/test/isolatedtestbase.cpp +++ b/resources/ews/test/isolatedtestbase.cpp @@ -1,350 +1,350 @@ /* Copyright (C) 2017 Krzysztof Nowicki 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 "isolatedtestbase.h" #include #include #include #include #include "ewssettings.h" #include "ewswallet.h" #include "fakeewsserverthread.h" using namespace Akonadi; constexpr int OnlineStateChangeTimeoutMs = 20000; IsolatedTestBase::IsolatedTestBase(QObject *parent) : QObject(parent) , mFakeServerThread(new FakeEwsServerThread(this)) { qsrand(QDateTime::currentDateTimeUtc().toTime_t()); } IsolatedTestBase::~IsolatedTestBase() { } void IsolatedTestBase::init() { QVERIFY(Control::start()); /* Switch all resources offline to reduce interference from them */ for (AgentInstance agent : AgentManager::self()->instances()) { agent.setIsOnline(false); } connect(AgentManager::self(), &AgentManager::instanceAdded, this, [](const Akonadi::AgentInstance &instance) { qDebug() << "AgentInstanceAdded" << instance.identifier(); }); mFakeServerThread->start(); QVERIFY(mFakeServerThread->waitServerStarted()); } void IsolatedTestBase::cleanup() { mFakeServerThread->exit(); mFakeServerThread->wait(); } QString IsolatedTestBase::loadResourceAsString(const QString &path) { QFile f(path); if (f.open(QIODevice::ReadOnly)) { return QString::fromUtf8(f.readAll()); } qWarning() << "Resource" << path << "not found"; return QString(); } template QDBusReply dBusSetAndWaitReply(std::function()> setFunc, std::function()> getFunc, const QString &name) { QDBusReply reply; int retryCnt = 4; do { setFunc(); reply = getFunc(); if (!reply.isValid()) { qDebug() << "Failed to set DBus config option" << name << reply.error().message(); QThread::msleep(250); } } while (!reply.isValid() && retryCnt-- > 0); return reply; } TestAgentInstance::TestAgentInstance(const QString &url) { AgentType ewsType = AgentManager::self()->type(QStringLiteral("akonadi_ews_resource")); AgentInstanceCreateJob *agentCreateJob = new AgentInstanceCreateJob(ewsType); QVERIFY(agentCreateJob->exec()); mEwsInstance.reset(new AgentInstance(agentCreateJob->instance())); mIdentifier = mEwsInstance->identifier(); const QString akonadiInstanceIdentifier = QProcessEnvironment::systemEnvironment().value(QStringLiteral("AKONADI_INSTANCE")); mEwsSettingsInterface.reset(new OrgKdeAkonadiEwsSettingsInterface( QStringLiteral("org.freedesktop.Akonadi.Resource.") + mIdentifier + QLatin1Char('.') + akonadiInstanceIdentifier, QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this)); QVERIFY(mEwsSettingsInterface->isValid()); mEwsWalletInterface.reset(new OrgKdeAkonadiEwsWalletInterface( QStringLiteral("org.freedesktop.Akonadi.Resource.") + mIdentifier + QLatin1Char('.') + akonadiInstanceIdentifier, QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this)); QVERIFY(mEwsWalletInterface->isValid()); /* The EWS resource initializes its DBus adapters asynchronously. Therefore it can happen that * due to a race access is attempted prior to their initialization. To fix this retry the DBus * communication a few times before declaring failure. */ const auto baseUrlReply = dBusSetAndWaitReply( std::bind(&OrgKdeAkonadiEwsSettingsInterface::setBaseUrl, mEwsSettingsInterface.data(), url), std::bind(&OrgKdeAkonadiEwsSettingsInterface::baseUrl, mEwsSettingsInterface.data()), QStringLiteral("Base URL")); QVERIFY(baseUrlReply.isValid()); QVERIFY(baseUrlReply.value() == url); const auto hasDomainReply = dBusSetAndWaitReply( std::bind(&OrgKdeAkonadiEwsSettingsInterface::setHasDomain, mEwsSettingsInterface.data(), false), std::bind(&OrgKdeAkonadiEwsSettingsInterface::hasDomain, mEwsSettingsInterface.data()), QStringLiteral("has domain")); QVERIFY(hasDomainReply.isValid()); QVERIFY(hasDomainReply.value() == false); const auto username = QStringLiteral("test"); const auto usernameReply = dBusSetAndWaitReply( std::bind(&OrgKdeAkonadiEwsSettingsInterface::setUsername, mEwsSettingsInterface.data(), username), std::bind(&OrgKdeAkonadiEwsSettingsInterface::username, mEwsSettingsInterface.data()), QStringLiteral("Username")); QVERIFY(usernameReply.isValid()); QVERIFY(usernameReply.value() == username); mEwsWalletInterface->setTestPassword(QStringLiteral("test")); AgentManager::self()->instance(mIdentifier).reconfigure(); } TestAgentInstance::~TestAgentInstance() { if (mEwsInstance) { AgentManager::self()->removeInstance(*mEwsInstance); } } bool TestAgentInstance::isValid() const { return mEwsInstance && mEwsSettingsInterface && mEwsWalletInterface && !mIdentifier.isEmpty(); } const QString &TestAgentInstance::identifier() const { return mIdentifier; } bool TestAgentInstance::setOnline(bool online, bool wait) { if (wait) { QEventLoop loop; auto conn = connect(AgentManager::self(), &AgentManager::instanceOnline, this, [&](const AgentInstance &instance, bool state) { if (instance == *mEwsInstance && state == online) { qDebug() << "is online" << state; loop.exit(0); } }, Qt::QueuedConnection); QTimer timer; timer.setSingleShot(true); connect(&timer, &QTimer::timeout, this, [&]() { qWarning() << "Timeout waiting for desired resource online state."; loop.exit(1); }); timer.start(OnlineStateChangeTimeoutMs); mEwsInstance->setIsOnline(online); bool status = (loop.exec() == 0); disconnect(conn); return status; } else { mEwsInstance->setIsOnline(online); return true; } } MsgRootInboxDialogEntry::MsgRootInboxDialogEntry(const QString &rootId, const QString &inboxId, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-inbox-msgroot")) .arg(rootId).arg(inboxId); description = QStringLiteral("GetFolder request for inbox and msgroot"); } SubscribedFoldersDialogEntry::SubscribedFoldersDialogEntry(const IsolatedTestBase::FolderList &list, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { static const QVector specialFolders = { IsolatedTestBase::Folder::Inbox, IsolatedTestBase::Folder::Calendar, IsolatedTestBase::Folder::Tasks, IsolatedTestBase::Folder::Contacts }; QHash folderHash; for (const auto &folder : list) { if (specialFolders.contains(folder.type)) { folderHash.insert(folder.type, &folder); } } QString xml; for (auto special : specialFolders) { const IsolatedTestBase::Folder *folder = folderHash[special]; if (QTest::qVerify(folder != nullptr, "folder != nullptr", "", __FILE__, __LINE__)) { xml += QStringLiteral(""); xml += QStringLiteral("NoError"); xml += QStringLiteral(""); xml += QStringLiteral("").arg(folder->id); xml += QStringLiteral(""); xml += QStringLiteral(""); } } xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-subscribedfolders")).arg(xml); } SpecialFoldersDialogEntry::SpecialFoldersDialogEntry(const IsolatedTestBase::FolderList &list, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { static const QVector specialFolders = { IsolatedTestBase::Folder::Inbox, IsolatedTestBase::Folder::Outbox, IsolatedTestBase::Folder::Sent, IsolatedTestBase::Folder::Trash, IsolatedTestBase::Folder::Drafts }; QHash folderHash; for (const auto &folder : list) { if (specialFolders.contains(folder.type)) { folderHash.insert(folder.type, &folder); } } QString xml; for (auto special : specialFolders) { const IsolatedTestBase::Folder *folder = folderHash[special]; if (QTest::qVerify(folder != nullptr, "folder != nullptr", "", __FILE__, __LINE__)) { xml += QStringLiteral(""); xml += QStringLiteral("NoError"); xml += QStringLiteral(""); xml += QStringLiteral("").arg(folder->id); xml += QStringLiteral(""); xml += QStringLiteral(""); } } xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-specialfolders")).arg(xml); } GetTagsEmptyDialogEntry::GetTagsEmptyDialogEntry(const QString &rootId, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-tags")).arg(rootId); } SubscribeStreamingDialogEntry::SubscribeStreamingDialogEntry(const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/subscribe-streaming")); } SyncFolderHierInitialDialogEntry::SyncFolderHierInitialDialogEntry(const IsolatedTestBase::FolderList &list, const QString &syncState, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { QHash childCount; for (const auto &folder : list) { ++childCount[folder.parentId]; } QString xml; for (const auto &folder : list) { if (folder.type == IsolatedTestBase::Folder::Root) { continue; } xml += QStringLiteral(""); xml += QStringLiteral(""); xml += QStringLiteral("").arg(folder.id); xml += QStringLiteral("").arg(folder.parentId); QString folderClass; QString extraXml; switch (folder.type) { case IsolatedTestBase::Folder::Calendar: folderClass = QStringLiteral("IPF.Calendar"); break; case IsolatedTestBase::Folder::Contacts: folderClass = QStringLiteral("IPF.Contacts"); break; case IsolatedTestBase::Folder::Tasks: folderClass = QStringLiteral("IPF.Tasks"); break; default: folderClass = QStringLiteral("IPF.Note"); extraXml = QStringLiteral("0"); } xml += QStringLiteral("%1").arg(folderClass); xml += QStringLiteral("0"); xml += QStringLiteral("%1").arg(folder.name); xml += QStringLiteral("%1").arg(childCount[folder.id]); xml += extraXml; xml += QStringLiteral(""); xml += QStringLiteral(""); } xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/syncfolderhierarhy-emptystate")) .arg(syncState).arg(xml); } UnsubscribeDialogEntry::UnsubscribeDialogEntry(const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/unsubscribe")); } ValidateFolderIdsDialogEntry::ValidateFolderIdsDialogEntry(const QStringList &ids, const QString &descr, const ReplyCallback &callback) : DialogEntryBase(descr, callback) { QStringList xQueryFolderIds; QString responseXml; int folderIndex = 0; - for (auto folderId : ids) { + for (const auto &folderId : ids) { xQueryFolderIds.append(QStringLiteral("//m:GetFolder/m:FolderIds/t:FolderId[position()=%1 and @Id=\"%2\"]") .arg(++folderIndex).arg(folderId)); responseXml += QStringLiteral(""); responseXml += QStringLiteral("NoError"); responseXml += QStringLiteral(""); responseXml += QStringLiteral("").arg(folderId); responseXml += QStringLiteral(""); responseXml += QStringLiteral(""); } xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-validateids")) .arg(folderIndex).arg(xQueryFolderIds.join(QStringLiteral(" and "))).arg(responseXml); } diff --git a/resources/ews/test/unittests/ewsoauth_ut.cpp b/resources/ews/test/unittests/ewsoauth_ut.cpp index d12bc588f..c6d7f92a2 100644 --- a/resources/ews/test/unittests/ewsoauth_ut.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut.cpp @@ -1,346 +1,346 @@ /* Copyright (C) 2018 Krzysztof Nowicki 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 #include "auth/ewsoauth.h" #include "ewsoauth_ut_mock.h" static const QString testEmail = QStringLiteral("joe.bloggs@unknown.com"); static const QString testClientId = QStringLiteral("b43c59cd-dd1c-41fd-bb9a-b0a1d5696a93"); static const QString testReturnUri = QStringLiteral("urn:ietf:wg:oauth:2.0:oob"); //static const QString testReturnUriPercent = QUrl::toPercentEncoding(testReturnUri); static const QString testState = QStringLiteral("joidsiuhq"); static const QString resource = QStringLiteral("https://outlook.office365.com/"); //static const QString resourcePercent = QUrl::toPercentEncoding(resource); static const QString authUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize"); static const QString tokenUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/token"); static const QString accessToken1 = QStringLiteral("IERbOTo5NSdtY5HMntWTH1wgrRt98KmbF7nNloIdZ4SSYOU7pziJJakpHy8r6kxQi+7T9w36mWv9IWLrvEwTsA"); static const QString refreshToken1 = QStringLiteral("YW7lJFWcEISynbraq4NiLLke3rOieFdvoJEDxpjCXorJblIGM56OJSu1PZXMCQL5W3KLxS9ydxqLHxRTSdw"); static const QString idToken1 = QStringLiteral("gz7l0chu9xIi1MMgPkpHGQTmo3W7L1rQbmWAxEL5VSKHeqdIJ7E3K7vmMYTl/C1fWihB5XiLjD2GSVQoOzTfCw"); class UtEwsOAuth : public QObject { Q_OBJECT private Q_SLOTS: void initialInteractiveSuccessful(); void initialRefreshSuccessful(); void refreshSuccessful(); private: static QString formatJsonSorted(const QVariantMap &map); static int performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn); static void setUpAccessFunction(const QString &refreshToken); static void setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData); static void dumpEvents(const QStringList &events, const QStringList &expectedEvents); void setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map); }; void UtEwsOAuth::initialInteractiveSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; setUpOAuth(oAuth, events, QString(), QMap()); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); } void UtEwsOAuth::initialRefreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; QMap map = { {QStringLiteral("refresh-token"), refreshToken1} }; setUpOAuth(oAuth, events, QString(), map); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); } void UtEwsOAuth::refreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; setUpOAuth(oAuth, events, QString(), QMap()); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); events.clear(); oAuth.notifyRequestAuthFailed(); const auto reauthStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(false); }); QVERIFY(reauthStatus == 0); const QStringList expectedEventsRefresh = { Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEventsRefresh); } QString UtEwsOAuth::formatJsonSorted(const QVariantMap &map) { QStringList keys = map.keys(); keys.sort(); QStringList elems; for (const auto &key : keys) { QString val = map[key].toString(); val.replace(QLatin1Char('"'), QStringLiteral("\\\"")); elems.append(QStringLiteral("\"%1\":\"%2\"").arg(key, val)); } return QStringLiteral("{") + elems.join(QLatin1Char(',')) + QStringLiteral("}"); } int UtEwsOAuth::performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn) { QEventLoop loop; int status = -1; QTimer timer; connect(&oAuth, &EwsOAuth::authSucceeded, &timer, [&]() { qDebug() << "succeeded"; loop.exit(0); status = 0; }); connect(&oAuth, &EwsOAuth::authFailed, &timer, [&](const QString &msg) { qDebug() << "failed" << msg; loop.exit(1); status = 1; }); connect(&timer, &QTimer::timeout, &timer, [&]() { qDebug() << "timeout"; loop.exit(1); status = 1; }); timer.setSingleShot(true); timer.start(timeout); if (!actionFn(&oAuth)) { return -1; } if (status == -1) { status = loop.exec(); } return status; } void UtEwsOAuth::setUpAccessFunction(const QString &refreshToken) { Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken); }); } void UtEwsOAuth::setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData) { Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( [=, &tokenReplyData](QString &data, QMap &headers) { QVariantMap map; map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); map[QStringLiteral("not_before")] = QString::number(time); map[QStringLiteral("resource")] = resource; map[QStringLiteral("access_token")] = accessToken; map[QStringLiteral("refresh_token")] = refreshToken; map[QStringLiteral("foci")] = QStringLiteral("1"); map[QStringLiteral("id_token")] = idToken; tokenReplyData = formatJsonSorted(map); data = tokenReplyData; headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); return Mock::QNetworkReply::NoError; }); } void UtEwsOAuth::dumpEvents(const QStringList &events, const QStringList &expectedEvents) { - for (const auto event : events) { + for (const auto &event : events) { qDebug() << "Got event:" << event; } if (events != expectedEvents) { - for (const auto event : expectedEvents) { + for (const auto &event : expectedEvents) { qDebug() << "Expected event:" << event; } } } void UtEwsOAuth::setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map) { connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&events](const QString &event) { events.append(event); }); connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, [&events](const QString &event) { events.append(event); }); connect(&oAuth, &EwsOAuth::requestWalletPassword, this, [&oAuth, &events, password](bool) { events.append(QStringLiteral("RequestWalletPassword")); oAuth.walletPasswordRequestFinished(password); }); connect(&oAuth, &EwsOAuth::requestWalletMap, this, [&oAuth, &events, map]() { events.append(QStringLiteral("RequestWalletMap")); oAuth.walletMapRequestFinished(map); }); } QTEST_MAIN(UtEwsOAuth) #include "ewsoauth_ut.moc" diff --git a/resources/ews/test/unittests/ewssettings_ut.cpp b/resources/ews/test/unittests/ewssettings_ut.cpp index 71c958da6..87b09a14d 100644 --- a/resources/ews/test/unittests/ewssettings_ut.cpp +++ b/resources/ews/test/unittests/ewssettings_ut.cpp @@ -1,859 +1,859 @@ /* Copyright (C) 2017-2018 Krzysztof Nowicki 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 #include #include "ewssettings.h" #include "ewssettings_ut_mock.h" Q_LOGGING_CATEGORY(EWSRES_LOG, "org.kde.pim.ews", QtInfoMsg) static const QString accessTokenMapKey = QStringLiteral("access-token"); static const QString refreshTokenMapKey = QStringLiteral("refresh-token"); class UtEwsSettings : public QObject { Q_OBJECT private Q_SLOTS: void readNoPassword(); void readNullWallet(); void readTimeout(); void readTimeoutInterrupted(); void readValidPassword(); void writeNullPassword(); void writeNullWallet(); void writeTimeout(); void writeValidPassword(); void readValidMap(); void writeValidMap(); }; namespace KWallet { class MyWallet : public Wallet { Q_OBJECT public: MyWallet(); ~MyWallet() override; void doOpen(bool success); bool hasFolder(const QString &folder) override; bool setFolder(const QString &folder) override; bool createFolder(const QString &folder) override; int readPassword(const QString &key, QString &value) override; int writePassword(const QString &key, const QString &value) override; int readMap(const QString &key, QMap &value) override; int writeMap(const QString &key, const QMap &value) override; std::function hasFolderCallback; std::function setFolderCallback; std::function createFolderCallback; std::function readPasswordCallback; std::function writePasswordCallback; std::function &)> readMapCallback; std::function &)> writeMapCallback; }; static std::function errorCallback; static std::function openWalletCallback; void reportError() { if (errorCallback) { errorCallback(); } } Wallet *openWallet(MyWallet *wallet) { if (openWalletCallback) { return openWalletCallback(wallet); } else { qDebug() << "Wallet open callback not registered!"; errorCallback(); return wallet; } } static const QString networkWallet = QStringLiteral("test_network_wallet"); const QString Wallet::NetworkWallet() { return networkWallet; } Wallet *Wallet::openWallet(const QString &name, WId, OpenType ot) { qDebug() << "intercepted openWallet"; if (name != networkWallet) { qDebug() << "Incorrect wallet name"; reportError(); } if (ot != Asynchronous) { qDebug() << "Unsopported open type"; reportError(); } auto wallet = new MyWallet(); return KWallet::openWallet(wallet); } MyWallet::MyWallet() : Wallet(0, networkWallet) { } MyWallet::~MyWallet() { } void MyWallet::doOpen(bool success) { Q_EMIT walletOpened(success); } bool MyWallet::hasFolder(const QString &folder) { if (hasFolderCallback) { return hasFolderCallback(folder); } else { qWarning() << "hasFolder() callback not set!"; reportError(); return false; } } bool MyWallet::setFolder(const QString &folder) { if (setFolderCallback) { return setFolderCallback(folder); } else { qWarning() << "setFolder() callback not set!"; reportError(); return false; } } bool MyWallet::createFolder(const QString &folder) { if (createFolderCallback) { return createFolderCallback(folder); } else { qWarning() << "createFolder() callback not set!"; reportError(); return false; } } int MyWallet::readPassword(const QString &key, QString &value) { if (readPasswordCallback) { return readPasswordCallback(key, value); } else { qWarning() << "readPasswordCallback() callback not set!"; reportError(); return 0; } } int MyWallet::writePassword(const QString &key, const QString &value) { if (writePasswordCallback) { return writePasswordCallback(key, value); } else { qWarning() << "writePasswordCallback() callback not set!"; reportError(); return 0; } } int MyWallet::readMap(const QString &key, QMap &value) { if (readMapCallback) { return readMapCallback(key, value); } else { qWarning() << "readMapCallback() callback not set!"; reportError(); return 0; } } int MyWallet::writeMap(const QString &key, const QMap &value) { if (writeMapCallback) { return writeMapCallback(key, value); } else { qWarning() << "writeMapCallback() callback not set!"; reportError(); return 0; } } } void UtEwsSettings::readNoPassword() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; QString password; EwsSettings settings(0); connect(&settings, &EwsSettings::passwordRequestFinished, this, [&](const QString &p) { password = p; loop.exit(0); }); QTimer::singleShot(100, [&]() { settings.requestPassword(false); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [](const QString &) { return false; }; wallet->doOpen(true); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password.isNull()); QVERIFY(hasFolderCalled); } void UtEwsSettings::readNullWallet() { KWallet::MyWallet *wallet = nullptr; - KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *) { + KWallet::openWalletCallback = [](KWallet::MyWallet *) { return nullptr; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; QString password; EwsSettings settings(0); connect(&settings, &EwsSettings::passwordRequestFinished, this, [&](const QString &p) { password = p; loop.exit(0); }); QTimer::singleShot(100, [&]() { settings.requestPassword(false); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password.isNull()); } void UtEwsSettings::readTimeout() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; QString password; EwsSettings settings(0); connect(&settings, &EwsSettings::passwordRequestFinished, this, [&](const QString &p) { password = p; loop.exit(0); }); QTimer::singleShot(100, [&]() { settings.requestPassword(false); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [](const QString &) { return false; }; }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(5000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password.isNull()); QVERIFY(!hasFolderCalled); } void UtEwsSettings::readTimeoutInterrupted() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; QString password; EwsSettings settings(0); bool testSecondSignal = false; connect(&settings, &EwsSettings::passwordRequestFinished, this, [&](const QString &p) { if (!testSecondSignal) { password = p; loop.exit(0); } else { loop.exit(1); } }); QTimer::singleShot(100, [&]() { settings.requestPassword(false); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [](const QString &) { return false; }; }); QTimer::singleShot(1000, [&]() { settings.setTestPassword(QStringLiteral("foo")); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(2); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(5000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password == QStringLiteral("foo")); QVERIFY(!hasFolderCalled); // Check for second passwordRequestFinished signal QVERIFY(loop.exec() == 2); } void UtEwsSettings::readValidPassword() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; bool setFolderCalled = false; QString password; EwsSettings settings(0); connect(&settings, &EwsSettings::passwordRequestFinished, this, [&](const QString &p) { password = p; loop.exit(0); }); QTimer::singleShot(100, [&]() { settings.requestPassword(false); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return true; }; wallet->createFolderCallback = [](const QString &) { return false; }; wallet->setFolderCallback = [&setFolderCalled](const QString &) { setFolderCalled = true; return true; }; wallet->readPasswordCallback = [](const QString &, QString &password) { password = QStringLiteral("foo"); return true; }; wallet->doOpen(true); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password == QStringLiteral("foo")); QVERIFY(hasFolderCalled); QVERIFY(setFolderCalled); } void UtEwsSettings::writeNullPassword() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; EwsSettings settings(0); QTimer::singleShot(100, [&]() { settings.setPassword(QString()); if (wallet) { qDebug() << "Wallet is not null"; loop.exit(1); return; } loop.exit(0); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); } void UtEwsSettings::writeNullWallet() { KWallet::MyWallet *wallet = nullptr; - KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *) { + KWallet::openWalletCallback = [](KWallet::MyWallet *) { return nullptr; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; EwsSettings settings(0); QTimer::singleShot(100, [&]() { settings.setPassword(QStringLiteral("foo")); if (wallet) { qDebug() << "Wallet is not null"; loop.exit(1); return; } loop.exit(0); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); } void UtEwsSettings::writeTimeout() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; bool createFolderCalled = false; bool setFolderCalled = false; QString password; EwsSettings settings(0); QTimer::singleShot(100, [&]() { settings.setPassword(QStringLiteral("foo")); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [&createFolderCalled](const QString &) { createFolderCalled = true; return true; }; wallet->setFolderCallback = [&setFolderCalled](const QString &) { setFolderCalled = true; return false; }; wallet->writePasswordCallback = [&](const QString &, const QString &p) { password = p; loop.exit(0); return true; }; }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(2); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(5000); QVERIFY(error != true); QVERIFY(loop.exec() == 2); QVERIFY(password.isNull()); QVERIFY(!hasFolderCalled); QVERIFY(!setFolderCalled); QVERIFY(!createFolderCalled); } void UtEwsSettings::writeValidPassword() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; bool createFolderCalled = false; bool setFolderCalled = false; QString password; EwsSettings settings(0); QTimer::singleShot(100, [&]() { settings.setPassword(QStringLiteral("foo")); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [&createFolderCalled](const QString &) { createFolderCalled = true; return true; }; wallet->setFolderCallback = [&setFolderCalled](const QString &) { setFolderCalled = true; return false; }; wallet->writePasswordCallback = [&](const QString &, const QString &p) { password = p; loop.exit(0); return true; }; wallet->doOpen(true); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(password == QStringLiteral("foo")); QVERIFY(hasFolderCalled); QVERIFY(setFolderCalled); QVERIFY(createFolderCalled); } void UtEwsSettings::readValidMap() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; bool setFolderCalled = false; QMap map; const QMap expectedMap = { {accessTokenMapKey, QStringLiteral("afoo")}, {refreshTokenMapKey, QStringLiteral("rfoo")} }; EwsSettings settings(0); connect(&settings, &EwsSettings::mapRequestFinished, this, [&](const QMap &m) { map = m; loop.exit(0); }); QTimer::singleShot(100, [&]() { settings.requestMap(); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return true; }; wallet->createFolderCallback = [](const QString &) { return false; }; wallet->setFolderCallback = [&setFolderCalled](const QString &) { setFolderCalled = true; return true; }; wallet->readMapCallback = [](const QString &, QMap &map) { map[accessTokenMapKey] = QStringLiteral("afoo"); map[refreshTokenMapKey] = QStringLiteral("rfoo"); return true; }; wallet->doOpen(true); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(map == expectedMap); QVERIFY(hasFolderCalled); QVERIFY(setFolderCalled); } void UtEwsSettings::writeValidMap() { KWallet::MyWallet *wallet = nullptr; KWallet::openWalletCallback = [&wallet](KWallet::MyWallet *w) { wallet = w; return w; }; QEventLoop loop; bool error = false; KWallet::errorCallback = [&]() { if (loop.isRunning()) { loop.exit(1); } else { error = true; } }; bool hasFolderCalled = false; bool createFolderCalled = false; bool setFolderCalled = false; const QMap expectedMap = { {accessTokenMapKey, QStringLiteral("afoo")}, {refreshTokenMapKey, QStringLiteral("rfoo")} }; QMap map; EwsSettings settings(0); QTimer::singleShot(100, [&]() { settings.setMap(expectedMap); if (!wallet) { qDebug() << "Wallet is null"; loop.exit(1); return; } wallet->hasFolderCallback = [&hasFolderCalled](const QString &) { hasFolderCalled = true; return false; }; wallet->createFolderCallback = [&createFolderCalled](const QString &) { createFolderCalled = true; return true; }; wallet->setFolderCallback = [&setFolderCalled](const QString &) { setFolderCalled = true; return false; }; wallet->writeMapCallback = [&](const QString &, const QMap &m) { map = m; loop.exit(0); return true; }; wallet->doOpen(true); }); QTimer timeoutTimer; connect(&timeoutTimer, &QTimer::timeout, this, [&]() { qDebug() << "Test timeout"; loop.exit(1); }); timeoutTimer.setSingleShot(true); timeoutTimer.start(2000); QVERIFY(error != true); QVERIFY(loop.exec() == 0); QVERIFY(map == expectedMap); QVERIFY(hasFolderCalled); QVERIFY(setFolderCalled); QVERIFY(createFolderCalled); } QTEST_MAIN(UtEwsSettings) #include "ewssettings_ut.moc" diff --git a/resources/imap/autotests/testresourcetask.cpp b/resources/imap/autotests/testresourcetask.cpp index e8bf3c6b7..3d270ba1d 100644 --- a/resources/imap/autotests/testresourcetask.cpp +++ b/resources/imap/autotests/testresourcetask.cpp @@ -1,171 +1,171 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU 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 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 "imaptestbase.h" #include #include "resourcetask.h" Q_DECLARE_METATYPE(ResourceTask::ActionIfNoSession) class DummyResourceTask : public ResourceTask { public: explicit DummyResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent = nullptr) : ResourceTask(action, resource, parent) { } void doStart(KIMAP::Session */*session*/) override { cancelTask(QStringLiteral("Dummy task")); } }; class TestResourceTask : public ImapTestBase { Q_OBJECT private Q_SLOTS: void shouldRequestSession_data() { QTest::addColumn("state"); QTest::addColumn< QList >("scenario"); QTest::addColumn("shouldConnect"); QTest::addColumn("shouldRequestSession"); QTest::addColumn("actionIfNoSession"); QTest::addColumn("callNames"); QTest::addColumn("firstCallParameter"); DummyResourceState::Ptr state; QList scenario; QStringList callNames; state = DummyResourceState::Ptr(new DummyResourceState); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed"; callNames.clear(); callNames << QStringLiteral("cancelTask"); QTest::newRow("normal case") << state << scenario << true << false << ResourceTask::DeferIfNoSession << callNames << QVariant(QLatin1String("Dummy task")); state = DummyResourceState::Ptr(new DummyResourceState); callNames.clear(); callNames << QStringLiteral("deferTask"); QTest::newRow("all sessions allocated (defer)") << state << scenario << true << true << ResourceTask::DeferIfNoSession << callNames << QVariant(); state = DummyResourceState::Ptr(new DummyResourceState); callNames.clear(); callNames << QStringLiteral("cancelTask"); QTest::newRow("all sessions allocated (cancel)") << state << scenario << true << true << ResourceTask::CancelIfNoSession << callNames << QVariant(); state = DummyResourceState::Ptr(new DummyResourceState); scenario.clear(); callNames.clear(); callNames << QStringLiteral("deferTask") << QStringLiteral("scheduleConnectionAttempt"); QTest::newRow("disconnected pool (defer)") << state << scenario << false << false << ResourceTask::DeferIfNoSession << callNames << QVariant(); state = DummyResourceState::Ptr(new DummyResourceState); scenario.clear(); callNames.clear(); callNames << QStringLiteral("cancelTask") << QStringLiteral("scheduleConnectionAttempt"); QTest::newRow("disconnected pool (cancel)") << state << scenario << false << false << ResourceTask::CancelIfNoSession << callNames << QVariant(); } void shouldRequestSession() { QFETCH(DummyResourceState::Ptr, state); QFETCH(QList, scenario); QFETCH(bool, shouldConnect); QFETCH(bool, shouldRequestSession); QFETCH(ResourceTask::ActionIfNoSession, actionIfNoSession); QFETCH(QStringList, callNames); QFETCH(QVariant, firstCallParameter); FakeServer server; server.setScenario(scenario); server.startAndWait(); SessionPool pool(1); if (shouldConnect) { - QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString))); + QSignalSpy poolSpy(&pool, &SessionPool::connectDone); pool.setPasswordRequester(createDefaultRequester()); QVERIFY(pool.connect(createDefaultAccount())); QTRY_COMPARE(poolSpy.count(), 1); QCOMPARE(poolSpy.at(0).at(0).toInt(), (int)SessionPool::NoError); } if (shouldRequestSession) { - QSignalSpy requestSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); + QSignalSpy requestSpy(&pool, &SessionPool::sessionRequestDone); pool.requestSession(); QTRY_COMPARE(requestSpy.count(), 1); } - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); DummyResourceTask *task = new DummyResourceTask(actionIfNoSession, state); task->start(&pool); if (shouldConnect) { QTRY_COMPARE(sessionSpy.count(), 1); } else { //We want to ensure the signal isn't emitted, so we have to wait QTest::qWait(500); QCOMPARE(sessionSpy.count(), 0); } QCOMPARE(state->calls().count(), callNames.size()); for (int i = 0; i < callNames.size(); i++) { QString command = QString::fromUtf8(state->calls().at(i).first); QCOMPARE(command, callNames[i]); } if (firstCallParameter.toString() == QLatin1String("Dummy task")) { QCOMPARE(state->calls().first().second, firstCallParameter); } QVERIFY(server.isAllScenarioDone()); server.quit(); } }; QTEST_GUILESS_MAIN(TestResourceTask) #include "testresourcetask.moc" diff --git a/resources/imap/autotests/testsessionpool.cpp b/resources/imap/autotests/testsessionpool.cpp index 038725711..717eb83b2 100644 --- a/resources/imap/autotests/testsessionpool.cpp +++ b/resources/imap/autotests/testsessionpool.cpp @@ -1,843 +1,843 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU 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 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 "imaptestbase.h" #include #include class TestSessionPool : public ImapTestBase { Q_OBJECT private Q_SLOTS: void shouldPrepareFirstSessionOnConnect_data() { QTest::addColumn("account"); QTest::addColumn("requester"); QTest::addColumn< QList >("scenario"); QTest::addColumn("password"); QTest::addColumn("errorCode"); QTest::addColumn("capabilities"); ImapAccount *account = nullptr; DummyPasswordRequester *requester = nullptr; QList scenario; QString password; QStringList capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE" << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )" << "S: A000003 OK Completed"; password = QStringLiteral("foobar"); int errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("NAMESPACE") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("normal case") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed"; password = QStringLiteral("foobar"); errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("no NAMESPACE support") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IDLE" << "S: A000002 OK Completed" << "C: A000003 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::IncompatibleServerError; capabilities.clear(); QTest::newRow("incompatible server") << account << requester << scenario << password << errorCode << capabilities; QList requests; QList results; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::UserRejected; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::LoginFailError; capabilities.clear(); QTest::newRow("login fail, user reject password entry") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::PasswordRetrieved; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000002 OK Login succeeded" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000003 OK Completed"; password = QStringLiteral("foobar"); errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("login fail, user provide new password") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::EmptyPasswordEntered; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::LoginFailError; capabilities.clear(); QTest::newRow("login fail, user provided empty password") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::ReconnectNeeded; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::ReconnectNeededError; capabilities.clear(); QTest::newRow("login fail, user change the settings") << account << requester << scenario << password << errorCode << capabilities; } void shouldPrepareFirstSessionOnConnect() { QFETCH(ImapAccount *, account); QFETCH(DummyPasswordRequester *, requester); QFETCH(QList, scenario); QFETCH(QString, password); QFETCH(int, errorCode); QFETCH(QStringList, capabilities); - QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); + QSignalSpy requesterSpy(requester, &PasswordRequesterInterface::done); FakeServer server; server.setScenario(scenario); server.startAndWait(); SessionPool pool(2); QVERIFY(!pool.isConnected()); - QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString))); + QSignalSpy poolSpy(&pool, &SessionPool::connectDone); pool.setPasswordRequester(requester); QVERIFY(pool.connect(account)); QTest::qWait(200); QVERIFY(requesterSpy.count() > 0); if (requesterSpy.count() == 1) { QCOMPARE(requesterSpy.at(0).at(0).toInt(), 0); QCOMPARE(requesterSpy.at(0).at(1).toString(), password); } QCOMPARE(poolSpy.count(), 1); QCOMPARE(poolSpy.at(0).at(0).toInt(), errorCode); if (errorCode == SessionPool::NoError) { QVERIFY(pool.isConnected()); } else { QVERIFY(!pool.isConnected()); } QCOMPARE(pool.serverCapabilities(), capabilities); QVERIFY(pool.serverNamespaces().isEmpty()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldManageSeveralSessions() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE" << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )" << "S: A000003 OK Completed" ); server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); - QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); + QSignalSpy requesterSpy(requester, &PasswordRequesterInterface::done); SessionPool pool(2); pool.setPasswordRequester(requester); - QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); + QSignalSpy connectSpy(&pool, &SessionPool::connectDone); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); // Before connect we can't get any session qint64 requestId = pool.requestSession(); QCOMPARE(requestId, qint64(-1)); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); // Requesting a first session shouldn't create a new one, // only sessionRequestDone is emitted right away requestId = pool.requestSession(); QCOMPARE(requestId, qint64(1)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 1); QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); QCOMPARE(sessionSpy.at(0).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(0).at(3).toString(), QString()); // Requesting an extra session should create a new one // So for instance password will be requested requestId = pool.requestSession(); QCOMPARE(requestId, qint64(2)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 2); QCOMPARE(sessionSpy.at(1).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(1).at(1).value() != nullptr); // Should be different sessions... QVERIFY(sessionSpy.at(0).at(1).value() != sessionSpy.at(1).at(1).value()); QCOMPARE(sessionSpy.at(1).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(1).at(3).toString(), QString()); // Requesting yet another session should fail as we reached the // maximum pool size, and they're all reserved requestId = pool.requestSession(); QCOMPARE(requestId, qint64(3)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 3); QCOMPARE(sessionSpy.at(2).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(2).at(1).value() == nullptr); QCOMPARE(sessionSpy.at(2).at(2).toInt(), (int)SessionPool::NoAvailableSessionError); QVERIFY(!sessionSpy.at(2).at(3).toString().isEmpty()); // OTOH, if we release one now, and then request another one // it should succeed without even creating a new session KIMAP::Session *session = sessionSpy.at(0).at(1).value(); pool.releaseSession(session); requestId = pool.requestSession(); QCOMPARE(requestId, qint64(4)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 4); QCOMPARE(sessionSpy.at(3).at(0).toLongLong(), requestId); // Only one session was available, so that should be the one we get gack QVERIFY(sessionSpy.at(3).at(1).value() == session); QCOMPARE(sessionSpy.at(3).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(3).at(3).toString(), QString()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldNotifyConnectionLost() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); - QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy connectSpy(&pool, &SessionPool::connectDone); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); qint64 requestId = pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId); KIMAP::Session *s = sessionSpy.at(0).at(1).value(); KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(s); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), s); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldNotifyOnDisconnect_data() { QTest::addColumn< QList >("scenario"); QTest::addColumn("termination"); QList scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 LOGOUT"; QTest::newRow("logout session") << scenario << (int)SessionPool::LogoutSession; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed"; QTest::newRow("close session") << scenario << (int)SessionPool::CloseSession; } void shouldNotifyOnDisconnect() { QFETCH(QList, scenario); QFETCH(int, termination); FakeServer server; server.addScenario(scenario); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); - QSignalSpy disconnectSpy(&pool, SIGNAL(disconnectDone())); + QSignalSpy disconnectSpy(&pool, &SessionPool::disconnectDone); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(disconnectSpy.count(), 0); pool.disconnect((SessionPool::SessionTermination)termination); QTest::qWait(100); QCOMPARE(disconnectSpy.count(), 1); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldCleanupOnClosingDuringLogin_data() { QTest::addColumn< QList >("scenario"); { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""; QTest::newRow("during login") << scenario; } { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY"; QTest::newRow("during capability") << scenario; } { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE"; QTest::newRow("during namespace") << scenario; } } void shouldCleanupOnClosingDuringLogin() { QFETCH(QList, scenario); FakeServer server; server.addScenario(scenario); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); - QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy connectSpy(&pool, &SessionPool::connectDone); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 0); // Login not done yet QPointer session = pool.findChild(); QVERIFY(session.data()); QCOMPARE(sessionSpy.count(), 0); pool.disconnect(SessionPool::CloseSession); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); // We're informed that connect failed QCOMPARE(connectSpy.at(0).at(0).toInt(), int(SessionPool::CancelledError)); QCOMPARE(lostSpy.count(), 0); // We're not supposed to know the session pointer, so no connectionLost emitted // Make the session->deleteLater work, it can't happen in qWait (nested event loop) QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); QVERIFY(session.isNull()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldHonorCancelRequest() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); - QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy connectSpy(&pool, &SessionPool::connectDone); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); qint64 requestId = pool.requestSession(); // Cancel the request pool.cancelSessionRequest(requestId); // The request should not be processed anymore QTest::qWait(100); QCOMPARE(sessionSpy.count(), 0); } void shouldBeDisconnectedOnAllSessionLost() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(2); pool.setPasswordRequester(requester); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); // We should be connected now QVERIFY(pool.isConnected()); // Ask for a session pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); // Still connected obviously QVERIFY(pool.isConnected()); // Ask for a second session pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 2); QVERIFY(sessionSpy.at(1).at(1).value() != nullptr); // Still connected of course QVERIFY(pool.isConnected()); KIMAP::Session *session1 = sessionSpy.at(0).at(1).value(); KIMAP::Session *session2 = sessionSpy.at(1).at(1).value(); // Prepare for session disconnects - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); // Make the first session drop KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session1); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), session1); // We're still connected (one session being alive) QVERIFY(pool.isConnected()); // Make the second session drop job = new KIMAP::CapabilitiesJob(session2); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 2); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(1).at(0).value(), session2); // We're not connected anymore! All sessions dropped! QVERIFY(!pool.isConnected()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldHandleDisconnectDuringPasswordRequest() { ImapAccount *account = createDefaultAccount(); // This requester will delay the second reply by a second DummyPasswordRequester *requester = createDefaultRequester(); requester->setDelays(QList() << 20 << 1000); - QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); + QSignalSpy requesterSpy(requester, &PasswordRequesterInterface::done); FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); // The connect should work nicely SessionPool pool(2); QVERIFY(!pool.isConnected()); - QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy poolSpy(&pool, &SessionPool::connectDone); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); pool.setPasswordRequester(requester); QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(poolSpy.count(), 1); QCOMPARE(poolSpy.at(0).at(0).toInt(), (int)SessionPool::NoError); QVERIFY(pool.isConnected()); // Ask for the session we just created pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); KIMAP::Session *session = sessionSpy.at(0).at(1).value(); // Ask for the second session, the password requested will never reply // and we'll get a disconnect in parallel (by triggering the capability // job on the first session // Done this way to simulate a disconnect during a password request // Ask for the extra session, and make sure the call is placed by waiting // just a bit (but not too long so that the requester didn't reply yet, // we set the reply timeout to 1000 earlier in this test) pool.requestSession(); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); // Requester didn't reply yet QCOMPARE(sessionSpy.count(), 1); // Make the first (and only) session drop while we wait for the requester // to reply KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), session); // The requester didn't reply yet QCOMPARE(requesterSpy.count(), 1); QCOMPARE(sessionSpy.count(), 1); // Now wait the remaining time to get the session creation to fail QTest::qWait(1000); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(sessionSpy.count(), 2); QCOMPARE(sessionSpy.at(1).at(2).toInt(), (int)SessionPool::LoginFailError); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldHandleDisconnectionDuringSecondLogin_data() { QTest::addColumn >("scenario"); QTest::newRow("immediate_disconnect") << QList{}; QTest::newRow("disconnect_after_greeting") << QList{FakeServer::greeting()}; QTest::newRow("disconnect_after_login_command") << QList{FakeServer::greeting(), "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""}; } void shouldHandleDisconnectionDuringSecondLogin() { QFETCH(QList, scenario); FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" << "S: A000002 OK Completed" ); server.addScenario(scenario << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(2); pool.setPasswordRequester(requester); - QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString))); + QSignalSpy sessionSpy(&pool, &SessionPool::sessionRequestDone); QVERIFY(pool.connect(account)); // We should be connected now QTRY_VERIFY(pool.isConnected()); // Ask for a session pool.requestSession(); QTRY_COMPARE(sessionSpy.count(), 1); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); // Prepare for session disconnects - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); // Ask for a second session, where we'll lose the connection during the Login job. pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 2); QVERIFY(sessionSpy.at(1).at(1).value() == nullptr); QCOMPARE(lostSpy.count(), 1); // The pool itself is still connected QVERIFY(pool.isConnected()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldNotifyFailureToConnect() { // This tests what happens when we can't connect to the server, e.g. due to being offline. // In this test we just use 0.0.0.0 as an invalid server IP, instead. ImapAccount *account = createDefaultAccount(); account->setServer(QStringLiteral("0.0.0.0")); // so that the connexion fails DummyPasswordRequester *requester = createDefaultRequester(); QList requests; QList results; // I don't want to see "WrongPasswordRequest". A password popup is annoying when we're offline or the server is down. requests << DummyPasswordRequester::StandardRequest; results << DummyPasswordRequester::PasswordRetrieved; requester->setScenario(requests, results); - QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); + QSignalSpy requesterSpy(requester, &PasswordRequesterInterface::done); SessionPool pool(2); - QSignalSpy connectDoneSpy(&pool, SIGNAL(connectDone(int,QString))); - QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*))); + QSignalSpy connectDoneSpy(&pool, &SessionPool::connectDone); + QSignalSpy lostSpy(&pool, &SessionPool::connectionLost); QVERIFY(!pool.isConnected()); pool.setPasswordRequester(requester); pool.connect(account); QVERIFY(!pool.isConnected()); QTRY_COMPARE(requesterSpy.count(), requests.count()); QTRY_COMPARE(connectDoneSpy.count(), 1); QCOMPARE(connectDoneSpy.at(0).at(0).toInt(), (int)SessionPool::CouldNotConnectError); QCOMPARE(lostSpy.count(), 0); // don't want this, it makes the resource reconnect immediately (and fail, and reconnect, and so on...) } }; QTEST_GUILESS_MAIN(TestSessionPool) #include "testsessionpool.moc" diff --git a/resources/mixedmaildir/autotests/collectionfetchtest.cpp b/resources/mixedmaildir/autotests/collectionfetchtest.cpp index bc73801dc..5bacd92c2 100644 --- a/resources/mixedmaildir/autotests/collectionfetchtest.cpp +++ b/resources/mixedmaildir/autotests/collectionfetchtest.cpp @@ -1,477 +1,477 @@ /* This file is part of the KDE project Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com 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 "mixedmaildirstore.h" #include "filestore/collectionfetchjob.h" #include "libmaildir/maildir.h" #include #include #include #include #include #include using namespace Akonadi; static Collection::List collectionsFromSpy(QSignalSpy *spy) { Collection::List collections; QListIterator > it(*spy); while (it.hasNext()) { const QList invocation = it.next(); Q_ASSERT(invocation.count() == 1); collections << invocation.first().value(); } return collections; } class CollectionFetchTest : public QObject { Q_OBJECT public: CollectionFetchTest() : QObject() , mStore(nullptr) , mDir(nullptr) { // for monitoring signals qRegisterMetaType(); } ~CollectionFetchTest() { delete mStore; delete mDir; } private: MixedMaildirStore *mStore = nullptr; QTemporaryDir *mDir = nullptr; private Q_SLOTS: void init(); void cleanup(); void testEmptyDir(); void testMixedTree(); }; void CollectionFetchTest::init() { mStore = new MixedMaildirStore; mDir = new QTemporaryDir; QVERIFY(mDir->isValid()); QVERIFY(QDir(mDir->path()).exists()); } void CollectionFetchTest::cleanup() { delete mStore; mStore = nullptr; delete mDir; mDir = nullptr; } void CollectionFetchTest::testEmptyDir() { mStore->setPath(mDir->path()); FileStore::CollectionFetchJob *job = nullptr; QSignalSpy *spy = nullptr; Collection::List collections; // test base fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Base); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(spy->count(), 1); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 1); QCOMPARE(collections.first(), mStore->topLevelCollection()); QCOMPARE(job->collections(), collections); // test first level fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::FirstLevel); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(spy->count(), 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 0); QCOMPARE(job->collections(), collections); // test recursive fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Recursive); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(spy->count(), 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 0); QCOMPARE(job->collections(), collections); // test fail of base fetching non existent collection Collection collection; collection.setName(QStringLiteral("collection")); collection.setRemoteId(QStringLiteral("collection")); collection.setParentCollection(mStore->topLevelCollection()); job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Base); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); QCOMPARE(spy->count(), 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 0); QCOMPARE(job->collections(), collections); // test fail of first level fetching non existent collection job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); QCOMPARE(spy->count(), 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 0); QCOMPARE(job->collections(), collections); // test fail of recursive fetching non existent collection job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); QCOMPARE(spy->count(), 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 0); QCOMPARE(job->collections(), collections); } void CollectionFetchTest::testMixedTree() { QDir topDir(mDir->path()); KPIM::Maildir topLevelMd(mDir->path(), true); QVERIFY(topLevelMd.isValid()); KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false); KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false); KPIM::Maildir md1_2_1(md1_2.addSubFolder(QStringLiteral("collection1_2_1")), false); // simulate second level mbox in maildir parent QFileInfo fileInfo1_1(KPIM::Maildir::subDirPathForFolderPath(md1.path()), QStringLiteral("collection1_1")); QFile file1_1(fileInfo1_1.absoluteFilePath()); file1_1.open(QIODevice::WriteOnly); file1_1.close(); QVERIFY(fileInfo1_1.exists()); QFileInfo subDirInfo1_1(KPIM::Maildir::subDirPathForFolderPath(fileInfo1_1.absoluteFilePath())); QVERIFY(topDir.mkpath(subDirInfo1_1.absoluteFilePath())); KPIM::Maildir md1_1(subDirInfo1_1.absoluteFilePath(), true); KPIM::Maildir md1_1_1(md1_1.addSubFolder(QStringLiteral("collection1_1_1")), false); // simulate third level mbox in mbox parent QFileInfo fileInfo1_1_2(md1_1.path(), QStringLiteral("collection1_1_2")); QFile file1_1_2(fileInfo1_1_2.absoluteFilePath()); file1_1_2.open(QIODevice::WriteOnly); file1_1_2.close(); QVERIFY(fileInfo1_1_2.exists()); KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false); // simulate first level mbox QFileInfo fileInfo3(mDir->path(), QStringLiteral("collection3")); QFile file3(fileInfo3.absoluteFilePath()); file3.open(QIODevice::WriteOnly); file3.close(); QVERIFY(fileInfo3.exists()); // simulate first level mbox with subtree QFileInfo fileInfo4(mDir->path(), QStringLiteral("collection4")); QFile file4(fileInfo4.absoluteFilePath()); file4.open(QIODevice::WriteOnly); file4.close(); QVERIFY(fileInfo4.exists()); QFileInfo subDirInfo4(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath())); QVERIFY(topDir.mkpath(subDirInfo4.absoluteFilePath())); KPIM::Maildir md4(subDirInfo4.absoluteFilePath(), true); KPIM::Maildir md4_1(md4.addSubFolder(QStringLiteral("collection4_1")), false); // simulate second level mbox in mbox parent QFileInfo fileInfo4_2(subDirInfo4.absoluteFilePath(), QStringLiteral("collection4_2")); QFile file4_2(fileInfo4_2.absoluteFilePath()); file4_2.open(QIODevice::WriteOnly); file4_2.close(); QVERIFY(fileInfo4_2.exists()); QSet firstLevelNames; firstLevelNames << md1.name() << md2.name() << fileInfo3.fileName() << fileInfo4.fileName(); QSet secondLevelNames; secondLevelNames << md1_2.name() << md4_1.name() << fileInfo1_1.fileName() << fileInfo4_2.fileName(); QSet thirdLevelNames; thirdLevelNames << md1_1_1.name() << fileInfo1_1_2.fileName() << md1_2_1.name(); mStore->setPath(mDir->path()); //mDir = 0; FileStore::CollectionFetchJob *job = nullptr; QSignalSpy *spy = nullptr; Collection::List collections; // test base fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Base); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(spy->count(), 1); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), 1); QCOMPARE(collections.first(), mStore->topLevelCollection()); QCOMPARE(job->collections(), collections); // test first level fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::FirstLevel); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QVERIFY(spy->count() > 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), firstLevelNames.count()); QCOMPARE(job->collections(), collections); Q_FOREACH (const Collection &collection, collections) { QVERIFY(!collection.remoteId().isEmpty()); QCOMPARE(collection.remoteId(), collection.name()); QCOMPARE(collection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType()); QCOMPARE(collection.rights(), Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); QCOMPARE(collection.parentCollection(), mStore->topLevelCollection()); QVERIFY(firstLevelNames.contains(collection.name())); } // test recursive fetch of top level collection job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Recursive); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QVERIFY(spy->count() > 0); collections = collectionsFromSpy(spy); QCOMPARE(collections.count(), firstLevelNames.count() + secondLevelNames.count() + thirdLevelNames.count()); QCOMPARE(job->collections(), collections); Q_FOREACH (const Collection &collection, collections) { QVERIFY(!collection.remoteId().isEmpty()); QCOMPARE(collection.remoteId(), collection.name()); QCOMPARE(collection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType()); QCOMPARE(collection.rights(), Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); if (firstLevelNames.contains(collection.name())) { QCOMPARE(collection.parentCollection(), mStore->topLevelCollection()); } else if (secondLevelNames.contains(collection.name())) { QVERIFY(firstLevelNames.contains(collection.parentCollection().name())); QCOMPARE(collection.parentCollection().parentCollection(), mStore->topLevelCollection()); } else if (thirdLevelNames.contains(collection.name())) { QVERIFY(secondLevelNames.contains(collection.parentCollection().name())); QCOMPARE(collection.parentCollection().parentCollection().parentCollection(), mStore->topLevelCollection()); } } // test base fetching all collections Q_FOREACH (const Collection &collection, collections) { job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Base); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); QCOMPARE(spy->count(), 1); const Collection::List list = collectionsFromSpy(spy); QCOMPARE(list.count(), 1); QCOMPARE(list.first(), collection); QCOMPARE(job->collections(), list); const Collection col = list.first(); QVERIFY(!col.remoteId().isEmpty()); QCOMPARE(col.remoteId(), col.name()); QCOMPARE(col.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType()); QCOMPARE(col.rights(), Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } // test first level fetching all collections Q_FOREACH (const Collection &collection, collections) { job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); const Collection::List list = collectionsFromSpy(spy); QCOMPARE(job->collections(), list); Q_FOREACH (const Collection &childCollection, list) { QCOMPARE(childCollection.parentCollection(), collection); QVERIFY(!childCollection.remoteId().isEmpty()); QCOMPARE(childCollection.remoteId(), childCollection.name()); QCOMPARE(childCollection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType()); QCOMPARE(childCollection.rights(), Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } if (firstLevelNames.contains(collection.name())) { Q_FOREACH (const Collection &childCollection, list) { QVERIFY(secondLevelNames.contains(childCollection.name())); } } else if (secondLevelNames.contains(collection.name())) { Q_FOREACH (const Collection &childCollection, list) { QVERIFY(thirdLevelNames.contains(childCollection.name())); } if (collection.name() == md1_2.name()) { QCOMPARE(list.count(), 1); QCOMPARE(list.first().name(), md1_2_1.name()); } else if (collection.name() == fileInfo1_1.fileName()) { QCOMPARE(list.count(), 2); } } else { QCOMPARE(list.count(), 0); } } // test recursive fetching all collections Q_FOREACH (const Collection &collection, collections) { job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Recursive); - spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List))); + spy = new QSignalSpy(job, &FileStore::CollectionFetchJob::collectionsReceived); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); const Collection::List list = collectionsFromSpy(spy); QCOMPARE(job->collections(), list); Q_FOREACH (const Collection &childCollection, list) { QVERIFY(childCollection.parentCollection() == collection || childCollection.parentCollection().parentCollection() == collection); QVERIFY(!childCollection.remoteId().isEmpty()); QCOMPARE(childCollection.remoteId(), childCollection.name()); QCOMPARE(childCollection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType()); QCOMPARE(childCollection.rights(), Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } if (firstLevelNames.contains(collection.name())) { Q_FOREACH (const Collection &childCollection, list) { QVERIFY(secondLevelNames.contains(childCollection.name()) || thirdLevelNames.contains(childCollection.name())); } } else if (secondLevelNames.contains(collection.name())) { Q_FOREACH (const Collection &childCollection, list) { QVERIFY(thirdLevelNames.contains(childCollection.name())); } if (collection.name() == md1_2.name()) { QCOMPARE(list.count(), 1); QCOMPARE(list.first().name(), md1_2_1.name()); } else if (collection.name() == fileInfo1_1.fileName()) { QCOMPARE(list.count(), 2); } } else { QCOMPARE(list.count(), 0); } } } QTEST_MAIN(CollectionFetchTest) #include "collectionfetchtest.moc" diff --git a/resources/mixedmaildir/autotests/itemdeletetest.cpp b/resources/mixedmaildir/autotests/itemdeletetest.cpp index dc3deba66..12a2f2be8 100644 --- a/resources/mixedmaildir/autotests/itemdeletetest.cpp +++ b/resources/mixedmaildir/autotests/itemdeletetest.cpp @@ -1,610 +1,610 @@ /* This file is part of the KDE project Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com 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 "mixedmaildirstore.h" #include "testdatautil.h" #include "filestore/entitycompactchangeattribute.h" #include "filestore/itemdeletejob.h" #include "filestore/itemfetchjob.h" #include "filestore/storecompactjob.h" #include "libmaildir/maildir.h" #include #include #include #include #include #include #include #include using namespace Akonadi; static Collection::List collectionsFromSpy(QSignalSpy *spy) { Collection::List collections; QListIterator > it(*spy); while (it.hasNext()) { const QList invocation = it.next(); Q_ASSERT(invocation.count() == 1); collections << invocation.first().value(); } return collections; } static Item::List itemsFromSpy(QSignalSpy *spy) { Item::List items; QListIterator > it(*spy); while (it.hasNext()) { const QList invocation = it.next(); Q_ASSERT(invocation.count() == 1); items << invocation.first().value(); } return items; } static bool fullEntryCompare(const KMBox::MBoxEntry &a, const KMBox::MBoxEntry &b) { return a.messageOffset() == b.messageOffset() && a.separatorSize() == b.separatorSize() && a.messageSize() == b.messageSize(); } class ItemDeleteTest : public QObject { Q_OBJECT public: ItemDeleteTest() : QObject() , mStore(nullptr) , mDir(nullptr) { // for monitoring signals qRegisterMetaType(); qRegisterMetaType(); } ~ItemDeleteTest() { delete mStore; delete mDir; } private: MixedMaildirStore *mStore = nullptr; QTemporaryDir *mDir = nullptr; private Q_SLOTS: void init(); void cleanup(); void testMaildir(); void testMBox(); void testCachePreservation(); void testExpectedFailure(); }; void ItemDeleteTest::init() { mStore = new MixedMaildirStore; mDir = new QTemporaryDir; QVERIFY(mDir->isValid()); QVERIFY(QDir(mDir->path()).exists()); } void ItemDeleteTest::cleanup() { delete mStore; mStore = nullptr; delete mDir; mDir = nullptr; } void ItemDeleteTest::testMaildir() { QDir topDir(mDir->path()); QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1"))); KPIM::Maildir topLevelMd(topDir.path(), true); KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1")); QVERIFY(md1.isValid()); QSet entrySet1 = QSet::fromList(md1.entryList()); QCOMPARE((int)entrySet1.count(), 4); mStore->setPath(topDir.path()); // common variables FileStore::ItemDeleteJob *job = nullptr; QSet entrySet; QSet delIdSet; QString delId; // test deleting one message Collection collection1; collection1.setName(QStringLiteral("collection1")); collection1.setRemoteId(QStringLiteral("collection1")); collection1.setParentCollection(mStore->topLevelCollection()); Item item1; item1.setMimeType(KMime::Message::mimeType()); item1.setId(KRandom::random()); item1.setRemoteId(*entrySet1.cbegin()); item1.setParentCollection(collection1); job = mStore->deleteItem(item1); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); Item item = job->item(); QCOMPARE(item.id(), item1.id()); entrySet = QSet::fromList(md1.entryList()); QCOMPARE((int)entrySet.count(), 3); delIdSet = entrySet1.subtract(entrySet); QCOMPARE((int)delIdSet.count(), 1); delId = *delIdSet.cbegin(); QCOMPARE(delId, *entrySet1.cbegin()); QCOMPARE(delId, item.remoteId()); // test failure of deleting again job = mStore->deleteItem(item1); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); } void ItemDeleteTest::testMBox() { QDir topDir(mDir->path()); QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection1"))); QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1")); KMBox::MBox mbox1; QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); KMBox::MBoxEntry::List entryList1 = mbox1.entries(); QCOMPARE((int)entryList1.count(), 4); int size1 = fileInfo1.size(); mStore->setPath(topDir.path()); // common variables FileStore::ItemDeleteJob *job = nullptr; FileStore::ItemFetchJob *itemFetch = nullptr; FileStore::StoreCompactJob *storeCompact = nullptr; Item::List items; Collection::List collections; KMBox::MBoxEntry::List entryList; QSignalSpy *collectionsSpy = nullptr; QSignalSpy *itemsSpy = nullptr; QVariant var; // test deleting last item in mbox // file stays untouched, message still accessible through MBox, but item gone Collection collection1; collection1.setName(QStringLiteral("collection1")); collection1.setRemoteId(QStringLiteral("collection1")); collection1.setParentCollection(mStore->topLevelCollection()); Item item4; item4.setMimeType(KMime::Message::mimeType()); item4.setId(KRandom::random()); item4.setRemoteId(QString::number(entryList1.value(3).messageOffset())); item4.setParentCollection(collection1); job = mStore->deleteItem(item4); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); Item item = job->item(); QCOMPARE(item.id(), item4.id()); fileInfo1.refresh(); QCOMPARE((int)fileInfo1.size(), size1); QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); entryList = mbox1.entries(); QCOMPARE(entryList.count(), entryList1.count()); QCOMPARE(entryList.value(3).messageOffset(), entryList1.value(3).messageOffset()); var = job->property("compactStore"); QVERIFY(var.isValid()); QCOMPARE(var.type(), QVariant::Bool); QCOMPARE(var.toBool(), true); itemFetch = mStore->fetchItems(collection1); QVERIFY(itemFetch->exec()); QCOMPARE(itemFetch->error(), 0); items = itemFetch->items(); QCOMPARE((int)items.count(), 3); QCOMPARE(items.value(0).remoteId(), QString::number(entryList1.value(0).messageOffset())); QCOMPARE(items.value(1).remoteId(), QString::number(entryList1.value(1).messageOffset())); QCOMPARE(items.value(2).remoteId(), QString::number(entryList1.value(2).messageOffset())); // test that the item is purged from the file on store compaction // last item purging does not change any others storeCompact = mStore->compactStore(); - collectionsSpy = new QSignalSpy(storeCompact, SIGNAL(collectionsChanged(Akonadi::Collection::List))); - itemsSpy = new QSignalSpy(storeCompact, SIGNAL(itemsChanged(Akonadi::Item::List))); + collectionsSpy = new QSignalSpy(storeCompact, &FileStore::StoreCompactJob::collectionsChanged); + itemsSpy = new QSignalSpy(storeCompact, &FileStore::StoreCompactJob::itemsChanged); QVERIFY(storeCompact->exec()); QCOMPARE(storeCompact->error(), 0); collections = storeCompact->changedCollections(); QCOMPARE(collections.count(), 0); items = storeCompact->changedItems(); QCOMPARE(items.count(), 0); QCOMPARE(collectionsFromSpy(collectionsSpy), collections); QCOMPARE(itemsFromSpy(itemsSpy), items); fileInfo1.refresh(); QVERIFY(fileInfo1.size() < size1); size1 = fileInfo1.size(); QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); entryList = mbox1.entries(); entryList1.pop_back(); QVERIFY(std::equal(entryList1.begin(), entryList1.end(), entryList.begin(), fullEntryCompare)); // test deleting item somewhere between first and last // again, file stays untouched, message still accessible through MBox, but item gone Item item2; item2.setMimeType(KMime::Message::mimeType()); item2.setId(KRandom::random()); item2.setRemoteId(QString::number(entryList1.value(1).messageOffset())); item2.setParentCollection(collection1); job = mStore->deleteItem(item2); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); item = job->item(); QCOMPARE(item.id(), item2.id()); fileInfo1.refresh(); QCOMPARE((int)fileInfo1.size(), size1); QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); entryList = mbox1.entries(); QCOMPARE(entryList.count(), entryList1.count()); QCOMPARE(entryList.value(1).messageOffset(), entryList1.value(1).messageOffset()); var = job->property("compactStore"); QVERIFY(var.isValid()); QCOMPARE(var.type(), QVariant::Bool); QCOMPARE(var.toBool(), true); itemFetch = mStore->fetchItems(collection1); QVERIFY(itemFetch->exec()); QCOMPARE(itemFetch->error(), 0); items = itemFetch->items(); QCOMPARE((int)items.count(), 2); QCOMPARE(items.value(0).remoteId(), QString::number(entryList1.value(0).messageOffset())); QCOMPARE(items.value(1).remoteId(), QString::number(entryList1.value(2).messageOffset())); // test that the item is purged from the file on store compaction // non-last item purging changes all items after it storeCompact = mStore->compactStore(); - collectionsSpy = new QSignalSpy(storeCompact, SIGNAL(collectionsChanged(Akonadi::Collection::List))); - itemsSpy = new QSignalSpy(storeCompact, SIGNAL(itemsChanged(Akonadi::Item::List))); + collectionsSpy = new QSignalSpy(storeCompact, &FileStore::StoreCompactJob::collectionsChanged); + itemsSpy = new QSignalSpy(storeCompact, &FileStore::StoreCompactJob::itemsChanged); QVERIFY(storeCompact->exec()); QCOMPARE(storeCompact->error(), 0); collections = storeCompact->changedCollections(); QCOMPARE(collections.count(), 1); items = storeCompact->changedItems(); QCOMPARE(items.count(), 1); QCOMPARE(collectionsFromSpy(collectionsSpy), collections); QCOMPARE(itemsFromSpy(itemsSpy), items); Item item3; item3.setRemoteId(QString::number(entryList1.value(2).messageOffset())); item = items.first(); QCOMPARE(item3.remoteId(), item.remoteId()); QVERIFY(item.hasAttribute()); FileStore::EntityCompactChangeAttribute *attribute = item.attribute(); QString newRemoteId = attribute->remoteId(); QVERIFY(!newRemoteId.isEmpty()); fileInfo1.refresh(); QVERIFY(fileInfo1.size() < size1); size1 = fileInfo1.size(); QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); entryList = mbox1.entries(); QCOMPARE(QString::number(entryList.value(1).messageOffset()), newRemoteId); entryList1.removeAt(1); QCOMPARE(entryList1.count(), entryList.count()); QCOMPARE(QString::number(entryList1.value(1).messageOffset()), item3.remoteId()); } void ItemDeleteTest::testCachePreservation() { QDir topDir(mDir->path()); QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1"))); QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2"))); KPIM::Maildir topLevelMd(topDir.path(), true); KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1")); QVERIFY(md1.isValid()); QSet entrySet1 = QSet::fromList(md1.entryList()); QCOMPARE((int)entrySet1.count(), 4); QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2")); KMBox::MBox mbox2; QVERIFY(mbox2.load(fileInfo2.absoluteFilePath())); KMBox::MBoxEntry::List entryList2 = mbox2.entries(); QCOMPARE((int)entryList2.count(), 4); mStore->setPath(topDir.path()); // common variables const QVariant colListVar = QVariant::fromValue(Collection::List()); QVariant var; Collection::List collections; Item::List items; QMap flagCounts; FileStore::ItemDeleteJob *job = nullptr; FileStore::ItemFetchJob *itemFetch = nullptr; // test deleting from maildir Collection collection1; collection1.setName(QStringLiteral("collection1")); collection1.setRemoteId(QStringLiteral("collection1")); collection1.setParentCollection(mStore->topLevelCollection()); Item item1; item1.setMimeType(KMime::Message::mimeType()); item1.setId(KRandom::random()); item1.setRemoteId(*entrySet1.cbegin()); item1.setParentCollection(collection1); job = mStore->deleteItem(item1); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); Item item = job->item(); QCOMPARE(item.id(), item1.id()); // check for index preservation var = job->property("onDiskIndexInvalidated"); QVERIFY(var.isValid()); QCOMPARE(var.userType(), colListVar.userType()); collections = var.value(); QCOMPARE((int)collections.count(), 1); QCOMPARE(collections.first(), collection1); // get the items and check the flags (see data/README) itemFetch = mStore->fetchItems(collection1); QVERIFY(itemFetch->exec()); QCOMPARE(itemFetch->error(), 0); items = itemFetch->items(); QCOMPARE((int)items.count(), 3); Q_FOREACH (const Item &item, items) { Q_FOREACH (const QByteArray &flag, item.flags()) { ++flagCounts[ flag ]; } } // TODO since we don't know which message we've deleted, we can only check if some flags are present int flagCountTotal = 0; Q_FOREACH (int count, flagCounts) { flagCountTotal += count; } QVERIFY(flagCountTotal > 0); flagCounts.clear(); // test deleting from mbox Collection collection2; collection2.setName(QStringLiteral("collection2")); collection2.setRemoteId(QStringLiteral("collection2")); collection2.setParentCollection(mStore->topLevelCollection()); Item item2; item2.setMimeType(KMime::Message::mimeType()); item2.setId(KRandom::random()); item2.setRemoteId(QString::number(entryList2.value(1).messageOffset())); item2.setParentCollection(collection2); job = mStore->deleteItem(item2); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); item = job->item(); QCOMPARE(item.id(), item2.id()); // at this point no change has been written to disk yet, so index and mbox file are // still in sync var = job->property("onDiskIndexInvalidated"); QVERIFY(!var.isValid()); FileStore::StoreCompactJob *storeCompact = mStore->compactStore(); QVERIFY(storeCompact->exec()); QCOMPARE(storeCompact->error(), 0); // check for index preservation var = storeCompact->property("onDiskIndexInvalidated"); QVERIFY(var.isValid()); QCOMPARE(var.userType(), colListVar.userType()); collections = var.value(); QCOMPARE((int)collections.count(), 1); QCOMPARE(collections.first(), collection2); // get the items and check the flags (see data/README) itemFetch = mStore->fetchItems(collection2); QVERIFY(itemFetch->exec()); QCOMPARE(itemFetch->error(), 0); items = itemFetch->items(); QCOMPARE((int)items.count(), 3); Q_FOREACH (const Item &item, items) { Q_FOREACH (const QByteArray &flag, item.flags()) { ++flagCounts[ flag ]; } } // we've deleted message 2, it flagged TODO and seen QCOMPARE(flagCounts[ "\\SEEN" ], 1); QCOMPARE(flagCounts[ "\\FLAGGED" ], 1); flagCounts.clear(); } void ItemDeleteTest::testExpectedFailure() { QDir topDir(mDir->path()); QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1"))); QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2"))); KPIM::Maildir topLevelMd(topDir.path(), true); KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1")); QVERIFY(md1.isValid()); QSet entrySet1 = QSet::fromList(md1.entryList()); QCOMPARE((int)entrySet1.count(), 4); QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2")); KMBox::MBox mbox2; QVERIFY(mbox2.load(fileInfo2.absoluteFilePath())); KMBox::MBoxEntry::List entryList2 = mbox2.entries(); QCOMPARE((int)entryList2.count(), 4); mStore->setPath(topDir.path()); // common variables FileStore::ItemDeleteJob *job = nullptr; FileStore::ItemFetchJob *itemFetch = nullptr; FileStore::StoreCompactJob *storeCompact = nullptr; // test failure of fetching an item previously deleted from maildir Collection collection1; collection1.setName(QStringLiteral("collection1")); collection1.setRemoteId(QStringLiteral("collection1")); collection1.setParentCollection(mStore->topLevelCollection()); Item item1_1; item1_1.setRemoteId(*entrySet1.cbegin()); item1_1.setParentCollection(collection1); job = mStore->deleteItem(item1_1); QVERIFY(job->exec()); itemFetch = mStore->fetchItem(item1_1); QVERIFY(!itemFetch->exec()); QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext); // test failure of deleting an item from maildir again job = mStore->deleteItem(item1_1); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); // test failure of fetching an item previously deleted from mbox Collection collection2; collection2.setName(QStringLiteral("collection2")); collection2.setRemoteId(QStringLiteral("collection2")); collection2.setParentCollection(mStore->topLevelCollection()); Item item2_1; item2_1.setRemoteId(QString::number(entryList2.value(0).messageOffset())); item2_1.setParentCollection(collection2); job = mStore->deleteItem(item2_1); QVERIFY(job->exec()); itemFetch = mStore->fetchItem(item2_1); QVERIFY(!itemFetch->exec()); QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext); // test failure of deleting an item from mbox again job = mStore->deleteItem(item2_1); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext); // compact store and check that offset 0 is a valid remoteId again, but // offset of other items (e.f. item 4) are no longer valid (moved to the front of the file) storeCompact = mStore->compactStore(); QVERIFY(storeCompact->exec()); itemFetch = mStore->fetchItem(item2_1); QVERIFY(itemFetch->exec()); Item item4_1; item4_1.setRemoteId(QString::number(entryList2.value(3).messageOffset())); item4_1.setParentCollection(collection2); itemFetch = mStore->fetchItem(item4_1); QVERIFY(!itemFetch->exec()); QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext); } QTEST_MAIN(ItemDeleteTest) #include "itemdeletetest.moc" diff --git a/resources/mixedmaildir/autotests/storecompacttest.cpp b/resources/mixedmaildir/autotests/storecompacttest.cpp index 02a8cf12b..b4ff51bbc 100644 --- a/resources/mixedmaildir/autotests/storecompacttest.cpp +++ b/resources/mixedmaildir/autotests/storecompacttest.cpp @@ -1,409 +1,409 @@ /* This file is part of the KDE project Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com 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 "mixedmaildirstore.h" #include "testdatautil.h" #include "filestore/entitycompactchangeattribute.h" #include "filestore/itemdeletejob.h" #include "filestore/storecompactjob.h" #include #include #include #include #include #include using namespace Akonadi; using namespace KMBox; static Collection::List collectionsFromSpy(QSignalSpy *spy) { Collection::List collections; QListIterator > it(*spy); while (it.hasNext()) { const QList invocation = it.next(); Q_ASSERT(invocation.count() == 1); collections << invocation.first().value(); } return collections; } static Item::List itemsFromSpy(QSignalSpy *spy) { Item::List items; QListIterator > it(*spy); while (it.hasNext()) { const QList invocation = it.next(); Q_ASSERT(invocation.count() == 1); items << invocation.first().value(); } return items; } static bool fullEntryCompare(const MBoxEntry &a, const MBoxEntry &b) { return a.messageOffset() == b.messageOffset() && a.separatorSize() == b.separatorSize() && a.messageSize() == b.messageSize(); } static quint64 changedOffset(const Item &item) { Q_ASSERT(item.hasAttribute()); const QString remoteId = item.attribute()->remoteId(); Q_ASSERT(!remoteId.isEmpty()); bool ok = false; const quint64 result = remoteId.toULongLong(&ok); Q_ASSERT(ok); return result; } class StoreCompactTest : public QObject { Q_OBJECT public: StoreCompactTest() : QObject() , mStore(nullptr) , mDir(nullptr) { // for monitoring signals qRegisterMetaType(); qRegisterMetaType(); } ~StoreCompactTest() { delete mStore; delete mDir; } private: MixedMaildirStore *mStore = nullptr; QTemporaryDir *mDir = nullptr; private Q_SLOTS: void init(); void cleanup(); void testCompact(); }; void StoreCompactTest::init() { mStore = new MixedMaildirStore; mDir = new QTemporaryDir; QVERIFY(mDir->isValid()); QVERIFY(QDir(mDir->path()).exists()); } void StoreCompactTest::cleanup() { delete mStore; mStore = nullptr; delete mDir; mDir = nullptr; } void StoreCompactTest::testCompact() { QDir topDir(mDir->path()); QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection1"))); QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2"))); QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection3"))); QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection4"))); QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1")); MBox mbox1; QVERIFY(mbox1.load(fileInfo1.absoluteFilePath())); MBoxEntry::List entryList1 = mbox1.entries(); QCOMPARE((int)entryList1.count(), 4); QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2")); MBox mbox2; QVERIFY(mbox2.load(fileInfo2.absoluteFilePath())); MBoxEntry::List entryList2 = mbox2.entries(); QCOMPARE((int)entryList2.count(), 4); QFileInfo fileInfo3(topDir.path(), QStringLiteral("collection3")); MBox mbox3; QVERIFY(mbox3.load(fileInfo3.absoluteFilePath())); MBoxEntry::List entryList3 = mbox3.entries(); QCOMPARE((int)entryList3.count(), 4); QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4")); MBox mbox4; QVERIFY(mbox4.load(fileInfo4.absoluteFilePath())); MBoxEntry::List entryList4 = mbox4.entries(); QCOMPARE((int)entryList4.count(), 4); mStore->setPath(topDir.path()); // common variables FileStore::CollectionFetchJob *collectionFetch = nullptr; FileStore::ItemDeleteJob *itemDelete = nullptr; FileStore::StoreCompactJob *job = nullptr; Collection::List collections; Item::List items; QSignalSpy *collectionSpy = nullptr; QSignalSpy *itemSpy = nullptr; MBoxEntry::List entryList; Collection collection; FileStore::EntityCompactChangeAttribute *attribute = nullptr; const QVariant colListVar = QVariant::fromValue(Collection::List()); QVariant var; // test compact after delete from the end of an mbox Collection collection1; collection1.setName(QStringLiteral("collection1")); collection1.setRemoteId(QStringLiteral("collection1")); collection1.setParentCollection(mStore->topLevelCollection()); Item item1; item1.setRemoteId(QString::number(entryList1.last().messageOffset())); item1.setParentCollection(collection1); itemDelete = mStore->deleteItem(item1); QVERIFY(itemDelete->exec()); job = mStore->compactStore(); - collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List))); - itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List))); + collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged); + itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); collections = job->changedCollections(); items = job->changedItems(); QCOMPARE(collections.count(), 0); QCOMPARE(items.count(), 0); QCOMPARE(collectionSpy->count(), 0); QCOMPARE(itemSpy->count(), 0); QVERIFY(mbox1.load(mbox1.fileName())); entryList = mbox1.entries(); entryList1.pop_back(); QVERIFY(std::equal(entryList.begin(), entryList.end(), entryList1.begin(), fullEntryCompare)); var = job->property("onDiskIndexInvalidated"); QVERIFY(var.isValid()); QCOMPARE(var.userType(), colListVar.userType()); collections = var.value(); QCOMPARE((int)collections.count(), 1); QCOMPARE(collections, Collection::List() << collection1); // test compact after delete from before the end of an mbox Collection collection2; collection2.setName(QStringLiteral("collection2")); collection2.setRemoteId(QStringLiteral("collection2")); collection2.setParentCollection(mStore->topLevelCollection()); Item item2; item2.setRemoteId(QString::number(entryList2.first().messageOffset())); item2.setParentCollection(collection2); itemDelete = mStore->deleteItem(item2); QVERIFY(itemDelete->exec()); job = mStore->compactStore(); - collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List))); - itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List))); + collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged); + itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); collections = job->changedCollections(); items = job->changedItems(); QCOMPARE(collections.count(), 1); QCOMPARE(items.count(), 3); QCOMPARE(collectionSpy->count(), 1); QCOMPARE(itemSpy->count(), 1); QCOMPARE(collectionsFromSpy(collectionSpy), collections); QCOMPARE(itemsFromSpy(itemSpy), items); collection = collections.first(); QCOMPARE(collection, collection2); QVERIFY(collection.hasAttribute()); attribute = collection.attribute(); QCOMPARE(attribute->remoteRevision().toInt(), collection2.remoteRevision().toInt() + 1); QVERIFY(mbox2.load(mbox2.fileName())); entryList = mbox2.entries(); entryList2.pop_front(); for (int i = 0; i < items.count(); ++i) { entryList2[ i ] = MBoxEntry(changedOffset(items[ i ])); } QCOMPARE(entryList, entryList2); var = job->property("onDiskIndexInvalidated"); QVERIFY(var.isValid()); QCOMPARE(var.userType(), colListVar.userType()); collections = var.value(); QCOMPARE((int)collections.count(), 1); QCOMPARE(collections, Collection::List() << collection2); collectionFetch = mStore->fetchCollections(collection2, FileStore::CollectionFetchJob::Base); QVERIFY(collectionFetch->exec()); collections = collectionFetch->collections(); QCOMPARE((int)collections.count(), 1); collection = collections.first(); QCOMPARE(collection, collection2); QCOMPARE(collection.remoteRevision(), attribute->remoteRevision()); // test compact after delete from before the end of more than one mbox Collection collection3; collection3.setName(QStringLiteral("collection3")); collection3.setRemoteId(QStringLiteral("collection3")); collection3.setParentCollection(mStore->topLevelCollection()); Item item3; item3.setRemoteId(QString::number(entryList3.first().messageOffset())); item3.setParentCollection(collection3); itemDelete = mStore->deleteItem(item3); QVERIFY(itemDelete->exec()); Collection collection4; collection4.setName(QStringLiteral("collection4")); collection4.setRemoteId(QStringLiteral("collection4")); collection4.setParentCollection(mStore->topLevelCollection()); Item item4; item4.setRemoteId(QString::number(entryList3.value(1).messageOffset())); item4.setParentCollection(collection4); itemDelete = mStore->deleteItem(item4); QVERIFY(itemDelete->exec()); job = mStore->compactStore(); - collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List))); - itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List))); + collectionSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::collectionsChanged); + itemSpy = new QSignalSpy(job, &FileStore::StoreCompactJob::itemsChanged); QVERIFY(job->exec()); QCOMPARE(job->error(), 0); collections = job->changedCollections(); items = job->changedItems(); QCOMPARE(collections.count(), 2); QCOMPARE(items.count(), 5); QCOMPARE(collectionSpy->count(), 2); QCOMPARE(itemSpy->count(), 2); QCOMPARE(collectionsFromSpy(collectionSpy), collections); QCOMPARE(itemsFromSpy(itemSpy), items); QHash compactedCollections; Q_FOREACH (const Collection &col, collections) { compactedCollections.insert(col.remoteId(), col); } QCOMPARE(compactedCollections.count(), 2); QVERIFY(compactedCollections.contains(collection3.remoteId())); collection = compactedCollections[ collection3.remoteId() ]; QCOMPARE(collection, collection3); QVERIFY(collection.hasAttribute()); attribute = collection.attribute(); QCOMPARE(attribute->remoteRevision().toInt(), collection3.remoteRevision().toInt() + 1); QVERIFY(mbox3.load(mbox3.fileName())); entryList = mbox3.entries(); // The order of items depends on the order of iteration of a QHash in MixedMaildirStore. // This makes sure that the items are always sorted by collection and offset std::sort(items.begin(), items.end(), [](const Akonadi::Item &left, const Akonadi::Item &right) { return left.parentCollection().remoteId().compare(right.parentCollection().remoteId()) < 0 || (left.parentCollection().remoteId() == right.parentCollection().remoteId() && changedOffset(left) < changedOffset(right)); }); entryList3.pop_front(); for (int i = 0; i < entryList3.count(); ++i) { entryList3[ i ] = MBoxEntry(changedOffset(items.first())); items.pop_front(); } QCOMPARE(entryList, entryList3); QVERIFY(compactedCollections.contains(collection4.remoteId())); collection = compactedCollections[ collection4.remoteId() ]; QCOMPARE(collection, collection4); QVERIFY(collection.hasAttribute()); attribute = collection.attribute(); QCOMPARE(attribute->remoteRevision().toInt(), collection4.remoteRevision().toInt() + 1); QVERIFY(mbox4.load(mbox4.fileName())); entryList = mbox4.entries(); entryList4.removeAt(1); for (int i = 0; i < items.count(); ++i) { entryList4[ i + 1 ] = MBoxEntry(changedOffset(items[ i ])); } QCOMPARE(entryList, entryList4); var = job->property("onDiskIndexInvalidated"); QVERIFY(var.isValid()); QCOMPARE(var.userType(), colListVar.userType()); collections = var.value(); QCOMPARE((int)collections.count(), 2); QCOMPARE(collections, Collection::List() << collection3 << collection4); } QTEST_MAIN(StoreCompactTest) #include "storecompacttest.moc"