#pragma once // «Фейковый интернет» для тестов сетевых сервисов без правки их кода. // // Сервисы лаунчера ходят на захардкоженные https-адреса через собственные // QNetworkAccessManager. Прокси и SSL-конфигурация по умолчанию у Qt общие // на весь процесс, поэтому start(): // - ставит QNetworkProxy::setApplicationProxy() на локальный порт; // - отключает проверку сертификата в QSslConfiguration::defaultConfiguration(). // Дальше каждое https-соединение приходит сюда как CONNECT host:443; сокет // переводится в TLS с тестовым самоподписанным сертификатом и обслуживается // как обычный HTTP/1.1 — по таблице маршрутов (host + path). // // Запрос на неизвестный адрес получает 404 и попадает в unmatched(): тест // может проверить, что код не ушёл куда-то, куда не ожидали. #include #include #include #include #include #include #include #include #include #include #include #include #include class QSslSocket; class QTcpSocket; struct FakeRequest { QByteArray method; QString scheme; // https | http QString host; QString path; QString query; // без «?» QHash headers; // имена в нижнем регистре QByteArray body; QUrl url() const; QUrlQuery queryItems() const { return QUrlQuery(query); } QUrlQuery formItems() const { return QUrlQuery(QString::fromUtf8(body)); } QByteArray header(const QByteArray &name) const { return headers.value(name.toLower()); } }; struct FakeResponse { int status = 200; QByteArray body; QByteArray contentType = "application/octet-stream"; QList> headers; int delayMs = 0; // ответить с задержкой bool hang = false; // не отвечать вовсе (для отмены и таймаутов) bool deferred = false; // ждать явного releaseResponse(), без таймера bool drop = false; // оборвать соединение без ответа int truncateTo = -1; // объявить полный размер, отдать столько байт и оборвать static FakeResponse json(const QByteArray &body, int status = 200); static FakeResponse json(const class QJsonObject &object, int status = 200); static FakeResponse text(const QByteArray &body, int status = 200); static FakeResponse bytes(const QByteArray &body); static FakeResponse error(int status, const QByteArray &body = {}); }; class FakeInternet : public QTcpServer { Q_OBJECT public: using Handler = std::function; explicit FakeInternet(QObject *parent = nullptr); ~FakeInternet() override; // Слушает 127.0.0.1 и перенаправляет на себя весь трафик процесса. // При ошибке пишет причину в error (например, нет TLS-бэкенда с поддержкой сервера). bool start(QString *error = nullptr); void stop(); // Есть ли TLS вообще (без него сетевые тесты пропускаются). static bool tlsAvailable(QString *why = nullptr); // Маршрут: "https://host/path" — точное совпадение пути (query не учитывается), // "https://host/prefix/*" — всё, что начинается с prefix/. void route(const QString &pattern, const Handler &handler); void route(const QString &pattern, const FakeResponse &response); void unroute(const QString &pattern); // Ответ на всё, что не подошло ни к одному маршруту (по умолчанию 404). void setFallback(const FakeResponse &response) { m_fallback = response; } // Сбросить маршруты и журнал. void reset(); bool releaseResponse(const QString &url); QString diagnostics() const; QList requests() const; // Сколько раз был запрошен адрес "https://host/path" (query не учитывается). int hits(const QString &url) const; QStringList unmatched() const; QStringList requestedUrls() const; signals: void requestServed(const QString &url); protected: void incomingConnection(qintptr descriptor) override; private: struct Connection; struct Route { QString pattern; QString host; QString path; bool prefix = false; Handler handler; }; void onReadyRead(Connection *connection); void processBuffer(Connection *connection); void dispatch(Connection *connection, FakeRequest request); void respond(QPointer socket, const FakeResponse &response, bool keepAlive); void startTls(Connection *connection); const Route *match(const FakeRequest &request) const; QList m_routes; FakeResponse m_fallback; QList m_requests; QStringList m_unmatched; struct DeferredResponse { QString url; QPointer socket; FakeResponse response; bool keepAlive; }; QList m_deferred; QStringList m_diagnostics; QByteArray m_certificate; QByteArray m_key; bool m_started = false; };