File indexing completed on 2024-11-24 04:44:44

0001 /*
0002    SPDX-FileCopyrightText: 2008 Omat Holding B.V. <info@omat.nl>
0003 
0004    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0005    SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0006 
0007    SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #pragma once
0011 
0012 #include <QMutex>
0013 #include <QSsl>
0014 #include <QThread>
0015 class QTcpSocket;
0016 class QTcpServer;
0017 namespace KIMAP
0018 {
0019 class ImapStreamParser;
0020 }
0021 
0022 Q_DECLARE_METATYPE(QList<QByteArray>)
0023 
0024 /**
0025  * Pretends to be an IMAP server for the purposes of unit tests.
0026  *
0027  * FakeServer does not really understand the IMAP protocol.  Instead,
0028  * you give it a script, or scenario, that lists how an IMAP session
0029  * exchange should go.  When it receives the client parts of the
0030  * scenario, it will respond with the following server parts.
0031  *
0032  * The server can be furnished with several scenarios.  The first
0033  * scenario will be played out to the first client that connects, the
0034  * second scenario to the second client connection and so on.
0035  *
0036  * The fake server runs as a separate thread in the same process it
0037  * is started from, and listens for connections on port 5989 on the
0038  * local machine.
0039  *
0040  * Scenarios are in the form of protocol messages, with a tag at the
0041  * start to indicate whether it is message that will be sent by the
0042  * client ("C:") or a response that should be sent by the server
0043  * ("S:").  For example:
0044  * @code
0045  * C: A000001 LIST "" *
0046  * S: * LIST ( \HasChildren ) / INBOX
0047  * S: * LIST ( \HasNoChildren ) / INBOX/&AOQ- &APY- &APw- @ &IKw-
0048  * S: * LIST ( \HasChildren ) / INBOX/lost+found
0049  * S: * LIST ( \HasNoChildren ) / "INBOX/lost+found/Calendar Public-20080128"
0050  * S: A000001 OK LIST completed
0051  * @endcode
0052  *
0053  * A line starting with X indicates that the connection should be
0054  * closed by the server.  This should be the last line in the
0055  * scenario.  For example, the following simulates the server closing
0056  * the connection after receiving too many bad commands:
0057  * @code
0058  * C: A000001 madhatter
0059  * S: A000001 BAD Command madhatter
0060  * X
0061  * @endcode
0062  *
0063  * FakeServer::preauth() and FakeServer::greeting() provide standard
0064  * PREAUTH and OK responses, respectively, that can be used (unmodified)
0065  * as the first line of a scenario.
0066  *
0067  * A typical usage is something like
0068  * @code
0069  * QList<QByteArray> scenario;
0070  * scenario << FakeServer::preauth()
0071  *          << "C: A000001 CAPABILITY"
0072  *          << "S: * CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI"
0073  *          << "S: A000001 OK CAPABILITY completed";
0074  *
0075  * FakeServer fakeServer;
0076  * fakeServer.setScenario( scenario );
0077  * fakeServer.startAndWait();
0078  *
0079  * KIMAP::Session session( QStringLiteral("127.0.0.1"), 5989 );
0080  * KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(&session);
0081  * QVERIFY( job->exec() );
0082  * // check the returned capabilities
0083  *
0084  * fakeServer.quit();
0085  * @endcode
0086  */
0087 class FakeServer : public QThread
0088 {
0089     Q_OBJECT
0090 
0091 public:
0092     /**
0093      * Get the default PREAUTH response
0094      *
0095      * This is the initial PREAUTH message that the server
0096      * sends at the start of a session to indicate that the
0097      * user is already authenticated by some other mechanism.
0098      *
0099      * Can be used as the first line in a scenario where
0100      * you want to skip the LOGIN stage of the protocol.
0101      */
0102     static QByteArray preauth();
0103     /**
0104      * Get the default greeting
0105      *
0106      * This is the initial OK message that the server sends at the
0107      * start of a session to indicate that a LOGIN is required.
0108      *
0109      * Can be used as the first line in a scenario where
0110      * you want to use the LOGIN command.
0111      */
0112     static QByteArray greeting();
0113 
0114     explicit FakeServer(QObject *parent = nullptr);
0115     ~FakeServer() override;
0116 
0117     /**
0118      * Sets the encryption mode used by the server socket.
0119      */
0120     void setEncrypted(QSsl::SslProtocol protocol);
0121 
0122     /**
0123      * Won't start encryption until client sends STARTTLS
0124      */
0125     void setWaitForStartTls(bool wait);
0126 
0127     /**
0128      * Starts the server and waits for it to be ready
0129      *
0130      * You should use this instead of start() to avoid race conditions.
0131      */
0132     void startAndWait();
0133 
0134     /**
0135      * Starts the fake IMAP server
0136      *
0137      * You should not call this directly.  Use start() instead.
0138      *
0139      * @reimp
0140      */
0141     void run() override;
0142 
0143     /**
0144      * Removes any previously-added scenarios, and adds a new one
0145      *
0146      * After this, there will only be one scenario, and so the fake
0147      * server will only be able to service a single request.  More
0148      * scenarios can be added with addScenario, though.
0149      *
0150      * @see addScenario()\n
0151      * addScenarioFromFile()
0152      */
0153     void setScenario(const QList<QByteArray> &scenario);
0154 
0155     /**
0156      * Adds a new scenario
0157      *
0158      * Note that scenarios will be used in the order that clients
0159      * connect.  If this is the 5th scenario that has been added
0160      * (bearing in mind that setScenario() resets the scenario
0161      * count), it will be used to service the 5th client that
0162      * connects.
0163      *
0164      * @see addScenarioFromFile()
0165      *
0166      * @param scenario  the scenario as a list of messages
0167      */
0168     void addScenario(const QList<QByteArray> &scenario);
0169     /**
0170      * Adds a new scenario from a local file
0171      *
0172      * Note that scenarios will be used in the order that clients
0173      * connect.  If this is the 5th scenario that has been added
0174      * (bearing in mind that setScenario() resets the scenario
0175      * count), it will be used to service the 5th client that
0176      * connects.
0177      *
0178      * @see addScenario()
0179      *
0180      * @param fileName  the name of the file that contains the
0181      *                  scenario; it will be split at line
0182      *                  boundaries, and excess whitespace will
0183      *                  be trimmed from the start and end of lines
0184      */
0185     void addScenarioFromFile(const QString &fileName);
0186 
0187     /**
0188      * Checks whether a particular scenario has completed
0189      *
0190      * @param scenarioNumber  the number of the scenario to check,
0191      *                        in order of addition/client connection
0192      */
0193     bool isScenarioDone(int scenarioNumber) const;
0194     /**
0195      * Whether all the scenarios that were added to the fake
0196      * server have been completed.
0197      */
0198     bool isAllScenarioDone() const;
0199 
0200 protected:
0201     /**
0202      * Whether the received content is the same as the expected.
0203      * Use QCOMPARE, if creating subclasses.
0204      */
0205     virtual void compareReceived(const QByteArray &received, const QByteArray &expected) const;
0206 
0207 private Q_SLOTS:
0208     void newConnection();
0209     void dataAvailable();
0210     void started();
0211 
0212 private:
0213     void writeServerPart(int scenarioNumber);
0214     void readClientPart(int scenarioNumber);
0215 
0216     QList<QList<QByteArray>> m_scenarios;
0217     QTcpServer *m_tcpServer;
0218     mutable QMutex m_mutex;
0219     QList<QTcpSocket *> m_clientSockets;
0220     QList<KIMAP::ImapStreamParser *> m_clientParsers;
0221     bool m_encrypted;
0222     bool m_starttls;
0223     bool m_waitForStartTls = false;
0224     QSsl::SslProtocol m_sslProtocol;
0225 };