diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8558cd..9c938bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ - Dev: Refactored types used for emoji parsing. (#6714) - Dev: `LinkParser` now parses `QStringView`s. (#6715) - Dev: Cleaned up `AccountController` connections. (#6735) +- Dev: Send multipart body for HTTP `PUT` requests. (#6746) - Dev: Removed unused variables. (#6748) ## 2.5.4 diff --git a/src/common/network/NetworkTask.cpp b/src/common/network/NetworkTask.cpp index d1b31d25..3c6d9c00 100644 --- a/src/common/network/NetworkTask.cpp +++ b/src/common/network/NetworkTask.cpp @@ -87,12 +87,24 @@ QNetworkReply *NetworkTask::createReply() case NetworkRequestType::Get: return accessManager->get(request); - case NetworkRequestType::Put: - return accessManager->put(request, data->payload); - case NetworkRequestType::Delete: return accessManager->deleteResource(data->request); + case NetworkRequestType::Put: + if (data->multiPartPayload) + { + assert(data->payload.isNull()); + + return accessManager->put(request, + data->multiPartPayload.get()); + } + else + { + assert(data->multiPartPayload == nullptr); + + return accessManager->put(request, data->payload); + } + case NetworkRequestType::Post: if (data->multiPartPayload) { @@ -103,8 +115,11 @@ QNetworkReply *NetworkTask::createReply() } else { + assert(data->multiPartPayload == nullptr); + return accessManager->post(request, data->payload); } + case NetworkRequestType::Patch: if (data->multiPartPayload) { @@ -115,6 +130,8 @@ QNetworkReply *NetworkTask::createReply() } else { + assert(data->multiPartPayload == nullptr); + return NetworkManager::accessManager->sendCustomRequest( request, "PATCH", data->payload); } diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index c8945942..3b9ecd7e 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -8,6 +8,7 @@ #include "common/network/NetworkResult.hpp" #include "NetworkHelpers.hpp" #include "Test.hpp" +#include "util/QMagicEnum.hpp" #include @@ -25,6 +26,24 @@ QString getDelayURL(int delay) return QString("%1/delay/%2").arg(HTTPBIN_BASE_URL).arg(delay); } +QString getHttpbinUrl(QStringView path) +{ + return QString("%1/%2").arg(HTTPBIN_BASE_URL, path); +} + +QHttpMultiPart *makeMultipart() +{ + auto *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart part; + part.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"text\"")); + part.setBody("my text"); + + multipart->append(part); + return multipart; +} + } // namespace TEST(NetworkRequest, Success) @@ -281,3 +300,86 @@ TEST(NetworkRequest, BatchedTimeouts) } #endif } + +TEST(NetworkRequest, HttpGetHeaders) +{ + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); + + RequestWaiter waiter; + NetworkRequest(getHttpbinUrl(u"headers"), NetworkRequestType::Get) + .timeout(1000) + .onSuccess([&](const NetworkResult &result) { + // make sure we don't attempt to send a body + auto body = QString::fromUtf8(result.getData()); + EXPECT_FALSE(body.contains("content-length", Qt::CaseInsensitive)); + waiter.requestDone(); + }) + .execute(); + + waiter.waitForRequest(); +} + +TEST(NetworkRequest, HttpBody) +{ + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); + + for (auto ty : std::array{ + NetworkRequestType::Post, + NetworkRequestType::Put, + NetworkRequestType::Patch, + }) + { + auto path = qmagicenum::enumNameString(ty).toLower(); + RequestWaiter waiter; + bool success = false; + NetworkRequest(getHttpbinUrl(path), ty) + .timeout(1000) + .payload("foobar") + .header("Content-Type", "text/plain") + .onSuccess([&](const NetworkResult &result) { + EXPECT_EQ(result.getData(), "foobar") << path; + success = true; + }) + .finally([&] { + waiter.requestDone(); + }) + .execute(); + + waiter.waitForRequest(); + ASSERT_TRUE(success) << path; + } +} + +TEST(NetworkRequest, HttpBodyMultipart) +{ + EXPECT_TRUE(NetworkManager::workerThread->isRunning()); + + for (auto ty : std::array{ + NetworkRequestType::Post, + NetworkRequestType::Put, + NetworkRequestType::Patch, + }) + { + auto path = qmagicenum::enumNameString(ty).toLower(); + RequestWaiter waiter; + bool success = false; + NetworkRequest(getHttpbinUrl(path), ty) + .timeout(1000) + .multiPart(makeMultipart()) + .onSuccess([&](const NetworkResult &result) { + auto body = QString::fromUtf8(result.getData()); + EXPECT_TRUE(body.contains("Content-Disposition: form-data; " + "name=\"text\"\r\n\r\nmy text", + Qt::CaseInsensitive)) + << "Path:" << path << "Body:" << body; + success = true; + }) + .finally([&] { + waiter.requestDone(); + }) + .execute(); + + waiter.waitForRequest(); + ASSERT_TRUE(success) << path; + } +}