1 /*
2  *  Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
3  *
4  *  This program is free software: you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation, either version 2 or (at your option)
7  *  version 3 of the License.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "TestBrowser.h"
19 
20 #include "TestGlobal.h"
21 #include "browser/BrowserSettings.h"
22 #include "core/Tools.h"
23 #include "crypto/Crypto.h"
24 #include "sodium/crypto_box.h"
25 
26 #include <QString>
27 
28 QTEST_GUILESS_MAIN(TestBrowser)
29 
30 const QString PUBLICKEY = "UIIPObeoya1G8g1M5omgyoPR/j1mR1HlYHu0wHCgMhA=";
31 const QString SECRETKEY = "B8ei4ZjQJkWzZU2SK/tBsrYRwp+6ztEMf5GFQV+i0yI=";
32 const QString SERVERPUBLICKEY = "lKnbLhrVCOqzEjuNoUz1xj9EZlz8xeO4miZBvLrUPVQ=";
33 const QString SERVERSECRETKEY = "tbPQcghxfOgbmsnEqG2qMIj1W2+nh+lOJcNsHncaz1Q=";
34 const QString NONCE = "zBKdvTjL5bgWaKMCTut/8soM/uoMrFoZ";
35 const QString CLIENTID = "testClient";
36 
initTestCase()37 void TestBrowser::initTestCase()
38 {
39     QVERIFY(Crypto::init());
40     m_browserService = browserService();
41     browserSettings()->setBestMatchOnly(false);
42 }
43 
init()44 void TestBrowser::init()
45 {
46     m_browserAction.reset(new BrowserAction());
47 }
48 
49 /**
50  * Tests for BrowserAction
51  */
52 
testChangePublicKeys()53 void TestBrowser::testChangePublicKeys()
54 {
55     QJsonObject json;
56     json["action"] = "change-public-keys";
57     json["publicKey"] = PUBLICKEY;
58     json["nonce"] = NONCE;
59 
60     auto response = m_browserAction->processClientMessage(json);
61     QCOMPARE(response["action"].toString(), QString("change-public-keys"));
62     QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
63     QCOMPARE(response["success"].toString(), TRUE_STR);
64 }
65 
testEncryptMessage()66 void TestBrowser::testEncryptMessage()
67 {
68     QJsonObject message;
69     message["action"] = "test-action";
70 
71     m_browserAction->m_publicKey = SERVERPUBLICKEY;
72     m_browserAction->m_secretKey = SERVERSECRETKEY;
73     m_browserAction->m_clientPublicKey = PUBLICKEY;
74     auto encrypted = m_browserAction->encryptMessage(message, NONCE);
75 
76     QCOMPARE(encrypted, QString("+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP"));
77 }
78 
testDecryptMessage()79 void TestBrowser::testDecryptMessage()
80 {
81     QString message = "+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP";
82     m_browserAction->m_publicKey = SERVERPUBLICKEY;
83     m_browserAction->m_secretKey = SERVERSECRETKEY;
84     m_browserAction->m_clientPublicKey = PUBLICKEY;
85     auto decrypted = m_browserAction->decryptMessage(message, NONCE);
86 
87     QCOMPARE(decrypted["action"].toString(), QString("test-action"));
88 }
89 
testGetBase64FromKey()90 void TestBrowser::testGetBase64FromKey()
91 {
92     unsigned char pk[crypto_box_PUBLICKEYBYTES];
93 
94     for (unsigned int i = 0; i < crypto_box_PUBLICKEYBYTES; ++i) {
95         pk[i] = i;
96     }
97 
98     auto response = m_browserAction->getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES);
99     QCOMPARE(response, QString("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="));
100 }
101 
testIncrementNonce()102 void TestBrowser::testIncrementNonce()
103 {
104     auto result = m_browserAction->incrementNonce(NONCE);
105     QCOMPARE(result, QString("zRKdvTjL5bgWaKMCTut/8soM/uoMrFoZ"));
106 }
107 
108 /**
109  * Tests for BrowserService
110  */
testBaseDomain()111 void TestBrowser::testBaseDomain()
112 {
113     QString url1 = "https://another.example.co.uk";
114     QString url2 = "https://www.example.com";
115     QString url3 = "http://test.net";
116     QString url4 = "http://so.many.subdomains.co.jp";
117 
118     QString res1 = m_browserService->baseDomain(url1);
119     QString res2 = m_browserService->baseDomain(url2);
120     QString res3 = m_browserService->baseDomain(url3);
121     QString res4 = m_browserService->baseDomain(url4);
122 
123     QCOMPARE(res1, QString("example.co.uk"));
124     QCOMPARE(res2, QString("example.com"));
125     QCOMPARE(res3, QString("test.net"));
126     QCOMPARE(res4, QString("subdomains.co.jp"));
127 }
128 
testSortPriority()129 void TestBrowser::testSortPriority()
130 {
131     QFETCH(QString, entryUrl);
132     QFETCH(QString, siteUrl);
133     QFETCH(QString, formUrl);
134     QFETCH(int, expectedScore);
135 
136     QScopedPointer<Entry> entry(new Entry());
137     entry->setUrl(entryUrl);
138 
139     QCOMPARE(m_browserService->sortPriority(m_browserService->getEntryURLs(entry.data()), siteUrl, formUrl),
140              expectedScore);
141 }
142 
testSortPriority_data()143 void TestBrowser::testSortPriority_data()
144 {
145     const QString siteUrl = "https://github.com/login";
146     const QString formUrl = "https://github.com/session";
147 
148     QTest::addColumn<QString>("entryUrl");
149     QTest::addColumn<QString>("siteUrl");
150     QTest::addColumn<QString>("formUrl");
151     QTest::addColumn<int>("expectedScore");
152 
153     QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
154     QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
155     QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
156     QTest::newRow("Exact Match No Trailing Slash") << "https://github.com"
157                                                    << "https://github.com/" << formUrl << 100;
158     QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
159     QTest::newRow("Exact Match with Query") << "https://github.com/login?test=test#fragment"
160                                             << "https://github.com/login?test=test" << formUrl << 100;
161 
162     QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;
163 
164     QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 80;
165     QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 80;
166     QTest::newRow("Path Mismatch (form)") << "https://github.com/"
167                                           << "https://github.net" << formUrl << 70;
168 
169     QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/"
170                                                << "https://github.net/" << 60;
171     QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/"
172                                                << "https://sub.github.com/" << 50;
173 
174     QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
175     QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
176     QTest::newRow("Invalid URL") << "http://github" << siteUrl << formUrl << 0;
177 }
178 
testSearchEntries()179 void TestBrowser::testSearchEntries()
180 {
181     auto db = QSharedPointer<Database>::create();
182     auto* root = db->rootGroup();
183 
184     QStringList urls = {"https://github.com/login_page",
185                         "https://github.com/login",
186                         "https://github.com/",
187                         "github.com/login",
188                         "http://github.com",
189                         "http://github.com/login",
190                         "github.com",
191                         "github.com/login",
192                         "https://github", // Invalid URL
193                         "github.com"};
194 
195     createEntries(urls, root);
196 
197     browserSettings()->setMatchUrlScheme(false);
198     auto result =
199         m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl
200 
201     QCOMPARE(result.length(), 9);
202     QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
203     QCOMPARE(result[1]->url(), QString("https://github.com/login"));
204     QCOMPARE(result[2]->url(), QString("https://github.com/"));
205     QCOMPARE(result[3]->url(), QString("github.com/login"));
206     QCOMPARE(result[4]->url(), QString("http://github.com"));
207     QCOMPARE(result[5]->url(), QString("http://github.com/login"));
208 
209     // With matching there should be only 3 results + 4 without a scheme
210     browserSettings()->setMatchUrlScheme(true);
211     result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
212     QCOMPARE(result.length(), 7);
213     QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
214     QCOMPARE(result[1]->url(), QString("https://github.com/login"));
215     QCOMPARE(result[2]->url(), QString("https://github.com/"));
216     QCOMPARE(result[3]->url(), QString("github.com/login"));
217 }
218 
testSearchEntriesWithPort()219 void TestBrowser::testSearchEntriesWithPort()
220 {
221     auto db = QSharedPointer<Database>::create();
222     auto* root = db->rootGroup();
223 
224     QStringList urls = {"http://127.0.0.1:443", "http://127.0.0.1:80"};
225 
226     createEntries(urls, root);
227 
228     auto result = m_browserService->searchEntries(db, "http://127.0.0.1:443", "http://127.0.0.1");
229     QCOMPARE(result.length(), 1);
230     QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
231 }
232 
testSearchEntriesWithAdditionalURLs()233 void TestBrowser::testSearchEntriesWithAdditionalURLs()
234 {
235     auto db = QSharedPointer<Database>::create();
236     auto* root = db->rootGroup();
237 
238     QStringList urls = {"https://github.com/", "https://www.example.com", "http://domain.com"};
239 
240     auto entries = createEntries(urls, root);
241 
242     // Add an additional URL to the first entry
243     entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org");
244 
245     auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
246     QCOMPARE(result.length(), 1);
247     QCOMPARE(result[0]->url(), QString("https://github.com/"));
248 
249     // Search the additional URL. It should return the same entry
250     auto additionalResult = m_browserService->searchEntries(db, "https://keepassxc.org", "https://keepassxc.org");
251     QCOMPARE(additionalResult.length(), 1);
252     QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
253 }
254 
testInvalidEntries()255 void TestBrowser::testInvalidEntries()
256 {
257     auto db = QSharedPointer<Database>::create();
258     auto* root = db->rootGroup();
259     const QString url("https://github.com");
260     const QString submitUrl("https://github.com/session");
261 
262     QStringList urls = {
263         "https://github.com/login",
264         "https:///github.com/", // Extra '/'
265         "http://github.com/**//*",
266         "http://*.github.com/login",
267         "//github.com", // fromUserInput() corrects this one.
268         "github.com/{}<>",
269         "http:/example.com",
270     };
271 
272     createEntries(urls, root);
273 
274     browserSettings()->setMatchUrlScheme(true);
275     auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
276     QCOMPARE(result.length(), 2);
277     QCOMPARE(result[0]->url(), QString("https://github.com/login"));
278     QCOMPARE(result[1]->url(), QString("//github.com"));
279 
280     // Test the URL's directly
281     QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true);
282     QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false);
283     QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false);
284     QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false);
285     QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true);
286     QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false);
287 }
288 
testSubdomainsAndPaths()289 void TestBrowser::testSubdomainsAndPaths()
290 {
291     auto db = QSharedPointer<Database>::create();
292     auto* root = db->rootGroup();
293 
294     QStringList urls = {
295         "https://www.github.com/login/page.xml",
296         "https://login.github.com/",
297         "https://github.com",
298         "http://www.github.com",
299         "http://login.github.com/pathtonowhere",
300         ".github.com", // Invalid URL
301         "www.github.com/",
302         "https://github", // Invalid URL
303         "https://hub.com" // Should not return
304     };
305 
306     createEntries(urls, root);
307 
308     browserSettings()->setMatchUrlScheme(false);
309     auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
310     QCOMPARE(result.length(), 1);
311     QCOMPARE(result[0]->url(), QString("https://github.com"));
312 
313     // With www subdomain
314     result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session");
315     QCOMPARE(result.length(), 4);
316     QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
317     QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain
318     QCOMPARE(result[2]->url(), QString("http://www.github.com"));
319     QCOMPARE(result[3]->url(), QString("www.github.com/"));
320 
321     // With scheme matching there should be only 1 result
322     browserSettings()->setMatchUrlScheme(true);
323     result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
324     QCOMPARE(result.length(), 1);
325     QCOMPARE(result[0]->url(), QString("https://github.com"));
326 
327     // Test site with subdomain in the site URL
328     QStringList entryURLs = {
329         "https://accounts.example.com",
330         "https://accounts.example.com/path",
331         "https://subdomain.example.com/",
332         "https://another.accounts.example.com/",
333         "https://another.subdomain.example.com/",
334         "https://example.com/",
335         "https://example" // Invalid URL
336     };
337 
338     createEntries(entryURLs, root);
339 
340     result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
341     QCOMPARE(result.length(), 3);
342     QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
343     QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
344     QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
345 
346     result = m_browserService->searchEntries(
347         db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
348     QCOMPARE(result.length(), 4);
349     QCOMPARE(result[0]->url(),
350              QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
351     QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
352     QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/"));
353     QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains
354 
355     // Test local files. It should be a direct match.
356     QStringList localFiles = {"file:///Users/testUser/tests/test.html"};
357 
358     createEntries(localFiles, root);
359 
360     // With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL.
361     result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html");
362     QCOMPARE(result.length(), 1);
363 }
364 
testSortEntries()365 void TestBrowser::testSortEntries()
366 {
367     auto db = QSharedPointer<Database>::create();
368     auto* root = db->rootGroup();
369 
370     QStringList urls = {"https://github.com/login_page",
371                         "https://github.com/login",
372                         "https://github.com/",
373                         "github.com/login",
374                         "http://github.com",
375                         "http://github.com/login",
376                         "github.com",
377                         "github.com/login?test=test",
378                         "https://github", // Invalid URL
379                         "github.com"};
380 
381     auto entries = createEntries(urls, root);
382 
383     browserSettings()->setBestMatchOnly(false);
384     browserSettings()->setSortByUsername(true);
385     auto result = m_browserService->sortEntries(entries, "https://github.com/login", "https://github.com/session");
386     QCOMPARE(result.size(), 10);
387     QCOMPARE(result[0]->username(), QString("User 1"));
388     QCOMPARE(result[0]->url(), urls[1]);
389     QCOMPARE(result[1]->username(), QString("User 3"));
390     QCOMPARE(result[1]->url(), urls[3]);
391     QCOMPARE(result[2]->username(), QString("User 7"));
392     QCOMPARE(result[2]->url(), urls[7]);
393     QCOMPARE(result[3]->username(), QString("User 0"));
394     QCOMPARE(result[3]->url(), urls[0]);
395 
396     // Test with a perfect match. That should be first in the list.
397     result = m_browserService->sortEntries(entries, "https://github.com/login_page", "https://github.com/session");
398     QCOMPARE(result.size(), 10);
399     QCOMPARE(result[0]->username(), QString("User 0"));
400     QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
401     QCOMPARE(result[1]->username(), QString("User 1"));
402     QCOMPARE(result[1]->url(), QString("https://github.com/login"));
403 }
404 
createEntries(QStringList & urls,Group * root) const405 QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
406 {
407     QList<Entry*> entries;
408     for (int i = 0; i < urls.length(); ++i) {
409         auto entry = new Entry();
410         entry->setGroup(root);
411         entry->beginUpdate();
412         entry->setUrl(urls[i]);
413         entry->setUsername(QString("User %1").arg(i));
414         entry->endUpdate();
415         entries.push_back(entry);
416     }
417 
418     return entries;
419 }
testValidURLs()420 void TestBrowser::testValidURLs()
421 {
422     QHash<QString, bool> urls;
423     urls["https://github.com/login"] = true;
424     urls["https:///github.com/"] = false;
425     urls["http://github.com/**//*"] = false;
426     urls["http://*.github.com/login"] = false;
427     urls["//github.com"] = true;
428     urls["github.com/{}<>"] = false;
429     urls["http:/example.com"] = false;
430     urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true;
431     urls["file:///Users/testUser/Code/test.html"] = true;
432     urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true;
433 
434     QHashIterator<QString, bool> i(urls);
435     while (i.hasNext()) {
436         i.next();
437         QCOMPARE(Tools::checkUrlValid(i.key()), i.value());
438     }
439 }
440 
testBestMatchingCredentials()441 void TestBrowser::testBestMatchingCredentials()
442 {
443     auto db = QSharedPointer<Database>::create();
444     auto* root = db->rootGroup();
445 
446     // Test with simple URL entries
447     QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
448 
449     auto entries = createEntries(urls, root);
450 
451     browserSettings()->setBestMatchOnly(true);
452 
453     QString siteUrl = "https://github.com/loginpage";
454     auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
455     auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
456     QCOMPARE(sorted.size(), 1);
457     QCOMPARE(sorted[0]->url(), siteUrl);
458 
459     siteUrl = "https://github.com/justsomepage";
460     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
461     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
462     QCOMPARE(sorted.size(), 1);
463     QCOMPARE(sorted[0]->url(), siteUrl);
464 
465     siteUrl = "https://github.com/";
466     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
467     sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
468     QCOMPARE(sorted.size(), 1);
469     QCOMPARE(sorted[0]->url(), siteUrl);
470 
471     // Without best-matching the URL with the path should be returned first
472     browserSettings()->setBestMatchOnly(false);
473     siteUrl = "https://github.com/loginpage";
474     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
475     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
476     QCOMPARE(sorted.size(), 3);
477     QCOMPARE(sorted[0]->url(), siteUrl);
478 
479     // Test with subdomains
480     QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
481                                   "https://sub.github.com/justsomepage",
482                                   "https://bus.github.com/justsomepage",
483                                   "https://subdomain.example.com/",
484                                   "https://subdomain.example.com",
485                                   "https://example.com"};
486 
487     entries = createEntries(subdomainsUrls, root);
488 
489     browserSettings()->setBestMatchOnly(true);
490     siteUrl = "https://sub.github.com/justsomepage";
491     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
492     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
493     QCOMPARE(sorted.size(), 1);
494     QCOMPARE(sorted[0]->url(), siteUrl);
495 
496     siteUrl = "https://github.com/justsomepage";
497     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
498     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
499     QCOMPARE(sorted.size(), 1);
500     QCOMPARE(sorted[0]->url(), siteUrl);
501 
502     siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
503     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
504     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
505     QCOMPARE(sorted.size(), 1);
506     QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
507 
508     // The matching should not care if there's a / path or not.
509     siteUrl = "https://subdomain.example.com/";
510     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
511     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
512     QCOMPARE(sorted.size(), 2);
513     QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com/"));
514     QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com"));
515 
516     // Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
517     // best match.
518     db = QSharedPointer<Database>::create();
519     root = db->rootGroup();
520     QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
521     entries = createEntries(domainUrls, root);
522     siteUrl = "https://subdomain.example.com";
523     result = m_browserService->searchEntries(db, siteUrl, siteUrl);
524     sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
525 
526     QCOMPARE(sorted.size(), 2);
527     QCOMPARE(sorted[0]->url(), QString("https://example.com"));
528     QCOMPARE(sorted[1]->url(), QString("https://example.com"));
529 
530     // https://github.com/keepassxreboot/keepassxc/issues/4754
531     db = QSharedPointer<Database>::create();
532     root = db->rootGroup();
533     QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
534     entries = createEntries(fooUrls, root);
535 
536     for (const auto& url : fooUrls) {
537         result = m_browserService->searchEntries(db, url, url);
538         sorted = m_browserService->sortEntries(result, url, url);
539         QCOMPARE(sorted.size(), 1);
540         QCOMPARE(sorted[0]->url(), QString(url));
541     }
542 
543     // https://github.com/keepassxreboot/keepassxc/issues/4734
544     db = QSharedPointer<Database>::create();
545     root = db->rootGroup();
546     QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
547     entries = createEntries(testUrls, root);
548 
549     for (const auto& url : testUrls) {
550         result = m_browserService->searchEntries(db, url, url);
551         sorted = m_browserService->sortEntries(result, url, url);
552         QCOMPARE(sorted.size(), 1);
553         QCOMPARE(sorted[0]->url(), QString(url));
554     }
555 }
556 
testBestMatchingWithAdditionalURLs()557 void TestBrowser::testBestMatchingWithAdditionalURLs()
558 {
559     auto db = QSharedPointer<Database>::create();
560     auto* root = db->rootGroup();
561 
562     QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
563 
564     auto entries = createEntries(urls, root);
565     browserSettings()->setBestMatchOnly(true);
566 
567     // Add an additional URL to the first entry
568     entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://test.github.com/anotherpage");
569 
570     // The first entry should be triggered
571     auto result = m_browserService->searchEntries(
572         db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
573     auto sorted = m_browserService->sortEntries(
574         result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
575     QCOMPARE(sorted.length(), 1);
576     QCOMPARE(sorted[0]->url(), urls[0]);
577 }
578