File indexing completed on 2024-04-28 11:35:24

0001 /*
0002     SPDX-FileCopyrightText: 2008 Omat Holding B .V. <info@omat.nl>
0003     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB , a KDAB Group company <info@kdab.com>
0004     SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0005     SPDX-FileCopyrightText: 2017 Sandro Kanuß <knauss@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #ifndef FAKESERVER_H
0011 #define FAKESERVER_H
0012 
0013 #include <QMutex>
0014 #include <QTcpServer>
0015 #include <QTcpSocket>
0016 #include <QThread>
0017 
0018 Q_DECLARE_METATYPE(QList<QByteArray>)
0019 
0020 /**
0021  * Pretends to be an DAV server for the purposes of unit tests.
0022  *
0023  * FakeServer does not really understand the DAV protocol.  Instead,
0024  * you give it a script, or scenario, that lists how an DAV session
0025  * exchange should go.  When it receives the client parts of the
0026  * scenario, it will respond with the following server parts.
0027  *
0028  * The server can be furnished with several scenarios.  The first
0029  * scenario will be played out to the first client that connects, the
0030  * second scenario to the second client connection and so on.
0031  *
0032  * The fake server runs as a separate thread in the same process it
0033  * is started from, and listens for connections (see port() method) on the
0034  * local machine.
0035  *
0036  * Scenarios are in the form of protocol messages, with a tag at the
0037  * start to indicate whether it is message that will be sent by the
0038  * client ("C: ") or a response that should be sent by the server
0039  * ("S: "). Or ("D: ") for the exchanged data. Content-length header is added
0040  * automatically with the current length and also the empty line between Header
0041  * and Content. For example:
0042  * @code
0043  * C: GET /item HTTP/1.1
0044  * S: HTTP/1.0 200 OK
0045  * D: much data
0046  * D: more data
0047  * X
0048  * @endcode
0049  *
0050  * A line starting with X indicates that the connection should be
0051  * closed by the server.  This should be the last line in the
0052  * scenario.
0053 
0054  * A typical usage is something like
0055  * @code
0056  * QList<QByteArray> scenario;
0057  * scenario << "C: GET /item HTTP/1.1"
0058  *          << "S: HTTP/1.0 200 OK"
0059  *          << "D: much data"
0060  *          << "D: more data"
0061  *          << "X";
0062  *
0063  * FakeServer fakeServer;
0064  * fakeServer.setScenario(scenario);
0065  * fakeServer.startAndWait();
0066  *
0067  * QUrl url(QStringLiteral("http://localhost/item"));
0068  * url.setPort(fakeServer.port());
0069  * KDAV::DavUrl davUrl(url, KDAV::CardDav);
0070  * KDAV::DavItem item(davUrl, QString(), QByteArray(), QString());
0071  *
0072  * auto job = new KDAV::DavItemFetchJob(item);
0073  * job->exec();
0074  * fakeServer.quit();
0075  * QVERIFY(fakeServer.isAllScenarioDone());
0076  * @endcode
0077  */
0078 
0079 class FakeServer : public QObject
0080 {
0081     Q_OBJECT
0082 
0083 public:
0084     /**
0085      * Each unittest should use a different port so that they can be run in parallel
0086      */
0087     FakeServer(int port = 5989, QObject *parent = nullptr);
0088     ~FakeServer() override;
0089 
0090     /**
0091      * Starts the server and waits for it to be ready
0092      *
0093      * You should use this instead of start() to avoid race conditions.
0094      */
0095     void startAndWait();
0096 
0097     /**
0098      * Removes any previously-added scenarios, and adds a new one
0099      *
0100      * After this, there will only be one scenario, and so the fake
0101      * server will only be able to service a single request.  More
0102      * scenarios can be added with addScenario, though.
0103      *
0104      * @see addScenario()\n
0105      * addScenarioFromFile()
0106      */
0107     void setScenario(const QList<QByteArray> &scenario);
0108 
0109     /**
0110      * Adds a new scenario
0111      *
0112      * Note that scenarios will be used in the order that clients
0113      * connect.  If this is the 5th scenario that has been added
0114      * (bearing in mind that setScenario() resets the scenario
0115      * count), it will be used to service the 5th client that
0116      * connects.
0117      *
0118      * @see addScenarioFromFile()
0119      *
0120      * @param scenario  the scenario as a list of messages
0121      */
0122     void addScenario(const QList<QByteArray> &scenario);
0123     /**
0124      * Adds a new scenario from a local file
0125      *
0126      * Note that scenarios will be used in the order that clients
0127      * connect.  If this is the 5th scenario that has been added
0128      * (bearing in mind that setScenario() resets the scenario
0129      * count), it will be used to service the 5th client that
0130      * connects.
0131      *
0132      * @see addScenario()
0133      *
0134      * @param fileName  the name of the file that contains the
0135      *                  scenario; it will be split at line
0136      *                  boundaries, and excess whitespace will
0137      *                  be trimmed from the start and end of lines
0138      */
0139     void addScenarioFromFile(const QString &fileName);
0140 
0141     /**
0142      * Checks whether a particular scenario has completed
0143      *
0144      * @param scenarioNumber  the number of the scenario to check,
0145      *                        in order of addition/client connection
0146      */
0147     bool isScenarioDone(int scenarioNumber) const;
0148     /**
0149      * Whether all the scenarios that were added to the fake
0150      * server have been completed.
0151      */
0152     bool isAllScenarioDone() const;
0153 
0154     /**
0155      * Returns the port where the fake server is listening.
0156      */
0157     int port() const;
0158 
0159 private Q_SLOTS:
0160     void newConnection();
0161     void dataAvailable();
0162     void init();
0163     void cleanup();
0164 
0165 private:
0166     void writeServerPart(QTcpSocket *clientSocket, int scenarioNumber);
0167     void readClientPart(QTcpSocket *socket, int *scenarioNumber);
0168 
0169     QList<QList<QByteArray>> m_scenarios;
0170     QTcpServer *m_tcpServer = nullptr;
0171     mutable QMutex m_mutex;
0172     QList<QTcpSocket *> m_clientSockets;
0173     QThread *m_thread;
0174     int m_port;
0175 };
0176 
0177 #endif