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 };