summaryrefslogtreecommitdiffstats
path: root/tests/auto/network/access/http2/tst_http2.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/network/access/http2/tst_http2.cpp')
-rw-r--r--tests/auto/network/access/http2/tst_http2.cpp426
1 files changed, 370 insertions, 56 deletions
diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp
index 432b35e5c6..b624f6e436 100644
--- a/tests/auto/network/access/http2/tst_http2.cpp
+++ b/tests/auto/network/access/http2/tst_http2.cpp
@@ -1,30 +1,5 @@
-/****************************************************************************
-**
-** Copyright (C) 2016 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the test suite of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:GPL-EXCEPT$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 3 as published by the Free Software
-** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtNetwork/qtnetworkglobal.h>
@@ -62,6 +37,8 @@ Q_DECLARE_METATYPE(QNetworkRequest::Attribute)
QT_BEGIN_NAMESPACE
+using namespace Qt::StringLiterals;
+
QHttp2Configuration qt_defaultH2Configuration()
{
QHttp2Configuration config;
@@ -92,8 +69,11 @@ public slots:
void init();
private slots:
// Tests:
+ void defaultQnamHttp2Configuration();
void singleRequest_data();
void singleRequest();
+ void informationalRequest_data();
+ void informationalRequest();
void multipleRequests();
void flowControlClientSide();
void flowControlServerSide();
@@ -115,6 +95,16 @@ private slots:
void authenticationRequired_data();
void authenticationRequired();
+ void h2cAllowedAttribute_data();
+ void h2cAllowedAttribute();
+
+ void redirect_data();
+ void redirect();
+
+ void trailingHEADERS();
+
+ void duplicateRequestsWithAborts();
+
protected slots:
// Slots to listen to our in-process server:
void serverStarted(quint16 port);
@@ -225,6 +215,12 @@ void tst_Http2::init()
manager.reset(new QNetworkAccessManager);
}
+void tst_Http2::defaultQnamHttp2Configuration()
+{
+ // The configuration we also implicitly use in QNAM.
+ QCOMPARE(qt_defaultH2Configuration(), QNetworkRequest().http2Configuration());
+}
+
void tst_Http2::singleRequest_data()
{
QTest::addColumn<QNetworkRequest::Attribute>("h2Attribute");
@@ -255,7 +251,7 @@ void tst_Http2::singleRequest()
// we have to use TLS sockets (== private key) and thus suppress a
// keychain UI asking for permission to use a private key.
// Our CI has this, but somebody testing locally - will have a problem.
- qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
+ qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = qScopeGuard([](){
qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN");
});
@@ -276,6 +272,7 @@ void tst_Http2::singleRequest()
url.setPath("/index.html");
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
QFETCH(const QNetworkRequest::Attribute, h2Attribute);
request.setAttribute(h2Attribute, QVariant(true));
@@ -292,7 +289,7 @@ void tst_Http2::singleRequest()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -301,10 +298,74 @@ void tst_Http2::singleRequest()
#if QT_CONFIG(ssl)
if (connectionType == H2Type::h2Alpn || connectionType == H2Type::h2Direct)
- QCOMPARE(encSpy.count(), 1);
+ QCOMPARE(encSpy.size(), 1);
#endif // QT_CONFIG(ssl)
}
+void tst_Http2::informationalRequest_data()
+{
+ QTest::addColumn<int>("statusCode");
+
+ // 'Clear text' that should always work, either via the protocol upgrade
+ // or as direct.
+ QTest::addRow("statusCode-100") << 100;
+ QTest::addRow("statusCode-125") << 125;
+ QTest::addRow("statusCode-150") << 150;
+ QTest::addRow("statusCode-175") << 175;
+}
+
+void tst_Http2::informationalRequest()
+{
+ clearHTTP2State();
+
+ serverPort = 0;
+ nRequests = 1;
+
+ ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
+
+ QFETCH(const int, statusCode);
+ srv->setInformationalStatusCode(statusCode);
+
+ QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ auto url = requestUrl(defaultConnectionType());
+ url.setPath("/index.html");
+
+ QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
+
+ auto reply = manager->get(request);
+
+ connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
+ // Since we're using self-signed certificates,
+ // ignore SSL errors:
+ reply->ignoreSslErrors();
+
+ runEventLoop();
+ STOP_ON_FAILURE
+
+ QCOMPARE(nRequests, 0);
+ QVERIFY(prefaceOK);
+ QVERIFY(serverGotSettingsACK);
+
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+ QVERIFY(reply->isFinished());
+
+ const QVariant code(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute));
+
+ // We are discarding informational headers if the status code is in the range of
+ // 102-199 or if it is 100. As these header fields were part of the informational
+ // header used for this test case, we should not see them at this point and the
+ // status code should be 200.
+
+ QCOMPARE(code.value<int>(), 200);
+ QVERIFY(!reply->hasRawHeader("a_random_header_field"));
+ QVERIFY(!reply->hasRawHeader("another_random_header_field"));
+}
+
void tst_Http2::multipleRequests()
{
clearHTTP2State();
@@ -333,7 +394,7 @@ void tst_Http2::multipleRequests()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -378,7 +439,7 @@ void tst_Http2::flowControlClientSide()
runEventLoop(120000);
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QVERIFY(windowUpdates > 0);
@@ -419,7 +480,7 @@ void tst_Http2::flowControlServerSide()
runEventLoop(120000);
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -451,6 +512,7 @@ void tst_Http2::pushPromise()
url.setPath("/index.html");
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true));
request.setHttp2Configuration(params);
@@ -462,7 +524,7 @@ void tst_Http2::pushPromise()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -477,6 +539,7 @@ void tst_Http2::pushPromise()
url.setPath("/script.js");
QNetworkRequest promisedRequest(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
promisedRequest.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true));
reply = manager->get(promisedRequest);
connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
@@ -531,6 +594,7 @@ void tst_Http2::goaway()
for (int i = 0; i < nRequests; ++i) {
url.setPath(QString("/%1").arg(i));
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true));
replies[i] = manager->get(request);
QCOMPARE(replies[i]->error(), QNetworkReply::NoError);
@@ -586,7 +650,7 @@ void tst_Http2::earlyResponse()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -643,7 +707,7 @@ void tst_Http2::connectToHost()
// we have to use TLS sockets (== private key) and thus suppress a
// keychain UI asking for permission to use a private key.
// Our CI has this, but somebody testing locally - will have a problem.
- qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
+ qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = qScopeGuard([](){
qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN");
});
@@ -672,6 +736,7 @@ void tst_Http2::connectToHost()
auto copyUrl = url;
copyUrl.setScheme(QLatin1String("preconnect-https"));
QNetworkRequest request(copyUrl);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(requestAttribute, true);
reply = manager->get(request);
// Since we're using self-signed certificates, ignore SSL errors:
@@ -684,6 +749,7 @@ void tst_Http2::connectToHost()
auto copyUrl = url;
copyUrl.setScheme(QLatin1String("preconnect-http"));
QNetworkRequest request(copyUrl);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(requestAttribute, true);
reply = manager->get(request);
}
@@ -704,6 +770,7 @@ void tst_Http2::connectToHost()
QCOMPARE(nRequests, 1);
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(requestAttribute, QVariant(true));
reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
@@ -714,7 +781,7 @@ void tst_Http2::connectToHost()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -738,7 +805,7 @@ void tst_Http2::maxFrameSize()
// we have to use TLS sockets (== private key) and thus suppress a
// keychain UI asking for permission to use a private key.
// Our CI has this, but somebody testing locally - will have a problem.
- qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
+ qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = qScopeGuard([](){
qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN");
});
@@ -769,6 +836,7 @@ void tst_Http2::maxFrameSize()
url.setPath(QString("/stream1.html"));
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(attribute, QVariant(true));
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));
request.setHttp2Configuration(h2Config);
@@ -782,9 +850,9 @@ void tst_Http2::maxFrameSize()
// Normally, with a 16kb limit, our server would split such
// a response into 3 'DATA' frames (16kb + 16kb + 0|END_STREAM).
- QCOMPARE(frameCounter.count(), 1);
+ QCOMPARE(frameCounter.size(), 1);
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -902,7 +970,7 @@ void tst_Http2::moreActivitySignals()
// we have to use TLS sockets (== private key) and thus suppress a
// keychain UI asking for permission to use a private key.
// Our CI has this, but somebody testing locally - will have a problem.
- qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
+ qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); });
#endif
@@ -915,13 +983,14 @@ void tst_Http2::moreActivitySignals()
auto url = requestUrl(connectionType);
url.setPath(QString("/stream1.html"));
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
QFETCH(const QNetworkRequest::Attribute, h2Attribute);
request.setAttribute(h2Attribute, QVariant(true));
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));
QSharedPointer<QNetworkReply> reply(manager->get(request));
nRequests = 1;
connect(reply.data(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
- QSignalSpy spy1(reply.data(), SIGNAL(socketConnecting()));
+ QSignalSpy spy1(reply.data(), SIGNAL(socketStartedConnecting()));
QSignalSpy spy2(reply.data(), SIGNAL(requestSent()));
QSignalSpy spy3(reply.data(), SIGNAL(metaDataChanged()));
// Since we're using self-signed certificates,
@@ -935,7 +1004,7 @@ void tst_Http2::moreActivitySignals()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -1012,7 +1081,7 @@ void tst_Http2::contentEncoding()
// we have to use TLS sockets (== private key) and thus suppress a
// keychain UI asking for permission to use a private key.
// Our CI has this, but somebody testing locally - will have a problem.
- qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
+ qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); });
#endif
@@ -1035,6 +1104,7 @@ void tst_Http2::contentEncoding()
url.setPath("/index.html");
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
QFETCH(const QNetworkRequest::Attribute, h2Attribute);
request.setAttribute(h2Attribute, QVariant(true));
@@ -1047,7 +1117,7 @@ void tst_Http2::contentEncoding()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -1060,13 +1130,18 @@ void tst_Http2::authenticationRequired_data()
{
QTest::addColumn<bool>("success");
QTest::addColumn<bool>("responseHEADOnly");
+ QTest::addColumn<bool>("withChallenge");
- QTest::addRow("failed-auth") << false << true;
- QTest::addRow("successful-auth") << true << true;
+ QTest::addRow("failed-auth") << false << true << true;
+ QTest::addRow("successful-auth") << true << true << true;
// Include a DATA frame in the response from the remote server. An example would be receiving a
// JSON response on a request along with the 401 error.
- QTest::addRow("failed-auth-with-response") << false << false;
- QTest::addRow("successful-auth-with-response") << true << false;
+ QTest::addRow("failed-auth-with-response") << false << false << true;
+ QTest::addRow("successful-auth-with-response") << true << false << true;
+
+ // Don't provide a challenge header. This is valid if you are actually just
+ // denied access for whatever reason.
+ QTest::addRow("no-challenge") << false << false << false;
}
void tst_Http2::authenticationRequired()
@@ -1077,10 +1152,15 @@ void tst_Http2::authenticationRequired()
POSTResponseHEADOnly = responseHEADOnly;
QFETCH(const bool, success);
+ QFETCH(const bool, withChallenge);
ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
- targetServer->setResponseBody("Hello");
- targetServer->setAuthenticationHeader("Basic realm=\"Shadow\"");
+ QByteArray responseBody = "Hello"_ba;
+ targetServer->setResponseBody(responseBody);
+ if (withChallenge)
+ targetServer->setAuthenticationHeader("Basic realm=\"Shadow\"");
+ else
+ targetServer->setAuthenticationRequired(true);
QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
runEventLoop();
@@ -1092,6 +1172,7 @@ void tst_Http2::authenticationRequired()
auto url = requestUrl(defaultConnectionType());
url.setPath("/index.html");
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
QByteArray expectedBody = "Hello, World!";
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
@@ -1115,24 +1196,30 @@ void tst_Http2::authenticationRequired()
receivedBody += body;
});
- if (success)
+ if (success) {
connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
- else
- connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
+ } else {
+ // Use queued connection so that the finished signal can be emitted and the isFinished
+ // property can be set.
+ connect(reply.get(), &QNetworkReply::errorOccurred, this,
+ &tst_Http2::replyFinishedWithError, Qt::QueuedConnection);
+ }
// Since we're using self-signed certificates,
// ignore SSL errors:
reply->ignoreSslErrors();
runEventLoop();
STOP_ON_FAILURE
+ QVERIFY2(reply->isFinished(),
+ "The reply should error out if authentication fails, or finish if it succeeds");
if (!success)
QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError);
// else: no error (is checked in tst_Http2::replyFinished)
- QVERIFY(authenticationRequested);
+ QVERIFY(authenticationRequested || !withChallenge);
- const auto isAuthenticated = [](QByteArray bv) {
+ const auto isAuthenticated = [](const QByteArray &bv) {
return bv == "Basic YWRtaW46YWRtaW4="; // admin:admin
};
// Get the "authorization" header out from the server and make sure it's as expected:
@@ -1140,12 +1227,238 @@ void tst_Http2::authenticationRequired()
QCOMPARE(isAuthenticated(reqAuthHeader), success);
if (success)
QCOMPARE(receivedBody, expectedBody);
+ if (responseHEADOnly) {
+ const QVariant contentLenHeader = reply->header(QNetworkRequest::ContentLengthHeader);
+ QVERIFY2(!contentLenHeader.isValid(), "We expect no DATA frames to be received");
+ QCOMPARE(reply->readAll(), QByteArray());
+ } else {
+ const qint32 contentLen = reply->header(QNetworkRequest::ContentLengthHeader).toInt();
+ QCOMPARE(contentLen, responseBody.length());
+ QCOMPARE(reply->bytesAvailable(), responseBody.length());
+ QCOMPARE(reply->readAll(), QByteArray("Hello"));
+ }
// In the `!success` case we need to wait for the server to emit this or it might cause issues
// in the next test running after this. In the `success` case we anyway expect it to have been
// received.
QTRY_VERIFY(serverGotSettingsACK);
}
+void tst_Http2::h2cAllowedAttribute_data()
+{
+ QTest::addColumn<bool>("h2cAllowed");
+ QTest::addColumn<bool>("useAttribute"); // true: use attribute, false: use environment variable
+ QTest::addColumn<bool>("success");
+
+ QTest::addRow("h2c-not-allowed") << false << false << false;
+ // Use the attribute to enable/disable the H2C:
+ QTest::addRow("attribute") << true << true << true;
+ // Use the QT_NETWORK_H2C_ALLOWED environment variable to enable/disable the H2C:
+ QTest::addRow("environment-variable") << true << false << true;
+}
+
+void tst_Http2::h2cAllowedAttribute()
+{
+ QFETCH(const bool, h2cAllowed);
+ QFETCH(const bool, useAttribute);
+ QFETCH(const bool, success);
+
+ clearHTTP2State();
+ serverPort = 0;
+
+ ServerPtr targetServer(newServer(defaultServerSettings, H2Type::h2c));
+ targetServer->setResponseBody("Hello");
+
+ QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ nRequests = 1;
+
+ auto url = requestUrl(H2Type::h2c);
+ url.setPath("/index.html");
+ QNetworkRequest request(url);
+ if (h2cAllowed) {
+ if (useAttribute)
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
+ else
+ qputenv("QT_NETWORK_H2C_ALLOWED", "1");
+ }
+ auto envCleanup = qScopeGuard([]() { qunsetenv("QT_NETWORK_H2C_ALLOWED"); });
+
+ QScopedPointer<QNetworkReply> reply;
+ reply.reset(manager->get(request));
+
+ if (success)
+ connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
+ else
+ connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
+
+ // Since we're using self-signed certificates,
+ // ignore SSL errors:
+ reply->ignoreSslErrors();
+
+ runEventLoop();
+ STOP_ON_FAILURE
+
+ if (!success) {
+ QCOMPARE(reply->error(), QNetworkReply::ConnectionRefusedError);
+ } else {
+ QCOMPARE(reply->readAll(), QByteArray("Hello"));
+ QTRY_VERIFY(serverGotSettingsACK);
+ }
+}
+
+void tst_Http2::redirect_data()
+{
+ QTest::addColumn<int>("maxRedirects");
+ QTest::addColumn<int>("redirectCount");
+ QTest::addColumn<bool>("success");
+
+ QTest::addRow("1-redirects-none-allowed-failure") << 0 << 1 << false;
+ QTest::addRow("1-redirects-success") << 1 << 1 << true;
+ QTest::addRow("2-redirects-1-allowed-failure") << 1 << 2 << false;
+}
+
+void tst_Http2::redirect()
+{
+ QFETCH(const int, maxRedirects);
+ QFETCH(const int, redirectCount);
+ QFETCH(const bool, success);
+ const QByteArray redirectUrl = "/b.html"_ba;
+
+ clearHTTP2State();
+ serverPort = 0;
+
+ ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
+ targetServer->setRedirect(redirectUrl, redirectCount);
+
+ QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ nRequests = 1;
+
+ auto originalUrl = requestUrl(defaultConnectionType());
+ auto url = originalUrl;
+ url.setPath("/index.html");
+ QNetworkRequest request(url);
+ request.setMaximumRedirectsAllowed(maxRedirects);
+ // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN
+ qputenv("QT_NETWORK_H2C_ALLOWED", "1");
+ auto envCleanup = qScopeGuard([]() { qunsetenv("QT_NETWORK_H2C_ALLOWED"); });
+
+ QScopedPointer<QNetworkReply> reply;
+ reply.reset(manager->get(request));
+
+ if (success) {
+ connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
+ } else {
+ connect(reply.get(), &QNetworkReply::errorOccurred, this,
+ &tst_Http2::replyFinishedWithError);
+ }
+
+ // Since we're using self-signed certificates,
+ // ignore SSL errors:
+ reply->ignoreSslErrors();
+
+ runEventLoop();
+ STOP_ON_FAILURE
+
+ QCOMPARE(nRequests, 0);
+ if (success) {
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+ QCOMPARE(reply->url().toString(),
+ originalUrl.resolved(QString::fromLatin1(redirectUrl)).toString());
+ } else if (maxRedirects < redirectCount) {
+ QCOMPARE(reply->error(), QNetworkReply::TooManyRedirectsError);
+ }
+ QTRY_VERIFY(serverGotSettingsACK);
+}
+
+void tst_Http2::trailingHEADERS()
+{
+ clearHTTP2State();
+ serverPort = 0;
+
+ ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
+ targetServer->setSendTrailingHEADERS(true);
+
+ QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ nRequests = 1;
+
+ const auto url = requestUrl(defaultConnectionType());
+ QNetworkRequest request(url);
+ // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
+
+ std::unique_ptr<QNetworkReply> reply{ manager->get(request) };
+ connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
+
+ // Since we're using self-signed certificates, ignore SSL errors:
+ reply->ignoreSslErrors();
+
+ runEventLoop();
+ STOP_ON_FAILURE
+
+ QCOMPARE(nRequests, 0);
+
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+ QTRY_VERIFY(serverGotSettingsACK);
+}
+
+void tst_Http2::duplicateRequestsWithAborts()
+{
+ clearHTTP2State();
+ serverPort = 0;
+
+ ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
+
+ QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ constexpr int ExpectedSuccessfulRequests = 1;
+ nRequests = ExpectedSuccessfulRequests;
+
+ const auto url = requestUrl(defaultConnectionType());
+ QNetworkRequest request(url);
+ // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
+
+ qint32 finishedCount = 0;
+ auto connectToSlots = [this, &finishedCount](QNetworkReply *reply){
+ const auto onFinished = [&finishedCount, reply, this]() {
+ ++finishedCount;
+ if (reply->error() == QNetworkReply::NoError)
+ replyFinished();
+ };
+ connect(reply, &QNetworkReply::finished, reply, onFinished);
+ };
+
+ std::vector<QNetworkReply *> replies;
+ for (qint32 i = 0; i < 3; ++i) {
+ auto &reply = replies.emplace_back(manager->get(request));
+ connectToSlots(reply);
+ if (i < 2) // Delete and abort all-but-one:
+ reply->deleteLater();
+ // Since we're using self-signed certificates, ignore SSL errors:
+ reply->ignoreSslErrors();
+ }
+
+ runEventLoop();
+ STOP_ON_FAILURE
+
+ QCOMPARE(nRequests, 0);
+ QCOMPARE(finishedCount, ExpectedSuccessfulRequests);
+}
+
void tst_Http2::serverStarted(quint16 port)
{
serverPort = port;
@@ -1203,6 +1516,7 @@ void tst_Http2::sendRequest(int streamNumber,
url.setPath(QString("/stream%1.html").arg(streamNumber));
QNetworkRequest request(url);
+ request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));