diff options
Diffstat (limited to 'tests/auto/core')
86 files changed, 5330 insertions, 419 deletions
diff --git a/tests/auto/core/CMakeLists.txt b/tests/auto/core/CMakeLists.txt new file mode 100644 index 000000000..5908756b4 --- /dev/null +++ b/tests/auto/core/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +add_subdirectory(qwebenginecookiestore) +add_subdirectory(qwebengineloadinginfo) +add_subdirectory(qwebenginesettings) +if(QT_FEATURE_ssl) + # only tests doh, and requires ssl + add_subdirectory(qwebengineglobalsettings) +endif() +add_subdirectory(qwebengineurlrequestinterceptor) +add_subdirectory(qwebengineurlrequestjob) +add_subdirectory(origins) +add_subdirectory(devtools) +add_subdirectory(getdomainandregistry) +add_subdirectory(qtversion) + +if(QT_FEATURE_ssl) + add_subdirectory(qwebengineclientcertificatestore) + add_subdirectory(certificateerror) +endif() + +if(QT_FEATURE_webenginedriver) + add_subdirectory(webenginedriver) +endif() diff --git a/tests/auto/core/certificateerror/BLACKLIST b/tests/auto/core/certificateerror/BLACKLIST new file mode 100644 index 000000000..a8fd16bf3 --- /dev/null +++ b/tests/auto/core/certificateerror/BLACKLIST @@ -0,0 +1,2 @@ +[fatalError] +* diff --git a/tests/auto/core/certificateerror/CMakeLists.txt b/tests/auto/core/certificateerror/CMakeLists.txt new file mode 100644 index 000000000..6223ca25c --- /dev/null +++ b/tests/auto/core/certificateerror/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../httpserver/httpserver.cmake) +include(../../util/util.cmake) + +qt_internal_add_test(tst_certificateerror + SOURCES + tst_certificateerror.cpp + LIBRARIES + Qt::WebEngineCore + Test::HttpServer + Test::Util +) + +set(tst_certificateerror_resource_files + "resources/server.pem" + "resources/server.key" +) + +qt_internal_add_resource(tst_certificateerror "tst_certificateerror" + PREFIX + "/" + FILES + ${tst_certificateerror_resource_files} +) diff --git a/tests/auto/core/certificateerror/resources/server.key b/tests/auto/core/certificateerror/resources/server.key new file mode 100644 index 000000000..9bf87aee3 --- /dev/null +++ b/tests/auto/core/certificateerror/resources/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAqAygFPG5ILLb3G51D0OIN4Kpm5t3Oh1nByTnvi1kMz+sCBBd +CSugt4NnKkB6kiGtMEsrEm1/xg8Bkfbpet3v3+jAidRpjvCISqy3Z9D1cgCFM46h +iob/AvLZpqITiAgsU4fJ4auuIKhFplIGrIKMv2gK8haoBGBoRUD1RM+irwjEr6TN +XTQt2Ap+Ouxs53NLPhAOgumpfzzRR8/Umbhen+G5MhH+XTzzreiClz2V6A79ePJj +y1uQ8NJ79feOOWBDRizRDWwxsnNd24GjkpvcaTwafiK6Vdqeub+XTtiB5RPal2on +Cj0TQDcnaacecl/zmUWsIFNkNJWDcd3/vEdyOQIDAQABAoIBAQCW93icOCdim6tu +FIDu7HEjxSsPUpPCToWu4lWaAHcinxGx0NlzkpD4K4DzcSdrvfszBmQ0UtBVokd7 +1IAdU+HZmePWLk+CDM2zoAPHrO3Cs3r2PS0cIHhZMsearcG0E/uWMseHB08PoXuo +lcnPEhzVGueyYe4guGcTx+5PGeUBLf+fJcEc3rIQnT2LYulM2aqBZSQM3jRUaPYs +F0awDpCNwajW/Bt2VB14Pr+H5MJ+WSznFCqW7SolBkqDGfKckXPSHgX6xZ0y7VCI +MM8vwlVI4mPkaHvSQMSI8vS4Qh+SGQCSs/AuuNLjjPoz1YotV3Ih4YbLj6BjFP2g +CrqzT6VNAoGBANOHmsqE0nRkLzonTDrMdla5b0TjTxwtNM5DjLgJa6UBBqPe+1Lv +JFoBP9bIfYDRWZOZrxXItfMmM43nK/ST6Xqgx1IpHUCLKVr2pA9RXrP+m4oawfgn +frW212fHibeOYiLy+DaQXQ0VRFxsc/VbwKVyVlMEcNg3N93x2E67M7vjAoGBAMtg +7wDa+5gjwuyNr7LKkp5VDTmtKQhoDtg4sw6MSQSMF6fJT9Z4kGTZ23+G85/LsM/k +iXbceabGJ0CQJvGn6oW4dI2Ut2c2nCNVbQCxJ6Nyn/yW7bRLShMnwXvbGAVxVUax +5ohJPZGJ8ar2CP76A0bkvm2Nwylq2gp6Y8h7+iwzAoGBAKizwfQ6sk45iKDsrpNG +dir8gY2DbJigRTksDpLIkJ1skAspz295YpiV3oBCLjYKwVJCg6zwAo0FrqBB+oB5 +ZwByMgWI3NeZJUZy5q2Ay/Lp4MroRELR3PC3/lu6fE90szgEZ4m84TmJ+Jdtt527 +q41H/yj+pbELePb95vIDw2LZAoGBAJBZ+MmupCzUFSI5Xp+UUIS48W4ijaE92mt1 +swF8aMcleBTLOjOL11D9oGHfs0OUG6czGq6WxnGs62dT6ZBUEo1e4rsq9xH3HNOn +anq3Qt8sGIn7xjPVzHnUGeyDEYWrb0+CLZJGCcEnG7SwdKolYfYLnW281Oysvp35 +SKGf/W0pAoGAa2+sZmhb1mpGAf6Bi4z+uym/6qOJmG6CnrBSM9e/r8nujwFVkCYF +3iz48qx3GbuliO6za8aM1drX2u8KWp1uP5KzwYvtW5SfpQ1eusFblHEYQQNRcKLT +j/wZBXnU961eMKkkTe2XsPirO8rVhVmxuFLqT/aEPffcragQFFIGOEQ= +-----END RSA PRIVATE KEY----- diff --git a/tests/auto/core/certificateerror/resources/server.pem b/tests/auto/core/certificateerror/resources/server.pem new file mode 100644 index 000000000..a201ed08e --- /dev/null +++ b/tests/auto/core/certificateerror/resources/server.pem @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIUFZEIIzeR7lEA10rb14w7MfhP87MwDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy +bGluMRUwEwYDVQQKDAxUaGVRdENvbXBhbnkxEjAQBgNVBAsMCXdlYmVuZ2luZTAe +Fw0yMTA1MTAyMTM1MTJaFw0yMjA1MTAyMTM1MTJaMGAxCzAJBgNVBAYTAkRFMQ8w +DQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEVMBMGA1UECgwMVGhlUXRD +b21wYW55MRgwFgYDVQQDDA93ZWJlbmdpbmUucXQuaW8wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCoDKAU8bkgstvcbnUPQ4g3gqmbm3c6HWcHJOe+LWQz +P6wIEF0JK6C3g2cqQHqSIa0wSysSbX/GDwGR9ul63e/f6MCJ1GmO8IhKrLdn0PVy +AIUzjqGKhv8C8tmmohOICCxTh8nhq64gqEWmUgasgoy/aAryFqgEYGhFQPVEz6Kv +CMSvpM1dNC3YCn467Gznc0s+EA6C6al/PNFHz9SZuF6f4bkyEf5dPPOt6IKXPZXo +Dv148mPLW5Dw0nv19445YENGLNENbDGyc13bgaOSm9xpPBp+IrpV2p65v5dO2IHl +E9qXaicKPRNANydppx5yX/OZRawgU2Q0lYNx3f+8R3I5AgMBAAGjMzAxMBoGA1Ud +EQQTMBGCD3dlYmVuZ2luZS5xdC5pbzATBgNVHSUEDDAKBggrBgEFBQcDATANBgkq +hkiG9w0BAQsFAAOCAQEAjThKpP0sBv1vEmaqBc1wTu//7RHmFcoStTt3scADzb2C +9gjOVC4NzxBneLkv01444Z1p/Iiu/ZZ+VKu7aJElJgnBWEisYwJ09t3cdZRA0UY7 +XRvTVAqV0OlsB1Jn0afE+aTLGjWo+jSYzua0O+NK74e23p9jkdSmXxH9w0FB/oyM +FGIOFnnfP0+QR4ZVvAGk2H60tBHQKmCM6b87TiD4GQIfOghCQWH+qJYSuyGu4hkE +uis+n1KHHhed3GIJOHpm7gt1C9qtjcp1nOpv0ycQjfc9CGvr02BcQjhMeO65hX0A +TvCgKN9/XMFv5jwwjjPCL12GBhwnN2k9hM/tEYpe2A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDOzCCAiMCFDwWg4NZxCplj3qyBxAUTi1wmj4jMA0GCSqGSIb3DQEBCwUAMFox +CzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEV +MBMGA1UECgwMVGhlUXRDb21wYW55MRIwEAYDVQQLDAl3ZWJlbmdpbmUwHhcNMjEw +NTEwMjEzMTE4WhcNMjIwNTEwMjEzMTE4WjBaMQswCQYDVQQGEwJERTEPMA0GA1UE +CAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xFTATBgNVBAoMDFRoZVF0Q29tcGFu +eTESMBAGA1UECwwJd2ViZW5naW5lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAuc/8xVrfSzOsI6kYul+o1QIPBh1I86eQm1PhTBDMAAPHuzyPaEMgBkn2 +XAUmvkynGpNioaJDU2ndV2fBHvsoeQCdNNmjFTe1rKYjrN6U2X5KoYSzN93TOYzK +aR38fEFx+w4qV76nnxSjYtGNe9z74GrfWFMdDQ0NJKzvaO4gaZ+OOg0OzWy4MJQ0 +aINo3UV55Y7Nt92AxFweiuHucKu+rjf3BX7n0Af/Tcs2c84f0R3HA7euReSibVvX +f33eHLRKwu2bvDjXiUzOdkxBn9GTo6Q09LyY6wDG0ZdWnyCKj3NBQKBVrq+bs3Q0 +ATsWhj/PvYlZhhZh4EOlqYOhCpwv4wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCC +pLSFGJcG0zhHW+2A6ogmpn2tA8gKUZx7f0J1nwgPEoAXQqWQv/299ZtmWfMKHUkk +ygG4u80C87wWPH42XWXo/KDrP9iYzoqAvtqbRuPG9PAxefQ/JUSnuhikA51g9+Mu +IDKKKSI+y/JW9u0Qo77fp/5n2DaFn5B+pBYvn/xLfaEa9bRdJMTEMsElGbPBzMZd +I/7X6B78X6Ow5TuRKSeZA7E1AZ/+e5A4Hj65bLAugoSKz3zaS0dV26LwAo18c2zP +TqtwHyIVj4QCoI6Z694q9KH4Pkml3fz8VSkk+MvZMWapvUhHu/DneTgqGbp9POYg +nx6oWME6idhnvN6DljxB +-----END CERTIFICATE----- diff --git a/tests/auto/core/certificateerror/tst_certificateerror.cpp b/tests/auto/core/certificateerror/tst_certificateerror.cpp new file mode 100644 index 000000000..67e2d8ae4 --- /dev/null +++ b/tests/auto/core/certificateerror/tst_certificateerror.cpp @@ -0,0 +1,134 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include <httpsserver.h> +#include <util.h> + +#include <QWebEngineCertificateError> +#include <QWebEnginePage> +#include <QWebEngineSettings> + +#include <QtTest/QtTest> + +class tst_CertificateError : public QObject +{ + Q_OBJECT +public: + tst_CertificateError() { } + +private Q_SLOTS: + void handleError_data(); + void handleError(); + void fatalError(); +}; + +struct PageWithCertificateErrorHandler : QWebEnginePage +{ + Q_OBJECT + +public: + PageWithCertificateErrorHandler(bool defer, bool accept, QObject *p = nullptr) + : QWebEnginePage(p), deferError(defer), acceptCertificate(accept) + , loadSpy(this, &QWebEnginePage::loadFinished) + { + connect(this, &PageWithCertificateErrorHandler::certificateError, + this, &PageWithCertificateErrorHandler::onCertificateError); + } + + bool deferError, acceptCertificate; + + QSignalSpy loadSpy; + QScopedPointer<QWebEngineCertificateError> error; + +public Q_SLOTS: + void onCertificateError(QWebEngineCertificateError e) + { + error.reset(new QWebEngineCertificateError(e)); + if (deferError) { + error->defer(); + return; + } + + if (acceptCertificate) + error->acceptCertificate(); + else + error->rejectCertificate(); + } +}; + +void tst_CertificateError::handleError_data() +{ + QTest::addColumn<bool>("deferError"); + QTest::addColumn<bool>("acceptCertificate"); + QTest::addColumn<QString>("expectedContent"); + QTest::addRow("Reject") << false << false << QString(); + QTest::addRow("DeferReject") << true << false << QString(); + QTest::addRow("DeferAccept") << true << true << "TEST"; +} + +void tst_CertificateError::handleError() +{ + HttpsServer server(":/resources/server.pem", ":/resources/server.key", ""); + server.setExpectError(false); + QVERIFY(server.start()); + + connect(&server, &HttpsServer::newRequest, [&] (HttpReqRep *rr) { + rr->setResponseBody(QByteArrayLiteral("<html><body>TEST</body></html>")); + rr->sendResponse(); + }); + + QFETCH(bool, deferError); + QFETCH(bool, acceptCertificate); + QFETCH(QString, expectedContent); + + PageWithCertificateErrorHandler page(deferError, acceptCertificate); + page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + + page.setUrl(server.url()); + QTRY_VERIFY(page.error); + QVERIFY(page.error->isOverridable()); + auto chain = page.error->certificateChain(); + QCOMPARE(chain.size(), 2); + QCOMPARE(chain[0].serialNumber(), "15:91:08:23:37:91:ee:51:00:d7:4a:db:d7:8c:3b:31:f8:4f:f3:b3"); + QCOMPARE(chain[1].serialNumber(), "3c:16:83:83:59:c4:2a:65:8f:7a:b2:07:10:14:4e:2d:70:9a:3e:23"); + + if (deferError) { + QCOMPARE(page.loadSpy.size(), 0); + QCOMPARE(toPlainTextSync(&page), QString()); + + if (acceptCertificate) + page.error->acceptCertificate(); + else + page.error->rejectCertificate(); + + page.error.reset(); + } + QTRY_COMPARE_WITH_TIMEOUT(page.loadSpy.size(), 1, 30000); + QCOMPARE(page.loadSpy.takeFirst().value(0).toBool(), acceptCertificate); + QCOMPARE(toPlainTextSync(&page), expectedContent); + QVERIFY(server.stop()); +} + +void tst_CertificateError::fatalError() +{ + PageWithCertificateErrorHandler page(false, false); + page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished); + + page.setUrl(QUrl("https://revoked.badssl.com")); + if (!loadFinishedSpy.wait(10000)) { + QVERIFY2(!page.error, "There shouldn't be any certificate error if not loaded due to missing internet access!"); + QSKIP("Couldn't load page from network, skipping test."); + } + + // revoked certificate might not be reported as invalid by chromium and the load will silently succeed + bool failed = !loadFinishedSpy.first().first().toBool(), hasError = bool(page.error); + QCOMPARE(failed, hasError); + if (hasError) { + QVERIFY(!page.error->isOverridable()); + // Fatal certificate errors are implicitly rejected. But second call should not cause crash. + page.error->rejectCertificate(); + } +} + +QTEST_MAIN(tst_CertificateError) +#include <tst_certificateerror.moc> diff --git a/tests/auto/core/core.pro b/tests/auto/core/core.pro deleted file mode 100644 index 1dc5e052c..000000000 --- a/tests/auto/core/core.pro +++ /dev/null @@ -1,9 +0,0 @@ -TEMPLATE = subdirs -QT_FOR_CONFIG += network-private - -SUBDIRS += \ - qwebenginecookiestore \ - qwebengineurlrequestinterceptor - -qtConfig(ssl): SUBDIRS += qwebengineclientcertificatestore - diff --git a/tests/auto/core/devtools/CMakeLists.txt b/tests/auto/core/devtools/CMakeLists.txt new file mode 100644 index 000000000..efde75240 --- /dev/null +++ b/tests/auto/core/devtools/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_devtools + SOURCES + tst_devtools.cpp + LIBRARIES + Qt::WebEngineCore +) + diff --git a/tests/auto/core/devtools/tst_devtools.cpp b/tests/auto/core/devtools/tst_devtools.cpp new file mode 100644 index 000000000..57a2b83a3 --- /dev/null +++ b/tests/auto/core/devtools/tst_devtools.cpp @@ -0,0 +1,69 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> + +#include <qwebenginepage.h> + +class tst_DevTools : public QObject { + Q_OBJECT + +private Q_SLOTS: + void attachAndDestroyPageFirst(); + void attachAndDestroyInspectorFirst(); +}; + +void tst_DevTools::attachAndDestroyPageFirst() +{ + // External inspector + manual destruction of page first + QWebEnginePage* page = new QWebEnginePage(); + QWebEnginePage* inspector = new QWebEnginePage(); + + QSignalSpy spy(page, &QWebEnginePage::loadFinished); + page->load(QUrl("data:text/plain,foobarbaz")); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 12000); + + // shouldn't do anything until page is set + page->triggerAction(QWebEnginePage::InspectElement); + + inspector->setInspectedPage(page); + page->triggerAction(QWebEnginePage::InspectElement); + + // This is deliberately racy: + QTest::qWait(10); + + delete page; + delete inspector; +} + +void tst_DevTools::attachAndDestroyInspectorFirst() +{ + // External inspector + manual destruction of inspector first + QWebEnginePage* page = new QWebEnginePage(); + + // shouldn't do anything until page is set + page->triggerAction(QWebEnginePage::InspectElement); + + QWebEnginePage* inspector = new QWebEnginePage(); + inspector->setInspectedPage(page); + + QSignalSpy spy(page, &QWebEnginePage::loadFinished); + page->setHtml(QStringLiteral("<body><h1>FOO BAR!</h1></body>")); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 12000); + + page->triggerAction(QWebEnginePage::InspectElement); + + delete inspector; + + page->triggerAction(QWebEnginePage::InspectElement); + + // This is deliberately racy: + QTest::qWait(10); + + delete page; +} + + +QTEST_MAIN(tst_DevTools) + +#include "tst_devtools.moc" diff --git a/tests/auto/core/getdomainandregistry/CMakeLists.txt b/tests/auto/core/getdomainandregistry/CMakeLists.txt new file mode 100644 index 000000000..c2b65f9dc --- /dev/null +++ b/tests/auto/core/getdomainandregistry/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../util/util.cmake) + +qt_internal_add_test(tst_getdomainandregistry + SOURCES + tst_getdomainandregistry.cpp + LIBRARIES + Qt::WebEngineCore + Test::Util +) diff --git a/tests/auto/core/getdomainandregistry/tst_getdomainandregistry.cpp b/tests/auto/core/getdomainandregistry/tst_getdomainandregistry.cpp new file mode 100644 index 000000000..e9e0bf105 --- /dev/null +++ b/tests/auto/core/getdomainandregistry/tst_getdomainandregistry.cpp @@ -0,0 +1,30 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> +#include <QtWebEngineCore/qtwebenginecoreglobal.h> + +class tst_GetDomainAndRegistry final : public QObject { + Q_OBJECT + +private Q_SLOTS: + void getDomainAndRegistry(); +}; + +void tst_GetDomainAndRegistry::getDomainAndRegistry() { + QCOMPARE(qWebEngineGetDomainAndRegistry({"http://www.google.com/"}), QString("google.com")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"http://www.google.co.uk/"}), QString("google.co.uk")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"http://127.0.0.1/"}), QString()); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://qt.io/"}), QString("qt.io")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://download.qt.io/"}), QString("qt.io")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://foo.fr/"}), QString("foo.fr")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://foo.gouv.fr/"}), QString("foo.gouv.fr")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://bar.foo.gouv.fr/"}), QString("foo.gouv.fr")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://fr.wikipedia.org/wiki/%C3%89l%C3%A9phant"}), QString("wikipedia.org")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://foo.günstigbestellen.de/"}), QString("foo.xn--gnstigbestellen-zvb.de")); + QCOMPARE(qWebEngineGetDomainAndRegistry({"https://foo.g\u00fcnstigbestellen.de/"}), QString("foo.xn--gnstigbestellen-zvb.de")); +} + + +QTEST_MAIN(tst_GetDomainAndRegistry) +#include "tst_getdomainandregistry.moc" diff --git a/tests/auto/core/origins/CMakeLists.txt b/tests/auto/core/origins/CMakeLists.txt new file mode 100644 index 000000000..306074994 --- /dev/null +++ b/tests/auto/core/origins/CMakeLists.txt @@ -0,0 +1,52 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../httpserver/httpserver.cmake) +include(../../util/util.cmake) + +qt_internal_add_test(tst_origins + SOURCES + tst_origins.cpp + LIBRARIES + Qt::WebEngineCore + Qt::WebEngineWidgets + Test::HttpServer + Test::Util +) + +set(tst_origins_resource_files + "resources/createObjectURL.html" + "resources/dedicatedWorker.html" + "resources/dedicatedWorker.js" + "resources/mixedSchemes.html" + "resources/mixedSchemesWithCsp.html" + "resources/mixedSchemes_frame.html" + "resources/mixedXHR.html" + "resources/mixedXHR.txt" + "resources/serviceWorker.html" + "resources/serviceWorker.js" + "resources/sharedWorker.html" + "resources/sharedWorker.js" + "resources/subdir/frame2.html" + "resources/subdir/index.html" + "resources/subdir_frame1.html" + "resources/viewSource.html" + "resources/websocket.html" + "resources/websocket2.html" + "resources/red.png" + "resources/fetchApi.html" +) + +qt_internal_add_resource(tst_origins "tst_origins" + PREFIX + "/" + FILES + ${tst_origins_resource_files} +) + +qt_internal_extend_target(tst_origins CONDITION QT_FEATURE_webengine_webchannel AND TARGET Qt::WebSockets + DEFINES + WEBSOCKETS + LIBRARIES + Qt::WebSockets +) diff --git a/tests/auto/core/origins/resources/createObjectURL.html b/tests/auto/core/origins/resources/createObjectURL.html new file mode 100644 index 000000000..133f636bb --- /dev/null +++ b/tests/auto/core/origins/resources/createObjectURL.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>createObjectURL</title> + <script> + const blob = new Blob(['foo']); + const result = URL.createObjectURL(blob); + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/dedicatedWorker.html b/tests/auto/core/origins/resources/dedicatedWorker.html new file mode 100644 index 000000000..cb4f14e73 --- /dev/null +++ b/tests/auto/core/origins/resources/dedicatedWorker.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <title>dedicatedWorker</title> + <script> + var done = false; + var result; + var error; + try { + let worker = new Worker("dedicatedWorker.js"); + worker.onmessage = (e) => { done = true; result = e.data; }; + worker.postMessage(41); + } catch (e) { + done = true; error = e.message; + } + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/dedicatedWorker.js b/tests/auto/core/origins/resources/dedicatedWorker.js new file mode 100644 index 000000000..2631939d7 --- /dev/null +++ b/tests/auto/core/origins/resources/dedicatedWorker.js @@ -0,0 +1 @@ +onmessage = (e) => { postMessage(e.data + 1); }; diff --git a/tests/auto/core/origins/resources/fetchApi.html b/tests/auto/core/origins/resources/fetchApi.html new file mode 100644 index 000000000..7b54416fd --- /dev/null +++ b/tests/auto/core/origins/resources/fetchApi.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> +<body> + <script type="text/javascript"> + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const url = urlParams.get('url'); + const f = fetch(url); + if (urlParams.get('printRes') == 'true') { + f.then((r) => r.text()).then((p) => console.log(p)); + } + </script> +</body> +</html> diff --git a/tests/auto/core/origins/resources/link.html b/tests/auto/core/origins/resources/link.html new file mode 100644 index 000000000..297b9b273 --- /dev/null +++ b/tests/auto/core/origins/resources/link.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <title>Link</title> + </head> + <body> + <a id="link" href="">Link</a> + <script> + const urlParams = new URLSearchParams(window.location.search); + document.getElementById("link").href = urlParams.get('linkLocation'); + </script> + </body> +</html> diff --git a/tests/auto/core/origins/resources/media.html b/tests/auto/core/origins/resources/media.html new file mode 100644 index 000000000..091485b61 --- /dev/null +++ b/tests/auto/core/origins/resources/media.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Media</title> + <script> + function addAudio(src) { + let aud = document.createElement('audio') + aud.src = src + document.getElementsByTagName("body")[0].appendChild(aud) + } + </script> + </head> + <body> + </body> +</html> diff --git a/tests/auto/core/origins/resources/mixedSchemes.html b/tests/auto/core/origins/resources/mixedSchemes.html new file mode 100644 index 000000000..3e50c2c3b --- /dev/null +++ b/tests/auto/core/origins/resources/mixedSchemes.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Mixed</title> + <script> + var result; + var canary; + + function setIFrameUrl(frameUrl,imgUrl) { + result = undefined; + canary = undefined; + let img = document.createElement('img'); + img.onerror = function() { + console.log("TEST:cannotLoad"); + console.log("TEST:done"); + }; + img.onload = function() { + document.getElementById("iframe").setAttribute("src", frameUrl); + }; + img.src = imgUrl + } + + addEventListener("load", function() { + document.getElementById("iframe").addEventListener("load", function() { + if (canary && window[0].canary) { + console.log("TEST:canLoadAndAccess"); + console.log("TEST:done"); + } else { + console.log("TEST:canLoadButNotAccess"); + console.log("TEST:done"); + } + }); + }); + window.onerror = function(message, url, line, col, errorObj) { + return true; + }; + </script> + </head> + <body> + <iframe id="iframe"></iframe> + </body> +</html> diff --git a/tests/auto/core/origins/resources/mixedSchemesWithCsp.html b/tests/auto/core/origins/resources/mixedSchemesWithCsp.html new file mode 100644 index 000000000..ad7cbeeb7 --- /dev/null +++ b/tests/auto/core/origins/resources/mixedSchemesWithCsp.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Security-Policy" content="frame-src 'none'"> + <title>Mixed</title> + <script> + var result; + var canary; + + function setIFrameUrl(url) { + result = undefined; + canary = undefined; + document.getElementById("iframe").setAttribute("src", url); + // Early fire is OK unless the test is expecting cannotLoad. + // If timeout is too short then a false positive is possible. + setTimeout(() => { result = result || "cannotLoad"; }, 500); + } + + addEventListener("load", function() { + document.getElementById("iframe").addEventListener("load", function() { + if (canary && window[0].canary) + result = "canLoadAndAccess"; + else + result = "canLoadButNotAccess"; + }); + }); + </script> + </head> + <body> + <iframe id="iframe"></iframe> + </body> +</html> diff --git a/tests/auto/core/origins/resources/mixedSchemes_frame.html b/tests/auto/core/origins/resources/mixedSchemes_frame.html new file mode 100644 index 000000000..9499caa1f --- /dev/null +++ b/tests/auto/core/origins/resources/mixedSchemes_frame.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Mixed - Frame</title> + <script> + try{ + var canary = true; + parent.canary = true; + }catch(exception){ + }; + + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/mixedXHR.html b/tests/auto/core/origins/resources/mixedXHR.html new file mode 100644 index 000000000..3dfd90006 --- /dev/null +++ b/tests/auto/core/origins/resources/mixedXHR.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <title>Mixed</title> + <script> + var result; + function sendXHR(url) { + result = undefined; + let req = new XMLHttpRequest(); + req.addEventListener("load", () => { result = req.responseText }); + req.addEventListener("error", () => { result = "error"; }); + req.open("GET", url); + req.send(); + } + </script> + </head> + <body> + </body> +</html> diff --git a/tests/auto/core/origins/resources/mixedXHR.txt b/tests/auto/core/origins/resources/mixedXHR.txt new file mode 100644 index 000000000..b5754e203 --- /dev/null +++ b/tests/auto/core/origins/resources/mixedXHR.txt @@ -0,0 +1 @@ +ok
\ No newline at end of file diff --git a/tests/auto/core/origins/resources/red.png b/tests/auto/core/origins/resources/red.png Binary files differnew file mode 100644 index 000000000..5ae85192b --- /dev/null +++ b/tests/auto/core/origins/resources/red.png diff --git a/tests/auto/core/origins/resources/redirect.css b/tests/auto/core/origins/resources/redirect.css new file mode 100644 index 000000000..a6a03d8f5 --- /dev/null +++ b/tests/auto/core/origins/resources/redirect.css @@ -0,0 +1,3 @@ +body { + font-family: serif; +} diff --git a/tests/auto/core/origins/resources/redirect.html b/tests/auto/core/origins/resources/redirect.html new file mode 100644 index 000000000..603cb76f0 --- /dev/null +++ b/tests/auto/core/origins/resources/redirect.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <title>redirect</title> + <script> + function addStylesheetLink(src) { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = src; + document.getElementsByTagName("head")[0].appendChild(link); + } + </script> + </head> + <body> + Text + </body> +</html> diff --git a/tests/auto/core/origins/resources/serviceWorker.html b/tests/auto/core/origins/resources/serviceWorker.html new file mode 100644 index 000000000..27890c98f --- /dev/null +++ b/tests/auto/core/origins/resources/serviceWorker.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + <head> + <title>serviceWorker</title> + <script> + var done = false; + var error; + try { + navigator.serviceWorker.register("serviceWorker.js") + .then((r) => { done = true; }) + .catch((e) => { done = true; error = e.message; }); + } catch (e) { + done = true; error = e.message; + } + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/serviceWorker.js b/tests/auto/core/origins/resources/serviceWorker.js new file mode 100644 index 000000000..40a8c178f --- /dev/null +++ b/tests/auto/core/origins/resources/serviceWorker.js @@ -0,0 +1 @@ +/* empty */ diff --git a/tests/auto/core/origins/resources/sharedWorker.html b/tests/auto/core/origins/resources/sharedWorker.html new file mode 100644 index 000000000..8b5a0a794 --- /dev/null +++ b/tests/auto/core/origins/resources/sharedWorker.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <title>sharedWorker</title> + <script> + var done; + var result; + var error; + try { + let worker = new SharedWorker("sharedWorker.js"); + worker.port.onmessage = (e) => { done = true; result = e.data; }; + worker.port.postMessage(41); + } catch (e) { + done = true; error = e.message; + } + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/sharedWorker.js b/tests/auto/core/origins/resources/sharedWorker.js new file mode 100644 index 000000000..60ef93a5f --- /dev/null +++ b/tests/auto/core/origins/resources/sharedWorker.js @@ -0,0 +1,6 @@ +onconnect = function(e) { + let port = e.ports[0]; + port.onmessage = function(e) { + port.postMessage(e.data + 1); + }; +}; diff --git a/tests/auto/core/origins/resources/subdir/frame2.html b/tests/auto/core/origins/resources/subdir/frame2.html new file mode 100644 index 000000000..3a2f664ca --- /dev/null +++ b/tests/auto/core/origins/resources/subdir/frame2.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>Subdir - Frame 2</title> + <script> + parent.msg[1] = "world"; + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/subdir/index.html b/tests/auto/core/origins/resources/subdir/index.html new file mode 100644 index 000000000..9c5d5d782 --- /dev/null +++ b/tests/auto/core/origins/resources/subdir/index.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head> + <title>Subdir</title> + + <script> + var msg = []; + </script> + + <!-- for manual testing --> + <script> + window.addEventListener("load", () => { + for (let i of [0, 1]) { + let p = document.createElement("p"); + p.appendChild(document.createTextNode(`frame ${i+1} says: ${msg[i]}`)); + document.body.insertBefore(p, null); + } + }); + </script> + + </head> + <body> + <iframe src="../subdir_frame1.html"></iframe> + <iframe src="frame2.html"></iframe> + </body> +</html> diff --git a/tests/auto/core/origins/resources/subdir_frame1.html b/tests/auto/core/origins/resources/subdir_frame1.html new file mode 100644 index 000000000..63973f2f4 --- /dev/null +++ b/tests/auto/core/origins/resources/subdir_frame1.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>Subdir - Frame 1</title> + <script> + parent.msg[0] = "hello"; + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/viewSource.html b/tests/auto/core/origins/resources/viewSource.html new file mode 100644 index 000000000..977074c74 --- /dev/null +++ b/tests/auto/core/origins/resources/viewSource.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>viewSource</title> + </head> + <body> + <p>viewSource</p> + </body> +</html> diff --git a/tests/auto/core/origins/resources/websocket.html b/tests/auto/core/origins/resources/websocket.html new file mode 100644 index 000000000..31db66571 --- /dev/null +++ b/tests/auto/core/origins/resources/websocket.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> + <head> + <title>WebSocket</title> + <script src="qrc:/qtwebchannel/qwebchannel.js"></script> + <script> + var result; + new QWebChannel(qt.webChannelTransport, channel => { + const ws = new WebSocket(channel.objects.echoServer.url); + ws.addEventListener("open", event => { + ws.send("ok"); + }); + ws.addEventListener("message", event => { + result = event.data; + }); + ws.addEventListener("close", event => { + result = event.code; + }); + }) + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/resources/websocket2.html b/tests/auto/core/origins/resources/websocket2.html new file mode 100644 index 000000000..7365143de --- /dev/null +++ b/tests/auto/core/origins/resources/websocket2.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> + <head> + <title>WebSocket</title> + <script src="../qtwebchannel/qwebchannel.js"></script> + <script> + var result; + new QWebChannel(qt.webChannelTransport, channel => { + const ws = new WebSocket(channel.objects.echoServer.url); + ws.addEventListener("open", event => { + ws.send("ok"); + }); + ws.addEventListener("message", event => { + result = event.data; + }); + ws.addEventListener("close", event => { + result = event.code; + }); + }) + </script> + </head> + <body></body> +</html> diff --git a/tests/auto/core/origins/tst_origins.cpp b/tests/auto/core/origins/tst_origins.cpp new file mode 100644 index 000000000..81385701f --- /dev/null +++ b/tests/auto/core/origins/tst_origins.cpp @@ -0,0 +1,1730 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <util.h> +#include "httpserver.h" + +#include <QtCore/qfile.h> +#include <QtTest/QtTest> +#include <QtWebEngineCore/qwebengineurlrequestinterceptor.h> +#include <QtWebEngineCore/qwebengineurlrequestjob.h> +#include <QtWebEngineCore/qwebengineurlscheme.h> +#include <QtWebEngineCore/qwebengineurlschemehandler.h> +#include <QtWebEngineCore/qwebenginesettings.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineWidgets/qwebengineview.h> + +#if defined(WEBSOCKETS) +#include <QtWebSockets/qwebsocket.h> +#include <QtWebSockets/qwebsocketserver.h> +#include <QtWebChannel/qwebchannel.h> +#endif +#include <qaction.h> + +#define QSL QStringLiteral +#define QBAL QByteArrayLiteral + +Q_LOGGING_CATEGORY(lc, "qt.webengine.tests") + +void registerSchemes() +{ + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax")); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-Secure")); + scheme.setFlags(QWebEngineUrlScheme::SecureScheme); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-Secure-ServiceWorkersAllowed")); + scheme.setFlags(QWebEngineUrlScheme::SecureScheme | QWebEngineUrlScheme::ServiceWorkersAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-Local")); + scheme.setFlags(QWebEngineUrlScheme::LocalScheme); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-LocalAccessAllowed")); + scheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-NoAccessAllowed")); + scheme.setFlags(QWebEngineUrlScheme::NoAccessAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-ServiceWorkersAllowed")); + scheme.setFlags(QWebEngineUrlScheme::ServiceWorkersAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("PathSyntax-ViewSourceAllowed")); + scheme.setFlags(QWebEngineUrlScheme::ViewSourceAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("HostSyntax")); + scheme.setSyntax(QWebEngineUrlScheme::Syntax::Host); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("HostSyntax-ContentSecurityPolicyIgnored")); + scheme.setSyntax(QWebEngineUrlScheme::Syntax::Host); + scheme.setFlags(QWebEngineUrlScheme::ContentSecurityPolicyIgnored); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("HostAndPortSyntax")); + scheme.setSyntax(QWebEngineUrlScheme::Syntax::HostAndPort); + scheme.setDefaultPort(42); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("HostPortAndUserInformationSyntax")); + scheme.setSyntax(QWebEngineUrlScheme::Syntax::HostPortAndUserInformation); + scheme.setDefaultPort(42); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("redirect")); + scheme.setFlags(QWebEngineUrlScheme::CorsEnabled); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("redirect-secure")); + scheme.setFlags(QWebEngineUrlScheme::SecureScheme); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("redirect-local")); + scheme.setFlags(QWebEngineUrlScheme::LocalScheme | QWebEngineUrlScheme::LocalAccessAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + + { + QWebEngineUrlScheme scheme(QBAL("cors")); + scheme.setFlags(QWebEngineUrlScheme::CorsEnabled); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme(QBAL("secure-cors")); + scheme.setFlags(QWebEngineUrlScheme::SecureScheme | QWebEngineUrlScheme::CorsEnabled); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme(QBAL("localaccess")); + scheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme(QBAL("local")); + scheme.setFlags(QWebEngineUrlScheme::LocalScheme); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme(QBAL("local-localaccess")); + scheme.setFlags(QWebEngineUrlScheme::LocalScheme | QWebEngineUrlScheme::LocalAccessAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme(QBAL("local-cors")); + scheme.setFlags(QWebEngineUrlScheme::LocalScheme | QWebEngineUrlScheme::CorsEnabled); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme("fetchapi-allowed"); + scheme.setFlags(QWebEngineUrlScheme::CorsEnabled | QWebEngineUrlScheme::FetchApiAllowed); + QWebEngineUrlScheme::registerScheme(scheme); + } + { + QWebEngineUrlScheme scheme("fetchapi-not-allowed"); + QWebEngineUrlScheme::registerScheme(scheme); + } +} +Q_CONSTRUCTOR_FUNCTION(registerSchemes) + +class TstUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { + Q_OBJECT + +public: + TstUrlSchemeHandler(QWebEngineProfile *profile) + { + profile->installUrlSchemeHandler(QBAL("tst"), this); + + profile->installUrlSchemeHandler(QBAL("PathSyntax"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-Secure"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-Secure-ServiceWorkersAllowed"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-Local"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-LocalAccessAllowed"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-NoAccessAllowed"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-ServiceWorkersAllowed"), this); + profile->installUrlSchemeHandler(QBAL("PathSyntax-ViewSourceAllowed"), this); + profile->installUrlSchemeHandler(QBAL("HostSyntax"), this); + profile->installUrlSchemeHandler(QBAL("HostSyntax-ContentSecurityPolicyIgnored"), this); + profile->installUrlSchemeHandler(QBAL("HostAndPortSyntax"), this); + profile->installUrlSchemeHandler(QBAL("HostPortAndUserInformationSyntax"), this); + profile->installUrlSchemeHandler(QBAL("redirect"), this); + profile->installUrlSchemeHandler(QBAL("redirect-secure"), this); + profile->installUrlSchemeHandler(QBAL("redirect-local"), this); + profile->installUrlSchemeHandler(QBAL("cors"), this); + profile->installUrlSchemeHandler(QBAL("secure-cors"), this); + profile->installUrlSchemeHandler(QBAL("localaccess"), this); + profile->installUrlSchemeHandler(QBAL("local"), this); + profile->installUrlSchemeHandler(QBAL("local-localaccess"), this); + profile->installUrlSchemeHandler(QBAL("local-cors"), this); + } + + QList<QUrl> &requests() { return m_requests; } + +private: + void requestStarted(QWebEngineUrlRequestJob *job) override + { + QUrl url = job->requestUrl(); + m_requests << url; + + if (url.scheme().startsWith("redirect")) { + QString path = url.path(); + int idx = path.indexOf(QChar('/')); + if (idx > 0) { + url.setScheme(path.first(idx)); + url.setPath(path.mid(idx, -1)); + job->redirect(url); + return; + } + } + + QString pathPrefix = QDir(QT_TESTCASE_SOURCEDIR).canonicalPath(); + if (url.path().startsWith("/qtwebchannel/")) + pathPrefix = QSL(":"); + QString pathSuffix = url.path(); + auto file = std::make_unique<QFile>(pathPrefix + pathSuffix, job); + if (!file->open(QIODevice::ReadOnly)) { + qWarning() << "Failed to read data for:" << url << file->errorString(); + job->fail(QWebEngineUrlRequestJob::RequestFailed); + return; + } + QByteArray mimeType = QBAL("text/html"); + if (pathSuffix.endsWith(QSL(".js"))) + mimeType = QBAL("application/javascript"); + else if (pathSuffix.endsWith(QSL(".css"))) + mimeType = QBAL("text/css"); + job->reply(mimeType, file.release()); + } + + QList<QUrl> m_requests; +}; + +class TestRequestInterceptor : public QWebEngineUrlRequestInterceptor +{ +public: + TestRequestInterceptor() = default; + void interceptRequest(QWebEngineUrlRequestInfo &info) override + { + qCDebug(lc) << this << "Type:" << info.resourceType() << info.requestMethod() << "Navigation:" << info.navigationType() + << info.requestUrl() << "Initiator:" << info.initiator(); + + QUrl url = info.requestUrl(); + requests << url; + if (url.scheme().startsWith("redirect")) { + QString path = url.path(); + int idx = path.indexOf(QChar('/')); + if (idx > 0) { + url.setScheme(path.first(idx)); + url.setPath(path.mid(idx, -1)); + info.redirect(url); + } + } + } + QList<QUrl> requests; +}; + +class TestPage : public QWebEnginePage +{ +public: + TestPage(QWebEngineProfile *profile) : QWebEnginePage(profile, nullptr) + { + } + void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel, + const QString &message, int, + const QString &) override + { + messages << message; + qCDebug(lc) << message; + } + + bool logContainsDoneMarker() const { return messages.contains("TEST:done"); } + + QString findResultInLog() const + { + // make sure we do not have some extra logs from blink + for (auto message : messages) { + QStringList s = message.split(':'); + if (s.size() > 1 && s[0] == "TEST") + return s[1]; + } + return QString(); + } + + void clearLog() { messages.clear(); } + +private: + QStringList messages; +}; + +class tst_Origins final : public QObject { + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + + void jsUrlCanon(); + void jsUrlRelative(); + void jsUrlOrigin(); + void subdirWithAccess(); + void subdirWithoutAccess(); + void fileAccessRemoteUrl_data(); + void fileAccessRemoteUrl(); + void fileAccessLocalUrl_data(); + void fileAccessLocalUrl(); + void mixedSchemes_data(); + void mixedSchemes(); + void mixedSchemesWithCsp(); + void mixedXHR_data(); + void mixedXHR(); + void mixedContent_data(); + void mixedContent(); + void localMediaBlock_data(); + void localMediaBlock(); +#if defined(WEBSOCKETS) + void webSocket(); +#endif + void dedicatedWorker(); + void sharedWorker(); + void serviceWorker(); + void viewSource(); + void createObjectURL(); + void redirectScheme(); + void redirectSchemeLocal(); + void redirectSchemeSecure(); + void redirectInterceptor(); + void redirectInterceptorLocal(); + void redirectInterceptorSecure(); + void redirectInterceptorFile(); + void redirectInterceptorHttp(); + void fetchApiCustomUrl_data(); + void fetchApiCustomUrl(); + void fetchApiHttpUrl(); + +private: + bool verifyLoad(const QUrl &url) + { + QSignalSpy spy(m_page, &QWebEnginePage::loadFinished); + m_page->load(url); + [&spy]() { QTRY_VERIFY_WITH_TIMEOUT(!spy.isEmpty(), 90000); }(); + return !spy.isEmpty() && spy.front().value(0).toBool(); + } + + QVariant eval(const QString &code) + { + return evaluateJavaScriptSync(m_page, code); + } + + QWebEngineProfile m_profile; + TestPage *m_page = nullptr; + TstUrlSchemeHandler *m_handler = nullptr; +}; + +void tst_Origins::initTestCase() +{ + QTest::ignoreMessage( + QtWarningMsg, + QRegularExpression("Please register the custom scheme 'tst'.*")); + + m_handler = new TstUrlSchemeHandler(&m_profile); +} + +void tst_Origins::cleanupTestCase() +{ + QVERIFY(!m_page); + delete m_handler; +} + +void tst_Origins::init() +{ + m_page = new TestPage(&m_profile); +} + +void tst_Origins::cleanup() +{ + delete m_page; + m_page = nullptr; + m_handler->requests().clear(); +} + +// Test URL parsing and canonicalization in Blink. The implementation of this +// part is mostly shared between Blink and Chromium proper. +void tst_Origins::jsUrlCanon() +{ + QVERIFY(verifyLoad(QSL("about:blank"))); + + // Standard schemes are biased towards the authority part. + QCOMPARE(eval(QSL("new URL(\"http:foo/bar\").href")), QVariant(QSL("http://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"http:/foo/bar\").href")), QVariant(QSL("http://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"http://foo/bar\").href")), QVariant(QSL("http://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"http:///foo/bar\").href")), QVariant(QSL("http://foo/bar"))); + + // The file scheme is however a (particularly) special case. +#ifdef Q_OS_WIN + QCOMPARE(eval(QSL("new URL(\"file:foo/bar\").href")), QVariant(QSL("file://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file:/foo/bar\").href")), QVariant(QSL("file://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file://foo/bar\").href")), QVariant(QSL("file://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file:///foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); +#else + QCOMPARE(eval(QSL("new URL(\"file:foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file:/foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file://foo/bar\").href")), QVariant(QSL("file://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"file:///foo/bar\").href")), QVariant(QSL("file:///foo/bar"))); +#endif + + // The qrc scheme is a PathSyntax scheme, having only a path and nothing else. + QCOMPARE(eval(QSL("new URL(\"qrc:foo/bar\").href")), QVariant(QSL("qrc:foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"qrc:/foo/bar\").href")), QVariant(QSL("qrc:/foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"qrc://foo/bar\").href")), QVariant(QSL("qrc://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"qrc:///foo/bar\").href")), QVariant(QSL("qrc:///foo/bar"))); + + // Same for unregistered schemes. + QCOMPARE(eval(QSL("new URL(\"tst:foo/bar\").href")), QVariant(QSL("tst:foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"tst:/foo/bar\").href")), QVariant(QSL("tst:/foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"tst://foo/bar\").href")), QVariant(QSL("tst://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"tst:///foo/bar\").href")), QVariant(QSL("tst:///foo/bar"))); + + // A HostSyntax scheme is like http without the port & user information. + QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo:42/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostSyntax:a:b@foo/bar\").href")), QVariant(QSL("hostsyntax://foo/bar"))); + + // A HostAndPortSyntax scheme is like http without the user information. + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo/bar\").href")), + QVariant(QSL("hostandportsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:41/bar\").href")), + QVariant(QSL("hostandportsyntax://foo:41/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:42/bar\").href")), + QVariant(QSL("hostandportsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:a:b@foo/bar\").href")), + QVariant(QSL("hostandportsyntax://foo/bar"))); + + // A HostPortAndUserInformationSyntax scheme is exactly like http. + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo/bar\").href")), + QVariant(QSL("hostportanduserinformationsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:41/bar\").href")), + QVariant(QSL("hostportanduserinformationsyntax://foo:41/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:42/bar\").href")), + QVariant(QSL("hostportanduserinformationsyntax://foo/bar"))); + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:a:b@foo/bar\").href")), + QVariant(QSL("hostportanduserinformationsyntax://a:b@foo/bar"))); +} + +// Test relative URL resolution. +void tst_Origins::jsUrlRelative() +{ + QVERIFY(verifyLoad(QSL("about:blank"))); + + // Schemes with hosts, like http, work as expected. + QCOMPARE(eval(QSL("new URL('bar', 'http://foo').href")), QVariant(QSL("http://foo/bar"))); + QCOMPARE(eval(QSL("new URL('baz', 'http://foo/bar').href")), QVariant(QSL("http://foo/baz"))); + QCOMPARE(eval(QSL("new URL('baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/bar/baz"))); + QCOMPARE(eval(QSL("new URL('/baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); + QCOMPARE(eval(QSL("new URL('./baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/bar/baz"))); + QCOMPARE(eval(QSL("new URL('../baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); + QCOMPARE(eval(QSL("new URL('../../baz', 'http://foo/bar/').href")), QVariant(QSL("http://foo/baz"))); + QCOMPARE(eval(QSL("new URL('//baz', 'http://foo/bar/').href")), QVariant(QSL("http://baz/"))); + + // In the case of schemes without hosts, relative URLs only work if the URL + // starts with a single slash -- and canonicalization does not guarantee + // this. The following cases all fail with TypeErrors. + QCOMPARE(eval(QSL("new URL('bar', 'tst:foo').href")), QVariant()); + QCOMPARE(eval(QSL("new URL('baz', 'tst:foo/bar').href")), QVariant()); + QCOMPARE(eval(QSL("new URL('bar', 'tst://foo').href")), QVariant()); + QCOMPARE(eval(QSL("new URL('bar', 'tst:///foo').href")), QVariant()); + + // However, registered custom schemes have been patched to allow relative + // URLs even without an initial slash. + QCOMPARE(eval(QSL("new URL('bar', 'qrc:foo').href")), QVariant(QSL("qrc:bar"))); + QCOMPARE(eval(QSL("new URL('baz', 'qrc:foo/bar').href")), QVariant(QSL("qrc:foo/baz"))); + QCOMPARE(eval(QSL("new URL('bar', 'qrc://foo').href")), QVariant(QSL("qrc://bar"))); + QCOMPARE(eval(QSL("new URL('bar', 'qrc:///foo').href")), QVariant(QSL("qrc:///bar"))); + + // With a slash it works the same as http except 'foo' is part of the path and not the host. + QCOMPARE(eval(QSL("new URL('bar', 'qrc:/foo').href")), QVariant(QSL("qrc:/bar"))); + QCOMPARE(eval(QSL("new URL('bar', 'qrc:/foo/').href")), QVariant(QSL("qrc:/foo/bar"))); + QCOMPARE(eval(QSL("new URL('baz', 'qrc:/foo/bar').href")), QVariant(QSL("qrc:/foo/baz"))); + QCOMPARE(eval(QSL("new URL('baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/bar/baz"))); + QCOMPARE(eval(QSL("new URL('/baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); + QCOMPARE(eval(QSL("new URL('./baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/bar/baz"))); + QCOMPARE(eval(QSL("new URL('../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/foo/baz"))); + QCOMPARE(eval(QSL("new URL('../../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); + QCOMPARE(eval(QSL("new URL('../../../baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc:/baz"))); + + // If the relative URL begins with >= 2 slashes, then the scheme is treated + // not as a Syntax::Path scheme but as a Syntax::HostPortAndUserInformation + // scheme. + QCOMPARE(eval(QSL("new URL('//baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc://baz/"))); + QCOMPARE(eval(QSL("new URL('///baz', 'qrc:/foo/bar/').href")), QVariant(QSL("qrc://baz/"))); +} + +// Test origin serialization in Blink, implemented by blink::KURL and +// blink::SecurityOrigin as opposed to GURL and url::Origin. +void tst_Origins::jsUrlOrigin() +{ + QVERIFY(verifyLoad(QSL("about:blank"))); + + // For network protocols the origin string must include the domain and port. + QCOMPARE(eval(QSL("new URL(\"http://foo.com/page.html\").origin")), QVariant(QSL("http://foo.com"))); + QCOMPARE(eval(QSL("new URL(\"https://foo.com/page.html\").origin")), QVariant(QSL("https://foo.com"))); + + // Even though file URL can also have domains, these are not included in the + // origin string by Chromium. The standard does not specify a value here, + // but suggests 'null' (https://url.spec.whatwg.org/#origin). + QCOMPARE(eval(QSL("new URL(\"file:/etc/passwd\").origin")), QVariant(QSL("file://"))); + QCOMPARE(eval(QSL("new URL(\"file://foo.com/etc/passwd\").origin")), QVariant(QSL("file://"))); + + // Unregistered schemes behave like file. + QCOMPARE(eval(QSL("new URL(\"tst:/banana\").origin")), QVariant(QSL("tst://"))); + QCOMPARE(eval(QSL("new URL(\"tst://foo.com/banana\").origin")), QVariant(QSL("tst://"))); + + // The non-PathSyntax schemes should have hosts and potentially ports. + QCOMPARE(eval(QSL("new URL(\"HostSyntax:foo:41/bar\").origin")), + QVariant(QSL("hostsyntax://foo"))); + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:41/bar\").origin")), + QVariant(QSL("hostandportsyntax://foo:41"))); + QCOMPARE(eval(QSL("new URL(\"HostAndPortSyntax:foo:42/bar\").origin")), + QVariant(QSL("hostandportsyntax://foo"))); + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:41/bar\").origin")), + QVariant(QSL("hostportanduserinformationsyntax://foo:41"))); + QCOMPARE(eval(QSL("new URL(\"HostPortAndUserInformationSyntax:foo:42/bar\").origin")), + QVariant(QSL("hostportanduserinformationsyntax://foo"))); + + // A PathSyntax scheme should have a 'universal' origin. + QCOMPARE(eval(QSL("new URL(\"PathSyntax:foo\").origin")), QVariant(QSL("pathsyntax:"))); + QCOMPARE(eval(QSL("new URL(\"qrc:/crysis.css\").origin")), QVariant(QSL("qrc:"))); + QCOMPARE(eval(QSL("new URL(\"qrc://foo.com/crysis.css\").origin")), QVariant(QSL("qrc:"))); + + // The NoAccessAllowed flag forces opaque origins. + QCOMPARE(eval(QSL("new URL(\"PathSyntax-NoAccessAllowed:foo\").origin")), + QVariant(QSL("null"))); +} + +class ScopedAttribute { +public: + ScopedAttribute(QWebEngineSettings *settings, QWebEngineSettings::WebAttribute attribute, bool newValue) + : m_settings(settings) + , m_attribute(attribute) + , m_oldValue(m_settings->testAttribute(m_attribute)) + { + m_settings->setAttribute(m_attribute, newValue); + } + ~ScopedAttribute() + { + m_settings->setAttribute(m_attribute, m_oldValue); + } +private: + QWebEngineSettings *m_settings; + QWebEngineSettings::WebAttribute m_attribute; + bool m_oldValue; +}; + +// Test same-origin policy of file, qrc and custom schemes. +// +// Note the test case involves the main page trying to load an iframe from a +// file that resides in a parent directory. This is just a small detail to +// demonstrate the difference with Firefox where such access is not allowed. +void tst_Origins::subdirWithAccess() +{ + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, true); + + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/subdir/index.html")); + QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); + QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); + + QVERIFY(verifyLoad(QSL("qrc:/resources/subdir/index.html"))); + QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); + QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); + + QVERIFY(verifyLoad(QSL("tst:/resources/subdir/index.html"))); + QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); + QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); +} + +// In this variation the LocalContentCanAccessFileUrls attribute is disabled. As +// a result all file URLs will be considered to have unique/opaque origins, that +// is, they are not the 'same origin as' any other origin. +// +// Note that this applies only to file URLs and not qrc or custom schemes. +// +// See also (in Blink): +// - the allow_file_access_from_file_urls option and +// - the blink::SecurityOrigin::BlockLocalAccessFromLocalOrigin() method. +void tst_Origins::subdirWithoutAccess() +{ + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, false); + + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/subdir/index.html")); + QCOMPARE(eval(QSL("msg[0]")), QVariant()); + QCOMPARE(eval(QSL("msg[1]")), QVariant()); + + QVERIFY(verifyLoad(QSL("qrc:/resources/subdir/index.html"))); + QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); + QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); + + QVERIFY(verifyLoad(QSL("tst:/resources/subdir/index.html"))); + QCOMPARE(eval(QSL("msg[0]")), QVariant(QSL("hello"))); + QCOMPARE(eval(QSL("msg[1]")), QVariant(QSL("world"))); +} + +void tst_Origins::fileAccessRemoteUrl_data() +{ + QTest::addColumn<bool>("EnableAccess"); + QTest::addColumn<bool>("UserGesture"); + QTest::addRow("enabled, XHR") << true << false; + QTest::addRow("enabled, link click") << true << true; + QTest::addRow("disabled, XHR") << false << false; + QTest::addRow("disabled, link click") << false << true; +} + +void tst_Origins::fileAccessRemoteUrl() +{ + QFETCH(bool, EnableAccess); + QFETCH(bool, UserGesture); + + QWebEngineView view; + view.setPage(m_page); + view.resize(800, 600); + view.show(); + + HttpServer server; + server.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources" }); + QVERIFY(server.start()); + + ScopedAttribute sa1(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, EnableAccess); + ScopedAttribute sa2(m_page->settings(), QWebEngineSettings::ErrorPageEnabled, false); + + if (UserGesture) { + QString remoteUrl(server.url("/link.html").toString()); +#ifdef Q_OS_WIN + QString localUrl("file:///" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html?linkLocation=" + remoteUrl); +#else + QString localUrl("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html?linkLocation=" + remoteUrl); +#endif + + QVERIFY(verifyLoad(localUrl)); + + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link")); + // Succeed independently of EnableAccess == false + QTRY_COMPARE(m_page->url(), remoteUrl); + + // Back/forward navigation is also allowed, however they are not user gesture + m_page->triggerAction(QWebEnginePage::Back); + QTRY_COMPARE(m_page->url(), localUrl); + m_page->triggerAction(QWebEnginePage::Forward); + QTRY_COMPARE(m_page->url(), remoteUrl); + } else { + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/mixedXHR.html")); + eval("sendXHR('" + server.url("/mixedXHR.txt").toString() + "')"); + QTRY_COMPARE(eval("result"), (EnableAccess ? QString("ok") : QString("error"))); + } +} + +void tst_Origins::fileAccessLocalUrl_data() +{ + QTest::addColumn<bool>("EnableAccess"); + QTest::addColumn<bool>("UserGesture"); + QTest::addRow("enabled, XHR") << true << false; + QTest::addRow("enabled, link click") << true << true; + QTest::addRow("disabled, XHR") << false << false; + QTest::addRow("disabled, link click") << false << true; +} + +void tst_Origins::fileAccessLocalUrl() +{ + QFETCH(bool, EnableAccess); + QFETCH(bool, UserGesture); + + QWebEngineView view; + view.setPage(m_page); + view.resize(800, 600); + view.show(); + + ScopedAttribute sa1(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, EnableAccess); + ScopedAttribute sa2(m_page->settings(), QWebEngineSettings::ErrorPageEnabled, false); + + if (UserGesture) { +#ifdef Q_OS_WIN + QString localUrl1("file:///" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html?linkLocation=link.html"); + QString localUrl2("file:///" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html"); +#else + QString localUrl1("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html?linkLocation=link.html"); + QString localUrl2("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/link.html"); +#endif + + QVERIFY(verifyLoad(localUrl1)); + QTest::mouseClick(view.focusProxy(), Qt::LeftButton, {}, elementCenter(m_page, "link")); + // Succeed independently of EnableAccess == false + QTRY_COMPARE(m_page->url(), localUrl2); + + // Back/forward navigation is also allowed, however they are not user gesture + m_page->triggerAction(QWebEnginePage::Back); + QTRY_COMPARE(m_page->url(), localUrl1); + m_page->triggerAction(QWebEnginePage::Forward); + QTRY_COMPARE(m_page->url(), localUrl2); + } else { + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/mixedXHR.html")); + eval("sendXHR('file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/mixedXHR.txt" + "')"); + QTRY_COMPARE(eval("result"), (EnableAccess ? QString("ok") : QString("error"))); + } +} + +// Load the main page over one scheme with an iframe over another scheme. +// +// For file and qrc schemes, the iframe should load but it should not be +// possible for scripts in different frames to interact. +// +// Additionally for unregistered custom schemes and custom schemes without +// LocalAccessAllowed it should not be possible to load an iframe over the +// file: scheme. +void tst_Origins::mixedSchemes_data() +{ + QTest::addColumn<QString>("schemeFrom"); + QTest::addColumn<QVariantMap>("testPairs"); + + QVariant SLF = QVariant(QSL("canLoadAndAccess")), OK = QVariant(QSL("canLoadButNotAccess")), + ERR = QVariant(QSL("cannotLoad")); + std::vector<std::pair<const char *, std::vector<std::pair<const char *, QVariant>>>> data = { + { "file", + { + { "file", SLF }, + { "qrc", OK }, + { "tst", ERR }, + } }, + { "qrc", + { + { "file", ERR }, + { "qrc", SLF }, + { "tst", OK }, + } }, + { "tst", + { + { "file", ERR }, + { "qrc", OK }, + { "tst", SLF }, + } }, + { "PathSyntax", + { + { "PathSyntax", SLF }, + { "PathSyntax-Local", ERR }, + { "PathSyntax-LocalAccessAllowed", OK }, + { "PathSyntax-NoAccessAllowed", OK }, + } }, + { "PathSyntax-LocalAccessAllowed", + { + { "PathSyntax", OK }, + { "PathSyntax-Local", OK }, + { "PathSyntax-LocalAccessAllowed", SLF }, + { "PathSyntax-NoAccessAllowed", OK }, + } }, + { "PathSyntax-NoAccessAllowed", + { + { "PathSyntax", OK }, + { "PathSyntax-Local", ERR }, + { "PathSyntax-LocalAccessAllowed", OK }, + { "PathSyntax-NoAccessAllowed", OK }, + } }, + { "HostSyntax://a", + { + { "HostSyntax://a", SLF }, + { "HostSyntax://b", OK }, + } }, + { "local-localaccess", + { + { "local-cors", OK }, + { "local-localaccess", SLF }, + { "local", OK }, + } }, + { "local-cors", + { + { "local", OK }, + { "local-cors", SLF }, + } }, + }; + + for (auto &&d : data) { + auto schemeFrom = d.first; + QVariantMap testPairs; + for (auto &&destSchemes : d.second) { + auto &&destScheme = destSchemes.first; + testPairs[destScheme] = destSchemes.second; + } + QTest::addRow("%s", schemeFrom) << schemeFrom << testPairs; + } +} + +static QStringList protocolAndHost(const QString scheme) +{ + static QString srcDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()); + QStringList result; + if (scheme == QSL("file")) { + return QStringList{ scheme, srcDir }; + } + if (scheme.contains(QSL("HostSyntax:"))) { + const QStringList &res = scheme.split(':'); + Q_ASSERT(res.size() == 2); + return res; + } + return QStringList{ scheme, "" }; +} + +void tst_Origins::mixedSchemes() +{ + QFETCH(QString, schemeFrom); + QFETCH(QVariantMap, testPairs); + + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::ErrorPageEnabled, false); + QString srcDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()); + QString host; + auto pah = protocolAndHost(schemeFrom); + auto loadUrl = QString("%1:%2/resources/mixedSchemes.html").arg(pah[0]).arg(pah[1]); + QVERIFY(verifyLoad(loadUrl)); + + QStringList schemesTo, expected, results; + for (auto it = testPairs.begin(), end = testPairs.end(); it != end; ++it) { + + auto schemeTo = it.key(); + auto pah = protocolAndHost(schemeTo); + auto expectedResult = it.value().toString(); + auto frameUrl = QString("%1:%2/resources/mixedSchemes_frame.html").arg(pah[0]).arg(pah[1]); + auto imgUrl = QString("%1:%2/resources/red.png").arg(pah[0]).arg(pah[1]); + + eval(QString("setIFrameUrl('%1','%2')").arg(frameUrl).arg(imgUrl)); + + // wait for token in the log + QTRY_VERIFY(m_page->logContainsDoneMarker()); + const QString result = m_page->findResultInLog(); + m_page->clearLog(); + schemesTo.append(schemeTo.rightJustified(20)); + results.append(result.rightJustified(20)); + expected.append(expectedResult.rightJustified(20)); + } + + QVERIFY2(results == expected, + qPrintable(QString("\nFrom '%1' to:\n\tScheme: %2\n\tActual: %3\n\tExpect: %4") + .arg(schemeFrom) + .arg(schemesTo.join(' ')) + .arg(results.join(' ')) + .arg(expected.join(' ')))); +} + +// Like mixedSchemes but adds a Content-Security-Policy: frame-src 'none' header. +void tst_Origins::mixedSchemesWithCsp() +{ + QVERIFY(verifyLoad(QSL("HostSyntax://a/resources/mixedSchemesWithCsp.html"))); + eval(QSL("setIFrameUrl('HostSyntax://a/resources/mixedSchemes_frame.html')")); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + eval(QSL("setIFrameUrl('HostSyntax://b/resources/mixedSchemes_frame.html')")); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); + + QVERIFY(verifyLoad(QSL("HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemesWithCsp.html"))); + eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://a/resources/mixedSchemes_frame.html')")); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadAndAccess"))); + eval(QSL("setIFrameUrl('HostSyntax-ContentSecurityPolicyIgnored://b/resources/mixedSchemes_frame.html')")); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("canLoadButNotAccess"))); +} + +// Load the main page over one scheme, then make an XMLHttpRequest to a +// different scheme. +// +// Cross-origin XMLHttpRequests can only be made to CORS-enabled schemes. These +// include the builtin schemes http, https, data, and chrome, as well as custom +// schemes with the CorsEnabled flag. +void tst_Origins::mixedXHR_data() +{ + QTest::addColumn<QString>("schemeFrom"); + QTest::addColumn<bool>("canAccessFileUrls"); + QTest::addColumn<bool>("canAccessRemoteUrl"); + QTest::addColumn<QVariantMap>("testPairs"); + + bool defaultFileAccess = QWebEnginePage().settings()->testAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls); + bool defaultRemoteAccess = QWebEnginePage().settings()->testAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls); + Q_ASSERT(defaultFileAccess); + Q_ASSERT(!defaultRemoteAccess); + std::vector<std::pair<bool, bool>> settingCombinations = { + { defaultFileAccess, defaultRemoteAccess }, // tag: *schemeFrom*_local_noremote + { defaultFileAccess, !defaultRemoteAccess }, // tag: *schemeFrom*_local_remote + { !defaultFileAccess, defaultRemoteAccess }, // tag: *schemeFrom*_nolocal_noremote + { !defaultFileAccess, !defaultRemoteAccess } // tag: *schemeFrom*_nolocal_remote + }; + + QVariant OK = QString("ok"), ERR = QString("error"); + std::vector< + std::pair<const char *, std::vector< + std::pair<const char *, std::vector<QVariant>>>>> data = { + { "file", { + { "file", { OK, OK, ERR, ERR } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { OK, OK, ERR, ERR } }, } }, + + { "qrc", { + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "tst", { + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { OK, OK, OK, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "cors", { // -local +cors -local-access + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "local", { // +local -cors -local-access + { "file", { OK, OK, ERR, ERR } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { OK, OK, ERR, ERR } }, } }, + + { "local-cors", { // +local +cors -local-access + { "file", { OK, OK, ERR, ERR } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { OK, OK, ERR, ERR } }, } }, + + { "local-localaccess", { // +local -cors +local-access + { "file", { OK, OK, OK, OK } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, OK, OK } }, + { "local-cors", { OK, OK, OK, OK } }, } }, + + { "localaccess", { // -local -cors +local-access + { "file", { OK, OK, OK, OK } }, + { "qrc", { ERR, ERR, ERR, ERR } }, + { "tst", { ERR, ERR, ERR, ERR } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { OK, OK, OK, OK } }, + { "local-cors", { OK, OK, OK, OK } }, } }, + }; + + for (auto &&d : data) { + auto schemeFrom = d.first; + + for (int i = 0; i < 4; ++i) { + const auto &it = settingCombinations[i]; + bool canAccessFileUrls = it.first, canAccessRemoteUrl = it.second; + + QVariantMap testPairs; + for (auto &&destSchemes : d.second) { + auto &&destScheme = destSchemes.first; + auto &&expectedResults = destSchemes.second; + testPairs[destScheme] = expectedResults[i]; + } + + QTest::addRow("%s_%s_%s", schemeFrom, (canAccessFileUrls ? "local" : "nolocal"), (canAccessRemoteUrl ? "remote" : "noremote")) + << schemeFrom << canAccessFileUrls << canAccessRemoteUrl << testPairs; + } + } +} + +void tst_Origins::mixedXHR() +{ + QFETCH(QString, schemeFrom); + QFETCH(bool, canAccessFileUrls); + QFETCH(bool, canAccessRemoteUrl); + QFETCH(QVariantMap, testPairs); + + QString srcDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()); + auto loadUrl = QString("%1:%2/resources/mixedXHR.html").arg(schemeFrom).arg(schemeFrom == "file" ? srcDir : ""); + auto sendXHR = [&] (const QString &scheme) { + if (scheme == "data") + return QString("sendXHR('data:,ok')"); + return QString("sendXHR('%1:%2/resources/mixedXHR.txt')").arg(scheme).arg(scheme == "file" ? srcDir : ""); + }; + + QCOMPARE(testPairs.size(), 7); + ScopedAttribute sa0(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, canAccessFileUrls); + ScopedAttribute sa1(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, canAccessRemoteUrl); + QVERIFY(verifyLoad(loadUrl)); + + QStringList schemesTo, expected, results; + for (auto it = testPairs.begin(), end = testPairs.end(); it != end; ++it) { + auto schemeTo = it.key(); + auto expectedResult = it.value().toString(); + auto command = sendXHR(schemeTo); + + eval(command); + + QTRY_COMPARE(eval(QSL("result !== undefined")), QVariant(true)); + auto result = eval(QSL("result")).toString(); + + schemesTo.append(schemeTo.rightJustified(10)); + results.append(result.rightJustified(10)); + expected.append(expectedResult.rightJustified(10)); + } + QVERIFY2(results == expected, + qPrintable(QString("From '%1' to:\n\tScheme: %2\n\tActual: %3\n\tExpect: %4") + .arg(schemeFrom).arg(schemesTo.join(' ')).arg(results.join(' ')).arg(expected.join(' ')))); +} + +// Load the main page over one scheme, then load an iframe over a different scheme. This load is not considered CORS. +void tst_Origins::mixedContent_data() +{ + QTest::addColumn<QString>("schemeFrom"); + QTest::addColumn<bool>("canAccessFileUrls"); + QTest::addColumn<bool>("canAccessRemoteUrl"); + QTest::addColumn<QVariantMap>("testPairs"); + + bool defaultFileAccess = true; + bool defaultRemoteAccess = false; + std::vector<std::pair<bool, bool>> settingCombinations = { + { defaultFileAccess, defaultRemoteAccess }, // tag: *schemeFrom*_local_noremote + { defaultFileAccess, !defaultRemoteAccess }, // tag: *schemeFrom*_local_remote + { !defaultFileAccess, defaultRemoteAccess }, // tag: *schemeFrom*_nolocal_noremote + { !defaultFileAccess, !defaultRemoteAccess } // tag: *schemeFrom*_nolocal_remote + }; + + QVariant SLF = QVariant(QSL("canLoadAndAccess")), OK = QVariant(QSL("canLoadButNotAccess")), ERR = QVariant(QSL("cannotLoad")); + std::vector< + std::pair<const char *, std::vector< + std::pair<const char *, std::vector<QVariant>>>>> data = { + { "file", { + { "file", { SLF, SLF, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { ERR, OK, ERR, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { OK, OK, ERR, ERR } }, + } }, + + { "qrc", { + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { SLF, SLF, SLF, SLF } }, + { "tst", { OK, OK, OK, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "tst", { + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { SLF, SLF, SLF, SLF } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "cors", { // -local +cors -local-access + { "file", { ERR, ERR, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { OK, OK, OK, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { SLF, SLF, SLF, SLF } }, + { "local-localaccess", { ERR, ERR, ERR, ERR } }, + { "local-cors", { ERR, ERR, ERR, ERR } }, } }, + + { "local", { // +local -cors -local-access + { "file", { OK, OK, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { ERR, OK, ERR, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { OK, OK, ERR, ERR } }, + } }, + + { "local-cors", { // +local +cors -local-access + { "file", { OK, OK, ERR, ERR } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { ERR, OK, ERR, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { OK, OK, ERR, ERR } }, + { "local-cors", { SLF, SLF, ERR, ERR } }, + } }, + + { "local-localaccess", { // +local -cors + OK-access + { "file", { OK, OK, OK, OK } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { ERR, OK, ERR, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { ERR, OK, ERR, OK } }, + { "local-localaccess", { SLF, SLF, OK, OK } }, // ### should probably be: SLF, SLF, SLF, SLF + { "local-cors", { OK, OK, OK, OK } }, + } }, + + { "localaccess", { // -local -cors +local-access + { "file", { OK, OK, OK, OK } }, + { "qrc", { OK, OK, OK, OK } }, + { "tst", { OK, OK, OK, OK } }, + { "data", { OK, OK, OK, OK } }, + { "cors", { OK, OK, OK, OK } }, + { "local-localaccess", { OK, OK, OK, OK } }, + { "local-cors", { OK, OK, OK, OK } }, } }, + }; + + for (auto &&d : data) { + auto schemeFrom = d.first; + + for (int i = 0; i < 4; ++i) { + const auto &it = settingCombinations[i]; + bool canAccessFileUrls = it.first, canAccessRemoteUrl = it.second; + + QVariantMap testPairs; + for (auto &&destSchemes : d.second) { + auto &&destScheme = destSchemes.first; + auto &&expectedResults = destSchemes.second; + testPairs[destScheme] = expectedResults[i]; + } + + QTest::addRow("%s_%s_%s", schemeFrom, (canAccessFileUrls ? "local" : "nolocal"), (canAccessRemoteUrl ? "remote" : "noremote")) + << schemeFrom << canAccessFileUrls << canAccessRemoteUrl << testPairs; + } + } +} + +void tst_Origins::mixedContent() +{ + QFETCH(QString, schemeFrom); + QFETCH(bool, canAccessFileUrls); + QFETCH(bool, canAccessRemoteUrl); + QFETCH(QVariantMap, testPairs); + + QString srcDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()); + auto loadUrl = QString("%1:%2/resources/mixedSchemes.html").arg(schemeFrom).arg(schemeFrom == "file" ? srcDir : ""); + + QCOMPARE(testPairs.size(), 7); + ScopedAttribute sa2(m_page->settings(), QWebEngineSettings::ErrorPageEnabled, false); + ScopedAttribute sa0(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, canAccessFileUrls); + ScopedAttribute sa1(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, canAccessRemoteUrl); + QVERIFY(verifyLoad(loadUrl)); + + auto setIFrameUrl = [&] (const QString &scheme) { + if (scheme == "data") + return QString("setIFrameUrl('data:,<script>var canary = true; parent.canary = " + "true</script>','data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA" + "AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/" + "w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==')"); + auto frameUrl = QString("%1:%2/resources/mixedSchemes_frame.html").arg(scheme).arg(scheme == "file" ? srcDir : ""); + auto imgUrl = + QString("%1:%2/resources/red.png").arg(scheme).arg(scheme == "file" ? srcDir : ""); + return QString("setIFrameUrl('%1','%2')").arg(frameUrl).arg(imgUrl); + }; + + m_page->clearLog(); + QStringList schemesTo, expected, results; + for (auto it = testPairs.begin(), end = testPairs.end(); it != end; ++it) { + + auto schemeTo = it.key(); + auto expectedResult = it.value().toString(); + + eval(setIFrameUrl(schemeTo)); + + // wait for token in the log + QTRY_VERIFY(m_page->logContainsDoneMarker()); + const QString result = m_page->findResultInLog(); + m_page->clearLog(); + schemesTo.append(schemeTo.rightJustified(20)); + results.append(result.rightJustified(20)); + expected.append(expectedResult.rightJustified(20)); + } + QVERIFY2(results == expected, + qPrintable(QString("\nFrom '%1' to:\n\tScheme: %2\n\tActual: %3\n\tExpect: %4") + .arg(schemeFrom).arg(schemesTo.join(' ')).arg(results.join(' ')).arg(expected.join(' ')))); +} + +#if defined(WEBSOCKETS) +class EchoServer : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl url READ url NOTIFY urlChanged) +public: + EchoServer() : webSocketServer(QSL("EchoServer"), QWebSocketServer::NonSecureMode) + { + connect(&webSocketServer, &QWebSocketServer::newConnection, this, &EchoServer::onNewConnection); + } + + bool listen() + { + if (webSocketServer.listen(QHostAddress::Any)) { + Q_EMIT urlChanged(); + return true; + } + return false; + } + + QUrl url() const + { + return webSocketServer.serverUrl(); + } + +Q_SIGNALS: + void urlChanged(); + +private: + void onNewConnection() + { + QWebSocket *socket = webSocketServer.nextPendingConnection(); + connect(socket, &QWebSocket::textMessageReceived, this, &EchoServer::onTextMessageReceived); + connect(socket, &QWebSocket::disconnected, socket, &QObject::deleteLater); + } + + void onTextMessageReceived(const QString &message) + { + QWebSocket *socket = qobject_cast<QWebSocket *>(sender()); + socket->sendTextMessage(message); + } + + QWebSocketServer webSocketServer; +}; + +// Try opening a WebSocket from pages loaded over various URL schemes. +void tst_Origins::webSocket() +{ + EchoServer echoServer; + QWebChannel channel; + channel.registerObject(QSL("echoServer"), &echoServer); + m_page->setWebChannel(&channel); + QVERIFY(echoServer.listen()); + + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/websocket.html")); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); + + QVERIFY(verifyLoad(QSL("qrc:/resources/websocket.html"))); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); + + // Unregistered schemes can also open WebSockets (since Chromium 71) + QVERIFY(verifyLoad(QSL("tst:/resources/websocket2.html"))); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); + + // Even an insecure registered scheme can open WebSockets. + QVERIFY(verifyLoad(QSL("PathSyntax:/resources/websocket2.html"))); + QTRY_COMPARE(eval(QSL("result")), QVariant(QSL("ok"))); +} +#endif +// Create a (Dedicated)Worker. Since dedicated workers can only be accessed from +// one page, there is not much need for security restrictions. +void tst_Origins::dedicatedWorker() +{ + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/dedicatedWorker.html")); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + QVERIFY(verifyLoad(QSL("qrc:/resources/dedicatedWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + // Unregistered schemes can also create Workers (since Chromium 71) + QVERIFY(verifyLoad(QSL("tst:/resources/dedicatedWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + // Even an insecure registered scheme can create Workers. + QVERIFY(verifyLoad(QSL("PathSyntax:/resources/dedicatedWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + // But not if the NoAccessAllowed flag is set. + QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/dedicatedWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("cannot be accessed from origin 'null'"))); +} + +// Create a SharedWorker. Shared workers can be accessed from multiple pages, +// and therefore the same-origin policy applies. +void tst_Origins::sharedWorker() +{ + { + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, false); + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/sharedWorker.html")); + QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("cannot be accessed from origin 'null'"))); + } + + { + ScopedAttribute sa(m_page->settings(), QWebEngineSettings::LocalContentCanAccessFileUrls, true); + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/sharedWorker.html")); + QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); + QCOMPARE(eval(QSL("result")), QVariant(42)); + } + + QVERIFY(verifyLoad(QSL("qrc:/resources/sharedWorker.html"))); + QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + // Unregistered schemes should not create SharedWorkers. + + QVERIFY(verifyLoad(QSL("PathSyntax:/resources/sharedWorker.html"))); + QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); + QCOMPARE(eval(QSL("result")), QVariant(42)); + + QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/sharedWorker.html"))); + QTRY_VERIFY_WITH_TIMEOUT(eval(QSL("done")).toBool(), 10000); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("denied to origin 'null'"))); +} + +// Service workers have to be explicitly enabled for a scheme. +void tst_Origins::serviceWorker() +{ + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/serviceWorker.html")); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("The URL protocol of the current origin ('file://') is not supported."))); + + QVERIFY(verifyLoad(QSL("qrc:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("The URL protocol of the current origin ('qrc:') is not supported."))); + + QVERIFY(verifyLoad(QSL("tst:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("Cannot read properties of undefined"))); + + QVERIFY(verifyLoad(QSL("PathSyntax:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("Cannot read properties of undefined"))); + + QVERIFY(verifyLoad(QSL("PathSyntax-Secure:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("The URL protocol of the current origin ('pathsyntax-secure:') is not supported."))); + + QVERIFY(verifyLoad(QSL("PathSyntax-ServiceWorkersAllowed:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("Cannot read properties of undefined"))); + + QVERIFY(verifyLoad(QSL("PathSyntax-Secure-ServiceWorkersAllowed:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QCOMPARE(eval(QSL("error")), QVariant()); + + QVERIFY(verifyLoad(QSL("PathSyntax-NoAccessAllowed:/resources/serviceWorker.html"))); + QTRY_VERIFY(eval(QSL("done")).toBool()); + QVERIFY(eval(QSL("error")).toString() + .contains(QSL("Cannot read properties of undefined"))); +} + +// Support for view-source must be enabled explicitly. +void tst_Origins::viewSource() +{ + QVERIFY(verifyLoad("view-source:file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/viewSource.html")); +#ifdef Q_OS_WIN + QCOMPARE(m_page->requestedUrl().toString(), + "file:///" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/viewSource.html"); +#else + QCOMPARE(m_page->requestedUrl().toString(), + "file://" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/viewSource.html"); +#endif + + QVERIFY(verifyLoad(QSL("view-source:qrc:/resources/viewSource.html"))); + QCOMPARE(m_page->requestedUrl().toString(), QSL("qrc:/resources/viewSource.html")); + + QVERIFY(verifyLoad(QSL("view-source:tst:/resources/viewSource.html"))); + QCOMPARE(m_page->requestedUrl().toString(), QSL("about:blank")); + + QVERIFY(verifyLoad(QSL("view-source:PathSyntax:/resources/viewSource.html"))); + QCOMPARE(m_page->requestedUrl().toString(), QSL("about:blank")); + + QVERIFY(verifyLoad(QSL("view-source:PathSyntax-ViewSourceAllowed:/resources/viewSource.html"))); + QCOMPARE(m_page->requestedUrl().toString(), QSL("pathsyntax-viewsourceallowed:/resources/viewSource.html")); +} + +void tst_Origins::createObjectURL() +{ + // Legal for registered custom schemes. + QVERIFY(verifyLoad(QSL("qrc:/resources/createObjectURL.html"))); + QVERIFY(eval(QSL("result")).toString().startsWith(QSL("blob:qrc:"))); + + // Also legal for unregistered schemes (since Chromium 71) + QVERIFY(verifyLoad(QSL("tst:/resources/createObjectURL.html"))); + QVERIFY(eval(QSL("result")).toString().startsWith(QSL("blob:tst:"))); +} + +void tst_Origins::redirectScheme() +{ + QVERIFY(verifyLoad(QSL("redirect:cors/resources/redirect.html"))); + eval("addStylesheetLink('redirect:cors/resources/redirect.css')"); + QTRY_COMPARE(m_handler->requests().size(), 4); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("redirect:cors/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[2], QUrl(QStringLiteral("redirect:cors/resources/redirect.css"))); + QCOMPARE(m_handler->requests()[3], QUrl(QStringLiteral("cors:/resources/redirect.css"))); + + QVERIFY(!verifyLoad(QSL("redirect:file/resources/redirect.html"))); + QVERIFY(!verifyLoad(QSL("redirect:local/resources/redirect.html"))); + QVERIFY(!verifyLoad(QSL("redirect:local-cors/resources/redirect.html"))); +} + +void tst_Origins::redirectSchemeLocal() +{ + QVERIFY(verifyLoad(QSL("redirect-local:local/resources/redirect.html"))); + eval("addStylesheetLink('redirect-local:local/resources/redirect.css')"); + QTRY_COMPARE(m_handler->requests().size(), 4); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("redirect-local:local/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("local:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[2], QUrl(QStringLiteral("redirect-local:local/resources/redirect.css"))); + QCOMPARE(m_handler->requests()[3], QUrl(QStringLiteral("local:/resources/redirect.css"))); +} + +void tst_Origins::redirectSchemeSecure() +{ + QVERIFY(verifyLoad(QSL("redirect-secure:secure-cors/resources/redirect.html"))); + eval("addStylesheetLink('redirect-secure:secure-cors/resources/redirect.css')"); + QTRY_COMPARE(m_handler->requests().size(), 4); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("redirect-secure:secure-cors/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("secure-cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[2], QUrl(QStringLiteral("redirect-secure:secure-cors/resources/redirect.css"))); + QCOMPARE(m_handler->requests()[3], QUrl(QStringLiteral("secure-cors:/resources/redirect.css"))); +} + +void tst_Origins::redirectInterceptor() +{ + TestRequestInterceptor interceptor; + m_profile.setUrlRequestInterceptor(&interceptor); + + QVERIFY(verifyLoad(QSL("redirect:cors/resources/redirect.html"))); + eval("addStylesheetLink('redirect:cors/resources/redirect.css')"); + + QTRY_COMPARE(interceptor.requests.size(), 4); + QTRY_COMPARE(m_handler->requests().size(), 2); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("cors:/resources/redirect.css"))); + + QCOMPARE(interceptor.requests[0], QUrl(QStringLiteral("redirect:cors/resources/redirect.html"))); + QCOMPARE(interceptor.requests[1], QUrl(QStringLiteral("cors:/resources/redirect.html"))); + QCOMPARE(interceptor.requests[2], QUrl(QStringLiteral("redirect:cors/resources/redirect.css"))); + QCOMPARE(interceptor.requests[3], QUrl(QStringLiteral("cors:/resources/redirect.css"))); + + QVERIFY(!verifyLoad(QSL("redirect:file/resources/redirect.html"))); + QVERIFY(!verifyLoad(QSL("redirect:local/resources/redirect.html"))); + QVERIFY(!verifyLoad(QSL("redirect:local-cors/resources/redirect.html"))); +} + +void tst_Origins::redirectInterceptorLocal() +{ + TestRequestInterceptor interceptor; + m_profile.setUrlRequestInterceptor(&interceptor); + + QVERIFY(verifyLoad(QSL("redirect-local:local/resources/redirect.html"))); + eval("addStylesheetLink('redirect-local:local/resources/redirect.css')"); + + QTRY_COMPARE(interceptor.requests.size(), 4); + QTRY_COMPARE(m_handler->requests().size(), 2); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("local:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("local:/resources/redirect.css"))); + + QCOMPARE(interceptor.requests[0], QUrl(QStringLiteral("redirect-local:local/resources/redirect.html"))); + QCOMPARE(interceptor.requests[1], QUrl(QStringLiteral("local:/resources/redirect.html"))); + QCOMPARE(interceptor.requests[2], QUrl(QStringLiteral("redirect-local:local/resources/redirect.css"))); + QCOMPARE(interceptor.requests[3], QUrl(QStringLiteral("local:/resources/redirect.css"))); +} + +void tst_Origins::redirectInterceptorSecure() +{ + TestRequestInterceptor interceptor; + m_profile.setUrlRequestInterceptor(&interceptor); + + QVERIFY(verifyLoad(QSL("redirect-secure:secure-cors/resources/redirect.html"))); + eval("addStylesheetLink('redirect-secure:secure-cors/resources/redirect.css')"); + + QTRY_COMPARE(interceptor.requests.size(), 4); + QTRY_COMPARE(m_handler->requests().size(), 2); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("secure-cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("secure-cors:/resources/redirect.css"))); + + QCOMPARE(interceptor.requests[0], QUrl(QStringLiteral("redirect-secure:secure-cors/resources/redirect.html"))); + QCOMPARE(interceptor.requests[1], QUrl(QStringLiteral("secure-cors:/resources/redirect.html"))); + QCOMPARE(interceptor.requests[2], QUrl(QStringLiteral("redirect-secure:secure-cors/resources/redirect.css"))); + QCOMPARE(interceptor.requests[3], QUrl(QStringLiteral("secure-cors:/resources/redirect.css"))); +} + +class TestRedirectInterceptor : public QWebEngineUrlRequestInterceptor +{ +public: + TestRedirectInterceptor() = default; + void interceptRequest(QWebEngineUrlRequestInfo &info) override + { + qCDebug(lc) << this << "Type:" << info.resourceType() << info.requestMethod() << "Navigation:" << info.navigationType() + << info.requestUrl() << "Initiator:" << info.initiator(); + + QUrl url = info.requestUrl(); + requests << url; + if (url.path().startsWith("/redirect")) { + QString path = url.path(); + int idx = path.indexOf(QChar('/'), 10); + if (idx > 0) { + url.setScheme(path.mid(10, idx - 10)); + url.setPath(path.mid(idx, -1)); + url.setHost({}); + info.redirect(url); + } + } + } + QList<QUrl> requests; +}; + +void tst_Origins::redirectInterceptorFile() +{ + TestRedirectInterceptor interceptor; + m_profile.setUrlRequestInterceptor(&interceptor); + + QVERIFY(verifyLoad(QSL("file:///redirect/local-cors/resources/redirect.html"))); + eval("addStylesheetLink('file:///redirect/local-cors/resources/redirect.css')"); + + QTRY_COMPARE(interceptor.requests.size(), 4); + QTRY_COMPARE(m_handler->requests().size(), 2); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("local-cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("local-cors:/resources/redirect.css"))); + + QCOMPARE(interceptor.requests[0], QUrl(QStringLiteral("file:///redirect/local-cors/resources/redirect.html"))); + QCOMPARE(interceptor.requests[1], QUrl(QStringLiteral("local-cors:/resources/redirect.html"))); + QCOMPARE(interceptor.requests[2], QUrl(QStringLiteral("file:///redirect/local-cors/resources/redirect.css"))); + QCOMPARE(interceptor.requests[3], QUrl(QStringLiteral("local-cors:/resources/redirect.css"))); +} + +void tst_Origins::redirectInterceptorHttp() +{ + TestRedirectInterceptor interceptor; + m_profile.setUrlRequestInterceptor(&interceptor); + + QVERIFY(verifyLoad(QSL("http://hallo/redirect/cors/resources/redirect.html"))); + eval("addStylesheetLink('http://hallo/redirect/cors/resources/redirect.css')"); + + QTRY_COMPARE(interceptor.requests.size(), 4); + QTRY_COMPARE(m_handler->requests().size(), 2); + QCOMPARE(m_handler->requests()[0], QUrl(QStringLiteral("cors:/resources/redirect.html"))); + QCOMPARE(m_handler->requests()[1], QUrl(QStringLiteral("cors:/resources/redirect.css"))); + + QCOMPARE(interceptor.requests[0], QUrl(QStringLiteral("http://hallo/redirect/cors/resources/redirect.html"))); + QCOMPARE(interceptor.requests[1], QUrl(QStringLiteral("cors:/resources/redirect.html"))); + QCOMPARE(interceptor.requests[2], QUrl(QStringLiteral("http://hallo/redirect/cors/resources/redirect.css"))); + QCOMPARE(interceptor.requests[3], QUrl(QStringLiteral("cors:/resources/redirect.css"))); +} + +void tst_Origins::localMediaBlock_data() +{ + QTest::addColumn<bool>("enableAccess"); + QTest::addRow("enabled") << true; + QTest::addRow("disabled") << false; +} + +void tst_Origins::localMediaBlock() +{ + QFETCH(bool, enableAccess); + + std::atomic<bool> accessed = false; + HttpServer server; + server.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources" }); + connect(&server, &HttpServer::newRequest, [&](HttpReqRep *) { accessed.store(true); }); + QVERIFY(server.start()); + + ScopedAttribute sa1(m_page->settings(), QWebEngineSettings::LocalContentCanAccessRemoteUrls, enableAccess); + + QVERIFY(verifyLoad("file:" + QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources/media.html")); + eval("addAudio('" + server.url("/mixedXHR.txt").toString() + "')"); + + // Give it a chance to avoid a false positive on the default value of accessed. + if (!enableAccess) + QTest::qSleep(500); + QTRY_COMPARE(accessed.load(), enableAccess); + +} + +class FetchApiHandler : public QWebEngineUrlSchemeHandler +{ + Q_OBJECT +public: + FetchApiHandler(QByteArray schemeName, QObject *parent = nullptr) + : QWebEngineUrlSchemeHandler(parent), m_schemeName(schemeName) + { + } + + void requestStarted(QWebEngineUrlRequestJob *job) override + { + QCOMPARE(job->requestUrl(), QUrl(m_schemeName + ":about")); + fetchWasAllowed = true; + } + + bool fetchWasAllowed = false; + +private: + QByteArray m_schemeName; +}; + +class FetchApiPage : public QWebEnginePage +{ + Q_OBJECT + +signals: + void jsCalled(); + +public: + FetchApiPage(QWebEngineProfile *profile, QObject *parent = nullptr) + : QWebEnginePage(profile, parent) + { + } + +protected: + void javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel, + const QString &message, int, const QString &) override + { + qCritical() << "js:" << message; + emit jsCalled(); + } +}; + +void tst_Origins::fetchApiCustomUrl_data() +{ + QTest::addColumn<QUrl>("url"); + QTest::addColumn<QByteArray>("fetchApiScheme"); + QTest::addColumn<bool>("expectedFetchWasAllowed"); + + QTest::newRow("custom url with fetch allowed flag") + << QUrl("qrc:///resources/fetchApi.html?printRes=false&url=fetchapi-allowed:about") + << QBAL("fetchapi-allowed") << true; + QTest::newRow("custom url without fetch allowed flag") + << QUrl("qrc:///resources/fetchApi.html?printRes=false&url=fetchapi-not-allowed:about") + << QBAL("fetchapi-not-allowed") << false; +} + +void tst_Origins::fetchApiCustomUrl() +{ + QFETCH(QUrl, url); + QFETCH(QByteArray, fetchApiScheme); + QFETCH(bool, expectedFetchWasAllowed); + + QWebEngineProfile profile; + FetchApiHandler handler(fetchApiScheme); + + profile.installUrlSchemeHandler(fetchApiScheme, &handler); + + FetchApiPage page(&profile); + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + QSignalSpy jsSpy(&page, SIGNAL(jsCalled())); + + if (fetchApiScheme == "fetchapi-not-allowed") { + QTest::ignoreMessage(QtCriticalMsg, QRegularExpression("Failed to fetch")); + QTest::ignoreMessage( + QtCriticalMsg, + QRegularExpression("Fetch API cannot load fetchapi-not-allowed:about.")); + } + + page.load(url); + QTRY_VERIFY(loadSpy.count() > 0); + QTRY_COMPARE(handler.fetchWasAllowed, expectedFetchWasAllowed); + + if (fetchApiScheme == "fetchapi-not-allowed") { + QTRY_VERIFY(jsSpy.count() > 0); + } +} + +void tst_Origins::fetchApiHttpUrl() +{ + HttpServer httpServer; + QObject::connect(&httpServer, &HttpServer::newRequest, this, [](HttpReqRep *rr) { + rr->setResponseBody(QBAL("Fetch Was Allowed")); + rr->setResponseHeader(QBAL("Access-Control-Allow-Origin"), QBAL("*")); + rr->sendResponse(); + }); + QVERIFY(httpServer.start()); + + QWebEngineProfile profile; + FetchApiPage page(&profile); + + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + QSignalSpy jsSpy(&page, SIGNAL(jsCalled())); + + QTest::ignoreMessage(QtCriticalMsg, QRegularExpression("Fetch Was Allowed")); + + const QByteArray fullUrl = QByteArray("qrc:///resources/fetchApi.html?printRes=true&url=") + + httpServer.url("/somepage.html").toEncoded(); + page.load(QUrl(fullUrl)); + + QTRY_VERIFY(loadSpy.count() > 0); + QTRY_VERIFY(jsSpy.count() > 0); + QVERIFY(httpServer.stop()); +} + +QTEST_MAIN(tst_Origins) +#include "tst_origins.moc" diff --git a/tests/auto/core/qtversion/CMakeLists.txt b/tests/auto/core/qtversion/CMakeLists.txt new file mode 100644 index 000000000..9a5e89266 --- /dev/null +++ b/tests/auto/core/qtversion/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qtversion + SOURCES + tst_qtversion.cpp + LIBRARIES + Qt::WebEngineCore +) + diff --git a/tests/auto/core/qtversion/tst_qtversion.cpp b/tests/auto/core/qtversion/tst_qtversion.cpp new file mode 100644 index 000000000..44c2d4e5c --- /dev/null +++ b/tests/auto/core/qtversion/tst_qtversion.cpp @@ -0,0 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> +#include <QtWebEngineCore/qwebenginepage.h> + +class tst_QtVersion : public QObject +{ + Q_OBJECT +signals: + void done(); +private Q_SLOTS: + void checkVersion(); +}; + +void tst_QtVersion::checkVersion() +{ + QWebEnginePage page; + QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); + QSignalSpy doneSpy(this, &tst_QtVersion::done); + page.load(QUrl("chrome://qt")); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 12000); + page.toPlainText([this](const QString &result) { + QVERIFY(result.contains(qWebEngineVersion())); + QVERIFY(result.contains(qWebEngineChromiumVersion())); + QVERIFY(result.contains(qWebEngineChromiumSecurityPatchVersion())); + emit done(); + }); + QTRY_VERIFY(doneSpy.size()); +} + +QTEST_MAIN(tst_QtVersion) + +#include "tst_qtversion.moc" diff --git a/tests/auto/core/qwebengineclientcertificatestore/CMakeLists.txt b/tests/auto/core/qwebengineclientcertificatestore/CMakeLists.txt new file mode 100644 index 000000000..8cee7f630 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/CMakeLists.txt @@ -0,0 +1,80 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../httpserver/httpserver.cmake) +include(../../util/util.cmake) + +qt_internal_add_test(tst_qwebengineclientcertificatestore + SOURCES + tst_qwebengineclientcertificatestore.cpp + LIBRARIES + Qt::WebEngineCore + Test::HttpServer + Test::Util +) + +set(tst_qwebengineclientcertificatestore_resource_files + "resources/certificate.crt" + "resources/certificate1.crt" + "resources/privatekey.key" + "resources/privatekey1.key" + "resources/server.pem" + "resources/server.key" + "resources/client.pem" + "resources/client.key" + "resources/client2.pem" + "resources/client2.key" + "resources/ca.pem" +) + +qt_internal_add_resource(tst_qwebengineclientcertificatestore "tst_qwebengineclientcertificatestore" + PREFIX + "/" + FILES + ${tst_qwebengineclientcertificatestore_resource_files} +) + +if(LINUX AND NOT CMAKE_CROSSCOMPILING) + + get_filename_component(homePath $ENV{HOME} ABSOLUTE) + + find_program(pk12util_EXECUTABLE NAMES pk12util) + find_program(certutil_EXECUTABLE NAMES certutil) + + if(pk12util_EXECUTABLE AND certutil_EXECUTABLE) + add_custom_command( + DEPENDS resources/client2.p12 + COMMAND test -e "${homePath}/.pki/nssdb" || ${CMAKE_COMMAND} -E make_directory + "${homePath}/.pki/nssdb" + COMMAND test -e "${homePath}/.pki/nssdb/cert9.db" || ${certutil_EXECUTABLE} + -N --empty-password -d sql:${homePath}/.pki/nssdb + COMMAND test -e "${homePath}/.pki/nssdb/cert9.db" && ${pk12util_EXECUTABLE} + -d sql:${homePath}/.pki/nssdb + -i "${CMAKE_CURRENT_LIST_DIR}/resources/client2.p12" + -W "" + COMMAND ${CMAKE_COMMAND} -E touch pk12util.stamp + OUTPUT pk12util.stamp + VERBATIM + USES_TERMINAL + ) + add_custom_target( + add-user-personal-certificate + DEPENDS pk12util.stamp + ) + qt_internal_extend_target(tst_qwebengineclientcertificatestore DEFINES TEST_NSS) + add_dependencies(tst_qwebengineclientcertificatestore add-user-personal-certificate) + endif() + + find_program(certutil_EXECUTABLE NAMES certutil) + + if(certutil_EXECUTABLE) + add_custom_target(remove-user-personal-certificate + COMMAND ${CMAKE_COMMAND} -E remove pk12util.stamp + COMMAND ${certutil_EXECUTABLE} + -d sql:"${homePath}/.pki/nssdb" + -D + -n qwebengineclientcertificatestore + ) + endif() +endif() + diff --git a/tests/auto/core/qwebengineclientcertificatestore/qwebengineclientcertificatestore.pro b/tests/auto/core/qwebengineclientcertificatestore/qwebengineclientcertificatestore.pro deleted file mode 100644 index e99c7f493..000000000 --- a/tests/auto/core/qwebengineclientcertificatestore/qwebengineclientcertificatestore.pro +++ /dev/null @@ -1 +0,0 @@ -include(../tests.pri) diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/ca.pem b/tests/auto/core/qwebengineclientcertificatestore/resources/ca.pem new file mode 100644 index 000000000..cb62ad62c --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/ca.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIECzCCAvOgAwIBAgIUdhDW1WgGxF313LYA0JjEQpKbanQwDQYJKoZIhvcNAQEL +BQAwgZQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJl +cmxpbjEXMBUGA1UECgwOVGhlIFF0IENvbXBhbnkxFDASBgNVBAsMC1F0V2ViRW5n +aW5lMRIwEAYDVQQDDAl3d3cucXQuaW8xIDAeBgkqhkiG9w0BCQEWEXF0d2ViZW5n +aW5lQHF0LmlvMB4XDTIyMTExNjExMDQxNFoXDTMyMTExMzExMDQxNFowgZQxCzAJ +BgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEXMBUG +A1UECgwOVGhlIFF0IENvbXBhbnkxFDASBgNVBAsMC1F0V2ViRW5naW5lMRIwEAYD +VQQDDAl3d3cucXQuaW8xIDAeBgkqhkiG9w0BCQEWEXF0d2ViZW5naW5lQHF0Lmlv +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxyNLLwAA+FgNQavVJ19n +gdoy+NKLHQyhzcRFykKSp9aAbpAR6e4ukxwG7mWNBcuR7zv1Zw/JqLFE0gmVztVw +FeQWdw1cvTN/OlVEuM+0ShTDHHsCqRpx7/XJT6ytMKVU8jdZN4Vl1m7MubWv4aPy +0WYYd3zIAicciYgy/RHaRhPTKpPzWIPYhmHsM5w2cebL8I0aZXUkC0OeklJArnp9 +007Fr6SXXK0xQ3RO20n7X193gCfd5U70lug0ks/ZZqxtzPHmzIO1WGAOBura50HR +hxUKAu7qQHzBiW5Qwdn0af4FPLJR/SX8ADKTLCSWlMOo1FLYO5w6D8hB4K6/b9VQ +RwIDAQABo1MwUTAdBgNVHQ4EFgQUXuTuB85/iBgwJpLdOc+8TB0KESIwHwYDVR0j +BBgwFoAUXuTuB85/iBgwJpLdOc+8TB0KESIwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAvtucUJa0IECltWv8U6R+LQuZ1Q+ubbmstojO/h8tg6Wf +v6FZ5bH3oboSyGEcytRr6INf4G6znUNAypbodehAEW6/PETdzGM9CJyv2JPJAWzV +rxb1H5VTyiEs8924QOqcNATD+oe7G0vwnDkvprcqaWBA6yvQkWpCXoqMc+F95KnY +8VFt2VQw17l4L4nhaX3Us6hJLMiKV+dLeF0pN+pkCPRP9G5WKgW3mT2U6Gig+rLz +6L7rBbb5KWAttdAbuHCrMa65PgXoVD1P/GteFxUnghDd0PWgUaign8c/DyHGsrbA +uvJqSym0kmQQXptryRaKFsGcCrizdbE6FfrH2iE7vQ== +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/client.key b/tests/auto/core/qwebengineclientcertificatestore/resources/client.key new file mode 100644 index 000000000..21c8e3183 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqnHbq38y1VprEaV2xXzv2nAPyjqCuIfuick8qETkzEsNWPQi +dsBlLfcyf+15wEMhpRIwILXCrUM7Sb7WCGtg1XC00JZvCh2xPBMSD2fiQyHn4men +Fwh9vVbTf1v7w21ZT/pXQrwlgLgNWYZHE3JrcEAwlThQRIdQfzSE6/QeHfYZoGB9 +WfvbREsOWiUlZze/yrblS9vnAVhYwVurelc7lXyHA0dHmkcZ0HwMxVJZ/vLuCyIw +lNGT/ytnA9p1l8uFkAgTcbWZKoyJAsAZG9faZp46hk8+e3KAyKQ78aoUSbjAqnNQ +tBM3bnHeHanf3ddCxyej+k9PfSIY27a9FZxHpQIDAQABAoIBAFsomA8p8ZsQR9Fh +SJupDXMrmhZTotRkxxxkR4/LgP8OaO4ZbFFM5xBldFndPc+pV9Y8WwczjxIxsgTo +Dvrjyx98rwgcXPjxFniFzpP0wJudB7McMs5r2SwpwuYL4SQNWMYgowjrLbehOGqY +GW16NaIMgq9cNfng0RmnkivMHUtyE5GGdK+C6cyK+fIE+cNtQtHPRKfEnwbE9VHz +3EY/nCXGZvMFyj5uHaU4EeZFCzo19TUqhh8H7b0EA44pBtb5U/CxsH4xphZ7rpjt +iVjMfRSMR4qalQNIs6ZEj57We+M/zca/Qq1yhjW+0NYbZifcYo1Oj6e4lC9YlIgn +kGkcuUECgYEA1j0iVFjgBXS8pJP3jBgmbrbBBTNEUv27yjnJCAQx5TbplJkvBM4/ +qzum1uH2o6uRrFtrYJFiAhDHARtg+70rMeYqZp8WFvzJT5c5s+FOmGQPfFjgrD6e +wfnCwFzS7nohJ8TM2mPGJ88pBv0eBYW6D0f7fvcJmEk8hnGktdLRCrECgYEAy6tU +YFZDzGhbgrG2wWzBvAKVngUNhrYZHMiF1WVN8zZdCm7Z8b1S/NMe0rPA5orhAkSX +8fxlDfKOm+U2fKp43aiN0NDiP0TlGRbypAXe7FSnvDxNHbV+Ie0UbwuiJ4s3vJuc +6cdzgKqAs5/rjPXPdUpM8C7344HV7azgSzHIYTUCgYAtVmCmcuxtmye0uG+BoTa4 +5UnxvMivu2x7PkFRxfl9JWLHBKfTn4YPyZ7kCIu2VT+NtwcBN6MDBuPmUxHyFDVI +6Ql+EBqPoM1FX55hd8O3Mi2oxfI94T6dlCpnpP0qZIQRs28apFSx5gArr3Mj/gnC +5BvP4Z2RMaZyWShfJg8A8QKBgQClZEhswyDjiYtmorJqeMsKxn6BiFDnqFDUUvJ7 +zHx0mR0NL9/Es54Eud059ccccIMwuEs7s17M6MBuUMDik/z647nmbPqNroDs0vnP +wQS6njRoY/+rtIrtOf1x/9x6iE+G1keigNmHDu7c72z1V1hVQzUfhsS+99yl2dF6 +vr6eUQKBgF/OHW1bE3FruZ+53Arcb94N/IKnpH9VWoB3elIzr0w6pLtL4HHhmQ58 +TayEpq6YguUAjTvCBbaHuYuKPHiXCAy5DhtrXvP4YdMNH9X1nHc7jVEbGltVbnQU +bG/p5YfZSrDmsjf8w0z7feFOcovC6vF1YCXc8OHK/LQ6JFJ/gtO1 +-----END RSA PRIVATE KEY----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/client.pem b/tests/auto/core/qwebengineclientcertificatestore/resources/client.pem new file mode 100644 index 000000000..dd1f898f7 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/client.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApcCFFNQAgGBu5nr81tUMdXXLGkm8Li+MA0GCSqGSIb3DQEBCwUAMIGU +MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4x +FzAVBgNVBAoMDlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTES +MBAGA1UEAwwJd3d3LnF0LmlvMSAwHgYJKoZIhvcNAQkBFhFxdHdlYmVuZ2luZUBx +dC5pbzAeFw0yMjExMTYxMjExMDFaFw0zMjExMTMxMjExMDFaMIGSMQswCQYDVQQG +EwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xFzAVBgNVBAoM +DlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTEVMBMGA1UEAwwM +Y2xpZW50LnF0LmlvMRswGQYJKoZIhvcNAQkBFgxjbGllbnRAcXQuaW8wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqcdurfzLVWmsRpXbFfO/acA/KOoK4 +h+6JyTyoROTMSw1Y9CJ2wGUt9zJ/7XnAQyGlEjAgtcKtQztJvtYIa2DVcLTQlm8K +HbE8ExIPZ+JDIefiZ6cXCH29VtN/W/vDbVlP+ldCvCWAuA1ZhkcTcmtwQDCVOFBE +h1B/NITr9B4d9hmgYH1Z+9tESw5aJSVnN7/KtuVL2+cBWFjBW6t6VzuVfIcDR0ea +RxnQfAzFUln+8u4LIjCU0ZP/K2cD2nWXy4WQCBNxtZkqjIkCwBkb19pmnjqGTz57 +coDIpDvxqhRJuMCqc1C0Ezducd4dqd/d10LHJ6P6T099Ihjbtr0VnEelAgMBAAEw +DQYJKoZIhvcNAQELBQADggEBALE75ZQxmEXJA16cNAxxmxCKHkaqAE6Ulim1vXNH +jCFfNCDGYn/R28F3BVtMe+bIMoomaTh3h5eOd/9uc2nm8IiT5FUz9epJWPeRG/cl +I+hQ3fvaE7oJ3m3EwfGq1mdqUf1zi+DFjtkimNbn9ZRDocZfpO5VN0u23ptEuk0P +5cH4+Dst0giRMv5W0kXG6QD13H/eVH3jDZCtZa/8T4oxGGskHEa4yDr8s976lVOV +XLI1r7oN4a/KXKow8WN3oHFeKn4QJx86z1uecuZLtT8xjABKSWpZqgsIlmGTGE1a +9W06C+uPVamwn5ND3gnf93YQqn6PwrjlHdrQOTG/vngJLPw= +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/client2.key b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.key new file mode 100644 index 000000000..3c1346519 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv0vrzULGwDJBoZgnGXdkMFxCvkTqqQYCE/LlNtStLJfJH7Fo +CgenVFcJ8RIFHdkL7HeFAIZjDLSjIp2Ud41fd+VsaGgB/+j1/UeEN8nkArvYB9ol +OnKGq6CbSrCocrLo2o2X+6eyLtrtLG6RLr8/UiqB2OWNAdnw70S5RCvnbV6phr8z +bgYqPdPSBaedfZk5Kj6yM6XvIKSK6IjgZuo+Z5SyabJqk2VhaBlB7mjCf3Mj4zPD +XvQXsAq0ZNQXQVwKRfJ2I9uAeNAZiQP5i00pBqe2kIJEKnk8qbP4/Jho2Tp8XSBC +jHMn0oWrAZyO9vw3W940qmqmdRftyt+J8DO9xwIDAQABAoIBAGBpXTCYRR88tQNC +cgJNv/r3pNPMXBBP7OAs/QUDbzwYS89jVDIp5VWGgIY1NMr0RyQooKnBEU6oA8hA +b0FJySHeSSLduJRHzyKV1rdfU0Fldt2OPlEUw3bgfSPJoTwdm2n7DuxQemdPA1Xv +a9CJpto8fjDYkJasRtfwZQdMsVjXCfQ/cCzkOkblUDZcc7yTx3uiBKF8Jy8C+0qc +98btotYU88KWoE9A0ucWt/ik68MjYmccO6PYXKerNW2Ijgd1kik35G3TbEWxOFWW +y3zLFtfoD+21SdUgTMzM06owDVfSt/MER4tOxFyUPRuze7BJXrBofGQfuPiGiPuK +f5QZP8ECgYEA+x1PkClsqtRnjrzmRfi3OFez1Kbbzneucg5ssWR+Hd4EUFhhO42q +te1ZYoydy09tEqd002U7e5hob0/o+rVK9jldpZszMCBfVDYCDqdtw5rNI89bL1Uz +8krn6nk3BBx42lgAFU4C1JEaur4r14OOUtoFfRTAwjogQHcDmpyPNjcCgYEAwwSv +FJAKRjw1oOXKlGotoeYEAREVxH9HFnfM5IcVwcwMt+KUFEyrMtXeH1gk7jo+2ev4 +87njQ8hU3VPObCUcnTJHi2a6D9JIY+zA9bKTJjc8drcBathipmwtak14TsX2qe14 +JBIKlC3V0h1FqM3ep76p4dnt7sTmVc7ZOqBR7PECgYEA1HQE94wEkzdnch0hmbuG +kBWrYNPXDgS1w2uuzBqglPZcoflUMkV2U7s+r6EWc4d8WZbxwVRZkgTs/pgWHd66 +UD1SnKUFFsecv6t97BX9SMu0mYJ6vD4S2ABF3Fu3jzPjj596WowI2vz1J19zyj9U +b4ZjtGKVfv4cgU3v76RbidsCgYAx4CvKzX/jMJjimoJx7KnZAxO5Fh6ED60loOQE ++ktlMgN6r/cBLg6GxM23JHrldn4Gi+QyqTLnbf/OTxW28NLdnTNRAqfJThV3gOBk +thQOLQhIsEsrgUXRnE8NJd0EAHsyQGp+hyKvfP13bEcZgfVU311hRrQkYbUq8uj5 +pnDtcQKBgEFIpP7EzdJWrVOUjnjMQloqBhW8KVVtNwI5bmlcsUvVYjfZph016SiF +UTfZss1KkBmQClAVtyZsrKIfObIJ9KJ4hPAzzk+ca1D6XTLsYjxPwtB/U0ewB2Dm +yMxkXpT1kAiJ2Tdr1hZ8OcQhvnGWmrhtz+AkjyLXiYgST7Hubrxt +-----END RSA PRIVATE KEY----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/client2.p12 b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.p12 Binary files differnew file mode 100644 index 000000000..81e7eb624 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.p12 diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/client2.pem b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.pem new file mode 100644 index 000000000..39c0b3f09 --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/client2.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApkCFFNQAgGBu5nr81tUMdXXLGkm8LjBMA0GCSqGSIb3DQEBCwUAMIGU +MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4x +FzAVBgNVBAoMDlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTES +MBAGA1UEAwwJd3d3LnF0LmlvMSAwHgYJKoZIhvcNAQkBFhFxdHdlYmVuZ2luZUBx +dC5pbzAeFw0yMjExMTYxOTIwMzBaFw0zMjExMTMxOTIwMzBaMIGUMQswCQYDVQQG +EwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xFzAVBgNVBAoM +DlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTEWMBQGA1UEAwwN +Y2xpZW50Mi5xdC5pbzEcMBoGCSqGSIb3DQEJARYNY2xpZW50MkBxdC5pbzCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9L681CxsAyQaGYJxl3ZDBcQr5E +6qkGAhPy5TbUrSyXyR+xaAoHp1RXCfESBR3ZC+x3hQCGYwy0oyKdlHeNX3flbGho +Af/o9f1HhDfJ5AK72AfaJTpyhqugm0qwqHKy6NqNl/unsi7a7SxukS6/P1Iqgdjl +jQHZ8O9EuUQr521eqYa/M24GKj3T0gWnnX2ZOSo+sjOl7yCkiuiI4GbqPmeUsmmy +apNlYWgZQe5own9zI+Mzw170F7AKtGTUF0FcCkXydiPbgHjQGYkD+YtNKQantpCC +RCp5PKmz+PyYaNk6fF0gQoxzJ9KFqwGcjvb8N1veNKpqpnUX7crfifAzvccCAwEA +ATANBgkqhkiG9w0BAQsFAAOCAQEAic8F8q1TpP2ufnBRbrBp54Jgddl/zdVb7O3M +AAK67KiEpEr9xPPVcIowfns1ZTIsIB8D4VS4NQGJXBrwvGWL08SpSmi76I1E156x +9Hql0PHXCjqsJTOSEvljIgQ4sp33zs0DTmlyejSSGnG9sw2FtcYAGZNV+ImAhTO2 +DNxw3BnF++ilHsQbiWIKD5z14bOXb77SJrimup0YBzfwBWJO013k8g8lkiRRs5Ng +XYVr3NoTLcIJQ7BTFu4W1Wegxwrw3fQZ98BBlCVh0htrOcLpWKelJeI16MgZA/7T +P4MwvN5tkyjqrcsrDORldR6JKdX8i+GLF49MgRW4QispcZzoYA== +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/server.key b/tests/auto/core/qwebengineclientcertificatestore/resources/server.key new file mode 100644 index 000000000..632cc4d2e --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5gQoJryenjmvzy4RbqHdNXHK8Gk/8Lto1SwT8+Wbh5EyYRTt +hFdioT1JYcIe3XMwOmx3TjADY1jAXAPfeRcjTkMcnZwF76AXUK2XqBANhaG1wjsi +b7ISGU/U5/Jarm2iwQJ5zjKsNm8pZYqpmKsYAVFMErtfcpdLdSp6BG54SrbItcXh +WHfsUs5cuVEi9nCeugLkDzoPLlj/TeouKWOdzhyvLXkPvPmD4/hD0dULTXpCDZhf +73AuQBWTGsWeUnJQiQhDRwuXWhGRX8qFJQ4rzY8rIbaKhge+BQ6BL+pij2uzHKNQ +j12ZLFZgLihLDJogGp08y9Ud6Ru/3WGoFkY38wIDAQABAoIBABM/TczQA8XhteB0 +Tmkfik8qknzDkeInDIKqCZFjKTyS3dBZ2/YzCcHMSxOvFr4ZIXQCF4mnYuExUAdj +G5QaZ43o98AIikae8tSBcitSDI+eFIOIRz1pfTI5B+vQz93AttnHx0GF4/s6GhCx +JbfsuTmDAAahPz9rgZjwUP2F8PLvaAZqJrXBPY+QLWz0SN2zh6vWAHPbJA0sO/4E +oWUhRPXJDf33YCFxnwtbUBie5313suAfNspODcyH+AxBH2FFh63pe0ZGOhX7XFMJ +yxJqujeZrQdfwFZNPXAPVLJGbd7AIOrVE+O8/bYUB/uuj6pPJBqr+Ob/JhY48pRb +VG2qL4ECgYEA9n3PuL13F9XFcLeergGH7fUcSQeD1T6Z1qaI2Wth0Umfmer/fFZh +IKSCSwEGMTLsalFdlTj8jsSAasjuSorQTeSgHjzvzik1Ll2P6syputjsD1RX/nkl +8L50Pwdeey57Y9dgow7Cw/heGYs6dkXLe9H6qM7eoB8Vrk7/TAFuqNECgYEA7uOl +oKyOxeLn005cenc5enY2IxDhXTaAjTGHE64C0lmicD2OZB7/b+ZIb8M5R7GnCNox +4TxLSRhZYOMO/QcTrnSND5PXbX/HLd3nyQRIN1XtBbg7pJooxP/MQ/Ne5XTTMjCg +qPudkOe0ZgUHEcuH8m/YAFY3DDJC50uiXqYtxYMCgYBHfL+ExbZHfGExyp9Duf/x +PHhCmeJbMzessEnaPLF24FJgcm48YlTzAaMkG5zvIeS9BPIOOCPPSCAyWCn8BnxZ +SuhBPM0TzpG067+0ijzjiswTuhN3Iy2kv6e5K+rz8MwqbamCQOKtsVehMub2rFFS +jNiUosKgT8Oa9SBHq9arMQKBgQCE3EVEnFP3iOAILH/QeLiV/GLVk9DTR7mtTUtj +zZayKLnoFMQ5uOe182x8BCa6UfqlOL0fGKqCZ7Fl6kJuxV3T2+yMKlxZAQTk5JLB +wMjtRbPCR5mcTUS5c87GR/eSRCwlsNfZw775VXSGfOtWoUzlsACBB3IsLVP6UZ1n +aKLyQwKBgC61BvKiyGBEYIchqMI4dSF+zCJbSjNUtjwVobcgC6yERZtX2OeLFCoh +NEf9CcL2Eqb+RzwAD3OV65AiZcrThQNXZ8poBxvwWK8I6E6zB+LX7POAvNu/AV/5 +ANnxwHGGLqi+wTcdMZal2iXkdsrno1Ek/nGMCdA7IVs7l5k7fEpG +-----END RSA PRIVATE KEY----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/resources/server.pem b/tests/auto/core/qwebengineclientcertificatestore/resources/server.pem new file mode 100644 index 000000000..4706fa73e --- /dev/null +++ b/tests/auto/core/qwebengineclientcertificatestore/resources/server.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApcCFFNQAgGBu5nr81tUMdXXLGkm8Li/MA0GCSqGSIb3DQEBCwUAMIGU +MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4x +FzAVBgNVBAoMDlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTES +MBAGA1UEAwwJd3d3LnF0LmlvMSAwHgYJKoZIhvcNAQkBFhFxdHdlYmVuZ2luZUBx +dC5pbzAeFw0yMjExMTYxMjExMTRaFw0zMjExMTMxMjExMTRaMIGSMQswCQYDVQQG +EwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xFzAVBgNVBAoM +DlRoZSBRdCBDb21wYW55MRQwEgYDVQQLDAtRdFdlYkVuZ2luZTEVMBMGA1UEAwwM +c2VydmVyLnF0LmlvMRswGQYJKoZIhvcNAQkBFgxzZXJ2ZXJAcXQuaW8wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDmBCgmvJ6eOa/PLhFuod01ccrwaT/w +u2jVLBPz5ZuHkTJhFO2EV2KhPUlhwh7dczA6bHdOMANjWMBcA995FyNOQxydnAXv +oBdQrZeoEA2FobXCOyJvshIZT9Tn8lqubaLBAnnOMqw2bylliqmYqxgBUUwSu19y +l0t1KnoEbnhKtsi1xeFYd+xSzly5USL2cJ66AuQPOg8uWP9N6i4pY53OHK8teQ+8 ++YPj+EPR1QtNekINmF/vcC5AFZMaxZ5SclCJCENHC5daEZFfyoUlDivNjyshtoqG +B74FDoEv6mKPa7Mco1CPXZksVmAuKEsMmiAanTzL1R3pG7/dYagWRjfzAgMBAAEw +DQYJKoZIhvcNAQELBQADggEBAHotgaBbqIlG4EqjzSpX8kQnZnGJUsA51dbY3K5C +4tNCd+JquQfPmCIKDHkRsmmEU6pcU+LT8m+toJ8Gx0XG4nrdUIDt0Nlf/QrykbPj +hN8z+aSfP9J5tg4NsT7qMWmqUHOa3BcsgWcC4IwWVkbOMz/XbczEQqdBJMbE0+PC +32ihTKPZBPC2QlIvXyuwupvQtcXgEjw1r2FQeYcmItk3CKbJPE/Rk4/aXSCo4b0F +iXPphh8BJPZVvQ2cLpPaGvcse5qjIhF9ODb2HEK3myMwuJVi7teURy8mPlS23Li/ +8gRCNu/stjMlkic7d3dqV0LwaG8+Df1W2wzxsT7IkxN/Z+o= +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.cpp b/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.cpp index 6d51bf7af..7d82a5640 100644 --- a/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.cpp +++ b/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.cpp @@ -1,34 +1,14 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWebEngine module 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) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include <httpsserver.h> +#include <util.h> #include <QtTest/QtTest> #include <QtWebEngineCore/qwebengineclientcertificatestore.h> -#include <QtWebEngineWidgets/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginecertificateerror.h> +#include <QtWebEngineCore/qwebenginesettings.h> class tst_QWebEngineClientCertificateStore : public QObject { @@ -39,8 +19,12 @@ public: ~tst_QWebEngineClientCertificateStore(); private Q_SLOTS: + void init(); + void cleanup(); void addAndListCertificates(); void removeAndClearCertificates(); + void clientAuthentication_data(); + void clientAuthentication(); }; tst_QWebEngineClientCertificateStore::tst_QWebEngineClientCertificateStore() @@ -51,6 +35,19 @@ tst_QWebEngineClientCertificateStore::~tst_QWebEngineClientCertificateStore() { } +void tst_QWebEngineClientCertificateStore::init() +{ + QCOMPARE(0, + QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); +} + +void tst_QWebEngineClientCertificateStore::cleanup() +{ + QWebEngineProfile::defaultProfile()->clientCertificateStore()->clear(); + QCOMPARE(0, + QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); +} + void tst_QWebEngineClientCertificateStore::addAndListCertificates() { // Load QSslCertificate @@ -77,21 +74,93 @@ void tst_QWebEngineClientCertificateStore::addAndListCertificates() QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(cert, sslKey); QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(certSecond, sslKeySecond); - QCOMPARE(2, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().length()); + QCOMPARE(2, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); } void tst_QWebEngineClientCertificateStore::removeAndClearCertificates() { - QCOMPARE(2, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().length()); + addAndListCertificates(); + QCOMPARE(2, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); // Remove one certificate from in-memory store auto list = QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates(); QWebEngineProfile::defaultProfile()->clientCertificateStore()->remove(list[0]); - QCOMPARE(1, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().length()); + QCOMPARE(1, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); // Remove all certificates in-memory store QWebEngineProfile::defaultProfile()->clientCertificateStore()->clear(); - QCOMPARE(0, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().length()); + QCOMPARE(0, QWebEngineProfile::defaultProfile()->clientCertificateStore()->certificates().size()); +} + +void tst_QWebEngineClientCertificateStore::clientAuthentication_data() +{ + QTest::addColumn<QString>("client_certificate"); + QTest::addColumn<QString>("client_key"); + QTest::addColumn<bool>("in_memory"); + QTest::addColumn<bool>("add_more_in_memory_certificates"); + QTest::newRow("in_memory") << ":/resources/client.pem" + << ":/resources/client.key" << true << false; +#if defined(TEST_NSS) + QTest::newRow("nss") << ":/resources/client2.pem" + << ":/resources/client2.key" << false << false; + QTest::newRow("in_memory + nss") << ":/resources/client2.pem" + << ":/resources/client2.key" << false << true; +#endif +} + +void tst_QWebEngineClientCertificateStore::clientAuthentication() +{ + QFETCH(QString, client_certificate); + QFETCH(QString, client_key); + QFETCH(bool, in_memory); + QFETCH(bool, add_more_in_memory_certificates); + + HttpsServer server(":/resources/server.pem", ":/resources/server.key", ":resources/ca.pem"); + server.setExpectError(false); + QVERIFY(server.start()); + + connect(&server, &HttpsServer::newRequest, [&](HttpReqRep *rr) { + rr->setResponseBody(QByteArrayLiteral("<html><body>TEST</body></html>")); + rr->sendResponse(); + }); + + QFile certFile(client_certificate); + certFile.open(QIODevice::ReadOnly); + const QSslCertificate cert(certFile.readAll(), QSsl::Pem); + + QFile keyFile(client_key); + keyFile.open(QIODevice::ReadOnly); + const QSslKey sslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, ""); + + if (in_memory) + QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(cert, sslKey); + + if (add_more_in_memory_certificates) + addAndListCertificates(); + + QWebEnginePage page; + connect(&page, &QWebEnginePage::certificateError, [](QWebEngineCertificateError e) { + // ca is self signed in this test simply accept the certificate error + e.acceptCertificate(); + }); + connect(&page, &QWebEnginePage::selectClientCertificate, &page, + [&cert](QWebEngineClientCertificateSelection selection) { + QVERIFY(!selection.certificates().isEmpty()); + for (const QSslCertificate &sCert : selection.certificates()) { + if (cert == sCert) { + selection.select(sCert); + return; + } + } + QFAIL("No certificate found."); + }); + QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + page.setUrl(server.url()); + QTRY_COMPARE_WITH_TIMEOUT(loadFinishedSpy.size() > 0, true, 20000); + QCOMPARE(loadFinishedSpy.takeFirst().at(0).toBool(), true); + QCOMPARE(toPlainTextSync(&page), QStringLiteral("TEST")); + QVERIFY(server.stop()); } QTEST_MAIN(tst_QWebEngineClientCertificateStore) diff --git a/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.qrc b/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.qrc deleted file mode 100644 index db481fef6..000000000 --- a/tests/auto/core/qwebengineclientcertificatestore/tst_qwebengineclientcertificatestore.qrc +++ /dev/null @@ -1,8 +0,0 @@ -<RCC> - <qresource prefix="/"> - <file>resources/certificate.crt</file> - <file>resources/privatekey.key</file> - <file>resources/certificate1.crt</file> - <file>resources/privatekey1.key</file> - </qresource> -</RCC> diff --git a/tests/auto/core/qwebenginecookiestore/CMakeLists.txt b/tests/auto/core/qwebenginecookiestore/CMakeLists.txt new file mode 100644 index 000000000..cc14940f1 --- /dev/null +++ b/tests/auto/core/qwebenginecookiestore/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../httpserver/httpserver.cmake) +include(../../util/util.cmake) + +qt_internal_add_test(tst_qwebenginecookiestore + SOURCES + tst_qwebenginecookiestore.cpp + LIBRARIES + Qt::WebEngineCore + Test::HttpServer + Test::Util +) + +set(tst_qwebenginecookiestore_resource_files + "resources/content.html" + "resources/index.html" +) + +qt_internal_add_resource(tst_qwebenginecookiestore "tst_qwebenginecookiestore" + PREFIX + "/" + FILES + ${tst_qwebenginecookiestore_resource_files} +) diff --git a/tests/auto/core/qwebenginecookiestore/qwebenginecookiestore.pro b/tests/auto/core/qwebenginecookiestore/qwebenginecookiestore.pro deleted file mode 100644 index 9c239f1a7..000000000 --- a/tests/auto/core/qwebenginecookiestore/qwebenginecookiestore.pro +++ /dev/null @@ -1,2 +0,0 @@ -include(../tests.pri) -include(../../shared/http.pri) diff --git a/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.cpp b/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.cpp index 5290d5373..3fff2cd45 100644 --- a/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.cpp +++ b/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.cpp @@ -1,41 +1,19 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWebEngine module 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$ -** -****************************************************************************/ - -#include "../../widgets/util.h" +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <util.h> #include <QtTest/QtTest> -#include <QtWebEngineCore/qwebenginecallback.h> #include <QtWebEngineCore/qwebenginecookiestore.h> -#include <QtWebEngineWidgets/qwebenginepage.h> -#include <QtWebEngineWidgets/qwebengineprofile.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> #include "httpserver.h" #include "httpreqrep.h" +// locally overwrite the default timeout of QTY_(COMPARE|VERIFY) +#define QWE_TRY_COMPARE(x, y) QTRY_COMPARE_WITH_TIMEOUT(x, y, 30000) +#define QWE_TRY_VERIFY(x) QTRY_VERIFY_WITH_TIMEOUT(x, 30000) + class tst_QWebEngineCookieStore : public QObject { Q_OBJECT @@ -56,6 +34,7 @@ private Q_SLOTS: // as it checks storage manipulation without navigation void setAndDeleteCookie(); + void setInvalidCookie(); void cookieSignals(); void batchCookieTasks(); void basicFilter(); @@ -105,22 +84,22 @@ void tst_QWebEngineCookieStore::cookieSignals() page.load(QUrl("qrc:///resources/index.html")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVariant success = loadSpy.takeFirst().takeFirst(); QVERIFY(success.toBool()); - QTRY_COMPARE(cookieAddedSpy.count(), 2); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 2); // try whether updating a cookie to be expired results in that cookie being removed. QNetworkCookie expiredCookie(QNetworkCookie::parseCookies(QByteArrayLiteral("SessionCookie=delete; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=///resources")).first()); client->setCookie(expiredCookie, QUrl("qrc:///resources/index.html")); - QTRY_COMPARE(cookieRemovedSpy.count(), 1); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); cookieRemovedSpy.clear(); // try removing the other cookie. QNetworkCookie nonSessionCookie(QNetworkCookie::parseCookies(QByteArrayLiteral("CookieWithExpiresField=QtWebEngineCookieTest; path=///resources")).first()); client->deleteCookie(nonSessionCookie, QUrl("qrc:///resources/index.html")); - QTRY_COMPARE(cookieRemovedSpy.count(), 1); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); } void tst_QWebEngineCookieStore::setAndDeleteCookie() @@ -141,33 +120,64 @@ void tst_QWebEngineCookieStore::setAndDeleteCookie() client->loadAllCookies(); // /* FIXME remove 'blank' navigation once loadAllCookies api is fixed page.load(QUrl("about:blank")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); // */ // check if pending cookies are set and removed client->setCookie(cookie1); client->setCookie(cookie2); - QTRY_COMPARE(cookieAddedSpy.count(), 2); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 2); client->deleteCookie(cookie1); - QTRY_COMPARE(cookieRemovedSpy.count(), 1); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); page.load(QUrl("qrc:///resources/content.html")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 2); QVariant success = loadSpy.takeFirst().takeFirst(); QVERIFY(success.toBool()); - QTRY_COMPARE(cookieAddedSpy.count(), 2); - QTRY_COMPARE(cookieRemovedSpy.count(), 1); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 2); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); cookieAddedSpy.clear(); cookieRemovedSpy.clear(); client->setCookie(cookie3); - QTRY_COMPARE(cookieAddedSpy.count(), 1); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 1); // updating a cookie with an expired 'expires' field should remove the cookie with the same name client->setCookie(expiredCookie3); client->deleteCookie(cookie2); - QTRY_COMPARE(cookieAddedSpy.count(), 1); - QTRY_COMPARE(cookieRemovedSpy.count(), 2); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 1); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 2); +} + +void tst_QWebEngineCookieStore::setInvalidCookie() +{ + QWebEnginePage page(m_profile); + QWebEngineCookieStore *client = m_profile->cookieStore(); + + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + QSignalSpy cookieAddedSpy(client, SIGNAL(cookieAdded(const QNetworkCookie &))); + QSignalSpy cookieRemovedSpy(client, SIGNAL(cookieRemoved(const QNetworkCookie &))); + + QNetworkCookie goodCookie( + QNetworkCookie::parseCookies( + QByteArrayLiteral("khaos=I9GX8CWI; Domain=.example.com; Path=/docs")) + .first()); + QNetworkCookie badCookie( + QNetworkCookie::parseCookies(QByteArrayLiteral("TestCookie=foo\tbar;")).first()); + + // force to init storage as it's done lazily upon first navigation + client->loadAllCookies(); + // /* FIXME remove 'blank' navigation once loadAllCookies api is fixed + page.load(QUrl("about:blank")); + QWE_TRY_COMPARE(loadSpy.size(), 1); + // */ + + client->setCookie(badCookie); + client->setCookie(goodCookie); + client->deleteCookie(goodCookie); + // by the time the second cookie is removed, only one cookie should have been added + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 1); } void tst_QWebEngineCookieStore::batchCookieTasks() @@ -186,29 +196,29 @@ void tst_QWebEngineCookieStore::batchCookieTasks() client->loadAllCookies(); // /* FIXME remove 'blank' navigation once loadAllCookies api is fixed page.load(QUrl("about:blank")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); // */ client->setCookie(cookie1); client->setCookie(cookie2); - QTRY_COMPARE(cookieAddedSpy.count(), 2); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 2); page.load(QUrl("qrc:///resources/index.html")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 2, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 2); QVariant success = loadSpy.takeFirst().takeFirst(); QVERIFY(success.toBool()); - QTRY_COMPARE(cookieAddedSpy.count(), 4); - QTRY_COMPARE(cookieRemovedSpy.count(), 0); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 4); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 0); cookieAddedSpy.clear(); cookieRemovedSpy.clear(); client->deleteSessionCookies(); - QTRY_COMPARE(cookieRemovedSpy.count(), 3); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 3); client->deleteAllCookies(); - QTRY_COMPARE(cookieRemovedSpy.count(), 4); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 4); } void tst_QWebEngineCookieStore::basicFilter() @@ -225,22 +235,22 @@ void tst_QWebEngineCookieStore::basicFilter() page.load(QUrl("qrc:///resources/index.html")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); - QTRY_COMPARE(cookieAddedSpy.count(), 2); - QTRY_COMPARE(accessTested.loadAcquire(), 2); // FIXME? + QWE_TRY_COMPARE(cookieAddedSpy.size(), 2); + QWE_TRY_COMPARE(accessTested.loadAcquire(), 2); // FIXME? client->deleteAllCookies(); - QTRY_COMPARE(cookieRemovedSpy.count(), 2); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 2); client->setCookieFilter([&](const QWebEngineCookieStore::FilterRequest &){ ++accessTested; return false; }); page.triggerAction(QWebEnginePage::ReloadAndBypassCache); - QTRY_COMPARE(loadSpy.count(), 1); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); - QTRY_COMPARE(accessTested.loadAcquire(), 4); // FIXME? + QWE_TRY_COMPARE(accessTested.loadAcquire(), 4); // FIXME? // Test cookies are NOT added: QTest::qWait(100); - QCOMPARE(cookieAddedSpy.count(), 2); + QCOMPARE(cookieAddedSpy.size(), 2); } void tst_QWebEngineCookieStore::basicFilterOverHTTP() @@ -249,22 +259,27 @@ void tst_QWebEngineCookieStore::basicFilterOverHTTP() QWebEngineCookieStore *client = m_profile->cookieStore(); QAtomicInt accessTested = 0; - client->setCookieFilter([&](const QWebEngineCookieStore::FilterRequest &) { ++accessTested; return true; }); + QList<QPair<QUrl, QUrl>> resourceFirstParty; + client->setCookieFilter([&](const QWebEngineCookieStore::FilterRequest &request) { + resourceFirstParty.append(qMakePair(request.origin, request.firstPartyUrl)); + ++accessTested; + return true; + }); HttpServer httpServer; - - if (!httpServer.start()) - QSKIP("Failed to start http server"); + httpServer.setHostDomain(QString("sub.test.localhost")); + QVERIFY(httpServer.start()); QByteArray cookieRequestHeader; connect(&httpServer, &HttpServer::newRequest, [&cookieRequestHeader](HttpReqRep *rr) { - if (rr->requestPath().size() <= 1) { + if (rr->requestMethod() == "GET" && rr->requestPath() == "/test.html") { cookieRequestHeader = rr->requestHeader(QByteArrayLiteral("Cookie")); if (cookieRequestHeader.isEmpty()) rr->setResponseHeader(QByteArrayLiteral("Set-Cookie"), QByteArrayLiteral("Test=test")); + rr->setResponseBody("<head><link rel='icon' type='image/png' href='resources/Fav.png'/>" + "<title>Page with a favicon and an icon</title></head>" + "<body><img src='resources/Img.ico'></body>"); rr->sendResponse(); - } else { - rr->sendResponse(404); } }); @@ -273,43 +288,59 @@ void tst_QWebEngineCookieStore::basicFilterOverHTTP() QSignalSpy cookieRemovedSpy(client, SIGNAL(cookieRemoved(const QNetworkCookie &))); QSignalSpy serverSpy(&httpServer, SIGNAL(newRequest(HttpReqRep *))); - page.load(httpServer.url()); + QUrl firstPartyUrl = httpServer.url("/test.html"); + page.load(firstPartyUrl); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); - QTRY_COMPARE(cookieAddedSpy.count(), 1); - QTRY_COMPARE(accessTested.loadAcquire(), 3); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 1); + QWE_TRY_COMPARE(accessTested.loadAcquire(), 4); QVERIFY(cookieRequestHeader.isEmpty()); + QWE_TRY_COMPARE(serverSpy.size(), 3); + page.triggerAction(QWebEnginePage::Reload); - QTRY_COMPARE(loadSpy.count(), 1); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); QVERIFY(!cookieRequestHeader.isEmpty()); - QTRY_COMPARE(cookieAddedSpy.count(), 1); - QTRY_COMPARE(accessTested.loadAcquire(), 5); + QWE_TRY_COMPARE(cookieAddedSpy.size(), 1); + QWE_TRY_COMPARE(accessTested.loadAcquire(), 6); + + QWE_TRY_COMPARE(serverSpy.size(), 5); client->deleteAllCookies(); - QTRY_COMPARE(cookieRemovedSpy.count(), 1); + QWE_TRY_COMPARE(cookieRemovedSpy.size(), 1); - client->setCookieFilter([&](const QWebEngineCookieStore::FilterRequest &) { ++accessTested; return false; }); + client->setCookieFilter([&](const QWebEngineCookieStore::FilterRequest &request) { + resourceFirstParty.append(qMakePair(request.origin, request.firstPartyUrl)); + ++accessTested; + return false; + }); page.triggerAction(QWebEnginePage::ReloadAndBypassCache); - QTRY_COMPARE(loadSpy.count(), 1); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); QVERIFY(cookieRequestHeader.isEmpty()); // Test cookies are NOT added: QTest::qWait(100); - QCOMPARE(cookieAddedSpy.count(), 1); - QTRY_COMPARE(accessTested.loadAcquire(), 8); + QCOMPARE(cookieAddedSpy.size(), 1); + QWE_TRY_COMPARE(accessTested.loadAcquire(), 9); + + QWE_TRY_COMPARE(serverSpy.size(), 7); page.triggerAction(QWebEnginePage::Reload); - QTRY_COMPARE(loadSpy.count(), 1); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); QVERIFY(cookieRequestHeader.isEmpty()); - QCOMPARE(cookieAddedSpy.count(), 1); + QCOMPARE(cookieAddedSpy.size(), 1); // Wait for last GET /favicon.ico - QTRY_COMPARE(serverSpy.count(), 8); + QWE_TRY_COMPARE(serverSpy.size(), 9); (void) httpServer.stop(); + + QCOMPARE(resourceFirstParty.size(), accessTested.loadAcquire()); + for (auto &&p : std::as_const(resourceFirstParty)) + QVERIFY2(p.second == firstPartyUrl, + qPrintable(QString("Resource [%1] has wrong firstPartyUrl: %2").arg(p.first.toString(), p.second.toString()))); } void tst_QWebEngineCookieStore::html5featureFilter() @@ -324,17 +355,17 @@ void tst_QWebEngineCookieStore::html5featureFilter() page.load(QUrl("qrc:///resources/content.html")); - QTRY_COMPARE_WITH_TIMEOUT(loadSpy.count(), 1, 30000); + QWE_TRY_COMPARE(loadSpy.size(), 1); QVERIFY(loadSpy.takeFirst().takeFirst().toBool()); QCOMPARE(accessTested.loadAcquire(), 0); // FIXME? QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*Uncaught SecurityError.*sessionStorage.*")); page.runJavaScript("sessionStorage.test = 5;"); - QTRY_COMPARE(accessTested.loadAcquire(), 1); + QWE_TRY_COMPARE(accessTested.loadAcquire(), 1); QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*Uncaught SecurityError.*sessionStorage.*")); QAtomicInt callbackTriggered = 0; page.runJavaScript("sessionStorage.test", [&](const QVariant &v) { QVERIFY(!v.isValid()); callbackTriggered = 1; }); - QTRY_VERIFY(callbackTriggered); + QWE_TRY_VERIFY(callbackTriggered); } QTEST_MAIN(tst_QWebEngineCookieStore) diff --git a/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.qrc b/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.qrc deleted file mode 100644 index afeae268b..000000000 --- a/tests/auto/core/qwebenginecookiestore/tst_qwebenginecookiestore.qrc +++ /dev/null @@ -1,6 +0,0 @@ -<!DOCTYPE RCC><RCC version="1.0"> -<qresource> - <file>resources/index.html</file> - <file>resources/content.html</file> -</qresource> -</RCC> diff --git a/tests/auto/core/qwebengineglobalsettings/CMakeLists.txt b/tests/auto/core/qwebengineglobalsettings/CMakeLists.txt new file mode 100644 index 000000000..dd2f28857 --- /dev/null +++ b/tests/auto/core/qwebengineglobalsettings/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../httpserver/httpserver.cmake) +include(../../util/util.cmake) + +qt_internal_add_test(tst_qwebengineglobalsettings + SOURCES + tst_qwebengineglobalsettings.cpp + LIBRARIES + Qt::Network + Qt::WebEngineCore + Test::HttpServer + Qt::WebEngineWidgets + Test::Util +) + +# Resources: +set(tst_qwebengineglobalsettings_resource_files + "cert/localhost.crt" + "cert/localhost.key" + "cert/RootCA.pem" +) + +qt_add_resources(tst_qwebengineglobalsettings "tst_qwebengineglobalsettings" + PREFIX + "/" + FILES + ${tst_qwebengineglobalsettings_resource_files} +) diff --git a/tests/auto/core/qwebengineglobalsettings/cert/RootCA.pem b/tests/auto/core/qwebengineglobalsettings/cert/RootCA.pem new file mode 100644 index 000000000..16be384bb --- /dev/null +++ b/tests/auto/core/qwebengineglobalsettings/cert/RootCA.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTzCCAjegAwIBAgIUNYpmREIW67Fh7WNzCwPL6nnSgRMwDQYJKoZIhvcNAQEL +BQAwNzELMAkGA1UEBhMCREUxKDAmBgNVBAMMH1dlYkVuZ2luZUdsb2JhbFNldHRp +bmdzLVRlc3QtQ0EwHhcNMjMwMzI5MTYwMzMzWhcNMjYwMTE2MTYwMzMzWjA3MQsw +CQYDVQQGEwJERTEoMCYGA1UEAwwfV2ViRW5naW5lR2xvYmFsU2V0dGluZ3MtVGVz +dC1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJhZ9DwcdbVBzMyY +/nEqt5KUi74LFwEnS0G2ne8IYco9+Pbkpb8wV5u6n43IsQ2c3u8D/KVtu1Vy3tf2 +G3aKOwhFzaj7GWLE9FweZyMoL6ASOtWEa55myT5zAysVQtHAkePu0smAPP0gVq3E +vjSTwV1W1mVXv4wMwffR8AvNGhKrJIa3L2/uYKGbzEmaCk2kt0vIqfrx8095RlXC +lUcwTMJ6/d/e/DMDtqQ1ypUuz5QYQybIVKwuqkhojT2DXbitv0rE4HLQub8CxOZ+ +9GcQjeAt8Tzrlp1UP6c9OtlsMoo37gJYzb/XDE6OPnk42chQXDxGQjtVRs+60kcT +Dx/YHG0CAwEAAaNTMFEwHQYDVR0OBBYEFP1FK1U9CUHQEp7coaab7IdR18zDMB8G +A1UdIwQYMBaAFP1FK1U9CUHQEp7coaab7IdR18zDMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAGTlcxmRsuwBeRW0CsjX/qbdcB0OWkIC857Jn8RU +6yGa7P9i6EQb1O/DEF+Z7dkASx5zfN6LPIrph6J56/mmcNBeqArovWJwxQUTNO9i +1kOU3xoH5n/ya+gdBr3reA90bAMKWXwa6uI3smPJKy+2hOkdDaSBa5KECYWhniH0 +yRxL7YdhQhuCc7Ijf+S6WzeHRwdLkdiV8c2vAGWdunDFuGT2iYVOZ1qbp6O/tmjv +TxWAnvP4+0ykFIlMor0vYWD8xbnq28263VmNh7mrFwkBYnHJiY/nTDwxaL+g6Uji +9n6+VuxUDgfQWX1YRHC4a89zI/Zvnn2z/92To7zRmNd73RQ= +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineglobalsettings/cert/localhost.crt b/tests/auto/core/qwebengineglobalsettings/cert/localhost.crt new file mode 100644 index 000000000..c063bf268 --- /dev/null +++ b/tests/auto/core/qwebengineglobalsettings/cert/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtDCCApygAwIBAgIURotPFTfDJxwaqhZsr0IpAahl2EMwDQYJKoZIhvcNAQEL +BQAwNzELMAkGA1UEBhMCREUxKDAmBgNVBAMMH1dlYkVuZ2luZUdsb2JhbFNldHRp +bmdzLVRlc3QtQ0EwHhcNMjMwMzI5MTYwNDI1WhcNMjYwMTE2MTYwNDI1WjB0MQsw +CQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xKTAn +BgNVBAoMIFFXZWJFbmdpbmVHbG9iYWxTZXR0aW5ncy1FeGFtcGxlMRgwFgYDVQQD +DA9sb2NhbGhvc3QubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCY9FX/8YetuJKSFSoYPxMKPvjt2zJ5g13DywqZtbDzLAIASCxn4iad3qgxaoWB +uI0g4ykzrhUa98YHU8fDH4T4Vwhwu72SRYW4+MgT9ohc1oCKBX05b+BWSuTSeuHy +leqdL78bj3bu5TtFs2dJt2t8eA6SNR9lDa5g4v7oA4xVp93gMo2YqZwaLONmxIKY +cI4lcETnHHsvc6+dB2UqWHJEN75UkdC/XnDLM/VbL3/4zxU+9x34nvvfSJwCHVnE +u+zYwrZXkbiDVDovT855phVC/K5skVgBL2miz3eygljuw1tIwnmVix/e/xHZyqMg +Lje/LZN53v5G61Wut6bbdkeVAgMBAAGjezB5MB8GA1UdIwQYMBaAFP1FK1U9CUHQ +Ep7coaab7IdR18zDMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMB8GA1UdEQQYMBaC +CWxvY2FsaG9zdIIJMTI3LjAuMC4xMB0GA1UdDgQWBBQv20ImViFtvMm5gHqTeML6 +pi5BtTANBgkqhkiG9w0BAQsFAAOCAQEAGsL7eOms30+IPdKQZ2pjtX22GfM6EiUs +xsQfsX/Q3bus30B2m9GJ6AVIwVUJimOGiMauDCLjDeXWCMZpihzodExhC0D/X1B+ +FsCLagcjlgfWwekKEo8sUWUZp0DNCyacPtTPxqoS2RA7foEzQhRLViLSvf+UXU8g +jZAwWGB/5V849zcbbNBcWKzRsPvNOqeENWEn1ByGcsWhas2V0KzRcUODuA9UHv1x ++eDlLZYsWV9c1MuL8a1VDEluIR19eR/Gl9axjPZY2oiPvlp2b7I4z1bY+wV2i6vh +NeDCAxAxJ42tXeb86vtnVXfSDbzedDbLv0l2kEhcywVN3MwhsEpmpg== +-----END CERTIFICATE----- diff --git a/tests/auto/core/qwebengineglobalsettings/cert/localhost.key b/tests/auto/core/qwebengineglobalsettings/cert/localhost.key new file mode 100644 index 000000000..49502ab9a --- /dev/null +++ b/tests/auto/core/qwebengineglobalsettings/cert/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCY9FX/8YetuJKS +FSoYPxMKPvjt2zJ5g13DywqZtbDzLAIASCxn4iad3qgxaoWBuI0g4ykzrhUa98YH +U8fDH4T4Vwhwu72SRYW4+MgT9ohc1oCKBX05b+BWSuTSeuHyleqdL78bj3bu5TtF +s2dJt2t8eA6SNR9lDa5g4v7oA4xVp93gMo2YqZwaLONmxIKYcI4lcETnHHsvc6+d +B2UqWHJEN75UkdC/XnDLM/VbL3/4zxU+9x34nvvfSJwCHVnEu+zYwrZXkbiDVDov +T855phVC/K5skVgBL2miz3eygljuw1tIwnmVix/e/xHZyqMgLje/LZN53v5G61Wu +t6bbdkeVAgMBAAECggEAF7ADX5NivUsv29LORZoDE1ukRoXjX8Ex9MANoLdsM4S1 +vKBwzBfQfjN83cZO7cOMi7LSby//EcGcmAboEXZgq+siof7ZQX1l07snlTvha2tG +1dk6xvnmBscrf9NLCbwg7P33fUevFhlHICjEDr0KtuiK7Sav+YDwaA3Ph1QBWERd +GO3sVlnuGsDpf/0GijlwqEGuDKUePEEANOhXcByh693VmvlKVf9SbrimYeunKW1O +FvvcAiBMzqurhZotb9/juiq+fIMID29OULCCxlZZSySRYw0REpnAAxgWvaSZqyWd +tGosSKEgc4SptPTmC8DzRDDfN/zqvXmkmYnN9o4qMwKBgQC3tVYdEPn3fHX973Df +Ukp55cRpZFuNxjOiV3Qo1aTAKqpbmJ4/x8tUL7rhsmJXSxlW7xdNQ/WIHM1PJlZx +UAIr5eBq1MUVd5OENP8PuVIdAIumHXICB5FioJR/WnXRXLJEbGxSRr5gwaTw3sXd +ObPRQEUOrJtK+W0aeBKePRtIiwKBgQDVJN7A26vy8PMcE4TcDp75vAY/qasZl6ES +oksaHf3c4/jsnr70wRoOXi3fPo/DpWmVFylttMSEnzh3nfnOkDXZD3mQx3sdVw8l +12++t59733hllJZlwqk5OAc990kE1X44UW/gPA+5Vb9kpo6ahpFtqwhDmqa4RjtZ +0R/1H/KUXwKBgGjR7Qq0rwwJVgHIZ2zlNV2MPp+sBZlFaBzPLZZHILQNJBsTX+gg +heHJQiaZdAc+8Hxr+624gxZg6LyqsVQCRNrrVTtfn/x5uBANdSNxqGqn7wafcne5 +/bh6y4BHC0akT4s/Gidv+hyXIRfW5Ksvy2wv8bdHwWvsGdaqgGUNlM21AoGAe9Vc +BbibAh6zYBCHFEL6YiW3i61L1yadUnIwKBBcucVJjk/8qb63ILne9OEoLYcg/Jnk +W/S2aEcJS5Xg2P44CtBO1KrRAI7gIiA0sB2G7zU6gen+J0kdgDzpGDtflQtktdu6 +oBDFIeyLsjKCj4y3WXwQ5RYo3s8PFHPHmWbiTQkCgYBViqAHaAaPJQZG7Q6/Xqhf +rFosC0DeZk5PHrEDbpAnuJbySafGZ4LxY5Oq05+aM8BeR+IuGopciBBDWXoh7msO +N8WItu7WI86lIu7JZRbeju2w59tgj0EA+GyU1udPjTjseP5qpB27X1JEGV6TYBNs +LMmQSgdGWqwteKsc50UrkA== +-----END PRIVATE KEY----- diff --git a/tests/auto/core/qwebengineglobalsettings/tst_qwebengineglobalsettings.cpp b/tests/auto/core/qwebengineglobalsettings/tst_qwebengineglobalsettings.cpp new file mode 100644 index 000000000..5b6b64778 --- /dev/null +++ b/tests/auto/core/qwebengineglobalsettings/tst_qwebengineglobalsettings.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest> +#include <widgetutil.h> +#include <QNetworkAccessManager> +#include <QNetworkRequest> +#include <QWebEngineProfile> +#include <QWebEnginePage> +#include <QWebEngineGlobalSettings> +#include <QWebEngineLoadingInfo> + +#include "httpsserver.h" +#include "httpreqrep.h" + +class tst_QWebEngineGlobalSettings : public QObject +{ + Q_OBJECT + +public: + tst_QWebEngineGlobalSettings() { } + ~tst_QWebEngineGlobalSettings() { } + +public Q_SLOTS: + void init() { } + void cleanup() { } + +private Q_SLOTS: + void initTestCase() { } + void cleanupTestCase() { } + void dnsOverHttps_data(); + void dnsOverHttps(); +}; + +Q_LOGGING_CATEGORY(lc, "qt.webengine.tests") + +void tst_QWebEngineGlobalSettings::dnsOverHttps_data() +{ + QTest::addColumn<QWebEngineGlobalSettings::SecureDnsMode>("dnsMode"); + QTest::addColumn<QString>("uriTemplate"); + QTest::addColumn<bool>("isMockDnsServerCalledExpected"); + QTest::addColumn<bool>("isDnsResolutionSuccessExpected"); + QTest::addColumn<bool>("isConfigurationSuccessExpected"); + QTest::newRow("DnsMode::SystemOnly (no DoH server)") + << QWebEngineGlobalSettings::SecureDnsMode::SystemOnly << QStringLiteral("") << false + << true << true; + QTest::newRow("DnsMode::SecureOnly (mock DoH server)") + << QWebEngineGlobalSettings::SecureDnsMode::SecureOnly + << QStringLiteral("https://127.0.0.1:3000/dns-query{?dns}") << true << false << true; + QTest::newRow("DnsMode::SecureOnly (real DoH server)") + << QWebEngineGlobalSettings::SecureDnsMode::SecureOnly + << QStringLiteral("https://dns.google/dns-query{?dns}") << false << true << true; + QTest::newRow("DnsMode::SecureOnly (Empty URI Templates)") + << QWebEngineGlobalSettings::SecureDnsMode::SecureOnly << QStringLiteral("") << false + << false << false; + // Note: In the following test, we can't verify that the DoH server is called first and + // afterwards insecure DNS is tried, because for the DoH server to ever be used when the DNS + // mode is set to DnsMode::WithFallback, Chromium starts an asynchronous DoH server DnsProbe and + // requires that the connection is successful. That is, we'd have to implement a correct + // DNS response, which in turn requires that certificate errors aren't ignored and + // non-self-signed certificates are used for correct encryption. Instead of implementing + // all of that, this test verifies that Chromium tries probing the configured DoH server only. + QTest::newRow("DnsMode::SecureWithFallback (mock DoH server)") + << QWebEngineGlobalSettings::SecureDnsMode::SecureWithFallback + << QStringLiteral("https://127.0.0.1:3000/dns-query{?dns}") << true << true << true; + QTest::newRow("DnsMode::SecureWithFallback (Empty URI Templates)") + << QWebEngineGlobalSettings::SecureDnsMode::SecureWithFallback << QStringLiteral("") + << false << false << false; +} + +void tst_QWebEngineGlobalSettings::dnsOverHttps() +{ + const QUrl url = QStringLiteral("https://google.com/"); + // Verify network access with NAM because the result of loadFinished signal + // is used to verify that the DNS resolution was successful. + QNetworkAccessManager nam; + QSignalSpy namSpy(&nam, &QNetworkAccessManager::finished); + QScopedPointer<QNetworkReply> reply(nam.get(QNetworkRequest(url))); + if (!namSpy.wait(20000) || reply->error() != QNetworkReply::NoError) + QSKIP("Couldn't load page from network, skipping test."); + + QFETCH(QWebEngineGlobalSettings::SecureDnsMode, dnsMode); + QFETCH(QString, uriTemplate); + QFETCH(bool, isMockDnsServerCalledExpected); + QFETCH(bool, isDnsResolutionSuccessExpected); + QFETCH(bool, isConfigurationSuccessExpected); + bool isMockDnsServerCalled = false; + bool isLoadSuccessful = false; + + bool configurationSuccess = + QWebEngineGlobalSettings::setDnsMode({ dnsMode, QStringList{ uriTemplate } }); + QCOMPARE(configurationSuccess, isConfigurationSuccessExpected); + + if (!configurationSuccess) { + // In this case, DNS has invalid configuration, so the DNS change transaction is not + // triggered and the result of the DNS resolution depends on the current DNS mode, which is + // set by the previous run of this function. + return; + } + HttpsServer httpsServer(":/cert/localhost.crt", ":/cert/localhost.key", ":/cert/RootCA.pem", + 3000, this); + QObject::connect(&httpsServer, &HttpsServer::newRequest, this, + [&isMockDnsServerCalled](HttpReqRep *rr) { + QVERIFY(rr->requestPath().contains(QByteArrayLiteral("/dns-query?dns="))); + isMockDnsServerCalled = true; + rr->close(); + }); + QVERIFY(httpsServer.start()); + httpsServer.setExpectError(isMockDnsServerCalledExpected); + httpsServer.setVerifyMode(QSslSocket::PeerVerifyMode::VerifyNone); + + QWebEngineProfile profile; + QWebEnginePage page(&profile); + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + + connect(&page, &QWebEnginePage::loadFinished, this, + [&isLoadSuccessful](bool ok) { isLoadSuccessful = ok; }); + + page.load(url); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); + + QTRY_COMPARE(isMockDnsServerCalled, isMockDnsServerCalledExpected); + QCOMPARE(isLoadSuccessful, isDnsResolutionSuccessExpected); + QVERIFY(httpsServer.stop()); +} + +static QByteArrayList params = QByteArrayList() << "--ignore-certificate-errors"; + +W_QTEST_MAIN(tst_QWebEngineGlobalSettings, params) +#include "tst_qwebengineglobalsettings.moc" diff --git a/tests/auto/core/qwebengineloadinginfo/CMakeLists.txt b/tests/auto/core/qwebengineloadinginfo/CMakeLists.txt new file mode 100644 index 000000000..09d9c30f5 --- /dev/null +++ b/tests/auto/core/qwebengineloadinginfo/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qwebengineloadinginfo + SOURCES + tst_qwebengineloadinginfo.cpp + LIBRARIES + Qt::WebEngineCore + Test::HttpServer +) diff --git a/tests/auto/core/qwebengineloadinginfo/tst_qwebengineloadinginfo.cpp b/tests/auto/core/qwebengineloadinginfo/tst_qwebengineloadinginfo.cpp new file mode 100644 index 000000000..ccae7436b --- /dev/null +++ b/tests/auto/core/qwebengineloadinginfo/tst_qwebengineloadinginfo.cpp @@ -0,0 +1,93 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineCore/qwebengineloadinginfo.h> +#include <QtWebEngineCore/qwebenginehttprequest.h> +#include <QtWebEngineCore/qwebenginesettings.h> + +#include <httpserver.h> +#include <httpreqrep.h> + +typedef QMultiMap<QByteArray, QByteArray> Map; + +class tst_QWebEngineLoadingInfo : public QObject +{ + Q_OBJECT + +public: + tst_QWebEngineLoadingInfo() { } + +public slots: + void loadingInfoChanged(QWebEngineLoadingInfo loadingInfo) + { + const auto responseHeaders = loadingInfo.responseHeaders(); + QFETCH(Map, expected); + + if (loadingInfo.status() == QWebEngineLoadingInfo::LoadSucceededStatus + || loadingInfo.status() == QWebEngineLoadingInfo::LoadFailedStatus) { + if (!expected.empty()) + QCOMPARE(responseHeaders, expected); + } else { + QVERIFY(responseHeaders.size() == 0); + } + } + +private Q_SLOTS: + void responseHeaders_data() + { + QTest::addColumn<int>("responseCode"); + QTest::addColumn<Map>("input"); + QTest::addColumn<Map>("expected"); + + const Map empty; + const Map input { + std::make_pair("header1", "value1"), + std::make_pair("header2", "value2") + }; + const Map expected { + std::make_pair("header1", "value1"), + std::make_pair("header2", "value2"), + std::make_pair("Connection", "close") + }; + + + QTest::newRow("with headers HTTP 200") << 200 << input << expected; + QTest::newRow("with headers HTTP 500") << 500 << input << expected; + QTest::newRow("without headers HTTP 200") << 200 << empty << empty; + QTest::newRow("without headers HTTP 500") << 500 << empty << empty; + } + + void responseHeaders() + { + HttpServer httpServer; + + QFETCH(Map, input); + QFETCH(int, responseCode); + QObject::connect(&httpServer, &HttpServer::newRequest, this, [&](HttpReqRep *rr) { + for (auto it = input.cbegin(); it != input.cend(); ++it) + rr->setResponseHeader(it.key(), it.value()); + + rr->sendResponse(responseCode); + }); + QVERIFY(httpServer.start()); + + QWebEngineProfile profile; + QWebEnginePage page(&profile); + page.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + QObject::connect(&page, &QWebEnginePage::loadingChanged, this, &tst_QWebEngineLoadingInfo::loadingInfoChanged); + + + QWebEngineHttpRequest request(httpServer.url("/somepage.html")); + page.load(request); + + QTRY_VERIFY(spy.count() > 0); + QVERIFY(httpServer.stop()); + } +}; + +QTEST_MAIN(tst_QWebEngineLoadingInfo) +#include "tst_qwebengineloadinginfo.moc" diff --git a/tests/auto/core/qwebenginesettings/BLACKLIST b/tests/auto/core/qwebenginesettings/BLACKLIST new file mode 100644 index 000000000..4b99b8e84 --- /dev/null +++ b/tests/auto/core/qwebenginesettings/BLACKLIST @@ -0,0 +1,5 @@ +[javascriptClipboard] +ubuntu-20.04 + +[setInAcceptNavigationRequest] +macos diff --git a/tests/auto/core/qwebenginesettings/CMakeLists.txt b/tests/auto/core/qwebenginesettings/CMakeLists.txt new file mode 100644 index 000000000..756b99bbb --- /dev/null +++ b/tests/auto/core/qwebenginesettings/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../util/util.cmake) + +qt_internal_add_test(tst_qwebenginesettings + SOURCES + tst_qwebenginesettings.cpp + LIBRARIES + Qt::WebEngineCore + Qt::WebEngineWidgets + Test::Util +) diff --git a/tests/auto/core/qwebenginesettings/tst_qwebenginesettings.cpp b/tests/auto/core/qwebenginesettings/tst_qwebenginesettings.cpp new file mode 100644 index 000000000..e856dd094 --- /dev/null +++ b/tests/auto/core/qwebenginesettings/tst_qwebenginesettings.cpp @@ -0,0 +1,307 @@ +/* + Copyright (C) 2015 The Qt Company Ltd. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <util.h> + +#include <QtTest/QtTest> + +#include <qwebenginepage.h> +#include <qwebengineprofile.h> +#include <qwebenginesettings.h> + +#include <QtGui/qclipboard.h> +#include <QtGui/qguiapplication.h> +#include <QtWebEngineWidgets/qwebengineview.h> + +class tst_QWebEngineSettings: public QObject { + Q_OBJECT + +private Q_SLOTS: + void resetAttributes(); + void defaultFontFamily_data(); + void defaultFontFamily(); + void javascriptClipboard_data(); + void javascriptClipboard(); + void setInAcceptNavigationRequest(); + void disableReadingFromCanvas_data(); + void disableReadingFromCanvas(); + void forceDarkMode(); + void forceDarkModeMultiView(); +}; + +void tst_QWebEngineSettings::resetAttributes() +{ + QWebEngineProfile profile; + QWebEngineSettings *settings = profile.settings(); + + // Attribute + bool defaultValue = settings->testAttribute(QWebEngineSettings::FullScreenSupportEnabled); + settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, !defaultValue); + QCOMPARE(!defaultValue, settings->testAttribute(QWebEngineSettings::FullScreenSupportEnabled)); + settings->resetAttribute(QWebEngineSettings::FullScreenSupportEnabled); + QCOMPARE(defaultValue, settings->testAttribute(QWebEngineSettings::FullScreenSupportEnabled)); + + // Font family + QString defaultFamily = settings->fontFamily(QWebEngineSettings::StandardFont); + QString newFontFamily("PugDog"); + settings->setFontFamily(QWebEngineSettings::StandardFont, newFontFamily); + QCOMPARE(newFontFamily, settings->fontFamily(QWebEngineSettings::StandardFont)); + settings->resetFontFamily(QWebEngineSettings::StandardFont); + QCOMPARE(defaultFamily, settings->fontFamily(QWebEngineSettings::StandardFont)); + + // Font size + int defaultSize = settings->fontSize(QWebEngineSettings::MinimumFontSize); + int newSize = defaultSize + 10; + settings->setFontSize(QWebEngineSettings::MinimumFontSize, newSize); + QCOMPARE(newSize, settings->fontSize(QWebEngineSettings::MinimumFontSize)); + settings->resetFontSize(QWebEngineSettings::MinimumFontSize); + QCOMPARE(defaultSize, settings->fontSize(QWebEngineSettings::MinimumFontSize)); +} + +void tst_QWebEngineSettings::defaultFontFamily_data() +{ + QTest::addColumn<int>("fontFamily"); + + QTest::newRow("StandardFont") << static_cast<int>(QWebEngineSettings::StandardFont); + QTest::newRow("FixedFont") << static_cast<int>(QWebEngineSettings::FixedFont); + QTest::newRow("SerifFont") << static_cast<int>(QWebEngineSettings::SerifFont); + QTest::newRow("SansSerifFont") << static_cast<int>(QWebEngineSettings::SansSerifFont); + QTest::newRow("CursiveFont") << static_cast<int>(QWebEngineSettings::CursiveFont); + QTest::newRow("FantasyFont") << static_cast<int>(QWebEngineSettings::FantasyFont); +} + +void tst_QWebEngineSettings::defaultFontFamily() +{ + QWebEngineProfile profile; + QWebEngineSettings *settings = profile.settings(); + + QFETCH(int, fontFamily); + QVERIFY(!settings->fontFamily(static_cast<QWebEngineSettings::FontFamily>(fontFamily)).isEmpty()); +} + +void tst_QWebEngineSettings::javascriptClipboard_data() +{ + QTest::addColumn<bool>("javascriptCanAccessClipboard"); + QTest::addColumn<bool>("javascriptCanPaste"); + QTest::addColumn<bool>("copyResult"); + QTest::addColumn<bool>("pasteResult"); + + QTest::newRow("default") << false << false << false << false; + QTest::newRow("canCopy") << true << false << true << false; + // paste command requires both permissions + QTest::newRow("canPaste") << false << true << false << false; + QTest::newRow("canCopyAndPaste") << true << true << true << true; +} + +void tst_QWebEngineSettings::javascriptClipboard() +{ + QFETCH(bool, javascriptCanAccessClipboard); + QFETCH(bool, javascriptCanPaste); + QFETCH(bool, copyResult); + QFETCH(bool, pasteResult); + + QWebEnginePage page; + + // check defaults + QCOMPARE(page.settings()->testAttribute(QWebEngineSettings::JavascriptCanAccessClipboard), + false); + QCOMPARE(page.settings()->testAttribute(QWebEngineSettings::JavascriptCanPaste), false); + + // check accessors + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, + javascriptCanAccessClipboard); + page.settings()->setAttribute(QWebEngineSettings::JavascriptCanPaste, + javascriptCanPaste); + QCOMPARE(page.settings()->testAttribute(QWebEngineSettings::JavascriptCanAccessClipboard), + javascriptCanAccessClipboard); + QCOMPARE(page.settings()->testAttribute(QWebEngineSettings::JavascriptCanPaste), + javascriptCanPaste); + + QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + page.setHtml("<html><body>" + "<input type='text' value='OriginalText' id='myInput'/>" + "</body></html>"); + QVERIFY(loadFinishedSpy.wait()); + + // make sure that 'OriginalText' is selected + evaluateJavaScriptSync(&page, "document.getElementById('myInput').select()"); + QCOMPARE(evaluateJavaScriptSync(&page, "window.getSelection().toString()").toString(), + QStringLiteral("OriginalText")); + + // Check that the actual settings work by the + // - return value of queryCommandEnabled and + // - return value of execCommand + // - comparing the clipboard / input field + QGuiApplication::clipboard()->setText(QString()); + QCOMPARE(evaluateJavaScriptSync(&page, "document.queryCommandEnabled('copy')").toBool(), + copyResult); + QCOMPARE(evaluateJavaScriptSync(&page, "document.execCommand('copy')").toBool(), copyResult); + QTRY_COMPARE(QGuiApplication::clipboard()->text(), + (copyResult ? QString("OriginalText") : QString())); + + + QGuiApplication::clipboard()->setText("AnotherText"); + QCOMPARE(evaluateJavaScriptSync(&page, "document.queryCommandEnabled('paste')").toBool(), + pasteResult); + QCOMPARE(evaluateJavaScriptSync(&page, "document.execCommand('paste')").toBool(), pasteResult); + QCOMPARE(evaluateJavaScriptSync(&page, "document.getElementById('myInput').value").toString(), + (pasteResult ? QString("AnotherText") : QString("OriginalText"))); +} + +class NavigationRequestOverride : public QWebEnginePage +{ +protected: + bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame) override + { + Q_UNUSED(type); + + if (isMainFrame && url.scheme().startsWith("data")) + settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + // TODO: note this setting is flaky, consider settings().commit() + return true; + } +}; + +void tst_QWebEngineSettings::setInAcceptNavigationRequest() +{ + NavigationRequestOverride page; + QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + QWebEngineProfile::defaultProfile()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false); + QVERIFY(!page.settings()->testAttribute(QWebEngineSettings::JavascriptEnabled)); + + page.load(QUrl("about:blank")); + QVERIFY(loadFinishedSpy.wait()); + QVERIFY(!page.settings()->testAttribute(QWebEngineSettings::JavascriptEnabled)); + + page.setHtml("<html><body>" + "<script>document.write('PASS')</script>" + "<noscript>FAIL</noscript>" + "</body></html>"); + QVERIFY(loadFinishedSpy.wait()); + QVERIFY(page.settings()->testAttribute(QWebEngineSettings::JavascriptEnabled)); + QCOMPARE(toPlainTextSync(&page), QStringLiteral("PASS")); +} + +void tst_QWebEngineSettings::disableReadingFromCanvas_data() +{ + QTest::addColumn<bool>("disableReadingFromCanvas"); + QTest::addColumn<bool>("result"); + QTest::newRow("disabled") << false << true; + QTest::newRow("enabled") << true << false; +} + +void tst_QWebEngineSettings::disableReadingFromCanvas() +{ + QFETCH(bool, disableReadingFromCanvas); + QFETCH(bool, result); + + QWebEnginePage page; + QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + page.settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + page.settings()->setAttribute(QWebEngineSettings::ReadingFromCanvasEnabled, + !disableReadingFromCanvas); + page.setHtml("<html><body>" + "<canvas id='myCanvas' width='200' height='40' style='border:1px solid " + "#000000;'></canvas>" + "</body></html>"); + QVERIFY(loadFinishedSpy.wait()); + QCOMPARE(page.settings()->testAttribute(QWebEngineSettings::ReadingFromCanvasEnabled), + !disableReadingFromCanvas); + + const QString jsCode("(function(){" + " var canvas = document.getElementById(\"myCanvas\");" + " var ctx = canvas.getContext(\"2d\");" + " ctx.fillStyle = \"rgb(255,0,255)\";" + " ctx.fillRect(0, 0, 200, 40);" + " try {" + " src = canvas.toDataURL();" + " }" + " catch(err) {" + " src = \"\";" + " }" + " return src.length ? true : false;" + "})();"); + QCOMPARE(evaluateJavaScriptSync(&page, jsCode).toBool(), result); +} + +void tst_QWebEngineSettings::forceDarkMode() +{ + QWebEnginePage page; + QSignalSpy loadFinishedSpy(&page, SIGNAL(loadFinished(bool))); + page.settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + + // based on: https://developer.chrome.com/blog/auto-dark-theme/#detecting-auto-dark-theme + page.setHtml("<html><body>" + "<div id=\"detection\", style=\"display: none; background-color: canvas; color-scheme: light\"</div>" + "</body></html>"); + + const QString isAutoDark("(() => {" + " const detectionDiv = document.querySelector('#detection');" + " return getComputedStyle(detectionDiv).backgroundColor != 'rgb(255, 255, 255)';" + "})()"); + + QVERIFY(loadFinishedSpy.wait()); + QTRY_COMPARE(evaluateJavaScriptSync(&page, isAutoDark).toBool(), false); + page.settings()->setAttribute(QWebEngineSettings::ForceDarkMode, true); + QTRY_COMPARE(evaluateJavaScriptSync(&page, isAutoDark).toBool(), true); +} + +void tst_QWebEngineSettings::forceDarkModeMultiView() +{ + QWebEngineView view1; + QWebEngineView view2; + QWebEnginePage *page1 = view1.page(); + QWebEnginePage *page2 = view2.page(); + page1->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + page2->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + view1.resize(300,300); + view2.resize(300,300); + view1.show(); + view2.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view1)); + QVERIFY(QTest::qWaitForWindowExposed(&view2)); + + QSignalSpy loadFinishedSpy(page1, SIGNAL(loadFinished(bool))); + QSignalSpy loadFinishedSpy2(page2, SIGNAL(loadFinished(bool))); + QString html("<html><body>" + "<div id=\"detection\", style=\"display: none; background-color: canvas; color-scheme: light\"</div>" + "</body></html>"); + + const QString isAutoDark("(() => {" + " const detectionDiv = document.querySelector('#detection');" + " return getComputedStyle(detectionDiv).backgroundColor != 'rgb(255, 255, 255)';" + "})()"); + + view1.setHtml(html); + QVERIFY(loadFinishedSpy.wait()); + view2.setHtml(html); + QVERIFY(loadFinishedSpy2.wait()); + + // both views has light color-scheme + QTRY_COMPARE(evaluateJavaScriptSync(page1, isAutoDark).toBool(), false); + QTRY_COMPARE(evaluateJavaScriptSync(page2, isAutoDark).toBool(), false); + view1.settings()->setAttribute(QWebEngineSettings::ForceDarkMode, true); + // dark mode should apply only for view1 + QTRY_COMPARE(evaluateJavaScriptSync(page1, isAutoDark).toBool(), true); + QTRY_COMPARE(evaluateJavaScriptSync(page2, isAutoDark).toBool(), false); +} + +QTEST_MAIN(tst_QWebEngineSettings) + +#include "tst_qwebenginesettings.moc" diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/CMakeLists.txt b/tests/auto/core/qwebengineurlrequestinterceptor/CMakeLists.txt new file mode 100644 index 000000000..c12c0c45c --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestinterceptor/CMakeLists.txt @@ -0,0 +1,49 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../util/util.cmake) +include(../../httpserver/httpserver.cmake) + +qt_internal_add_test(tst_qwebengineurlrequestinterceptor + SOURCES + tst_qwebengineurlrequestinterceptor.cpp + LIBRARIES + Qt::WebEngineCore + Qt::WebEngineCorePrivate + Qt::CorePrivate + Test::HttpServer + Test::Util +) + +set(tst_qwebengineurlrequestinterceptor_resource_files + "resources/content.html" + "resources/content2.html" + "resources/content3.html" + "resources/favicon.html" + "resources/firstparty.html" + "resources/fontawesome.woff" + "resources/icons/favicon.png" + "resources/iframe.html" + "resources/iframe2.html" + "resources/iframe3.html" + "resources/image.html" + "resources/image_in_iframe.html" + "resources/index.html" + "resources/media.html" + "resources/media.mp4" + "resources/media_in_iframe.html" + "resources/resource.html" + "resources/resource_in_iframe.html" + "resources/script.js" + "resources/style.css" + "resources/sw.html" + "resources/sw.js" + "resources/postBodyFile.txt" +) + +qt_internal_add_resource(tst_qwebengineurlrequestinterceptor "tst_qwebengineurlrequestinterceptor" + PREFIX + "/" + FILES + ${tst_qwebengineurlrequestinterceptor_resource_files} +) diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/qwebengineurlrequestinterceptor.pro b/tests/auto/core/qwebengineurlrequestinterceptor/qwebengineurlrequestinterceptor.pro deleted file mode 100644 index 9c239f1a7..000000000 --- a/tests/auto/core/qwebengineurlrequestinterceptor/qwebengineurlrequestinterceptor.pro +++ /dev/null @@ -1,2 +0,0 @@ -include(../tests.pri) -include(../../shared/http.pri) diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html index 360ad65ef..84bf55036 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content.html @@ -1,5 +1,6 @@ <html> +<head><link rel="icon" href="data:,"></head> <body> -<a>This is test content</a> +<a>Simple test page without favicon (meaning no separate request from http server)</a> </body> </html> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html new file mode 100644 index 000000000..84bf55036 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content2.html @@ -0,0 +1,6 @@ +<html> +<head><link rel="icon" href="data:,"></head> +<body> +<a>Simple test page without favicon (meaning no separate request from http server)</a> +</body> +</html> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/content3.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content3.html new file mode 100644 index 000000000..84bf55036 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/content3.html @@ -0,0 +1,6 @@ +<html> +<head><link rel="icon" href="data:,"></head> +<body> +<a>Simple test page without favicon (meaning no separate request from http server)</a> +</body> +</html> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/postBodyFile.txt b/tests/auto/core/qwebengineurlrequestinterceptor/resources/postBodyFile.txt new file mode 100644 index 000000000..7729c4e0a --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/postBodyFile.txt @@ -0,0 +1,3 @@ +{ +"test": "1234" +} diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.html b/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.html index af44b45a2..fc3d9ded4 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.html +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.html @@ -2,13 +2,40 @@ <html> <body> <script> + function logState(state) { + console.log("Service worker " + state) + } + + const registerServiceWorker = async () => { + try { + var serviceWorker; + const registration = await navigator.serviceWorker.register('/sw.js'); + if (registration.installing) { + serviceWorker = registration.installing; + } else if (registration.waiting) { + serviceWorker = registration.waiting; + } else if (registration.active) { + serviceWorker = registration.active; + } + } catch (error) { + console.error("Service worker registration error: ${error}"); + } + if (serviceWorker) { + logState(serviceWorker.state); + serviceWorker.addEventListener('statechange', function(e) { + logState(e.target.state); + }); + } + }; if ('serviceWorker' in navigator) { - window.addEventListener('load', function() { - navigator.serviceWorker.register('/sw.js').then(function(registration) { - console.log('ServiceWorker registration successful with scope: ', registration.scope); - }, function(err) { - console.error('ServiceWorker registration failed: ', err); - }); + registerServiceWorker(); + navigator.serviceWorker.ready.then((registration) => { + navigator.serviceWorker.onmessage = (event) => { + if (event.data && event.data.type === 'PONG') { + console.log("Service worker done"); + } + }; + registration.active.postMessage({type: 'PING'}); }); } </script> diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.js b/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.js index 2216e2a07..196a9ad67 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.js +++ b/tests/auto/core/qwebengineurlrequestinterceptor/resources/sw.js @@ -1,3 +1,16 @@ self.addEventListener('install', function(event) { - console.log('ServiceWorker installed'); + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', function(event) { + event.waitUntil(self.clients.claim()); +}); +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'PING') { + self.clients.matchAll({includeUncontrolled: true, type: 'window'}).then((clients) => { + if (clients && clients.length) { + clients[0].postMessage({type: 'PONG'}); + } + }); + } }); diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp b/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp index 20e191a4f..7cea14c0c 100644 --- a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp +++ b/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.cpp @@ -1,44 +1,21 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtWebEngine module 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$ -** -****************************************************************************/ - -#include "../../widgets/util.h" +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + +#include <util.h> #include <QtTest/QtTest> #include <QtWebEngineCore/qwebengineurlrequestinfo.h> +#include <QtWebEngineCore/private/qwebengineurlrequestinfo_p.h> #include <QtWebEngineCore/qwebengineurlrequestinterceptor.h> -#include <QtWebEngineWidgets/qwebenginepage.h> -#include <QtWebEngineWidgets/qwebengineprofile.h> -#include <QtWebEngineWidgets/qwebenginesettings.h> +#include <QtWebEngineCore/qwebenginesettings.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineCore/qwebenginehttprequest.h> #include <httpserver.h> #include <httpreqrep.h> -typedef void (QWebEngineProfile::*InterceptorSetter)(QWebEngineUrlRequestInterceptor *interceptor); -Q_DECLARE_METATYPE(InterceptorSetter) class tst_QWebEngineUrlRequestInterceptor : public QObject { Q_OBJECT @@ -54,28 +31,30 @@ public Q_SLOTS: private Q_SLOTS: void initTestCase(); void cleanupTestCase(); - void interceptRequest_data(); void interceptRequest(); - void ipv6HostEncoding_data(); void ipv6HostEncoding(); void requestedUrl_data(); void requestedUrl(); void setUrlSameUrl_data(); void setUrlSameUrl(); - void firstPartyUrl_data(); void firstPartyUrl(); void firstPartyUrlNestedIframes_data(); void firstPartyUrlNestedIframes(); void requestInterceptorByResourceType_data(); void requestInterceptorByResourceType(); - void firstPartyUrlHttp_data(); void firstPartyUrlHttp(); - void passRefererHeader_data(); - void passRefererHeader(); - void initiator_data(); + void headers(); + void customHeaders(); void initiator(); - void jsServiceWorker_data(); void jsServiceWorker(); + void replaceInterceptor_data(); + void replaceInterceptor(); + void replaceOnIntercept(); + void multipleRedirects(); + void postWithBody_data(); + void postWithBody(); + void profilePreventsPageInterception_data(); + void profilePreventsPageInterception(); }; tst_QWebEngineUrlRequestInterceptor::tst_QWebEngineUrlRequestInterceptor() @@ -108,44 +87,53 @@ struct RequestInfo { , firstPartyUrl(info.firstPartyUrl()) , initiator(info.initiator()) , resourceType(info.resourceType()) + , headers(info.httpHeaders()) {} QUrl requestUrl; QUrl firstPartyUrl; QUrl initiator; int resourceType; + QHash<QByteArray, QByteArray> headers; }; -static const QByteArray kHttpHeaderReferrerValue = QByteArrayLiteral("http://somereferrer.com/"); -static const QByteArray kHttpHeaderRefererName = QByteArrayLiteral("referer"); static const QUrl kRedirectUrl = QUrl("qrc:///resources/content.html"); +Q_LOGGING_CATEGORY(lc, "qt.webengine.tests") + class TestRequestInterceptor : public QWebEngineUrlRequestInterceptor { public: QList<RequestInfo> requestInfos; bool shouldRedirect = false; + QUrl redirectUrl; QMap<QUrl, QSet<QUrl>> requestInitiatorUrls; QMap<QByteArray, QByteArray> headers; + std::function<bool (QWebEngineUrlRequestInfo &)> onIntercept; void interceptRequest(QWebEngineUrlRequestInfo &info) override { - QCOMPARE(QThread::currentThread() == QCoreApplication::instance()->thread(), !property("deprecated").toBool()); + QVERIFY(QThread::currentThread() == QCoreApplication::instance()->thread()); + qCDebug(lc) << this << "Type:" << info.resourceType() << info.requestMethod() << "Navigation:" << info.navigationType() + << info.requestUrl() << "Initiator:" << info.initiator(); // Since 63 we also intercept some unrelated blob requests.. if (info.requestUrl().scheme() == QLatin1String("blob")) return; + if (onIntercept && !onIntercept(info)) + return; + bool block = info.requestMethod() != QByteArrayLiteral("GET"); - bool redirect = shouldRedirect && info.requestUrl() != kRedirectUrl; + bool redirect = shouldRedirect && info.requestUrl() != redirectUrl; + + // set additional headers if any required by test + for (auto it = headers.begin(); it != headers.end(); ++it) info.setHttpHeader(it.key(), it.value()); if (block) { info.block(true); } else if (redirect) { - info.redirect(kRedirectUrl); - } else { - // set additional headers if any required by test - for (auto it = headers.begin(); it != headers.end(); ++it) info.setHttpHeader(it.key(), it.value()); + info.redirect(redirectUrl); } requestInitiatorUrls[info.requestUrl()].insert(info.initiator()); @@ -153,7 +141,7 @@ public: // MEMO avoid unintentionally changing request when it is not needed for test logic // since api behavior depends on 'changed' state of the info object - Q_ASSERT(info.changed() == (block || redirect || !headers.empty())); + Q_ASSERT(info.changed() == (block || redirect)); } bool shouldSkipRequest(const RequestInfo &requestInfo) @@ -195,8 +183,31 @@ public: return false; } - TestRequestInterceptor(bool redirect) - : shouldRedirect(redirect) + TestRequestInterceptor(bool redirect = false, const QUrl &url = kRedirectUrl) + : shouldRedirect(redirect), redirectUrl(url) + { + } +}; + +class TestMultipleRedirectsInterceptor : public QWebEngineUrlRequestInterceptor { +public: + QList<RequestInfo> requestInfos; + QMap<QUrl, QUrl> redirectPairs; + int redirectCount = 0; + void interceptRequest(QWebEngineUrlRequestInfo &info) override + { + QVERIFY(QThread::currentThread() == QCoreApplication::instance()->thread()); + qCDebug(lc) << this << "Type:" << info.resourceType() << info.requestMethod() << "Navigation:" << info.navigationType() + << info.requestUrl() << "Initiator:" << info.initiator(); + auto redirectUrl = redirectPairs.constFind(info.requestUrl()); + if (redirectUrl != redirectPairs.constEnd()) { + info.redirect(redirectUrl.value()); + requestInfos.append(info); + redirectCount++; + } + } + + TestMultipleRedirectsInterceptor() { } }; @@ -206,7 +217,7 @@ class ConsolePage : public QWebEnginePage { public: ConsolePage(QWebEngineProfile* profile) : QWebEnginePage(profile) {} - virtual void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID) + void javaScriptConsoleMessage(JavaScriptConsoleMessageLevel level, const QString& message, int lineNumber, const QString& sourceID) override { levels.append(level); messages.append(message); @@ -220,25 +231,17 @@ public: QStringList sourceIDs; }; -void tst_QWebEngineUrlRequestInterceptor::interceptRequest_data() -{ - QTest::addColumn<InterceptorSetter>("setter"); - QTest::newRow("ui") << &QWebEngineProfile::setUrlRequestInterceptor; - QTest::newRow("io") << &QWebEngineProfile::setRequestInterceptor; -} - void tst_QWebEngineUrlRequestInterceptor::interceptRequest() { - QFETCH(InterceptorSetter, setter); QWebEngineProfile profile; profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); page.load(QUrl("qrc:///resources/index.html")); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); QVariant success = loadSpy.takeFirst().takeFirst(); QVERIFY(success.toBool()); loadSpy.clear(); @@ -246,7 +249,7 @@ void tst_QWebEngineUrlRequestInterceptor::interceptRequest() page.runJavaScript("post();", [&ok](const QVariant result){ ok = result; }); QTRY_VERIFY(ok.toBool()); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE(loadSpy.size(), 1); success = loadSpy.takeFirst().takeFirst(); // We block non-GET requests, so this should not succeed. QVERIFY(!success.toBool()); @@ -254,22 +257,22 @@ void tst_QWebEngineUrlRequestInterceptor::interceptRequest() interceptor.shouldRedirect = true; page.load(QUrl("qrc:///resources/__placeholder__")); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); success = loadSpy.takeFirst().takeFirst(); // The redirection for __placeholder__ should succeed. QVERIFY(success.toBool()); loadSpy.clear(); - QCOMPARE(interceptor.requestInfos.count(), 4); + QCOMPARE(interceptor.requestInfos.size(), 4); // Make sure that registering an observer does not modify the request. TestRequestInterceptor observer(/* intercept */ false); - (profile.*setter)(&observer); + profile.setUrlRequestInterceptor(&observer); page.load(QUrl("qrc:///resources/__placeholder__")); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); success = loadSpy.takeFirst().takeFirst(); // Since we do not intercept, loading an invalid path should not succeed. QVERIFY(!success.toBool()); - QCOMPARE(observer.requestInfos.count(), 1); + QCOMPARE(observer.requestInfos.size(), 1); } class LocalhostContentProvider : public QWebEngineUrlRequestInterceptor @@ -292,53 +295,44 @@ public: QList<QUrl> requestedUrls; }; -void tst_QWebEngineUrlRequestInterceptor::ipv6HostEncoding_data() -{ - interceptRequest_data(); -} - void tst_QWebEngineUrlRequestInterceptor::ipv6HostEncoding() { - QFETCH(InterceptorSetter, setter); QWebEngineProfile profile; LocalhostContentProvider contentProvider; - (profile.*setter)(&contentProvider); + profile.setUrlRequestInterceptor(&contentProvider); QWebEnginePage page(&profile); QSignalSpy spyLoadFinished(&page, SIGNAL(loadFinished(bool))); page.setHtml("<p>Hi", QUrl::fromEncoded("http://[::1]/index.html")); - QTRY_COMPARE(spyLoadFinished.count(), 1); - QCOMPARE(contentProvider.requestedUrls.count(), 0); + QTRY_COMPARE(spyLoadFinished.size(), 1); + QCOMPARE(contentProvider.requestedUrls.size(), 0); evaluateJavaScriptSync(&page, "var r = new XMLHttpRequest();" "r.open('GET', 'http://[::1]/test.xml', false);" "r.send(null);" ); - QCOMPARE(contentProvider.requestedUrls.count(), 1); + QCOMPARE(contentProvider.requestedUrls.size(), 1); QCOMPARE(contentProvider.requestedUrls.at(0), QUrl::fromEncoded("http://[::1]/test.xml")); } void tst_QWebEngineUrlRequestInterceptor::requestedUrl_data() { - QTest::addColumn<InterceptorSetter>("setter"); QTest::addColumn<bool>("interceptInPage"); - QTest::newRow("ui profile intercept") << &QWebEngineProfile::setUrlRequestInterceptor << false; - QTest::newRow("ui page intercept") << &QWebEngineProfile::setUrlRequestInterceptor << true; - QTest::newRow("io profile intercept") << &QWebEngineProfile::setRequestInterceptor << false; + QTest::newRow("profile intercept") << false; + QTest::newRow("page intercept") << true; } void tst_QWebEngineUrlRequestInterceptor::requestedUrl() { - QFETCH(InterceptorSetter, setter); QFETCH(bool, interceptInPage); QWebEngineProfile profile; profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); TestRequestInterceptor interceptor(/* intercept */ true); if (!interceptInPage) - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); if (interceptInPage) @@ -347,8 +341,8 @@ void tst_QWebEngineUrlRequestInterceptor::requestedUrl() page.setUrl(QUrl("qrc:///resources/__placeholder__")); QVERIFY(spy.wait()); - QTRY_COMPARE(spy.count(), 1); - QVERIFY(interceptor.requestInfos.count() >= 1); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 20000); + QVERIFY(interceptor.requestInfos.size() >= 1); QCOMPARE(interceptor.requestInfos.at(0).requestUrl, QUrl("qrc:///resources/content.html")); QCOMPARE(page.requestedUrl(), QUrl("qrc:///resources/__placeholder__")); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); @@ -356,15 +350,15 @@ void tst_QWebEngineUrlRequestInterceptor::requestedUrl() interceptor.shouldRedirect = false; page.setUrl(QUrl("qrc:/non-existent.html")); - QTRY_COMPARE(spy.count(), 2); - QVERIFY(interceptor.requestInfos.count() >= 3); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 2, 20000); + QVERIFY(interceptor.requestInfos.size() >= 3); QCOMPARE(interceptor.requestInfos.at(2).requestUrl, QUrl("qrc:/non-existent.html")); QCOMPARE(page.requestedUrl(), QUrl("qrc:///resources/__placeholder__")); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); page.setUrl(QUrl("http://abcdef.abcdef")); - QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 3, 15000); - QVERIFY(interceptor.requestInfos.count() >= 4); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 3, 20000); + QVERIFY(interceptor.requestInfos.size() >= 4); QCOMPARE(interceptor.requestInfos.at(3).requestUrl, QUrl("http://abcdef.abcdef/")); QCOMPARE(page.requestedUrl(), QUrl("qrc:///resources/__placeholder__")); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); @@ -377,13 +371,12 @@ void tst_QWebEngineUrlRequestInterceptor::setUrlSameUrl_data() void tst_QWebEngineUrlRequestInterceptor::setUrlSameUrl() { - QFETCH(InterceptorSetter, setter); QFETCH(bool, interceptInPage); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ true); if (!interceptInPage) - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); if (interceptInPage) @@ -393,95 +386,89 @@ void tst_QWebEngineUrlRequestInterceptor::setUrlSameUrl() page.setUrl(QUrl("qrc:///resources/__placeholder__")); QVERIFY(spy.wait()); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); page.setUrl(QUrl("qrc:///resources/__placeholder__")); QVERIFY(spy.wait()); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); - QCOMPARE(spy.count(), 2); + QCOMPARE(spy.size(), 2); // Now a case without redirect. page.setUrl(QUrl("qrc:///resources/content.html")); QVERIFY(spy.wait()); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); - QCOMPARE(spy.count(), 3); + QCOMPARE(spy.size(), 3); page.setUrl(QUrl("qrc:///resources/__placeholder__")); QVERIFY(spy.wait()); QCOMPARE(page.url(), QUrl("qrc:///resources/content.html")); - QCOMPARE(spy.count(), 4); -} - -void tst_QWebEngineUrlRequestInterceptor::firstPartyUrl_data() -{ - interceptRequest_data(); + QCOMPARE(spy.size(), 4); } void tst_QWebEngineUrlRequestInterceptor::firstPartyUrl() { - QFETCH(InterceptorSetter, setter); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); page.setUrl(QUrl("qrc:///resources/firstparty.html")); QVERIFY(spy.wait()); - QVERIFY(interceptor.requestInfos.count() >= 2); + QVERIFY(interceptor.requestInfos.size() >= 2); QCOMPARE(interceptor.requestInfos.at(0).requestUrl, QUrl("qrc:///resources/firstparty.html")); QCOMPARE(interceptor.requestInfos.at(1).requestUrl, QUrl("qrc:///resources/content.html")); QCOMPARE(interceptor.requestInfos.at(0).firstPartyUrl, QUrl("qrc:///resources/firstparty.html")); QCOMPARE(interceptor.requestInfos.at(1).firstPartyUrl, QUrl("qrc:///resources/firstparty.html")); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); } void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlNestedIframes_data() { - QUrl url = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/iframe.html")); - QTest::addColumn<InterceptorSetter>("setter"); + QUrl url = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/iframe.html")); QTest::addColumn<QUrl>("requestUrl"); - QTest::newRow("ui file") << &QWebEngineProfile::setUrlRequestInterceptor << url; - QTest::newRow("io file") << &QWebEngineProfile::setRequestInterceptor << url; - QTest::newRow("ui qrc") << &QWebEngineProfile::setUrlRequestInterceptor - << QUrl("qrc:///resources/iframe.html"); - QTest::newRow("io qrc") << &QWebEngineProfile::setRequestInterceptor - << QUrl("qrc:///resources/iframe.html"); + QTest::newRow("ui file") << url; + QTest::newRow("ui qrc") << QUrl("qrc:///resources/iframe.html"); } void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlNestedIframes() { - QFETCH(InterceptorSetter, setter); QFETCH(QUrl, requestUrl); - if (requestUrl.scheme() == "file" && !QDir(TESTS_SOURCE_DIR).exists()) - W_QSKIP(QString("This test requires access to resources found in '%1'").arg(TESTS_SOURCE_DIR).toLatin1().constData(), SkipAll); + if (requestUrl.scheme() == "file" + && !QDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()).exists()) + W_QSKIP(QString("This test requires access to resources found in '%1'") + .arg(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()) + .toLatin1() + .constData(), + SkipAll); QString adjustedUrl = requestUrl.adjusted(QUrl::RemoveFilename).toString(); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); page.setUrl(requestUrl); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); - QVERIFY(interceptor.requestInfos.count() >= 1); + QVERIFY(interceptor.requestInfos.size() >= 1); RequestInfo info = interceptor.requestInfos.at(0); QCOMPARE(info.requestUrl, requestUrl); QCOMPARE(info.firstPartyUrl, requestUrl); QCOMPARE(info.resourceType, QWebEngineUrlRequestInfo::ResourceTypeMainFrame); - QVERIFY(interceptor.requestInfos.count() >= 2); + QVERIFY(interceptor.requestInfos.size() >= 2); info = interceptor.requestInfos.at(1); QCOMPARE(info.requestUrl, QUrl(adjustedUrl + "iframe2.html")); QCOMPARE(info.firstPartyUrl, requestUrl); QCOMPARE(info.resourceType, QWebEngineUrlRequestInfo::ResourceTypeSubFrame); - QVERIFY(interceptor.requestInfos.count() >= 3); + QVERIFY(interceptor.requestInfos.size() >= 3); info = interceptor.requestInfos.at(2); QCOMPARE(info.requestUrl, QUrl(adjustedUrl + "iframe3.html")); QCOMPARE(info.firstPartyUrl, requestUrl); @@ -490,84 +477,88 @@ void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlNestedIframes() void tst_QWebEngineUrlRequestInterceptor::requestInterceptorByResourceType_data() { - QUrl firstPartyUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/resource_in_iframe.html")); - QUrl styleRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/style.css")); - QUrl scriptRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/script.js")); - QUrl fontRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/fontawesome.woff")); - QUrl xhrRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/test")); - QUrl imageFirstPartyUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/image_in_iframe.html")); - QUrl imageRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/icons/favicon.png")); - QUrl mediaFirstPartyUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/media_in_iframe.html")); - QUrl mediaRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/media.mp4")); - QUrl faviconFirstPartyUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/favicon.html")); - QUrl faviconRequestUrl = QUrl::fromLocalFile(TESTS_SOURCE_DIR + QLatin1String("qwebengineurlrequestinterceptor/resources/icons/favicon.png")); - - QTest::addColumn<InterceptorSetter>("setter"); + QUrl firstPartyUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/resource_in_iframe.html")); + QUrl styleRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/style.css")); + QUrl scriptRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/script.js")); + QUrl fontRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/fontawesome.woff")); + QUrl xhrRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/test")); + QUrl imageFirstPartyUrl = + QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/image_in_iframe.html")); + QUrl imageRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/icons/favicon.png")); + QUrl mediaFirstPartyUrl = + QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/media_in_iframe.html")); + QUrl mediaRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/media.mp4")); + QUrl faviconFirstPartyUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/favicon.html")); + QUrl faviconRequestUrl = QUrl::fromLocalFile(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + QLatin1String("/resources/icons/favicon.png")); + QTest::addColumn<QUrl>("requestUrl"); QTest::addColumn<QUrl>("firstPartyUrl"); QTest::addColumn<int>("resourceType"); - QStringList name = { "ui", "io" }; - QVector<InterceptorSetter> setters = { &QWebEngineProfile::setUrlRequestInterceptor, - &QWebEngineProfile::setRequestInterceptor }; - for (int i = 0; i < 2; i++) { - QTest::newRow(qPrintable(name[i] + "StyleSheet")) - << setters[i] << styleRequestUrl << firstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeStylesheet); - QTest::newRow(qPrintable(name[i] + "Script")) << setters[i] << scriptRequestUrl << firstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeScript); - QTest::newRow(qPrintable(name[i] + "Image")) << setters[i] << imageRequestUrl << imageFirstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeImage); - QTest::newRow(qPrintable(name[i] + "FontResource")) - << setters[i] << fontRequestUrl << firstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeFontResource); - QTest::newRow(qPrintable(name[i] + "Media")) << setters[i] << mediaRequestUrl << mediaFirstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeMedia); - QTest::newRow(qPrintable(name[i] + "Favicon")) - << setters[i] << faviconRequestUrl << faviconFirstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeFavicon); - QTest::newRow(qPrintable(name[i] + "Xhr")) << setters[i] << xhrRequestUrl << firstPartyUrl - << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeXhr); - } + QTest::newRow("StyleSheet") + << styleRequestUrl << firstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeStylesheet); + QTest::newRow("Script") << scriptRequestUrl << firstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeScript); + QTest::newRow("Image") << imageRequestUrl << imageFirstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeImage); + QTest::newRow("FontResource") + << fontRequestUrl << firstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeFontResource); + QTest::newRow(qPrintable("Media")) << mediaRequestUrl << mediaFirstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeMedia); + QTest::newRow("Favicon") + << faviconRequestUrl << faviconFirstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeFavicon); + QTest::newRow(qPrintable("Xhr")) << xhrRequestUrl << firstPartyUrl + << static_cast<int>(QWebEngineUrlRequestInfo::ResourceTypeXhr); } void tst_QWebEngineUrlRequestInterceptor::requestInterceptorByResourceType() { - if (!QDir(TESTS_SOURCE_DIR).exists()) - W_QSKIP(QString("This test requires access to resources found in '%1'").arg(TESTS_SOURCE_DIR).toLatin1().constData(), SkipAll); - QFETCH(InterceptorSetter, setter); + if (!QDir(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()).exists()) + W_QSKIP(QString("This test requires access to resources found in '%1'") + .arg(QDir(QT_TESTCASE_SOURCEDIR).canonicalPath()) + .toLatin1() + .constData(), + SkipAll); QFETCH(QUrl, requestUrl); QFETCH(QUrl, firstPartyUrl); QFETCH(int, resourceType); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); page.setUrl(firstPartyUrl); - QTRY_COMPARE(loadSpy.count(), 1); + QTRY_COMPARE_WITH_TIMEOUT(loadSpy.size(), 1, 20000); - QTRY_COMPARE(interceptor.getUrlRequestForType(static_cast<QWebEngineUrlRequestInfo::ResourceType>(resourceType)).count(), 1); + QTRY_COMPARE(interceptor.getUrlRequestForType(static_cast<QWebEngineUrlRequestInfo::ResourceType>(resourceType)).size(), 1); QList<RequestInfo> infos = interceptor.getUrlRequestForType(static_cast<QWebEngineUrlRequestInfo::ResourceType>(resourceType)); - QVERIFY(infos.count() >= 1); + QVERIFY(infos.size() >= 1); QCOMPARE(infos.at(0).requestUrl, requestUrl); QCOMPARE(infos.at(0).firstPartyUrl, firstPartyUrl); QCOMPARE(infos.at(0).resourceType, resourceType); } -void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlHttp_data() -{ - interceptRequest_data(); -} - void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlHttp() { - QFETCH(InterceptorSetter, setter); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); @@ -578,12 +569,6 @@ void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlHttp() QList<RequestInfo> infos; - // SubFrame - QTRY_VERIFY(interceptor.hasUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeSubFrame)); - infos = interceptor.getUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeSubFrame); - foreach (auto info, infos) - QCOMPARE(info.firstPartyUrl, firstPartyUrl); - // Stylesheet QTRY_VERIFY(interceptor.hasUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeStylesheet)); infos = interceptor.getUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeStylesheet); @@ -627,56 +612,105 @@ void tst_QWebEngineUrlRequestInterceptor::firstPartyUrlHttp() QCOMPARE(info.firstPartyUrl, firstPartyUrl); } -void tst_QWebEngineUrlRequestInterceptor::passRefererHeader_data() +void tst_QWebEngineUrlRequestInterceptor::headers() { - interceptRequest_data(); + HttpServer httpServer; + httpServer.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources" }); + QVERIFY(httpServer.start()); + QWebEngineProfile profile; + TestRequestInterceptor interceptor(false); + profile.setUrlRequestInterceptor(&interceptor); + + QWebEnginePage page(&profile); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + + QWebEngineHttpRequest request(httpServer.url("/content.html")); + request.setHeader("X-HEADERNAME", "HEADERVALUE"); + page.load(request); + QVERIFY(spy.wait()); + QVERIFY(interceptor.requestInfos.last().headers.contains("X-HEADERNAME")); + QCOMPARE(interceptor.requestInfos.last().headers.value("X-HEADERNAME"), + QByteArray("HEADERVALUE")); + + bool jsFinished = false; + + page.runJavaScript(R"( +var request = new XMLHttpRequest(); +request.open('GET', 'resource.html', /* async = */ false); +request.setRequestHeader('X-FOO', 'BAR'); +request.send(); +)", + [&](const QVariant &) { jsFinished = true; }); + QTRY_VERIFY(jsFinished); + QVERIFY(interceptor.requestInfos.last().headers.contains("X-FOO")); + QCOMPARE(interceptor.requestInfos.last().headers.value("X-FOO"), QByteArray("BAR")); } -void tst_QWebEngineUrlRequestInterceptor::passRefererHeader() +void tst_QWebEngineUrlRequestInterceptor::customHeaders() { - QFETCH(InterceptorSetter, setter); // Create HTTP Server to parse the request. HttpServer httpServer; - - if (!httpServer.start()) - QSKIP("Failed to start http server"); - - bool succeeded = false; - connect(&httpServer, &HttpServer::newRequest, [&succeeded](HttpReqRep *rr) { - const QByteArray headerValue = rr->requestHeader(kHttpHeaderRefererName); - QCOMPARE(headerValue, kHttpHeaderReferrerValue); - succeeded = headerValue == kHttpHeaderReferrerValue; - rr->sendResponse(); - }); + httpServer.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + + "/resources" }); + QVERIFY(httpServer.start()); QWebEngineProfile profile; TestRequestInterceptor interceptor(false); - interceptor.headers.insert(kHttpHeaderRefererName, kHttpHeaderReferrerValue); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); - QWebEngineHttpRequest httpRequest; - QUrl requestUrl = httpServer.url(); - httpRequest.setUrl(requestUrl); - page.load(httpRequest); + interceptor.headers = { + { "referer", "http://somereferrer.com/" }, + { "from", "user@example.com" }, + { "user-agent", "mozilla/5.0 (x11; linux x86_64; rv:12.0) gecko/20100101 firefox/12.0" }, + }; + + QMap<QByteArray, QByteArray> actual, expected; + connect(&httpServer, &HttpServer::newRequest, [&] (HttpReqRep *rr) { + for (auto it = expected.begin(); it != expected.end(); ++it) { + auto headerValue = rr->requestHeader(it.key()); + actual[it.key()] = headerValue; + QCOMPARE(headerValue, it.value()); + } + }); + + auto dumpHeaders = [&] () { + QString s; QDebug d(&s); + for (auto it = expected.begin(); it != expected.end(); ++it) + d << "\n\tHeader:" << it.key() << "| actual:" << actual[it.key()] << "expected:" << it.value(); + return s; + }; + + expected = interceptor.headers; + page.load(httpServer.url("/content.html")); QVERIFY(spy.wait()); - (void) httpServer.stop(); - QVERIFY(succeeded); -} + QVERIFY2(actual == expected, qPrintable(dumpHeaders())); -void tst_QWebEngineUrlRequestInterceptor::initiator_data() -{ - interceptRequest_data(); + // test that custom headers are also applied on redirect + interceptor.shouldRedirect = true; + interceptor.redirectUrl = httpServer.url("/content2.html"); + interceptor.headers = { + { "referer", "http://somereferrer2.com/" }, + { "from", "user2@example.com" }, + { "user-agent", "mozilla/5.0 (compatible; googlebot/2.1; +http://www.google.com/bot.html)" }, + }; + + actual.clear(); + expected = interceptor.headers; + page.triggerAction(QWebEnginePage::Reload); + QVERIFY(spy.wait()); + QVERIFY2(actual == expected, qPrintable(dumpHeaders())); + + (void) httpServer.stop(); } void tst_QWebEngineUrlRequestInterceptor::initiator() { - QFETCH(InterceptorSetter, setter); QWebEngineProfile profile; TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QWebEnginePage page(&profile); QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); @@ -687,12 +721,6 @@ void tst_QWebEngineUrlRequestInterceptor::initiator() QList<RequestInfo> infos; - // SubFrame - QTRY_VERIFY(interceptor.hasUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeSubFrame)); - infos = interceptor.getUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeSubFrame); - foreach (auto info, infos) - QVERIFY(interceptor.requestInitiatorUrls[info.requestUrl].contains(info.initiator)); - // Stylesheet QTRY_VERIFY(interceptor.hasUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeStylesheet)); infos = interceptor.getUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeStylesheet); @@ -736,31 +764,31 @@ void tst_QWebEngineUrlRequestInterceptor::initiator() QVERIFY(interceptor.requestInitiatorUrls[info.requestUrl].contains(info.initiator)); } -void tst_QWebEngineUrlRequestInterceptor::jsServiceWorker_data() -{ - interceptRequest_data(); -} - void tst_QWebEngineUrlRequestInterceptor::jsServiceWorker() { - QFETCH(InterceptorSetter, setter); HttpServer server; - server.setResourceDirs({ TESTS_SOURCE_DIR "qwebengineurlrequestinterceptor/resources" }); + server.setResourceDirs({ QDir(QT_TESTCASE_SOURCEDIR).canonicalPath() + "/resources" }); QVERIFY(server.start()); - - QWebEngineProfile profile(QStringLiteral("Test")); + QWebEngineProfile profile; std::unique_ptr<ConsolePage> page; page.reset(new ConsolePage(&profile)); TestRequestInterceptor interceptor(/* intercept */ false); - (profile.*setter)(&interceptor); + profile.setUrlRequestInterceptor(&interceptor); QVERIFY(loadSync(page.get(), server.url("/sw.html"))); // We expect only one message here, because logging of services workers is not exposed in our API. - QTRY_COMPARE(page->messages.count(), 1); - QCOMPARE(page->levels.at(0), QWebEnginePage::InfoMessageLevel); + // Note this is very fragile setup , you need fresh profile otherwise install event might not get triggered + // and this in turn can lead to incorrect intercepted requests, therefore we should keep this off the record. + QTRY_COMPARE_WITH_TIMEOUT(page->messages.size(), 5, 20000); - QUrl firstPartyUrl = QUrl(server.url().toString(QUrl::RemovePort)); + QCOMPARE(page->levels.at(0), QWebEnginePage::InfoMessageLevel); + QCOMPARE(page->messages.at(0),QLatin1String("Service worker installing")); + QCOMPARE(page->messages.at(1),QLatin1String("Service worker installed")); + QCOMPARE(page->messages.at(2),QLatin1String("Service worker activating")); + QCOMPARE(page->messages.at(3),QLatin1String("Service worker activated")); + QCOMPARE(page->messages.at(4),QLatin1String("Service worker done")); + QUrl firstPartyUrl = QUrl(server.url().toString() + "sw.html"); QList<RequestInfo> infos; // Service Worker QTRY_VERIFY(interceptor.hasUrlRequestForType(QWebEngineUrlRequestInfo::ResourceTypeServiceWorker)); @@ -771,5 +799,324 @@ void tst_QWebEngineUrlRequestInterceptor::jsServiceWorker() QVERIFY(server.stop()); } +void tst_QWebEngineUrlRequestInterceptor::replaceInterceptor_data() +{ + QTest::addColumn<bool>("firstInterceptIsInPage"); + QTest::addColumn<bool>("keepInterceptionPoint"); + QTest::newRow("page") << true << true; + QTest::newRow("page-profile") << true << false; + QTest::newRow("profile") << false << true; + QTest::newRow("profile-page") << false << false; +} + +void tst_QWebEngineUrlRequestInterceptor::replaceInterceptor() +{ + QFETCH(bool, firstInterceptIsInPage); + QFETCH(bool, keepInterceptionPoint); + + HttpServer server; + server.setResourceDirs({ ":/resources" }); + QVERIFY(server.start()); + + QWebEngineProfile profile; + profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + QWebEnginePage page(&profile); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + bool fetchFinished = false; + + auto setInterceptor = [&] (QWebEngineUrlRequestInterceptor *interceptor, bool interceptInPage) { + interceptInPage ? page.setUrlRequestInterceptor(interceptor) : profile.setUrlRequestInterceptor(interceptor); + }; + + std::vector<TestRequestInterceptor> interceptors(3); + std::vector<int> requestsOnReplace; + setInterceptor(&interceptors.front(), firstInterceptIsInPage); + + auto sc = connect(&page, &QWebEnginePage::loadFinished, [&] () { + auto currentInterceptorIndex = requestsOnReplace.size(); + requestsOnReplace.push_back(interceptors[currentInterceptorIndex].requestInfos.size()); + + bool isFirstReinstall = currentInterceptorIndex == 0; + bool interceptInPage = keepInterceptionPoint ? firstInterceptIsInPage : (isFirstReinstall ^ firstInterceptIsInPage); + setInterceptor(&interceptors[++currentInterceptorIndex], interceptInPage); + if (!keepInterceptionPoint) + setInterceptor(nullptr, !interceptInPage); + + if (isFirstReinstall) { + page.triggerAction(QWebEnginePage::Reload); + } else { + page.runJavaScript("fetch('http://nonexistent.invalid').catch(() => {})", [&, interceptInPage] (const QVariant &) { + requestsOnReplace.push_back(interceptors.back().requestInfos.size()); + setInterceptor(nullptr, interceptInPage); + fetchFinished = true; + }); + } + }); + + page.setUrl(server.url("/favicon.html")); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 2, 20000); + QTRY_VERIFY(fetchFinished); + + QString s; QDebug d(&s); + for (auto i = 0u; i < interceptors.size(); ++i) { + auto &&interceptor = interceptors[i]; + auto &&requests = interceptor.requestInfos; + d << "\nInterceptor [" << i << "] with" << requestsOnReplace[i] << "requests on replace and" << requests.size() << "in the end:"; + for (int j = 0; j < requests.size(); ++j) { + auto &&r = requests[j]; + d << "\n\t" << j << "| url:" << r.requestUrl << "firstPartyUrl:" << r.firstPartyUrl; + } + QVERIFY2(!requests.isEmpty(), qPrintable(s)); + QVERIFY2(requests.size() == requestsOnReplace[i], qPrintable(s)); + } +} + +void tst_QWebEngineUrlRequestInterceptor::replaceOnIntercept() +{ + HttpServer server; + server.setResourceDirs({ ":/resources" }); + QVERIFY(server.start()); + + QWebEngineProfile profile; + profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + QWebEnginePage page(&profile); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + + struct Interceptor : QWebEngineUrlRequestInterceptor { + Interceptor(const std::function<void ()> &a) : action(a) { } + void interceptRequest(QWebEngineUrlRequestInfo &) override { action(); } + std::function<void ()> action; + int interceptRequestReceived = 0; + }; + + TestRequestInterceptor profileInterceptor, pageInterceptor1, pageInterceptor2; + page.setUrlRequestInterceptor(&pageInterceptor1); + profile.setUrlRequestInterceptor(&profileInterceptor); + profileInterceptor.onIntercept = [&] (QWebEngineUrlRequestInfo &) { + page.setUrlRequestInterceptor(&pageInterceptor2); + return true; + }; + + page.setUrl(server.url("/favicon.html")); + QTRY_COMPARE_WITH_TIMEOUT(spy.size(), 1, 20000); + QTRY_COMPARE(profileInterceptor.requestInfos.size(), 2); + + // if interceptor for page was replaced on intercept call in profile then, since request first + // comes to profile, forward to page's interceptor should land to second one + QCOMPARE(pageInterceptor1.requestInfos.size(), 0); + QCOMPARE(profileInterceptor.requestInfos.size(), pageInterceptor2.requestInfos.size()); + + page.setUrlRequestInterceptor(&pageInterceptor1); + bool fetchFinished = false; + page.runJavaScript("fetch('http://nonexistent.invalid').catch(() => {})", [&] (const QVariant &) { + page.setUrlRequestInterceptor(&pageInterceptor2); + fetchFinished = true; + }); + + QTRY_VERIFY(fetchFinished); + QCOMPARE(profileInterceptor.requestInfos.size(), 3); + QCOMPARE(pageInterceptor1.requestInfos.size(), 0); + QCOMPARE(profileInterceptor.requestInfos.size(), pageInterceptor2.requestInfos.size()); +} + +void tst_QWebEngineUrlRequestInterceptor::multipleRedirects() +{ + HttpServer server; + server.setResourceDirs({ ":/resources" }); + QVERIFY(server.start()); + + TestMultipleRedirectsInterceptor multiInterceptor; + multiInterceptor.redirectPairs.insert(QUrl(server.url("/content.html")), QUrl(server.url("/content2.html"))); + multiInterceptor.redirectPairs.insert(QUrl(server.url("/content2.html")), QUrl(server.url("/content3.html"))); + + QWebEngineProfile profile; + profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + profile.setUrlRequestInterceptor(&multiInterceptor); + QWebEnginePage page(&profile); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + + page.setUrl(server.url("/content.html")); + + QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 20000); + QTRY_COMPARE(multiInterceptor.redirectCount, 2); + QTRY_COMPARE(multiInterceptor.requestInfos.size(), 2); +} + +class TestPostRequestInterceptor : public QWebEngineUrlRequestInterceptor +{ +public: + TestPostRequestInterceptor(QString expected, bool isAppendFile, QObject *parent = nullptr) + : QWebEngineUrlRequestInterceptor(parent) + , m_expected(expected) + , m_isAppendFile(isAppendFile) + {}; + + void interceptRequest(QWebEngineUrlRequestInfo &info) override + { + info.block(true); + isCalled = true; + + QIODevice *requestBodyDevice = info.requestBody(); + + if (m_isAppendFile) { + info.d_ptr->appendFileToResourceRequestBodyForTest(":/resources/postBodyFile.txt"); + } + + requestBodyDevice->open(QIODevice::ReadOnly); + + const QString webKitBoundary = requestBodyDevice->read(40); + QVERIFY(webKitBoundary.contains("------WebKitFormBoundary")); + + const QString fullBodyWithoutBoundaries = (webKitBoundary + requestBodyDevice->readAll()) + .replace(webKitBoundary, "") + .replace("\r", "") + .replace("\n", "") + .replace(" ", ""); + + QCOMPARE(fullBodyWithoutBoundaries, m_expected); + + requestBodyDevice->close(); + } + + bool isCalled = false; + QString m_expected; + bool m_isAppendFile; +}; + +void tst_QWebEngineUrlRequestInterceptor::postWithBody_data() +{ + QTest::addColumn<QString>("input"); + QTest::addColumn<QString>("output"); + QTest::addColumn<bool>("isAppendFile"); + QTest::addRow("FormData append (DataElementByte)") + << "fd.append('userId', 1);" + "fd.append('title',' Test123');" + "fd.append('completed', false);" + << "Content-Disposition:form-data;name=\"userId" + "\"1Content-Disposition:form-data" + ";name=\"title\"Test123Content-Di" + "sposition:form-data;name=\"completed\"f" + "alse--" + << false; + QTest::addRow("FormData blob (DataElementPipe)") + << "const blob1 = new Blob(['blob1thisisablob']," + "{type: 'text/plain'});" + "fd.append('blob1', blob1);" + << "Content-Disposition:form-data;name=\"blob1" + "\";filename=\"blob\"Content-Type:text/plai" + "nblob1thisisablob--" + << false; + QTest::addRow("Append file (DataElementFile)") << "" + << "--{\"test\":\"1234\"}\"1234\"}" << true; + QTest::addRow("All combined") << "fd.append('userId', 1);" + "fd.append('title', 'Test123');" + "fd.append('completed', false);" + "const blob1 = new Blob(['blob1thisisablob']," + "{type: 'text/plain'});" + "const blob2 = new Blob(['blob2thisisanotherblob']," + "{type: 'text/plain'});" + "fd.append('blob1', blob1);" + "fd.append('userId', 2);" + "fd.append('title', 'Test456');" + "fd.append('completed', true);" + "fd.append('blob2', blob2);" + << "Content-Disposition:form-data;name=\"userId\"" + "1Content-Disposition:form-data;na" + "me=\"title\"Test123Content-Disposit" + "ion:form-data;name=\"completed\"false" + "Content-Disposition:form-data;name=\"blob1\";" + "filename=\"blob\"Content-Type:text/plain" + "blob1thisisablobContent-Disposition:form-" + "data;name=\"userId\"2Content-Dispos" + "ition:form-data;name=\"title\"Test456" + "Content-Disposition:form-data;name=\"complete" + "d\"trueContent-Disposition:form-da" + "ta;name=\"blob2\";filename=\"blob\"Content-Ty" + "pe:text/plainblob2thisisanotherblob--" + "{\"test\":\"1234\"}\"1234\"}" + << true; +} + +void tst_QWebEngineUrlRequestInterceptor::postWithBody() +{ + QFETCH(QString, input); + QFETCH(QString, output); + QFETCH(bool, isAppendFile); + + QString script; + script.append("const fd = new FormData();"); + script.append(input); + script.append("fetch('http://127.0.0.1', {method: 'POST',body: fd});"); + + QWebEngineProfile profile; + profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + TestPostRequestInterceptor interceptor(output, isAppendFile); + profile.setUrlRequestInterceptor(&interceptor); + QWebEnginePage page(&profile); + bool ok = false; + + page.runJavaScript(script, [&ok](const QVariant) { ok = true; }); + + QTRY_VERIFY(ok); + QVERIFY(interceptor.isCalled); +} + +class PageOrProfileInterceptor : public QWebEngineUrlRequestInterceptor +{ +public: + PageOrProfileInterceptor(const QString &profileAction) + : profileAction(profileAction) + { + } + + void interceptRequest(QWebEngineUrlRequestInfo &info) override + { + if (profileAction == "block") + info.block(true); + else if (profileAction == "redirect") + info.redirect(QUrl("data:text/html,<p>redirected")); + else if (profileAction == "add header") + info.setHttpHeader("Custom-Header", "Value"); + else + QVERIFY(info.httpHeaders().contains("Custom-Header")); + ran = true; + } + + QString profileAction; + bool ran = false; +}; + +void tst_QWebEngineUrlRequestInterceptor::profilePreventsPageInterception_data() +{ + QTest::addColumn<QString>("profileAction"); + QTest::addColumn<bool>("interceptInProfile"); + QTest::addColumn<bool>("interceptInPage"); + QTest::newRow("block") << "block" << true << false; + QTest::newRow("redirect") << "redirect" << true << false; + QTest::newRow("add header") << "add header" << true << true; +} + +void tst_QWebEngineUrlRequestInterceptor::profilePreventsPageInterception() +{ + QFETCH(QString, profileAction); + QFETCH(bool, interceptInProfile); + QFETCH(bool, interceptInPage); + + QWebEngineProfile profile; + PageOrProfileInterceptor profileInterceptor(profileAction); + profile.setUrlRequestInterceptor(&profileInterceptor); + profile.settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + + QWebEnginePage page(&profile); + PageOrProfileInterceptor pageInterceptor(""); + page.setUrlRequestInterceptor(&pageInterceptor); + QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); + + page.load(QUrl("qrc:///resources/index.html")); + QTRY_COMPARE(loadSpy.size(), 1); + QCOMPARE(profileInterceptor.ran, interceptInProfile); + QCOMPARE(pageInterceptor.ran, interceptInPage); +} + QTEST_MAIN(tst_QWebEngineUrlRequestInterceptor) #include "tst_qwebengineurlrequestinterceptor.moc" diff --git a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.qrc b/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.qrc deleted file mode 100644 index 6a34635f7..000000000 --- a/tests/auto/core/qwebengineurlrequestinterceptor/tst_qwebengineurlrequestinterceptor.qrc +++ /dev/null @@ -1,24 +0,0 @@ -<!DOCTYPE RCC><RCC version="1.0"> - <qresource prefix="/"> - <file>resources/content.html</file> - <file>resources/favicon.html</file> - <file>resources/firstparty.html</file> - <file>resources/fontawesome.woff</file> - <file>resources/iframe.html</file> - <file>resources/iframe2.html</file> - <file>resources/iframe3.html</file> - <file>resources/image.html</file> - <file>resources/image_in_iframe.html</file> - <file>resources/index.html</file> - <file>resources/media.html</file> - <file>resources/media.mp4</file> - <file>resources/media_in_iframe.html</file> - <file>resources/resource.html</file> - <file>resources/resource_in_iframe.html</file> - <file>resources/script.js</file> - <file>resources/style.css</file> - <file>resources/sw.html</file> - <file>resources/sw.js</file> - <file>resources/icons/favicon.png</file> - </qresource> -</RCC> diff --git a/tests/auto/core/qwebengineurlrequestjob/CMakeLists.txt b/tests/auto/core/qwebengineurlrequestjob/CMakeLists.txt new file mode 100644 index 000000000..02b668313 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestjob/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qwebengineurlrequestjob + SOURCES + tst_qwebengineurlrequestjob.cpp + LIBRARIES + Qt::WebEngineCore +) + +# Resources: +set(tst_qwebengineurlrequestjob_resource_files + "additionalResponseHeadersScript.html" + "requestBodyScript.html" +) + +qt_add_resources(tst_qwebengineurlrequestjob "tst_qwebengineurlrequestjob" + PREFIX + "/" + FILES + ${tst_qwebengineurlrequestjob_resource_files} +) diff --git a/tests/auto/core/qwebengineurlrequestjob/additionalResponseHeadersScript.html b/tests/auto/core/qwebengineurlrequestjob/additionalResponseHeadersScript.html new file mode 100644 index 000000000..a1b8a92d3 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestjob/additionalResponseHeadersScript.html @@ -0,0 +1,12 @@ +<!doctype html> +<html> +<body> + <script type="text/javascript"> + var request = new XMLHttpRequest(); + request.open('GET', 'additionalresponseheadershandler:about', /* async = */ false); + request.send(); + var headers = request.getAllResponseHeaders(); + console.log("TST_ADDITIONALRESPONSEHEADERS;" + headers); + </script> +</body> +</html> diff --git a/tests/auto/core/qwebengineurlrequestjob/requestBodyScript.html b/tests/auto/core/qwebengineurlrequestjob/requestBodyScript.html new file mode 100644 index 000000000..95b7ff1bf --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestjob/requestBodyScript.html @@ -0,0 +1,12 @@ +<!doctype html> +<html> +<body> + <script type="text/javascript"> + var request = new XMLHttpRequest(); + request.open('POST', 'requestbodyhandler:about', /* async = */ false); + request.setRequestHeader('Content-Type', 'text/plain'); + request.send('reading request body successful'); + console.log("TST_REQUESTBODY;" + request.response); + </script> +</body> +</html> diff --git a/tests/auto/core/qwebengineurlrequestjob/tst_qwebengineurlrequestjob.cpp b/tests/auto/core/qwebengineurlrequestjob/tst_qwebengineurlrequestjob.cpp new file mode 100644 index 000000000..d48da2c44 --- /dev/null +++ b/tests/auto/core/qwebengineurlrequestjob/tst_qwebengineurlrequestjob.cpp @@ -0,0 +1,187 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> +#include <QtWebEngineCore/qwebengineurlschemehandler.h> +#include <QtWebEngineCore/qwebengineurlscheme.h> +#include <QtWebEngineCore/qwebengineurlrequestjob.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineCore/qwebenginepage.h> + +class CustomPage : public QWebEnginePage +{ + Q_OBJECT + +public: + CustomPage(QWebEngineProfile *profile, QString compareStringPrefix, QString compareStringSuffix, + QObject *parent = nullptr) + : QWebEnginePage(profile, parent) + , m_compareStringPrefix(compareStringPrefix) + , m_compareStringSuffix(compareStringSuffix) + , m_comparedMessageCount(0) + { + } + + int comparedMessageCount() const { return m_comparedMessageCount; } + +signals: + void receivedMessage(); + +protected: + void javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, + const QString &message, int lineNumber, + const QString &sourceID) override + { + Q_UNUSED(level); + Q_UNUSED(lineNumber); + Q_UNUSED(sourceID); + + auto splitMessage = message.split(";"); + if (splitMessage[0] == m_compareStringPrefix) { + QCOMPARE(splitMessage[1], m_compareStringSuffix); + m_comparedMessageCount++; + emit receivedMessage(); + } + } + +private: + QString m_compareStringPrefix; + QString m_compareStringSuffix; + int m_comparedMessageCount; +}; + +class AdditionalResponseHeadersHandler : public QWebEngineUrlSchemeHandler +{ + Q_OBJECT +public: + AdditionalResponseHeadersHandler(bool addAdditionalResponseHeaders, QObject *parent = nullptr) + : QWebEngineUrlSchemeHandler(parent) + , m_addAdditionalResponseHeaders(addAdditionalResponseHeaders) + { + } + + void requestStarted(QWebEngineUrlRequestJob *job) override + { + QCOMPARE(job->requestUrl(), QUrl(schemeName + ":about")); + + QMultiMap<QByteArray, QByteArray> additionalResponseHeaders; + if (m_addAdditionalResponseHeaders) { + additionalResponseHeaders.insert(QByteArray::fromStdString("test1"), + QByteArray::fromStdString("test1VALUE")); + additionalResponseHeaders.insert(QByteArray::fromStdString("test2"), + QByteArray::fromStdString("test2VALUE")); + } + job->setAdditionalResponseHeaders(additionalResponseHeaders); + + QFile *file = new QFile(QStringLiteral(":additionalResponseHeadersScript.html"), job); + file->open(QIODevice::ReadOnly); + + job->reply(QByteArrayLiteral("text/html"), file); + } + + static void registerUrlScheme() + { + QWebEngineUrlScheme webUiScheme(schemeName); + webUiScheme.setFlags(QWebEngineUrlScheme::CorsEnabled); + QWebEngineUrlScheme::registerScheme(webUiScheme); + } + + const static inline QByteArray schemeName = + QByteArrayLiteral("additionalresponseheadershandler"); + +private: + bool m_addAdditionalResponseHeaders; +}; + +class RequestBodyHandler : public QWebEngineUrlSchemeHandler +{ + Q_OBJECT +public: + void requestStarted(QWebEngineUrlRequestJob *job) override + { + QCOMPARE(job->requestUrl(), QUrl(schemeName + ":about")); + QCOMPARE(job->requestMethod(), QByteArrayLiteral("POST")); + + QIODevice *requestBodyDevice = job->requestBody(); + requestBodyDevice->open(QIODevice::ReadOnly); + QByteArray requestBody = requestBodyDevice->readAll(); + requestBodyDevice->close(); + + QBuffer *buf = new QBuffer(job); + buf->open(QBuffer::ReadWrite); + buf->write(requestBody); + job->reply(QByteArrayLiteral("text/plain"), buf); + buf->close(); + } + + static void registerUrlScheme() + { + QWebEngineUrlScheme webUiScheme(schemeName); + webUiScheme.setFlags(QWebEngineUrlScheme::CorsEnabled + | QWebEngineUrlScheme::FetchApiAllowed); + QWebEngineUrlScheme::registerScheme(webUiScheme); + } + + const static inline QByteArray schemeName = QByteArrayLiteral("requestbodyhandler"); +}; + +class tst_QWebEngineUrlRequestJob : public QObject +{ + Q_OBJECT + +public: + tst_QWebEngineUrlRequestJob() { } + +private Q_SLOTS: + void initTestCase() + { + AdditionalResponseHeadersHandler::registerUrlScheme(); + RequestBodyHandler::registerUrlScheme(); + } + + void withAdditionalResponseHeaders_data() + { + QTest::addColumn<bool>("withHeaders"); + QTest::addColumn<QString>("expectedHeaders"); + QTest::newRow("headers enabled") + << true << "content-type: text/html\r\ntest1: test1value\r\ntest2: test2value\r\n"; + QTest::newRow("headers disabled") << false << "content-type: text/html\r\n"; + } + + void withAdditionalResponseHeaders() + { + QFETCH(bool, withHeaders); + QFETCH(QString, expectedHeaders); + + QWebEngineProfile profile; + + AdditionalResponseHeadersHandler handler(withHeaders); + profile.installUrlSchemeHandler(AdditionalResponseHeadersHandler::schemeName, &handler); + + CustomPage page(&profile, "TST_ADDITIONALRESPONSEHEADERS", expectedHeaders); + QSignalSpy spy(&page, SIGNAL(loadFinished(bool))); + + page.load(QUrl("qrc:///additionalResponseHeadersScript.html")); + QVERIFY(spy.wait()); + QCOMPARE(page.comparedMessageCount(), 1); + } + + void requestBody() + { + QWebEngineProfile profile; + + RequestBodyHandler handler; + profile.installUrlSchemeHandler(RequestBodyHandler::schemeName, &handler); + + const QString expected = "reading request body successful"; + CustomPage page(&profile, "TST_REQUESTBODY", expected); + QSignalSpy spy(&page, SIGNAL(receivedMessage())); + + page.load(QUrl("qrc:///requestBodyScript.html")); + QVERIFY(spy.wait()); + QCOMPARE(page.comparedMessageCount(), 1); + } +}; + +QTEST_MAIN(tst_QWebEngineUrlRequestJob) +#include "tst_qwebengineurlrequestjob.moc" diff --git a/tests/auto/core/tests.pri b/tests/auto/core/tests.pri deleted file mode 100644 index 59d6c0865..000000000 --- a/tests/auto/core/tests.pri +++ /dev/null @@ -1,17 +0,0 @@ -TEMPLATE = app - -CONFIG += testcase - -VPATH += $$_PRO_FILE_PWD_ -TARGET = tst_$$TARGET - -SOURCES += $${TARGET}.cpp -INCLUDEPATH += $$PWD - -exists($$_PRO_FILE_PWD_/$${TARGET}.qrc): RESOURCES += $${TARGET}.qrc - -QT += testlib network webenginewidgets widgets - -# This define is used by some tests to look up resources in the source tree -DEFINES += TESTS_SOURCE_DIR=\\\"$$PWD/\\\" -include(../embed_info_plist.pri) diff --git a/tests/auto/core/webenginedriver/CMakeLists.txt b/tests/auto/core/webenginedriver/CMakeLists.txt new file mode 100644 index 000000000..c8cf8b3ab --- /dev/null +++ b/tests/auto/core/webenginedriver/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_webenginedriver LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +add_subdirectory(test) +add_subdirectory(browser) + +add_dependencies(tst_webenginedriver + testbrowser +) diff --git a/tests/auto/core/webenginedriver/browser/CMakeLists.txt b/tests/auto/core/webenginedriver/browser/CMakeLists.txt new file mode 100644 index 000000000..25e162e7b --- /dev/null +++ b/tests/auto/core/webenginedriver/browser/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_executable(testbrowser + SOURCES + main.cpp + LIBRARIES + Qt::Core + Qt::Gui + Qt::WebEngineWidgets + OUTPUT_DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/.. +) diff --git a/tests/auto/core/webenginedriver/browser/main.cpp b/tests/auto/core/webenginedriver/browser/main.cpp new file mode 100644 index 000000000..4b8f3513f --- /dev/null +++ b/tests/auto/core/webenginedriver/browser/main.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineWidgets/qwebengineview.h> +#include <QtWidgets/qapplication.h> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QWebEngineView view; + QObject::connect(view.page(), &QWebEnginePage::windowCloseRequested, &app, &QApplication::quit); + QObject::connect(&app, &QApplication::aboutToQuit, + []() { fprintf(stderr, "Test browser is about to quit.\n"); }); + + view.resize(100, 100); + view.show(); + + return app.exec(); +} diff --git a/tests/auto/core/webenginedriver/resources/input.html b/tests/auto/core/webenginedriver/resources/input.html new file mode 100644 index 000000000..c21458350 --- /dev/null +++ b/tests/auto/core/webenginedriver/resources/input.html @@ -0,0 +1,5 @@ +<html> +<body> + <input type="text" id="text_input"> +</body> +</html> diff --git a/tests/auto/core/webenginedriver/test/CMakeLists.txt b/tests/auto/core/webenginedriver/test/CMakeLists.txt new file mode 100644 index 000000000..041bf955b --- /dev/null +++ b/tests/auto/core/webenginedriver/test/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +include(../../../util/util.cmake) + +qt_internal_add_test(tst_webenginedriver + SOURCES + ../tst_webenginedriver.cpp + LIBRARIES + Qt::Network + Qt::WebEngineCore + Qt::WebEngineWidgets + Test::Util + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.." +) + +set(tst_webenginedriver_resource_files + "../resources/input.html" +) + +qt_internal_add_resource(tst_webenginedriver "tst_webenginedriver" + PREFIX + "/" + FILES + ${tst_webenginedriver_resource_files} +) diff --git a/tests/auto/core/webenginedriver/tst_webenginedriver.cpp b/tests/auto/core/webenginedriver/tst_webenginedriver.cpp new file mode 100644 index 000000000..cd3098b25 --- /dev/null +++ b/tests/auto/core/webenginedriver/tst_webenginedriver.cpp @@ -0,0 +1,631 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtTest/QtTest> + +#include <QtCore/qjsondocument.h> +#include <QtCore/qlibraryinfo.h> +#include <QtCore/qobject.h> +#include <QtCore/qprocess.h> +#include <QtGui/qimage.h> +#include <QtNetwork/qnetworkaccessmanager.h> +#include <QtNetwork/qnetworkreply.h> +#include <QtNetwork/qnetworkrequest.h> +#include <QtWebEngineCore/qtwebenginecoreglobal.h> +#include <QtWebEngineCore/qwebenginepage.h> +#include <QtWebEngineCore/qwebengineprofile.h> +#include <QtWebEngineWidgets/qwebengineview.h> + +#include <widgetutil.h> + +#define REMOTE_DEBUGGING_PORT 12345 +#define WEBENGINEDRIVER_PORT 9515 + +class DriverServer : public QObject +{ + Q_OBJECT + +public: + DriverServer(QProcessEnvironment processEnvironment = {}, QStringList processArguments = {}) + : m_serverURL(QUrl( + QLatin1String("http://localhost:%1").arg(QString::number(WEBENGINEDRIVER_PORT)))) + { + QString driverPath = QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath) + + QLatin1String("/webenginedriver"); +#if defined(Q_OS_WIN) + driverPath += QLatin1String(".exe"); +#endif + m_process.setProgram(driverPath); + + if (processArguments.isEmpty()) + processArguments + << QLatin1String("--port=%1").arg(QString::number(WEBENGINEDRIVER_PORT)); + m_process.setArguments(processArguments); + + if (!processEnvironment.isEmpty()) + m_process.setProcessEnvironment(processEnvironment); + + connect(&m_process, &QProcess::errorOccurred, [this](QProcess::ProcessError error) { + qWarning() << "WebEngineDriver error occurred:" << error; + dumpConsoleMessages(); + }); + + connect(&m_process, &QProcess::readyReadStandardError, [this]() { + QProcess::ProcessChannel tmp = m_process.readChannel(); + m_process.setReadChannel(QProcess::StandardError); + while (m_process.canReadLine()) { + QString line = QString::fromUtf8(m_process.readLine()); + if (line.endsWith(QLatin1Char('\n'))) + line.truncate(line.size() - 1); + m_stderr << line; + } + m_process.setReadChannel(tmp); + }); + + connect(&m_process, &QProcess::readyReadStandardOutput, [this]() { + QProcess::ProcessChannel tmp = m_process.readChannel(); + m_process.setReadChannel(QProcess::StandardOutput); + while (m_process.canReadLine()) { + QString line = QString::fromUtf8(m_process.readLine()); + if (line.endsWith(QLatin1Char('\n'))) + line.truncate(line.size() - 1); + m_stdout << line; + } + m_process.setReadChannel(tmp); + }); + } + + ~DriverServer() + { + if (m_process.state() != QProcess::Running) + return; + + if (!m_sessionId.isEmpty()) + deleteSession(); + + shutdown(); + } + + bool start() + { + if (!QFileInfo::exists(m_process.program())) { + qWarning() << "WebEngineDriver executable not found:" << m_process.program(); + return false; + } + + connect(&m_process, &QProcess::finished, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + qWarning().nospace() + << "WebEngineDriver exited unexpectedly (exitCode: " << exitCode + << " exitStatus: " << exitStatus << "):"; + dumpConsoleMessages(); + }); + + m_process.start(); + + bool started = m_process.waitForStarted(); + if (!started) { + qWarning() << "Failed to start WebEngineDriver:" << m_process.errorString(); + return false; + } + + bool ready = QTest::qWaitFor( + [this]() { + if (m_process.state() != QProcess::Running) + return true; + + for (QString line : m_stdout) { + if (line.contains( + QLatin1String("WebEngineDriver was started successfully."))) + return true; + } + + return false; + }, + 10000); + + if (ready && m_process.state() != QProcess::Running) { + // Warning is already reported by handler of QProcess::finished() signal. + return false; + } + + if (!ready) { + if (m_stdout.empty()) + qWarning("Starting WebEngineDriver timed out."); + else + qWarning("Something went wrong while starting WebEngineDriver:"); + + dumpConsoleMessages(); + return false; + } + + return true; + } + + bool shutdown() + { + // Do not warn about unexpected exit. + disconnect(&m_process, &QProcess::finished, nullptr, nullptr); + + bool sent = sendCommand(QLatin1String("/shutdown")); + + bool finished = (m_process.state() == QProcess::NotRunning) || m_process.waitForFinished(); + if (!finished || !sent) + qWarning() << "Failed to properly shutdown WebEngineDriver:" << m_process.errorString(); + + return finished; + } + + bool sendCommand(QString command, const QJsonDocument ¶ms = {}, + QJsonDocument *result = nullptr) + { + if (command.contains(QLatin1String(":sessionId"))) { + if (m_sessionId.isEmpty()) { + qWarning("Unable to execute session command without session."); + return false; + } + + QStringList commandList = command.split(QLatin1Char('/')); + for (int i = 0; i < commandList.size(); ++i) { + if (commandList[i] == QLatin1String(":sessionId")) { + commandList[i] = m_sessionId; + break; + } + } + + command = commandList.join(QLatin1Char('/')); + } + + QNetworkReply::NetworkError replyError = QNetworkReply::NoError; + QString replyString; + + connect( + &m_qnam, &QNetworkAccessManager::finished, this, + [&replyError, &replyString](QNetworkReply *reply) { + replyError = reply->error(); + replyString = QString::fromUtf8(reply->readAll()); + }, + static_cast<Qt::ConnectionType>(Qt::SingleShotConnection)); + + QNetworkRequest request; + QUrl requestURL = m_serverURL; + + requestURL.setPath(command); + request.setUrl(requestURL); + + if (params.isEmpty()) { + m_qnam.get(request); + } else { + request.setHeader(QNetworkRequest::ContentTypeHeader, + QVariant::fromValue(QStringLiteral("application/json"))); + m_qnam.post(request, params.toJson(QJsonDocument::Compact)); + } + + bool ready = QTest::qWaitFor( + [&replyError, &replyString]() { + return replyError != QNetworkReply::NoError || !replyString.isEmpty(); + }, + 10000); + + if (!ready) { + qWarning() << "Command" << command << "timed out."; + dumpConsoleMessages(); + return false; + } + + if (replyError != QNetworkReply::NoError) { + qWarning() << "Network error:" << replyError; + if (!replyString.isEmpty()) { + QJsonDocument errorReply = QJsonDocument::fromJson(replyString.toLatin1()); + if (!errorReply.isNull()) { + QString error = errorReply["value"]["error"].toString(); + QString message = errorReply["value"]["message"].toString(); + if (!error.isEmpty() || message.isEmpty()) { + qWarning() << "error:" << error; + qWarning() << "message:" << message; + return false; + } + } + + qWarning() << replyString; + return false; + } + + dumpConsoleMessages(); + return false; + } + + if (result) { + if (replyString.isEmpty()) { + qWarning("Network reply is empty."); + return false; + } + + QJsonParseError jsonError; + *result = QJsonDocument::fromJson(replyString.toLatin1(), &jsonError); + + if (jsonError.error != QJsonParseError::NoError) { + qWarning() << "Unable to parse reply:" << jsonError.errorString(); + return false; + } + } + + return true; + } + + bool createSession(QJsonObject chromeOptions = {}, QString *sessionId = nullptr) + { + if (!m_sessionId.isEmpty()) { + qWarning("A session already exists."); + return false; + } + + QJsonObject root; + + if (chromeOptions.isEmpty()) { + // Connect to the test by default. + chromeOptions.insert( + QLatin1String("debuggerAddress"), + QLatin1String("localhost:%1").arg(QString::number(REMOTE_DEBUGGING_PORT))); + chromeOptions.insert(QLatin1String("w3c"), true); + } + + QJsonObject alwaysMatch; + alwaysMatch.insert(QLatin1String("goog:chromeOptions"), chromeOptions); + + QJsonObject seOptions; + seOptions.insert(QLatin1String("loggingPrefs"), QJsonObject()); + alwaysMatch.insert(QLatin1String("se:options"), seOptions); + + QJsonObject capabilities; + capabilities.insert(QLatin1String("alwaysMatch"), alwaysMatch); + root.insert(QLatin1String("capabilities"), capabilities); + + QJsonDocument params; + params.setObject(root); + + QJsonDocument sessionReply; + bool sent = sendCommand(QLatin1String("/session"), params, &sessionReply); + if (sent) { + m_sessionId = sessionReply["value"]["sessionId"].toString(); + if (sessionId) + *sessionId = m_sessionId; + } + + return sent; + } + + bool deleteSession() + { + if (m_sessionId.isEmpty()) { + qWarning("There is no active session."); + return false; + } + + QNetworkReply::NetworkError replyError = QNetworkReply::NoError; + QString replyString; + + connect( + &m_qnam, &QNetworkAccessManager::finished, this, + [&replyError, &replyString](QNetworkReply *reply) { + replyError = reply->error(); + replyString = QString::fromUtf8(reply->readAll()); + }, + static_cast<Qt::ConnectionType>(Qt::SingleShotConnection)); + + QNetworkRequest request; + QUrl requestURL = m_serverURL; + requestURL.setPath(QLatin1String("/session/%1").arg(m_sessionId)); + request.setUrl(requestURL); + m_qnam.deleteResource(request); + + bool ready = QTest::qWaitFor( + [&replyError, &replyString]() { + return replyError != QNetworkReply::NoError || !replyString.isEmpty(); + }, + 10000); + + if (!ready) { + qWarning("Deleting session timed out."); + return false; + } + + if (replyError != QNetworkReply::NoError) { + qWarning() << "Network error:" << replyError; + return false; + } + + return true; + } + + QStringList stderrLines() const { return m_stderr; } + + bool waitForMessageOnStderr(const QRegularExpression &re, QString *message = nullptr) const + { + return QTest::qWaitFor( + [this, &re, message]() { + for (QString line : m_stderr) { + if (line.contains(re)) { + if (message) + *message = line; + return true; + } + } + + return false; + }, + 10000); + } + +private: + void dumpConsoleMessages() + { + auto dumpLines = [](QStringList *lines) { + if (lines->empty()) + return; + + for (QString line : *lines) + qWarning() << qPrintable(line); + + lines->clear(); + }; + + dumpLines(&m_stdout); + dumpLines(&m_stderr); + } + + QProcess m_process; + QStringList m_stdout; + QStringList m_stderr; + + QUrl m_serverURL; + QString m_sessionId; + + QNetworkAccessManager m_qnam; +}; + +class tst_WebEngineDriver : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void status(); + void startBrowser(); + void navigate(); + void typeElement(); + void executeScript(); + void screenshot(); +}; + +void tst_WebEngineDriver::status() +{ + DriverServer driverServer; + QVERIFY(driverServer.start()); + + QJsonDocument statusReply; + QVERIFY(driverServer.sendCommand(QLatin1String("/status"), {}, &statusReply)); + + QString versionString = statusReply["value"]["build"]["version"].toString(); + QCOMPARE(qWebEngineChromiumVersion(), versionString.split(QLatin1Char(' '))[0]); + + bool ready = statusReply["value"]["ready"].toBool(); + QVERIFY(ready); +} + +void tst_WebEngineDriver::startBrowser() +{ + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QLatin1String("QTWEBENGINE_REMOTE_DEBUGGING"), + QString::number(REMOTE_DEBUGGING_PORT)); + + QStringList args; + args << QLatin1String("--port=%1").arg(QString::number(WEBENGINEDRIVER_PORT)); + args << QLatin1String("--log-level=ALL"); + + DriverServer driverServer(env, args); + QVERIFY(driverServer.start()); + + QString testBrowserPath = + QCoreApplication::applicationDirPath() + QLatin1String("/testbrowser"); +#if defined(Q_OS_WIN) + testBrowserPath += QLatin1String(".exe"); +#endif + + QJsonArray chromeArgs; + chromeArgs.append(QLatin1String("enable-logging=stderr")); + chromeArgs.append(QLatin1String("v=1")); + // To force graceful shutdown. + chromeArgs.append(QLatin1String("log-net-log")); + + QJsonObject chromeOptions; + chromeOptions.insert(QLatin1String("binary"), testBrowserPath); + chromeOptions.insert(QLatin1String("args"), chromeArgs); + chromeOptions.insert(QLatin1String("w3c"), true); + QString sessionId; + QVERIFY(driverServer.createSession(chromeOptions, &sessionId)); + + // Test if the browser is started. + QVERIFY(driverServer.waitForMessageOnStderr(QRegularExpression( + QLatin1String("^DevTools listening on ws://127.0.0.1:%1/devtools/browser/") + .arg(QString::number(REMOTE_DEBUGGING_PORT))))); + QVERIFY(driverServer.waitForMessageOnStderr(QRegularExpression( + QLatin1String("^Remote debugging server started successfully. " + "Try pointing a Chromium-based browser to http://127.0.0.1:%1") + .arg(QString::number(REMOTE_DEBUGGING_PORT))))); + + // Check custom chromeArgs via logging. + QVERIFY(driverServer.waitForMessageOnStderr(QRegularExpression(QLatin1String("VERBOSE1")))); + + QJsonDocument statusReply; + QVERIFY(driverServer.sendCommand(QLatin1String("/status"), {}, &statusReply)); + bool ready = statusReply["value"]["ready"].toBool(); + QVERIFY(ready); + + QVERIFY(driverServer.deleteSession()); + + // Test if the browser is stopped. + QVERIFY(driverServer.waitForMessageOnStderr( + QRegularExpression(QLatin1String("Test browser is about to quit.")))); + QVERIFY(driverServer.waitForMessageOnStderr( + QRegularExpression(QLatin1String("\\[%1\\] RESPONSE Quit").arg(sessionId)))); +} + +void tst_WebEngineDriver::navigate() +{ + DriverServer driverServer; + QVERIFY(driverServer.start()); + + QWebEnginePage page; + QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); + + page.load(QUrl(QLatin1String("about:blank"))); + QTRY_COMPARE(loadSpy.size(), 1); + + QVERIFY(driverServer.createSession()); + + QUrl url = QUrl(QLatin1String("qrc:///resources/input.html")); + QString paramsString = QLatin1String("{\"url\": \"%1\"}").arg(url.toString()); + QJsonDocument params = QJsonDocument::fromJson(paramsString.toUtf8()); + QVERIFY(driverServer.sendCommand(QLatin1String("/session/:sessionId/url"), params)); + QTRY_COMPARE(loadSpy.size(), 2); + QVERIFY(loadSpy.at(1).at(0).toBool()); + QCOMPARE(url, page.url()); + + QVERIFY(driverServer.deleteSession()); +} + +void tst_WebEngineDriver::typeElement() +{ + DriverServer driverServer; + QVERIFY(driverServer.start()); + + QWebEngineView view; + view.resize(300, 100); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + QUrl url = QUrl(QLatin1String("qrc:///resources/input.html")); + QSignalSpy loadSpy(view.page(), &QWebEnginePage::loadFinished); + view.load(url); + QTRY_COMPARE(loadSpy.size(), 1); + + QVERIFY(driverServer.createSession()); + + // Find <input id="text_input"> element and extract its id from the reply. + QString textInputId; + { + QString paramsString = QLatin1String( + "{\"using\": \"css selector\", \"value\": \"[id=\\\"text_input\\\"]\"}"); + QJsonDocument params = QJsonDocument::fromJson(paramsString.toUtf8()); + QJsonDocument elementReply; + QVERIFY(driverServer.sendCommand(QLatin1String("/session/:sessionId/element"), params, + &elementReply)); + + QVERIFY(!elementReply.isEmpty()); + QJsonObject value = elementReply["value"].toObject(); + QVERIFY(!value.isEmpty()); + QStringList keys = value.keys(); + QCOMPARE(keys.size(), 1); + textInputId = value[keys[0]].toString(); + QVERIFY(!textInputId.isEmpty()); + } + + // Type text into the input field. + QString inputText = QLatin1String("WebEngineDriver"); + { + QString command = QLatin1String("/session/:sessionId/element/%1/value").arg(textInputId); + + QJsonObject root; + root.insert(QLatin1String("text"), inputText); + QJsonArray value; + for (QChar ch : inputText) { + value.append(QJsonValue(ch)); + } + root.insert(QLatin1String("value"), value); + root.insert(QLatin1String("id"), textInputId); + QJsonDocument params; + params.setObject(root); + + QVERIFY(driverServer.sendCommand(command, params)); + + QTRY_COMPARE( + evaluateJavaScriptSync(view.page(), "document.getElementById('text_input').value") + .toString(), + inputText); + } + + QVERIFY(driverServer.deleteSession()); +} + +void tst_WebEngineDriver::executeScript() +{ + DriverServer driverServer; + QVERIFY(driverServer.start()); + + QUrl url = QUrl(QLatin1String("qrc:///resources/input.html")); + QWebEnginePage page; + QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished); + + page.load(url); + QTRY_COMPARE(loadSpy.size(), 1); + QCOMPARE(page.title(), url.toString()); + QSignalSpy titleSpy(&page, &QWebEnginePage::titleChanged); + + QString newTitle = QLatin1String("WebEngineDriver Test Page"); + QString script = QLatin1String("document.title = '%1';" + "return document.title;") + .arg(newTitle); + + QVERIFY(driverServer.createSession()); + QString paramsString = QLatin1String("{\"script\": \"%1\", \"args\": []}").arg(script); + QJsonDocument params = QJsonDocument::fromJson(paramsString.toUtf8()); + QJsonDocument executeReply; + QVERIFY(driverServer.sendCommand(QLatin1String("/session/:sessionId/execute/sync"), params, + &executeReply)); + + QTRY_COMPARE(titleSpy.size(), 1); + QCOMPARE(executeReply["value"].toString(), newTitle); + QCOMPARE(page.title(), newTitle); + + QVERIFY(driverServer.deleteSession()); +} + +void tst_WebEngineDriver::screenshot() +{ + DriverServer driverServer; + QVERIFY(driverServer.start()); + + QWebEngineView view; + view.resize(300, 100); + view.show(); + QVERIFY(QTest::qWaitForWindowExposed(&view)); + + QSignalSpy loadSpy(view.page(), &QWebEnginePage::loadFinished); + + view.setHtml(QLatin1String("<html><head><style>" + "html {background-color:red;}" + "</style></head><body></body></html>")); + QTRY_COMPARE(loadSpy.size(), 1); + + QVERIFY(driverServer.createSession()); + QJsonDocument screenshotReply; + QVERIFY(driverServer.sendCommand(QLatin1String("/session/:sessionId/screenshot"), {}, + &screenshotReply)); + + QByteArray base64 = screenshotReply["value"].toString().toLocal8Bit(); + QVERIFY(!base64.isEmpty()); + QImage screenshot; + screenshot.loadFromData(QByteArray::fromBase64(base64)); + QVERIFY(!screenshot.isNull()); + QCOMPARE(screenshot.pixel(screenshot.width() / 2, screenshot.height() / 2), 0xFFFF0000); + + QVERIFY(driverServer.deleteSession()); +} + +#define STRINGIFY_LITERAL(x) #x +#define STRINGIFY_EXPANDED(x) STRINGIFY_LITERAL(x) +static QByteArrayList params = QByteArrayList() + << "--remote-debugging-port=" STRINGIFY_EXPANDED(REMOTE_DEBUGGING_PORT); +W_QTEST_MAIN(tst_WebEngineDriver, params) + +#include "tst_webenginedriver.moc" |