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.cpp431
1 files changed, 375 insertions, 56 deletions
diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp
index 34b7e8fa37..d9e82330b2 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>
@@ -50,6 +25,7 @@
#include <QtCore/qobject.h>
#include <QtCore/qthread.h>
#include <QtCore/qurl.h>
+#include <QtCore/qset.h>
#include <cstdlib>
#include <memory>
@@ -62,6 +38,8 @@ Q_DECLARE_METATYPE(QNetworkRequest::Attribute)
QT_BEGIN_NAMESPACE
+using namespace Qt::StringLiterals;
+
QHttp2Configuration qt_defaultH2Configuration()
{
QHttp2Configuration config;
@@ -92,8 +70,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,9 +96,18 @@ private slots:
void authenticationRequired_data();
void authenticationRequired();
+ void unsupportedAuthenticateChallenge();
+
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);
@@ -228,6 +218,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");
@@ -258,7 +254,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");
});
@@ -296,7 +292,7 @@ void tst_Http2::singleRequest()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -305,10 +301,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();
@@ -337,7 +397,7 @@ void tst_Http2::multipleRequests()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -382,7 +442,7 @@ void tst_Http2::flowControlClientSide()
runEventLoop(120000);
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QVERIFY(windowUpdates > 0);
@@ -423,7 +483,7 @@ void tst_Http2::flowControlServerSide()
runEventLoop(120000);
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -467,7 +527,7 @@ void tst_Http2::pushPromise()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -593,7 +653,7 @@ void tst_Http2::earlyResponse()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
@@ -650,7 +710,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");
});
@@ -724,7 +784,7 @@ void tst_Http2::connectToHost()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -748,7 +808,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");
});
@@ -793,9 +853,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);
}
@@ -913,7 +973,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
@@ -933,7 +993,7 @@ void tst_Http2::moreActivitySignals()
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,
@@ -947,7 +1007,7 @@ void tst_Http2::moreActivitySignals()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -1024,7 +1084,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
@@ -1060,7 +1120,7 @@ void tst_Http2::contentEncoding()
runEventLoop();
STOP_ON_FAILURE
- QVERIFY(nRequests == 0);
+ QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
@@ -1073,13 +1133,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()
@@ -1090,10 +1155,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();
@@ -1129,24 +1199,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:
@@ -1154,12 +1230,105 @@ 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::unsupportedAuthenticateChallenge()
+{
+ clearHTTP2State();
+ serverPort = 0;
+
+ if (defaultConnectionType() == H2Type::h2c)
+ QSKIP("This test requires TLS with ALPN to work");
+
+ ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
+ QByteArray responseBody = "Hello"_ba;
+ targetServer->setResponseBody(responseBody);
+ targetServer->setAuthenticationHeader("Bearer realm=\"qt.io accounts\"");
+
+ QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection);
+ runEventLoop();
+
+ QVERIFY(serverPort != 0);
+
+ nRequests = 1;
+
+ QUrl url = requestUrl(defaultConnectionType());
+ url.setPath("/index.html");
+ QNetworkRequest request(url);
+
+ QByteArray expectedBody = "Hello, World!";
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
+ QScopedPointer<QNetworkReply> reply;
+ reply.reset(manager->post(request, expectedBody));
+
+ bool authenticationRequested = false;
+ connect(manager.get(), &QNetworkAccessManager::authenticationRequired, reply.get(),
+ [&](QNetworkReply *, QAuthenticator *auth) {
+ authenticationRequested = true;
+ });
+
+ bool finishedReceived = false;
+ connect(reply.get(), &QNetworkReply::finished, reply.get(),
+ [&]() { finishedReceived = true; });
+ bool errorReceived = false;
+ connect(reply.get(), &QNetworkReply::errorOccurred, reply.get(),
+ [&]() { errorReceived = true; });
+
+ QSet<quint32> receivedDataOnStreams;
+ connect(targetServer.get(), &Http2Server::receivedDATAFrame, reply.get(),
+ [&receivedDataOnStreams](quint32 streamID, const QByteArray &body) {
+ Q_UNUSED(body);
+ receivedDataOnStreams.insert(streamID);
+ });
+
+ // 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");
+
+ QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError);
+ QVERIFY(reply->isFinished());
+ QVERIFY(errorReceived);
+ QVERIFY(finishedReceived);
+ QCOMPARE(receivedDataOnStreams.size(), 1);
+ QVERIFY(receivedDataOnStreams.contains(1)); // the original, failed, request
+
+ QVERIFY(!authenticationRequested);
+
+ // We should not have sent any authentication headers to the server, since
+ // we don't support the challenge.
+ const QByteArray reqAuthHeader = targetServer->requestAuthorizationHeader();
+ QVERIFY(reqAuthHeader.isEmpty());
+
+ // 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");
@@ -1226,6 +1395,156 @@ void tst_Http2::h2cAllowedAttribute()
}
}
+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;