File indexing completed on 2024-05-19 05:01:16

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2018 Stefano Crocco <stefano.crocco@alice.it>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "webenginepartcookiejarkio_test.h"
0009 #include <cookies/webenginepartcookiejar_kio.h>
0010 
0011 #include <QTest>
0012 
0013 #include <QNetworkCookie>
0014 #include <QWebEngineCookieStore>
0015 #include <QWebEngineProfile>
0016 #include <QDBusInterface>
0017 #include <QDBusReply>
0018 
0019 #include <algorithm>
0020     
0021 //Cookie expiration dates returned by KCookieServer always have msecs set to 0
0022 static QDateTime currentDateTime(){return QDateTime::fromSecsSinceEpoch(QDateTime::currentMSecsSinceEpoch()/1000);}
0023 
0024 namespace QTest {
0025     template <>
0026     char *toString(const QNetworkCookie &cookie){
0027         QByteArray ba = "QNetworkCookie{";
0028         ba += "\nname: " + cookie.name();
0029         ba += "\ndomain: " + (cookie.domain().isEmpty() ? "<EMPTY>" : cookie.domain().toUtf8());
0030         ba += "\npath: " + (cookie.path().isEmpty() ? "<EMPTY>" : cookie.path().toUtf8());
0031         ba += "\nvalue: " + cookie.value();
0032         ba += "\nexpiration: " + (cookie.expirationDate().isValid() ? QByteArray::number(cookie.expirationDate().toMSecsSinceEpoch()) : "<INVALID>");
0033         ba += "\nsecure: " + QByteArray::number(cookie.isSecure());
0034         ba += "\nhttp: only" + QByteArray::number(cookie.isHttpOnly());
0035         return qstrdup(ba.data());
0036     }
0037 }
0038 
0039 QTEST_MAIN(TestWebEnginePartCookieJarKIO);
0040 
0041 void TestWebEnginePartCookieJarKIO::initTestCase()
0042 {
0043     m_cookieName = "webenginepartcookiejartest";
0044 }
0045 
0046 void TestWebEnginePartCookieJarKIO::init()
0047 {
0048     m_server = new QDBusInterface("org.kde.kcookiejar5", "/modules/kcookiejar", "org.kde.KCookieServer");
0049     m_profile = new QWebEngineProfile(this);
0050     m_store = m_profile->cookieStore();
0051     m_jar = new WebEnginePartCookieJarKIO(m_profile, this);
0052 }
0053 
0054 void TestWebEnginePartCookieJarKIO::cleanup()
0055 {
0056     if (m_server->isValid()) {
0057         deleteCookies(findTestCookies());
0058     }
0059     delete m_server;
0060     m_server = nullptr;
0061     delete m_jar;
0062     m_jar = nullptr;
0063     m_store = nullptr;
0064     delete m_profile;
0065     m_profile = nullptr;
0066 }
0067 
0068 QNetworkCookie TestWebEnginePartCookieJarKIO::CookieData::cookie() const
0069 {
0070     QNetworkCookie cookie;
0071     cookie.setName(name.toUtf8());
0072     cookie.setValue(value.toUtf8());
0073     cookie.setPath(path);
0074     cookie.setDomain(domain);
0075     cookie.setSecure(secure);
0076     cookie.setExpirationDate(expiration);
0077     return cookie;
0078 }
0079 
0080 void TestWebEnginePartCookieJarKIO::testCookieAddedToStoreAreAddedToKCookieServer_data()
0081 {
0082     QTest::addColumn<QNetworkCookie>("cookie");
0083     QTest::addColumn<QString>("name");
0084     QTest::addColumn<QString>("value");
0085     QTest::addColumn<QString>("domain");
0086     QTest::addColumn<QString>("path");
0087     QTest::addColumn<QString>("host");
0088     QTest::addColumn<QDateTime>("expiration");
0089     QTest::addColumn<bool>("secure");
0090     
0091     const QStringList labels{
0092         "persistent cookie with domain and path",
0093         "session cookie with domain and path",
0094         "persistent cookie with domain and no path",
0095         "persistent cookie with domain and / as path",
0096         "persistent cookie with path and no domain",
0097         "persistent cookie without secure",
0098     };
0099     
0100     const QList<CookieData> input{
0101         {m_cookieName + "-persistent", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0102         {m_cookieName + "-session", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime(), true},
0103         {m_cookieName + "-no-path", "test-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0104         {m_cookieName + "-slash-as-path", "test-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0105         {m_cookieName + "-no-domain", "test-value", "", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0106         {m_cookieName + "-no-secure", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), false}
0107     };
0108     
0109     QList<CookieData> expected(input);
0110     
0111     for (int i = 0; i < input.count(); ++i) {
0112         const CookieData &ex = expected.at(i);
0113         const CookieData &in = input.at(i);
0114         QNetworkCookie c = in.cookie();
0115         if (in.domain.isEmpty()) {
0116             c.normalize(QUrl("https://" + in.host));
0117         }
0118         QTest::newRow(labels.at(i).toLatin1().constData()) << c << ex.name << ex.value << ex.domain << ex.path << ex.host << ex.expiration << ex.secure;
0119     }
0120 }
0121 
0122 void TestWebEnginePartCookieJarKIO::testCookieAddedToStoreAreAddedToKCookieServer()
0123 {
0124     QFETCH(const QNetworkCookie, cookie);
0125     QFETCH(const QString, name);
0126     QFETCH(const QString, value);
0127     QFETCH(const QString, domain);
0128     QFETCH(const QString, path);
0129     QFETCH(const QString, host);
0130     QFETCH(const QDateTime, expiration);
0131     QFETCH(const bool, secure);
0132     
0133     QVERIFY2(m_server->isValid(), qPrintable(m_server->lastError().message()));
0134     
0135 //     domain=0, path=1, name=2, host=3, value=4, expirationDate=5, protocolVersion=6, secure=7;
0136     const QList<int> fields{0,1,2,3,4,5,6,7};
0137     
0138     emit m_store->cookieAdded(cookie);
0139     const QDBusReply<QStringList> res = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(fields), domain, host, cookie.path(), QString(cookie.name()));
0140     QVERIFY2(!m_server->lastError().isValid(), m_server->lastError().message().toLatin1().constData());
0141     QStringList resFields = res.value();
0142     
0143     QVERIFY(!resFields.isEmpty());
0144     QCOMPARE(fields.count(), resFields.count());
0145     
0146     QCOMPARE(resFields.at(0), domain);
0147     QCOMPARE(resFields.at(1), path);
0148     QCOMPARE(resFields.at(2), name);
0149     if (!domain.isEmpty()){
0150         QEXPECT_FAIL("", "The value returned by KCookieServer strips the leftmost part of the fqdn. Why?", Continue);
0151     }
0152     QCOMPARE(resFields.at(3), host);
0153     QCOMPARE(resFields.at(4), value);
0154     const int secsSinceEpoch = resFields.at(5).toInt();
0155     //KCookieServer gives a session cookie an expiration time equal to epoch, while QNetworkCookie uses an invalid QDateTime
0156     if (!expiration.isValid()) {
0157         QCOMPARE(secsSinceEpoch, 0);
0158     } else {
0159         QCOMPARE(secsSinceEpoch, expiration.toSecsSinceEpoch());
0160     }
0161     const bool sec = resFields.at(7).toInt();
0162     QCOMPARE(sec, secure);
0163 }
0164 
0165 QList<TestWebEnginePartCookieJarKIO::CookieData> TestWebEnginePartCookieJarKIO::findTestCookies()
0166 {
0167     QList<CookieData> cookies;
0168     if (!m_server->isValid()) {
0169         return cookies;
0170     }
0171     QDBusReply<QStringList> rep = m_server->call(QDBus::Block, "findDomains");
0172     if (!rep.isValid()) {
0173         qDebug() << rep.error().message();
0174         return cookies;
0175     }
0176     QStringList domains = rep.value();
0177     //domain, path, name, host
0178     const QList<int> fields{0,1,2,3};
0179     for (const QString &d: domains){
0180     rep = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(fields), d, "", "", "");
0181         if (!rep.isValid()) {
0182             qDebug() << rep.error().message();
0183             return cookies;
0184         }
0185         QStringList res = rep.value();
0186         for (int i = 0; i < res.count(); i+= fields.count()) {
0187             if (res.at(i+2).startsWith(m_cookieName)) {
0188                 CookieData d;
0189                 d.name = res.at(i+2);
0190                 d.domain = res.at(i);
0191                 d.path = res.at(i+1);
0192                 d.host = res.at(i+3);
0193                 cookies.append(d);
0194             }
0195         }
0196     }
0197     return cookies;
0198 }
0199 
0200 void TestWebEnginePartCookieJarKIO::deleteCookies(const QList<TestWebEnginePartCookieJarKIO::CookieData> &cookies)
0201 {
0202     if (!m_server->isValid()) {
0203         return;
0204     }
0205     for (const CookieData &c: cookies) {
0206         QDBusMessage deleteRep = m_server->call(QDBus::Block, "deleteCookie", c.domain, c.host, c.path, c.name);
0207         if (m_server->lastError().isValid()) {
0208             qDebug() << m_server->lastError().message();
0209         }
0210     }
0211 }
0212 
0213 void TestWebEnginePartCookieJarKIO::testCookieRemovedFromStoreAreRemovedFromKCookieServer_data()
0214 {
0215     QTest::addColumn<QNetworkCookie>("cookie");
0216     QTest::addColumn<QString>("name");
0217     QTest::addColumn<QString>("domain");
0218     QTest::addColumn<QString>("path");
0219     QTest::addColumn<QString>("host");
0220     
0221     const QStringList labels{
0222         "remove persistent cookie with domain and path",
0223         "remove session cookie with domain and path",
0224         "remove persistent cookie with domain and no path",
0225         "remove persistent cookie with domain and / as path",
0226         "remove persistent cookie with path and no domain",
0227         "remove persistent cookie without secure"
0228     };
0229     
0230     const QList<CookieData> input{
0231         {m_cookieName + "-persistent-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0232         {m_cookieName + "-session-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime(), true},
0233         {m_cookieName + "-no-path-remove", "test-remove-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0234         {m_cookieName + "-slash-as-path-remove", "test-remove-value", ".yyy.xxx.com", "/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0235         {m_cookieName + "-no-domain-remove", "test-remove-value", "", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), true},
0236         {m_cookieName + "-no-secure-remove", "test-remove-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", QDateTime::currentDateTime().addYears(1), false},
0237     };
0238     
0239     QList<CookieData> expected(input);
0240     
0241     for (int i = 0; i < input.count(); ++i) {
0242         const CookieData &ex = expected.at(i);
0243         const CookieData &in = input.at(i);
0244         QNetworkCookie c = in.cookie();
0245         if (in.domain.isEmpty()) {
0246             c.normalize(QUrl("https://" + in.host));
0247         }
0248         QTest::newRow(labels.at(i).toLatin1().constData()) << c << ex.name << ex.domain << ex.path << ex.host;
0249     }}
0250 
0251 void TestWebEnginePartCookieJarKIO::testCookieRemovedFromStoreAreRemovedFromKCookieServer()
0252 {
0253     QFETCH(const QNetworkCookie, cookie);
0254     QFETCH(const QString, name);
0255     QFETCH(const QString, domain);
0256     QFETCH(const QString, path);
0257     QFETCH(const QString, host);
0258     
0259     //Add cookie to KCookieServer
0260     QDBusError e = addCookieToKCookieServer(cookie, host);
0261     QVERIFY2(!e.isValid(), qPrintable(m_server->lastError().message()));
0262     
0263     auto findCookies=[this, &domain, &host, &path, &name](){
0264         QDBusReply<QStringList> reply = m_server->call(QDBus::Block, "findCookies", QVariant::fromValue(QList<int>{2}), domain, host, path, name);
0265         return reply;
0266     };
0267 
0268     //Ensure cookie has been added to KCookieServer
0269     QDBusReply<QStringList> reply = findCookies();
0270     QVERIFY2(reply.isValid(), qPrintable(reply.error().message()));
0271     QVERIFY2(reply.value().contains(name), "Cookie wasn't added to server");
0272 
0273     //Emit QWebEngineCookieStore::cookieRemoved signal and check that cookie has indeed been removed
0274     emit m_store->cookieRemoved(cookie);
0275 
0276     //Check that cookie is no longer in KCookieServer
0277     reply = findCookies();
0278     QVERIFY2(reply.isValid(), qPrintable(reply.error().message()));
0279     QVERIFY2(!reply.value().contains(name), "Cookie wasn't removed from server");
0280 }
0281 
0282 QDBusError TestWebEnginePartCookieJarKIO::addCookieToKCookieServer(const QNetworkCookie& _cookie, const QString& host)
0283 {
0284     QNetworkCookie cookie(_cookie);
0285     QUrl url;
0286     url.setHost(host);
0287     url.setScheme(cookie.isSecure() ? "https" : "http");
0288     if (!cookie.domain().startsWith('.')) {
0289         cookie.setDomain(QString());
0290     }
0291     const QByteArray setCookie = "Set-Cookie: " + cookie.toRawForm();
0292     m_server->call(QDBus::Block, "addCookies", url.toString(), setCookie, static_cast<qlonglong>(0));
0293     return m_server->lastError();
0294 }
0295 
0296 void TestWebEnginePartCookieJarKIO::testPersistentCookiesAreAddedToStoreOnCreation()
0297 {
0298     delete m_jar;
0299     QDateTime exp = QDateTime::currentDateTime().addYears(1);
0300     QString baseCookieName = m_cookieName + "-startup";
0301     QList<CookieData> data {
0302         {baseCookieName + "-persistent", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true},
0303         {baseCookieName + "-no-path", "test-value", ".yyy.xxx.com", "", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true},
0304         {baseCookieName + "-no-domain", "test-value", "", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), true},
0305         {baseCookieName + "-no-secure", "test-value", ".yyy.xxx.com", "/abc/def/", "zzz.yyy.xxx.com", currentDateTime().addYears(1), false}
0306     };
0307     QList<QNetworkCookie> expected;
0308     for(const CookieData &d: data){
0309         QNetworkCookie c = d.cookie();
0310         QDBusError e = addCookieToKCookieServer(c, d.host);
0311         QVERIFY2(!e.isValid(), qPrintable(e.message()));
0312         expected << c;
0313     }
0314     m_jar = new WebEnginePartCookieJarKIO(m_profile, this);
0315     QList<QNetworkCookie> cookiesInsertedIntoJar;
0316     for(const QNetworkCookie &c: qAsConst(m_jar->m_testCookies)){
0317         if(QString(c.name()).startsWith(baseCookieName)) {
0318             cookiesInsertedIntoJar << c;
0319         }
0320     }
0321     
0322     //Ensure that cookies in the two lists are in the same order before comparing them
0323     //(the order in cookiesInsertedIntoJar depends on the order KCookieServer::findCookies
0324     //returns them)
0325     auto sortLambda = [](const QNetworkCookie &c1, const QNetworkCookie &c2){
0326         return c1.name() < c2.name();
0327     };
0328     std::sort(cookiesInsertedIntoJar.begin(), cookiesInsertedIntoJar.end(), sortLambda);
0329     std::sort(expected.begin(), expected.end(), sortLambda);
0330     
0331     QCOMPARE(cookiesInsertedIntoJar, expected);
0332 }
0333 
0334 void TestWebEnginePartCookieJarKIO::testSessionCookiesAreNotAddedToStoreOnCreation()
0335 {
0336     delete m_jar;
0337     CookieData data{m_cookieName + "-startup-session", "test-value", ".yyy.xxx.com", "/abc/def", "zzz.yyy.xxx.com", QDateTime(), true};
0338     QDBusError e = addCookieToKCookieServer(data.cookie(), data.host);
0339     QVERIFY2(!e.isValid(), qPrintable(e.message()));
0340     m_jar = new WebEnginePartCookieJarKIO(m_profile, this);
0341     QList<QNetworkCookie> cookiesInsertedIntoJar;
0342     for(const QNetworkCookie &c: qAsConst(m_jar->m_testCookies)) {
0343         if (c.name() == data.name) {
0344             cookiesInsertedIntoJar << c;
0345         }
0346     }
0347     QVERIFY2(cookiesInsertedIntoJar.isEmpty(), "Session cookies inserted into cookie store");
0348 }
0349