File indexing completed on 2024-05-19 16:31:07

0001 /*
0002     SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include <QDebug>
0008 #include <QMutex>
0009 #include <QSignalSpy>
0010 #include <QTcpServer>
0011 #include <QTest>
0012 #include <QThread>
0013 #include <QTimer>
0014 #include <QWaitCondition>
0015 
0016 #include "../connection.h"
0017 
0018 namespace Bugzilla
0019 {
0020 class ConnectionTest : public QObject
0021 {
0022     Q_OBJECT
0023 private Q_SLOTS:
0024 
0025     void initTestCase()
0026     {
0027     }
0028 
0029     void testDefaultRoot()
0030     {
0031         // Make sure the default root is well formed.
0032         // This talks to bugzilla directly! To avoid flakeyness the actual
0033         // HTTP interaction is qwaiting and retrying a bunch of times.
0034         // Obviously still not ideal.
0035         Bugzilla::HTTPConnection c;
0036         QVERIFY(c.root().toString().endsWith("/rest"));
0037         QVERIFY(QTest::qWaitFor(
0038             [&]() {
0039                 APIJob *job = c.get("/version");
0040                 job->exec();
0041                 try {
0042                     job->document();
0043                 } catch (Bugzilla::Exception &e) {
0044                     QTest::qSleep(1000);
0045                     return false;
0046                 }
0047 
0048                 return true;
0049             },
0050             5000));
0051     }
0052 
0053     void testGet()
0054     {
0055         qDebug() << Q_FUNC_INFO;
0056         // qhttpserver is still in qt-labs. as a simple solution do some dumb
0057         // http socketing.
0058         QTcpServer t;
0059         QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true);
0060         connect(&t, &QTcpServer::newConnection, &t, [&t]() {
0061             QTcpSocket *socket = t.nextPendingConnection();
0062             socket->waitForReadyRead();
0063             QString httpBlob = socket->readAll();
0064             qDebug() << httpBlob;
0065             // The query is important to see if this actually gets properly
0066             // passed along!
0067             // Reason it has a plus:
0068             // https://bugs.kde.org/show_bug.cgi?id=413920
0069             // QUrlQuery doesn't encode plus characters, bugzilla serverside however
0070             // needs it encoded which is a bit weird because it doesn't actually
0071             // require full-form encoding either (i.e. space becomes plus and
0072             // plus becomes encoded).
0073             //
0074             // This further broke because we force-recoded the query items but then that caused over-decoding (%FF)
0075             // because QUrlQuery internally stores the DecodeReserved variant and we blindly FullDecode leading to the
0076             // verbatim percent value getting decoded. At the same time we can't DecodeReserved because that would
0077             // still decode verbatim reserved sequences in the input password (e.g. the password containing %3C aka <).
0078             // https://bugs.kde.org/show_bug.cgi?id=435442
0079             if (httpBlob.startsWith("GET /hi?informal=yes%2Bcertainly&password=%253C___m%26T9zSZ%3E0%2Cq%25FFDN")) {
0080                 QFile file(QFINDTESTDATA("data/hi.http"));
0081                 file.open(QFile::ReadOnly | QFile::Text);
0082                 socket->write(file.readAll());
0083                 socket->waitForBytesWritten();
0084                 socket->disconnect();
0085                 socket->close();
0086                 return;
0087             }
0088             qDebug() << httpBlob;
0089             Q_ASSERT_X(false, "server", "Unexpected request");
0090         });
0091 
0092         QUrl root("http://localhost");
0093         root.setPort(t.serverPort());
0094         HTTPConnection c(root);
0095         Query query;
0096         query.addQueryItem("informal", "yes+certainly");
0097         query.addQueryItem("password", "%3C___m&T9zSZ>0,q%FFDN");
0098         auto job = c.get("/hi", query);
0099         job->exec();
0100         QCOMPARE(job->data(), "Hello!\n");
0101     }
0102 
0103     void testGetJsonError()
0104     {
0105         qDebug() << Q_FUNC_INFO;
0106         // qhttpserver is still in qt-labs. as a simple solution do some dumb
0107         // http socketing.
0108         QTcpServer t;
0109         QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true);
0110         connect(&t, &QTcpServer::newConnection, &t, [&t]() {
0111             QTcpSocket *socket = t.nextPendingConnection();
0112             socket->waitForReadyRead();
0113             QString httpBlob = socket->readAll();
0114             qDebug() << httpBlob;
0115             QFile file(QFINDTESTDATA("data/error.http"));
0116             file.open(QFile::ReadOnly | QFile::Text);
0117             socket->write(file.readAll());
0118             socket->waitForBytesWritten();
0119             socket->disconnect();
0120             socket->close();
0121             return;
0122         });
0123 
0124         QUrl root("http://localhost");
0125         root.setPort(t.serverPort());
0126         HTTPConnection c(root);
0127         auto job = c.get("/hi");
0128         job->exec();
0129         QVERIFY_EXCEPTION_THROWN(job->document(), Bugzilla::APIException);
0130     }
0131 
0132     void testPut()
0133     {
0134         qDebug() << Q_FUNC_INFO;
0135         // qhttpserver is still in qt-labs. as a simple solution do some dumb
0136         // http socketing.
0137         QThread thread;
0138         // On the heap lest it gets destroyed on stack unwind (which would be
0139         // in the wrong thread!) and may fail assertions inside Qt when built
0140         // in debug mode as destruction entails posting events, which is ENOGOOD
0141         // across threads.
0142         auto *server = new QTcpServer;
0143         server->moveToThread(&thread);
0144 
0145         QString readBlob; // lambda member essentially
0146 
0147         connect(server, &QTcpServer::newConnection, server, [server, &readBlob]() { // clazy:exclude=lambda-in-connect
0148             QCOMPARE(server->thread(), QThread::currentThread());
0149             QTcpSocket *socket = server->nextPendingConnection();
0150             connect(socket, &QTcpSocket::readyRead, socket, [&readBlob, socket] { // clazy:exclude=lambda-in-connect
0151                 readBlob += socket->readAll();
0152                 readBlob.replace("\r\n", "\n");
0153                 auto parts = readBlob.split("\n");
0154                 if (parts.contains("PUT /put HTTP/1.1") && parts.contains("Content-Length: 12") && parts.contains("hello there!")) {
0155                     QFile file(QFINDTESTDATA("data/put.http"));
0156                     file.open(QFile::ReadOnly | QFile::Text);
0157                     QByteArray ret = file.readAll();
0158                     ret.replace("\n", "\r\n");
0159                     qDebug() << ret;
0160                     socket->write(ret);
0161                     socket->waitForBytesWritten();
0162                     socket->disconnect();
0163                     socket->close();
0164                     qDebug() << "socket closed";
0165                 }
0166             });
0167         });
0168         thread.start();
0169 
0170         QMutex portMutex;
0171         QWaitCondition portCondition;
0172         quint16 port;
0173         portMutex.lock();
0174         QTimer::singleShot(0, server, [server, &portMutex, &portCondition, &port]() {
0175             server->listen(QHostAddress::LocalHost, 0);
0176             QMutexLocker locker(&portMutex);
0177             port = server->serverPort();
0178             portCondition.wakeAll();
0179         });
0180         portCondition.wait(&portMutex);
0181         portMutex.unlock();
0182 
0183         QUrl root("http://localhost");
0184         root.setPort(server->serverPort());
0185         HTTPConnection c(root);
0186         APIJob *job = c.put("/put", "hello there!");
0187         KJob *kjob = job;
0188         QSignalSpy spy(job, &KJob::finished);
0189         kjob->start();
0190         // Because of how the request handling works the server may never return
0191         // anything, so wait for the reply, if it doesn't arrive something went
0192         // wrong with the server-side handling and the test cannot complete.
0193         QVERIFY(spy.wait());
0194 
0195         thread.quit();
0196         thread.wait();
0197         thread.terminate();
0198 
0199         QCOMPARE(job->error(), KJob::NoError);
0200         QCOMPARE(job->data(), "General Kenobi!\r\n");
0201     }
0202 };
0203 
0204 } // namespace Bugzilla
0205 
0206 QTEST_MAIN(Bugzilla::ConnectionTest)
0207 
0208 #include "connectiontest.moc"