diff --git a/autotests/davcollectionsmultifetchjobtest.cpp b/autotests/davcollectionsmultifetchjobtest.cpp index da79b31..283ce43 100644 --- a/autotests/davcollectionsmultifetchjobtest.cpp +++ b/autotests/davcollectionsmultifetchjobtest.cpp @@ -1,80 +1,101 @@ /* SPDX-FileCopyrightText: 2020 David Faure SPDX-License-Identifier: LGPL-2.0-or-later */ #include "davcollectionsmultifetchjobtest.h" #include "fakeserver.h" #include #include #include using KDAV::DavCollection; Q_DECLARE_METATYPE(KDAV::Protocol) void DavCollectionsMultiFetchJobTest::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To let ctest exit, we shouldn't start kio_http_cache_cleaner qputenv("KIO_DISABLE_CACHE_CLEANER", "yes"); qRegisterMetaType(); } void DavCollectionsMultiFetchJobTest::runSuccessfullTest() { FakeServer fakeServer(5990); QUrl url(QStringLiteral("http://localhost/caldav")); url.setPort(fakeServer.port()); KDAV::DavUrl davUrl1(url, KDAV::CalDav); QUrl url2(url); url2.setPath(QStringLiteral("/carddav")); KDAV::DavUrl davUrl2(url2, KDAV::CardDav); auto job = new KDAV::DavCollectionsMultiFetchJob({davUrl1, davUrl2}); QSignalSpy spy(job, &KDAV::DavCollectionsMultiFetchJob::collectionDiscovered); fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav.txt")); fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav-collections.txt")); fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-carddav.txt")); fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-carddav-collections.txt")); fakeServer.startAndWait(); job->exec(); - fakeServer.quit(); QVERIFY(fakeServer.isAllScenarioDone()); QCOMPARE(job->error(), 0); const KDAV::DavCollection::List collections = job->collections(); QCOMPARE(collections.count(), 2); const KDAV::DavCollection calendar = collections.at(0); QCOMPARE(calendar.displayName(), QStringLiteral("Test1 User")); QCOMPARE(calendar.contentTypes(), DavCollection::Events | DavCollection::Todos | DavCollection::FreeBusy | DavCollection::Journal); QCOMPARE(calendar.url().url().path(), QStringLiteral("/caldav.php/test1.user/home/")); QCOMPARE(calendar.CTag(), QStringLiteral("12345")); QCOMPARE(calendar.privileges(), KDAV::Read); const KDAV::DavCollection addressbook = collections.at(1); QCOMPARE(addressbook.displayName(), QStringLiteral("My Address Book")); QCOMPARE(addressbook.contentTypes(), DavCollection::Contacts); QCOMPARE(addressbook.url().url().path(), QStringLiteral("/carddav.php/test1.user/home/")); QCOMPARE(addressbook.CTag(), QStringLiteral("3145")); QCOMPARE(addressbook.privileges(), KDAV::All); QCOMPARE(spy.count(), 2); QCOMPARE(int(spy.at(0).at(0).value()), int(KDAV::CalDav)); QCOMPARE(spy.at(0).at(1).toString(), calendar.url().url().toString()); QCOMPARE(spy.at(0).at(2).toString(), url.toString()); QCOMPARE(int(spy.at(1).at(0).value()), int(KDAV::CardDav)); QCOMPARE(spy.at(1).at(1).toString(), addressbook.url().url().toString()); QCOMPARE(spy.at(1).at(2).toString(), url2.toString()); } +void DavCollectionsMultiFetchJobTest::shouldFailOnError() +{ + FakeServer fakeServer(5990); + QUrl url(QStringLiteral("http://localhost/caldav")); + url.setPort(fakeServer.port()); + KDAV::DavUrl davUrl1(url, KDAV::CalDav); + QUrl urlError(url); + urlError.setPath(QStringLiteral("/does_not_exist")); + KDAV::DavUrl davUrlError(urlError, KDAV::CalDav); + + auto job = new KDAV::DavCollectionsMultiFetchJob({davUrl1, davUrlError}); + + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav.txt")); + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav-collections.txt")); + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-error.txt")); + fakeServer.startAndWait(); + job->exec(); + + QVERIFY(fakeServer.isAllScenarioDone()); + QCOMPARE(job->error(), 300); +} + QTEST_MAIN(DavCollectionsMultiFetchJobTest) diff --git a/autotests/davcollectionsmultifetchjobtest.h b/autotests/davcollectionsmultifetchjobtest.h index a165c86..9a652af 100644 --- a/autotests/davcollectionsmultifetchjobtest.h +++ b/autotests/davcollectionsmultifetchjobtest.h @@ -1,21 +1,22 @@ /* SPDX-FileCopyrightText: 2020 David Faure SPDX-License-Identifier: LGPL-2.0-or-later */ #ifndef DAVITEMFETCHJOB_TEST_H #define DAVITEMFETCHJOB_TEST_H #include class DavCollectionsMultiFetchJobTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void runSuccessfullTest(); + void shouldFailOnError(); }; #endif diff --git a/autotests/davitemfetchjobtest.cpp b/autotests/davitemfetchjobtest.cpp index fbb2854..e3ea295 100644 --- a/autotests/davitemfetchjobtest.cpp +++ b/autotests/davitemfetchjobtest.cpp @@ -1,53 +1,52 @@ /* SPDX-FileCopyrightText: 2017 Sandro Knauß SPDX-License-Identifier: LGPL-2.0-or-later */ #include "davitemfetchjobtest.h" #include "fakeserver.h" #include #include void DavItemFetchJobTest::initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); // To let ctest exit, we shouldn't start kio_http_cache_cleaner qputenv("KIO_DISABLE_CACHE_CLEANER", "yes"); } void DavItemFetchJobTest::runSuccessfullTest() { FakeServer fakeServer(5989); QUrl url(QStringLiteral("http://localhost/item")); url.setPort(fakeServer.port()); KDAV::DavUrl davUrl(url, KDAV::CardDav); KDAV::DavItem item(davUrl, QString(), QByteArray(), QString()); auto job = new KDAV::DavItemFetchJob(item); fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemfetchjob.txt")); fakeServer.startAndWait(); job->exec(); - fakeServer.quit(); QVERIFY(fakeServer.isAllScenarioDone()); QCOMPARE(job->error(), 0); QCOMPARE(item.data(), QByteArray()); QCOMPARE(item.etag(), QString()); QCOMPARE(item.contentType(), QString()); item = job->item(); QByteArray data("BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Kolab//iRony DAV Server 0.3.1//Sabre//Sabre VObject 2.1.7//EN\r\nUID:12345678-1234-1234-1234-123456789abc\r\nFN:John2 Doe\r\nN:Doe;John2;;;\r\nEMAIL;TYPE=INTERNET;TYPE=HOME:john2.doe@example.com\r\nREV;VALUE=DATE-TIME:20170104T182647Z\r\nEND:VCARD\r\n"); QCOMPARE(item.data(), data); QCOMPARE(item.etag(), QStringLiteral("7a33141f192d904d-47")); QCOMPARE(item.contentType(), QStringLiteral("text/x-vcard")); } QTEST_MAIN(DavItemFetchJobTest) diff --git a/autotests/fakeserver.cpp b/autotests/fakeserver.cpp index 6538d2c..7b1969d 100644 --- a/autotests/fakeserver.cpp +++ b/autotests/fakeserver.cpp @@ -1,220 +1,224 @@ /* SPDX-FileCopyrightText: 2008 Omat Holding B .V. SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB , a KDAB Group company SPDX-FileContributor: Kevin Ottens SPDX-FileCopyrightText: 2017 Sandro Kanuß SPDX-License-Identifier: GPL-2.0-or-later */ // Own #include "fakeserver.h" // Qt #include #include #include #include FakeServer::FakeServer(int port, QObject *parent) - : QThread(parent) + : QObject(parent) + , m_thread(new QThread) , m_port(port) { - moveToThread(this); + moveToThread(m_thread); } FakeServer::~FakeServer() { - quit(); - wait(); + QMetaObject::invokeMethod(this, &FakeServer::cleanup); + m_thread->quit(); + m_thread->wait(); + delete m_thread; } void FakeServer::startAndWait() { - start(); - // this will block until the event queue starts - QMetaObject::invokeMethod(this, &FakeServer::started, Qt::BlockingQueuedConnection); + m_thread->start(); + // this will block until the init code happens and the event queue starts + QMetaObject::invokeMethod(this, &FakeServer::init, Qt::BlockingQueuedConnection); } void FakeServer::dataAvailable() { QMutexLocker locker(&m_mutex); QTcpSocket *socket = qobject_cast(sender()); Q_ASSERT(socket != nullptr); - int scenarioNumber = m_clientSockets.indexOf(socket); - - if (scenarioNumber >= m_scenarios.size()) { - qWarning() << "There is no scenario for socket" << scenarioNumber << ", we got more connections than expected"; + int scenarioNumber = -1; + readClientPart(socket, &scenarioNumber); + if (scenarioNumber >= 0) { + Q_ASSERT(scenarioNumber < m_scenarios.count()); + writeServerPart(socket, scenarioNumber); } - - readClientPart(scenarioNumber); - writeServerPart(scenarioNumber); } void FakeServer::newConnection() { QMutexLocker locker(&m_mutex); m_clientSockets << m_tcpServer->nextPendingConnection(); connect(m_clientSockets.last(), &QTcpSocket::readyRead, this, &FakeServer::dataAvailable); } -void FakeServer::run() +void FakeServer::init() { m_tcpServer = new QTcpServer(); if (!m_tcpServer->listen(QHostAddress(QHostAddress::LocalHost), m_port)) { qFatal("Unable to start the server"); } connect(m_tcpServer, &QTcpServer::newConnection, this, &FakeServer::newConnection); - - exec(); - - qDeleteAll(m_clientSockets); - - delete m_tcpServer; } -void FakeServer::started() +void FakeServer::cleanup() { - // do nothing: this is a dummy slot used by startAndWait() + qDeleteAll(m_clientSockets); + delete m_tcpServer; } void FakeServer::setScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios.clear(); m_scenarios << scenario; } void FakeServer::addScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios << scenario; } void FakeServer::addScenarioFromFile(const QString &fileName) { QFile file(fileName); file.open(QFile::ReadOnly); QList scenario; while (!file.atEnd()) { scenario << file.readLine().trimmed(); } file.close(); addScenario(scenario); } bool FakeServer::isScenarioDone(int scenarioNumber) const { QMutexLocker locker(&m_mutex); if (scenarioNumber < m_scenarios.size()) { return m_scenarios[scenarioNumber].isEmpty(); } else { return true; // Non existent hence empty, right? } } bool FakeServer::isAllScenarioDone() const { QMutexLocker locker(&m_mutex); for (const QList &scenario : qAsConst(m_scenarios)) { if (!scenario.isEmpty()) { return false; } } return true; } -void FakeServer::writeServerPart(int scenarioNumber) +void FakeServer::writeServerPart(QTcpSocket *clientSocket, int scenarioNumber) { QList scenario = m_scenarios[scenarioNumber]; - QTcpSocket *clientSocket = m_clientSockets[scenarioNumber]; while (!scenario.isEmpty() && scenario.first().startsWith("S: ")) { QByteArray rule = scenario.takeFirst(); QByteArray payload = rule.mid(3); clientSocket->write(payload + "\r\n"); } QByteArray data; while (!scenario.isEmpty() && scenario.first().startsWith("D: ")) { QByteArray rule = scenario.takeFirst(); QByteArray payload = rule.mid(3); data.append(payload + "\r\n"); } clientSocket->write(QStringLiteral("Content-Length: %1\r\n\r\n").arg(data.length()).toLatin1()); clientSocket->write(data); if (!scenario.isEmpty() && scenario.first().startsWith("X")) { scenario.takeFirst(); clientSocket->close(); } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("C: ")); } m_scenarios[scenarioNumber] = scenario; } -void FakeServer::readClientPart(int scenarioNumber) +void FakeServer::readClientPart(QTcpSocket *socket, int *scenarioNumber) { - QList scenario = m_scenarios[scenarioNumber]; - QTcpSocket *socket = m_clientSockets[scenarioNumber]; QByteArray line = socket->readLine(); qDebug() << "Read client request" << line; + const auto it = std::find_if(m_scenarios.begin(), m_scenarios.end(), [&](const QList &scenario) { + return !scenario.isEmpty() && scenario.at(0).mid(3) + "\r\n" == line; + }); + if (it == m_scenarios.end()) { + qWarning() << "No server response available for" << line; + QFAIL("Unexpected request"); + return; + } + QList scenario = *it; QVector header; while (line != "\r\n") { header << line; if (socket->bytesAvailable() == 0 && !socket->waitForReadyRead()) { qDebug() << header; QFAIL("could not read all headers"); return; } line = socket->readLine(); } while (!scenario.isEmpty() && scenario.first().startsWith("C: ")) { QByteArray expected = scenario.takeFirst().mid(3) + "\r\n"; if (!header.contains(expected)) { qWarning() << expected << "not found in header. Here's what we got:"; qWarning() << header; QVERIFY(false); break; } } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("S: ")); } - m_scenarios[scenarioNumber] = scenario; + *it = scenario; + *scenarioNumber = std::distance(m_scenarios.begin(), it); + QVERIFY(*scenarioNumber < m_scenarios.count()); } int FakeServer::port() const { return m_port; } diff --git a/autotests/fakeserver.h b/autotests/fakeserver.h index 9404b57..8b0635c 100644 --- a/autotests/fakeserver.h +++ b/autotests/fakeserver.h @@ -1,184 +1,177 @@ /* SPDX-FileCopyrightText: 2008 Omat Holding B .V. SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB , a KDAB Group company SPDX-FileContributor: Kevin Ottens SPDX-FileCopyrightText: 2017 Sandro Kanuß SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef FAKESERVER_H #define FAKESERVER_H #include #include #include #include Q_DECLARE_METATYPE(QList) /** * Pretends to be an DAV server for the purposes of unit tests. * * FakeServer does not really understand the DAV protocol. Instead, * you give it a script, or scenario, that lists how an DAV session * exchange should go. When it receives the client parts of the * scenario, it will respond with the following server parts. * * The server can be furnished with several scenarios. The first * scenario will be played out to the first client that connects, the * second scenario to the second client connection and so on. * * The fake server runs as a separate thread in the same process it * is started from, and listens for connections (see port() method) on the * local machine. * * Scenarios are in the form of protocol messages, with a tag at the * start to indicate whether it is message that will be sent by the * client ("C: ") or a response that should be sent by the server * ("S: "). Or ("D: ") for the exchanged data. Content-length header is added * automatically with the current length and also the empty line between Header * and Content. For example: * @code * C: GET /item HTTP/1.1 * S: HTTP/1.0 200 OK * D: much data * D: more data * X * @endcode * * A line starting with X indicates that the connection should be * closed by the server. This should be the last line in the * scenario. * A typical usage is something like * @code * QList scenario; * scenario << "C: GET /item HTTP/1.1" * << "S: HTTP/1.0 200 OK" * << "D: much data" * << "D: more data" * << "X"; * * FakeServer fakeServer; * fakeServer.setScenario(scenario); * fakeServer.startAndWait(); * * QUrl url(QStringLiteral("http://localhost/item")); * url.setPort(fakeServer.port()); * KDAV::DavUrl davUrl(url, KDAV::CardDav); * KDAV::DavItem item(davUrl, QString(), QByteArray(), QString()); * * auto job = new KDAV::DavItemFetchJob(item); * job->exec(); * fakeServer.quit(); * QVERIFY(fakeServer.isAllScenarioDone()); * @endcode */ -class FakeServer : public QThread +class FakeServer : public QObject { Q_OBJECT public: /** * Each unittest should use a different port so that they can be run in parallel */ FakeServer(int port = 5989, QObject *parent = nullptr); ~FakeServer(); /** * Starts the server and waits for it to be ready * * You should use this instead of start() to avoid race conditions. */ void startAndWait(); - /** - * Starts the fake server - * - * You should not call this directly. Use start() instead. - * - * @reimp - */ - void run() override; - /** * Removes any previously-added scenarios, and adds a new one * * After this, there will only be one scenario, and so the fake * server will only be able to service a single request. More * scenarios can be added with addScenario, though. * * @see addScenario()\n * addScenarioFromFile() */ void setScenario(const QList &scenario); /** * Adds a new scenario * * Note that scenarios will be used in the order that clients * connect. If this is the 5th scenario that has been added * (bearing in mind that setScenario() resets the scenario * count), it will be used to service the 5th client that * connects. * * @see addScenarioFromFile() * * @param scenario the scenario as a list of messages */ void addScenario(const QList &scenario); /** * Adds a new scenario from a local file * * Note that scenarios will be used in the order that clients * connect. If this is the 5th scenario that has been added * (bearing in mind that setScenario() resets the scenario * count), it will be used to service the 5th client that * connects. * * @see addScenario() * * @param fileName the name of the file that contains the * scenario; it will be split at line * boundaries, and excess whitespace will * be trimmed from the start and end of lines */ void addScenarioFromFile(const QString &fileName); /** * Checks whether a particular scenario has completed * * @param scenarioNumber the number of the scenario to check, * in order of addition/client connection */ bool isScenarioDone(int scenarioNumber) const; /** * Whether all the scenarios that were added to the fake * server have been completed. */ bool isAllScenarioDone() const; /** * Returns the port where the fake server is listening. */ int port() const; private Q_SLOTS: void newConnection(); void dataAvailable(); - void started(); + void init(); + void cleanup(); private: - void writeServerPart(int scenarioNumber); - void readClientPart(int scenarioNumber); + void writeServerPart(QTcpSocket *clientSocket, int scenarioNumber); + void readClientPart(QTcpSocket *socket, int *scenarioNumber); QList< QList > m_scenarios; - QTcpServer *m_tcpServer; + QTcpServer *m_tcpServer = nullptr; mutable QMutex m_mutex; QList m_clientSockets; + QThread *m_thread; int m_port; }; #endif