Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include "qoaicommonglobal.h"
++ #include "qoaihttprequest.h"
++
++ #include "compressiontestmyapisuffix.h"
++
++ #include <QtCore/qobject.h>
++ #include <QtCore/qprocess.h>
++ #include <QtCore/qregularexpression.h>
++ #include <QtNetwork/qnetworkaccessmanager.h>
++ #include <QtNetwork/qrestaccessmanager.h>
++ #include <QtTest/qtest.h>
++
++ using namespace Qt::StringLiterals;
++
++ class tst_Compression : public QObject
++ {
++ Q_OBJECT
++
++ private Q_SLOTS:
++ void initTestCase();
++ void localCompressionRoundtrip_data();
++ void localCompressionRoundtrip();
++ void generatorVersionCheck();
++ void toggleCompressionParameters_data();
++ void toggleCompressionParameters();
++ void cleanupTestCase();
++
++ private:
++ QProcess m_serverProcess;
++ };
++
++ void tst_Compression::initTestCase()
++ {
++ if (m_serverProcess.state() != QProcess::ProcessState::Running) {
++ m_serverProcess.start(SERVER_PATH);
++ if (!m_serverProcess.waitForStarted()) {
++ qFatal() << "Couldn't start the server: " << m_serverProcess.errorString();
++ exit(EXIT_FAILURE);
++ }
++ // give the process some time to properly start up the server
++ QThread::currentThread()->msleep(1000);
++ }
++ }
++
++ void tst_Compression::localCompressionRoundtrip_data()
++ {
++ using namespace QtOpenApiCommon::QOAIHttpRequestWorker;
++ QTest::addColumn<CompressionType>("compressionType");
++
++ QTest::newRow("gzip") << CompressionType::Gzip;
++ QTest::newRow("deflate") << CompressionType::Deflate;
++ }
++
++ void tst_Compression::localCompressionRoundtrip()
++ {
++ // Generate the data to compress
++ static constexpr int BlockSize = 1024;
++ static constexpr size_t MaxUChar = std::numeric_limits<uchar>::max();
++
++ QByteArray originalData;
++ originalData.reserve(MaxUChar * BlockSize);
++ for (uchar c = 0; c < MaxUChar; ++c)
++ originalData.append(BlockSize, c);
++
++ using namespace QtOpenApiCommon::QOAIHttpRequestWorker;
++ QFETCH(const CompressionType, compressionType);
++
++ const QByteArray compressed = compressData(originalData, 9, compressionType);
++ QCOMPARE_LE(compressed.size(), originalData.size());
++
++ // By default, limit = 0, which means no limit.
++ const QByteArray decompressed = decompressData(compressed, compressionType);
++ QCOMPARE(decompressed, originalData);
++
++ // Let's test decompression with ratio > 40
++ // Create highly repetitive data exceeding the 10 MB safety threshold.
++ // When compressed, the ratio will far exceed 40:1, triggering the bomb check.
++ static constexpr qsizetype bombDataSize = 11 * 1024 * 1024; // 11 MB
++ QByteArray bombData(bombDataSize, '\0');
++ const QByteArray bombCompressed = compressData(bombData, 9, compressionType);
++ // Verify the compression ratio would indeed exceed 40:1
++ QVERIFY(bombData.size() / bombCompressed.size() > 40);
++ // Don't use the entire message here, because of different ratio for deflate and gzip
++ // in the message body.
++ // The ratio value itself is not important here, important that ratio > 40.
++ QTest::ignoreMessage(QtWarningMsg,
++ QRegularExpression(u"Decompression ratio exceeds safety threshold"_s));
++ const QByteArray bombResult = decompressData(bombCompressed, compressionType);
++ QCOMPARE(bombResult.size(), 0);
++
++ // Let's test decompression with the threshold < 10 MB.
++ // Create highly repetitive data LESS then the 10 MB safety threshold.
++ // Unfortunately, bomb won't be detected.
++ static constexpr qsizetype smallerBombDataSize = 9 * 1024 * 1024;
++ QByteArray sBombData(smallerBombDataSize, '\0');
++ const QByteArray sBombCompressed = compressData(sBombData, 9, compressionType);
++ // Verify the compression ratio would indeed exceed 40:1
++ QVERIFY(sBombData.size() / sBombCompressed.size() > 40);
++ // Default 10Mb threshold is used
++ const QByteArray sBombResult = decompressData(sBombCompressed, compressionType);
++ QCOMPARE(sBombResult.size(), 9437184);
++
++ // Let's test decompression with a new threshold = 8 MB
++ // Fortunately, previously created bomb will be detected.
++ QTest::ignoreMessage(QtWarningMsg,
++ QRegularExpression(u"Decompression ratio exceeds safety threshold"_s));
++ const QByteArray nextResult
++ = decompressData(sBombCompressed, compressionType, 8 * 1024 * 1024);
++ QCOMPARE(nextResult.size(), 0);
++ }
++
++ void tst_Compression::generatorVersionCheck()
++ {
++ const QString generatorVer = u"%1.%2.%3"_s.arg(OPENAPI_GENERATOR_VERSION_MAJOR)
++ .arg(OPENAPI_GENERATOR_VERSION_MINOR)
++ .arg(OPENAPI_GENERATOR_VERSION_PATCH);
++ QCOMPARE_EQ(generatorVer, OPENAPI_GENERATOR_VERSION_STR);
++ }
++
++ class CustomNetworkAccessManager : public QNetworkAccessManager
++ {
++ public:
++ CustomNetworkAccessManager(QObject *parent = nullptr)
++ : QNetworkAccessManager(parent)
++ {}
++ ~CustomNetworkAccessManager() override
++ {}
++
++ protected:
++ QNetworkReply *createRequest(QNetworkAccessManager::Operation op,
++ const QNetworkRequest &originalReq,
++ QIODevice *outgoingData = nullptr) override;
++
++ public:
++ bool m_forceCompressedResponse = false;
++ QByteArray m_acceptEncodingHeader;
++ };
++
++ QNetworkReply *CustomNetworkAccessManager::createRequest(QNetworkAccessManager::Operation op,
++ const QNetworkRequest &originalReq,
++ QIODevice *outgoingData)
++ {
++ m_acceptEncodingHeader = originalReq.rawHeader("Accept-Encoding"_L1);
++ if (m_forceCompressedResponse) {
++ QNetworkRequest copy = originalReq;
++ copy.setRawHeader("Accept-Encoding", "gzip, deflate");
++ return QNetworkAccessManager::createRequest(op, copy, outgoingData);
++ }
++ return QNetworkAccessManager::createRequest(op, originalReq, outgoingData);
++ }
++
++ void tst_Compression::toggleCompressionParameters_data()
++ {
++ QTest::addColumn<bool>("requestCompression");
++ QTest::addColumn<bool>("responseCompression");
++ QTest::addColumn<bool>("forceCompressedResponse");
++
++ QTest::newRow("all_uncompressed") << false << false << false;
++ QTest::newRow("compressed_request") << true << false << false;
++ QTest::newRow("compressed_response") << false << true << false;
++ QTest::newRow("all_compressed") << true << true << false;
++ QTest::newRow("all_uncompressed_force_compressed_response") << false << false << true;
++ QTest::newRow("all_compressed_force_compressed_response") << true << true << true;
++ }
++
++ void tst_Compression::toggleCompressionParameters()
++ {
++ QFETCH(const bool, requestCompression);
++ QFETCH(const bool, responseCompression);
++ QFETCH(const bool, forceCompressedResponse);
++
++ CustomNetworkAccessManager manager;
++ manager.m_forceCompressedResponse = forceCompressedResponse;
++ QRestAccessManager restManager(&manager);
++
++ CompressionNamespace::CompressionTestMyApiSuffix api;
++ api.setRestAccessManager(&restManager);
++
++ // compression is disabled by default
++ QVERIFY(!api.requestCompressionEnabled());
++ QVERIFY(!api.responseCompressionEnabled());
++
++ api.setRequestCompressionEnabled(requestCompression);
++ api.setResponseCompressionEnabled(responseCompression);
++
++ const QString filePath = QDir::currentPath() + QDir::separator() + u"test.pdf"_s;
++ static QByteArray expectedContent; // do not read the file every time
++ if (expectedContent.isEmpty()) {
++ QFile f(filePath);
++ QVERIFY(f.open(QIODevice::ReadOnly));
++ expectedContent = f.readAll();
++ f.close();
++ }
++
++ QtOpenApiCommon::QOAIHttpFileElement file(filePath);
++
++ bool done = false;
++ QByteArray receivedData;
++ int responseCode = -1;
++ api.roundtrip(file, this,
++ [&](const QRestReply &reply,
++ const QtOpenApiCommon::QOAIHttpFileElement &summary) {
++ done = reply.isSuccess();
++ responseCode = reply.httpStatus();
++ receivedData = summary.loadFromLocalFile();
++ });
++ if (responseCompression)
++ QCOMPARE_EQ(manager.m_acceptEncodingHeader, "gzip, deflate");
++ else
++ QCOMPARE_EQ(manager.m_acceptEncodingHeader, "identity");
++
++ QTRY_COMPARE_EQ(done, true);
++ QCOMPARE_EQ(responseCode, 200);
++ QCOMPARE_EQ(receivedData, expectedContent);
++ }
++
++ void tst_Compression::cleanupTestCase()
++ {
++ if (m_serverProcess.state() == QProcess::ProcessState::Running) {
++ m_serverProcess.kill();
++ m_serverProcess.waitForFinished();
++ }
++ }
++
++ QTEST_MAIN(tst_Compression)
++ #include "tst_compression.moc"