keepassxc

Форк
0
/
TestGuiFdoSecrets.cpp 
1930 строк · 67.2 Кб
1
/*
2
 *  Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
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 "TestGuiFdoSecrets.h"
19

20
#include "fdosecrets/FdoSecretsPlugin.h"
21
#include "fdosecrets/FdoSecretsSettings.h"
22
#include "fdosecrets/objects/Collection.h"
23
#include "fdosecrets/objects/Item.h"
24
#include "fdosecrets/objects/SessionCipher.h"
25
#include "fdosecrets/widgets/AccessControlDialog.h"
26

27
#include "config-keepassx-tests.h"
28

29
#include "core/Tools.h"
30
#include "crypto/Crypto.h"
31
#include "gui/Application.h"
32
#include "gui/DatabaseTabWidget.h"
33
#include "gui/FileDialog.h"
34
#include "gui/MainWindow.h"
35
#include "gui/MessageBox.h"
36
#include "gui/PasswordWidget.h"
37
#include "gui/wizard/NewDatabaseWizard.h"
38
#include "util/FdoSecretsProxy.h"
39
#include "util/TemporaryFile.h"
40

41
#include <QCheckBox>
42
#include <QLineEdit>
43
#include <QSignalSpy>
44
#include <QTest>
45
#include <utility>
46

47
int main(int argc, char* argv[])
48
{
49
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
50
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
51
    QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
52
#endif
53
    Application app(argc, argv);
54
    app.setApplicationName("KeePassXC");
55
    app.setApplicationVersion(KEEPASSXC_VERSION);
56
    app.setQuitOnLastWindowClosed(false);
57
    app.setAttribute(Qt::AA_Use96Dpi, true);
58
    app.applyTheme();
59
    QTEST_DISABLE_KEYPAD_NAVIGATION
60
    TestGuiFdoSecrets tc;
61
    QTEST_SET_MAIN_SOURCE_PATH
62
    return QTest::qExec(&tc, argc, argv);
63
}
64

65
#define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default"
66

67
// assert macros compatible with function having return values
68
#define VERIFY2_RET(statement, msg)                                                                                    \
69
    do {                                                                                                               \
70
        if (!QTest::qVerify(static_cast<bool>(statement), #statement, (msg), __FILE__, __LINE__))                      \
71
            return {};                                                                                                 \
72
    } while (false)
73

74
#define COMPARE_RET(actual, expected)                                                                                  \
75
    do {                                                                                                               \
76
        if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))                                \
77
            return {};                                                                                                 \
78
    } while (false)
79

80
// by default use these with Qt macros
81
#define VERIFY QVERIFY
82
#define COMPARE QCOMPARE
83
#define VERIFY2 QVERIFY2
84

85
#define DBUS_COMPARE(actual, expected)                                                                                 \
86
    do {                                                                                                               \
87
        auto reply = (actual);                                                                                         \
88
        VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit());                                                  \
89
        COMPARE(reply.value(), (expected));                                                                            \
90
    } while (false)
91

92
#define DBUS_VERIFY(stmt)                                                                                              \
93
    do {                                                                                                               \
94
        auto reply = (stmt);                                                                                           \
95
        VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit());                                                  \
96
    } while (false)
97

98
#define DBUS_GET(var, stmt)                                                                                            \
99
    std::remove_cv<decltype((stmt).argumentAt<0>())>::type var;                                                        \
100
    do {                                                                                                               \
101
        const auto rep = (stmt);                                                                                       \
102
        VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit());                                                      \
103
        var = rep.argumentAt<0>();                                                                                     \
104
    } while (false)
105

106
#define DBUS_GET2(name1, name2, stmt)                                                                                  \
107
    std::remove_cv<decltype((stmt).argumentAt<0>())>::type name1;                                                      \
108
    std::remove_cv<decltype((stmt).argumentAt<1>())>::type name2;                                                      \
109
    do {                                                                                                               \
110
        const auto rep = (stmt);                                                                                       \
111
        VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit());                                                      \
112
        name1 = rep.argumentAt<0>();                                                                                   \
113
        name2 = rep.argumentAt<1>();                                                                                   \
114
    } while (false)
115

116
using namespace FdoSecrets;
117

118
class FakeClient : public DBusClient
119
{
120
public:
121
    explicit FakeClient(DBusMgr* dbus)
122
        : DBusClient(
123
            dbus,
124
            {QStringLiteral("local"), 0, true, {ProcInfo{0, 0, QStringLiteral("fake-client"), QString{}, QString{}}}})
125
    {
126
    }
127
};
128

129
// pretty print QDBusObjectPath in QCOMPARE
130
char* toString(const QDBusObjectPath& path)
131
{
132
    return QTest::toString("ObjectPath(" + path.path() + ")");
133
}
134

135
TestGuiFdoSecrets::~TestGuiFdoSecrets() = default;
136

137
void TestGuiFdoSecrets::initTestCase()
138
{
139
    VERIFY(Crypto::init());
140
    Config::createTempFileInstance();
141
    config()->set(Config::AutoSaveAfterEveryChange, false);
142
    config()->set(Config::AutoSaveOnExit, false);
143
    config()->set(Config::GUI_ShowTrayIcon, true);
144
    config()->set(Config::UpdateCheckMessageShown, true);
145
    // Disable quick unlock
146
    config()->set(Config::Security_QuickUnlock, false);
147
    // Disable secret service integration (activate within individual tests to test the plugin)
148
    FdoSecrets::settings()->setEnabled(false);
149
    // activate within individual tests
150
    FdoSecrets::settings()->setShowNotification(false);
151

152
    Application::bootstrap();
153

154
    m_mainWindow.reset(new MainWindow());
155
    m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
156
    VERIFY(m_tabWidget);
157
    m_plugin = FdoSecretsPlugin::getPlugin();
158
    VERIFY(m_plugin);
159
    m_mainWindow->show();
160

161
    auto key = QByteArray::fromHex("e407997e8b918419cf851cf3345358fdf"
162
                                   "ffb9564a220ac9c3934efd277cea20d17"
163
                                   "467ecdc56e817f75ac39501f38a4a04ff"
164
                                   "64d627e16c09981c7ad876da255b61c8e"
165
                                   "6a8408236c2a4523cfe6961c26dbdfc77"
166
                                   "c1a27a5b425ca71a019e829fae32c0b42"
167
                                   "0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
168
                                   "b263cee30bdb1a57af9bfa93f");
169
    m_clientCipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(key));
170

171
    // Load the NewDatabase.kdbx file into temporary storage
172
    QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx"));
173
    VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
174
    VERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
175
    sourceDbFile.close();
176

177
    // set a fake dbus client all the time so we can freely access DBusMgr anywhere
178
    m_client.reset(new FakeClient(m_plugin->dbus().data()));
179
    m_plugin->dbus()->overrideClient(m_client);
180
}
181

182
// Every test starts with opening the temp database
183
void TestGuiFdoSecrets::init()
184
{
185
    m_dbFile.reset(new TemporaryFile());
186
    // Write the temp storage to a temp database file for use in our tests
187
    VERIFY(m_dbFile->open());
188
    COMPARE(m_dbFile->write(m_dbData), static_cast<qint64>(m_dbData.size()));
189
    m_dbFile->close();
190

191
    // make sure window is activated or focus tests may fail
192
    m_mainWindow->activateWindow();
193
    processEvents();
194

195
    // open and unlock the database
196
    m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
197
    m_dbWidget = m_tabWidget->currentDatabaseWidget();
198
    m_db = m_dbWidget->database();
199

200
    // by default expose the root group
201
    FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
202
    VERIFY(m_dbWidget->save());
203

204
    // enforce consistent default settings at the beginning
205
    FdoSecrets::settings()->setUnlockBeforeSearch(false);
206
    FdoSecrets::settings()->setShowNotification(false);
207
    FdoSecrets::settings()->setConfirmAccessItem(false);
208
    FdoSecrets::settings()->setEnabled(false);
209
}
210

211
// Every test ends with closing the temp database without saving
212
void TestGuiFdoSecrets::cleanup()
213
{
214
    // restore to default settings
215
    FdoSecrets::settings()->setUnlockBeforeSearch(false);
216
    FdoSecrets::settings()->setShowNotification(false);
217
    FdoSecrets::settings()->setConfirmAccessItem(false);
218
    FdoSecrets::settings()->setEnabled(false);
219
    if (m_plugin) {
220
        m_plugin->updateServiceState();
221
    }
222

223
    // DO NOT save the database
224
    for (int i = 0; i != m_tabWidget->count(); ++i) {
225
        m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean();
226
    }
227

228
    // Close any dialogs
229
    while (auto w = QApplication::activeModalWidget()) {
230
        w->close();
231
    }
232

233
    VERIFY(m_tabWidget->closeAllDatabaseTabs());
234
    processEvents();
235

236
    if (m_dbFile) {
237
        m_dbFile->remove();
238
    }
239

240
    m_client->clearAuthorization();
241
}
242

243
void TestGuiFdoSecrets::cleanupTestCase()
244
{
245
    m_plugin->dbus()->overrideClient({});
246
    if (m_dbFile) {
247
        m_dbFile->remove();
248
    }
249
}
250

251
void TestGuiFdoSecrets::testServiceEnable()
252
{
253
    QSignalSpy sigError(m_plugin, SIGNAL(error(QString)));
254
    VERIFY(sigError.isValid());
255

256
    QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted()));
257
    VERIFY(sigStarted.isValid());
258

259
    // make sure no one else is holding the service
260
    VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
261

262
    // enable the service
263
    auto service = enableService();
264
    VERIFY(service);
265

266
    // service started without error
267
    VERIFY(sigError.isEmpty());
268
    COMPARE(sigStarted.size(), 1);
269

270
    processEvents();
271

272
    VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
273

274
    // there will be one default collection
275
    auto coll = getDefaultCollection(service);
276
    VERIFY(coll);
277

278
    DBUS_COMPARE(coll->locked(), false);
279
    DBUS_COMPARE(coll->label(), m_db->metadata()->name());
280

281
    DBUS_COMPARE(coll->created(),
282
                 static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000));
283
    DBUS_COMPARE(
284
        coll->modified(),
285
        static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000));
286
}
287

288
void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase()
289
{
290
    // reset the exposed group and then enable the service
291
    FdoSecrets::settings()->setExposedGroup(m_db, {});
292
    auto service = enableService();
293
    VERIFY(service);
294

295
    // no collections
296
    DBUS_COMPARE(service->collections(), QList<QDBusObjectPath>{});
297
}
298

299
void TestGuiFdoSecrets::testServiceSearch()
300
{
301
    auto service = enableService();
302
    VERIFY(service);
303
    auto coll = getDefaultCollection(service);
304
    VERIFY(coll);
305
    auto item = getFirstItem(coll);
306
    VERIFY(item);
307

308
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
309
    VERIFY(itemObj);
310
    auto entry = itemObj->backend();
311
    VERIFY(entry);
312

313
    entry->attributes()->set("fdosecrets-test", "1");
314
    entry->attributes()->set("fdosecrets-test-protected", "2", true);
315
    const QString crazyKey = "_a:bc&-+'-e%12df_d";
316
    const QString crazyValue = "[v]al@-ue";
317
    entry->attributes()->set(crazyKey, crazyValue);
318

319
    // search by title
320
    {
321
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
322
        COMPARE(locked, {});
323
        COMPARE(unlocked, {QDBusObjectPath(item->path())});
324
    }
325

326
    // search by attribute
327
    {
328
        DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test", "1"}}));
329
        COMPARE(locked, {});
330
        COMPARE(unlocked, {QDBusObjectPath(item->path())});
331
    }
332
    {
333
        DBUS_GET2(unlocked, locked, service->SearchItems({{crazyKey, crazyValue}}));
334
        COMPARE(locked, {});
335
        COMPARE(unlocked, {QDBusObjectPath(item->path())});
336
    }
337

338
    // searching using empty terms returns nothing
339
    {
340
        DBUS_GET2(unlocked, locked, service->SearchItems({}));
341
        COMPARE(locked, {});
342
        COMPARE(unlocked, {});
343
    }
344

345
    // searching using protected attributes or password returns nothing
346
    {
347
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Password", entry->password()}}));
348
        COMPARE(locked, {});
349
        COMPARE(unlocked, {});
350
    }
351
    {
352
        DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test-protected", "2"}}));
353
        COMPARE(locked, {});
354
        COMPARE(unlocked, {});
355
    }
356
}
357

358
void TestGuiFdoSecrets::testServiceSearchBlockingUnlock()
359
{
360
    auto service = enableService();
361
    VERIFY(service);
362
    auto coll = getDefaultCollection(service);
363
    VERIFY(coll);
364

365
    auto entries = m_db->rootGroup()->entriesRecursive();
366
    VERIFY(!entries.isEmpty());
367
    // assumes the db is not empty
368
    auto title = entries.first()->title();
369

370
    // NOTE: entries are no longer valid after locking
371
    lockDatabaseInBackend();
372

373
    // when database is locked, nothing is returned
374
    FdoSecrets::settings()->setUnlockBeforeSearch(false);
375
    {
376
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
377
        COMPARE(locked, {});
378
        COMPARE(unlocked, {});
379
    }
380

381
    // when database is locked, nothing is returned
382
    FdoSecrets::settings()->setUnlockBeforeSearch(true);
383
    {
384
        // SearchItems will block because the blocking wait is implemented
385
        // using a local QEventLoop.
386
        // so we do a little trick here to get the return value back
387
        bool unlockDialogWorks = false;
388
        QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(); });
389

390
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
391
        VERIFY(unlockDialogWorks);
392
        COMPARE(locked, {});
393
        COMPARE(unlocked.size(), 1);
394
        auto item = getProxy<ItemProxy>(unlocked.first());
395
        DBUS_COMPARE(item->label(), title);
396
    }
397
}
398

399
void TestGuiFdoSecrets::testServiceSearchBlockingUnlockMultiple()
400
{
401
    // setup: two databases, both locked, one with exposed db, the other not.
402

403
    // add another database tab with a database with no exposed group
404
    // to avoid modify the original, copy to a temp file first
405
    QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase2.kdbx"));
406
    QByteArray dbData;
407
    VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
408
    VERIFY(Tools::readAllFromDevice(&sourceDbFile, dbData));
409
    sourceDbFile.close();
410

411
    QTemporaryFile anotherFile;
412
    VERIFY(anotherFile.open());
413
    COMPARE(anotherFile.write(dbData), static_cast<qint64>(dbData.size()));
414
    anotherFile.close();
415

416
    m_tabWidget->addDatabaseTab(anotherFile.fileName(), false);
417
    auto anotherWidget = m_tabWidget->currentDatabaseWidget();
418

419
    auto service = enableService();
420
    VERIFY(service);
421

422
    // when there are multiple locked databases,
423
    // repeatedly show the dialog until there is at least one unlocked collection
424
    FdoSecrets::settings()->setUnlockBeforeSearch(true);
425

426
    // when only unlocking the one with no exposed group, a second dialog is shown
427
    lockDatabaseInBackend();
428
    {
429
        bool unlockDialogWorks = false;
430
        QTimer::singleShot(50, [&]() {
431
            unlockDialogWorks = driveUnlockDialog(anotherWidget);
432
            QTimer::singleShot(50, [&]() { unlockDialogWorks &= driveUnlockDialog(); });
433
        });
434

435
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
436
        VERIFY(unlockDialogWorks);
437
        COMPARE(locked, {});
438
        COMPARE(unlocked.size(), 1);
439
    }
440

441
    // when unlocking the one with exposed group, the other one remains locked
442
    lockDatabaseInBackend();
443
    {
444
        bool unlockDialogWorks = false;
445
        QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(m_dbWidget); });
446

447
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
448
        VERIFY(unlockDialogWorks);
449
        COMPARE(locked, {});
450
        COMPARE(unlocked.size(), 1);
451
        VERIFY(anotherWidget->isLocked());
452
    }
453
}
454

455
void TestGuiFdoSecrets::testServiceSearchForce()
456
{
457
    auto service = enableService();
458
    VERIFY(service);
459
    auto coll = getDefaultCollection(service);
460
    VERIFY(coll);
461
    auto item = getFirstItem(coll);
462
    VERIFY(item);
463

464
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
465
    VERIFY(itemObj);
466
    auto entry = itemObj->backend();
467
    VERIFY(entry);
468

469
    // fdosecrets should still find the item even if searching is disabled
470
    entry->group()->setSearchingEnabled(Group::Disable);
471

472
    // search by title
473
    {
474
        DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
475
        COMPARE(locked, {});
476
        COMPARE(unlocked, {QDBusObjectPath(item->path())});
477
    }
478
}
479

480
void TestGuiFdoSecrets::testServiceUnlock()
481
{
482
    lockDatabaseInBackend();
483

484
    auto service = enableService();
485
    VERIFY(service);
486
    auto coll = getDefaultCollection(service);
487
    VERIFY(coll);
488

489
    QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
490
    VERIFY(spyCollectionCreated.isValid());
491
    QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
492
    VERIFY(spyCollectionDeleted.isValid());
493
    QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
494
    VERIFY(spyCollectionChanged.isValid());
495

496
    DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
497
    // nothing is unlocked immediately without user's action
498
    COMPARE(unlocked, {});
499

500
    auto prompt = getProxy<PromptProxy>(promptPath);
501
    VERIFY(prompt);
502
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
503
    VERIFY(spyPromptCompleted.isValid());
504

505
    // nothing is unlocked yet
506
    VERIFY(waitForSignal(spyPromptCompleted, 0));
507
    DBUS_COMPARE(coll->locked(), true);
508

509
    // show the prompt
510
    DBUS_VERIFY(prompt->Prompt(""));
511

512
    // still not unlocked before user action
513
    VERIFY(waitForSignal(spyPromptCompleted, 0));
514
    DBUS_COMPARE(coll->locked(), true);
515

516
    VERIFY(driveUnlockDialog());
517

518
    VERIFY(waitForSignal(spyPromptCompleted, 1));
519
    {
520
        auto args = spyPromptCompleted.takeFirst();
521
        COMPARE(args.size(), 2);
522
        COMPARE(args.at(0).toBool(), false);
523
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
524
    }
525

526
    // check unlocked *AFTER* the prompt signal
527
    DBUS_COMPARE(coll->locked(), false);
528

529
    VERIFY(waitForSignal(spyCollectionCreated, 0));
530
    QTRY_VERIFY(!spyCollectionChanged.isEmpty());
531
    for (const auto& args : spyCollectionChanged) {
532
        COMPARE(args.size(), 1);
533
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
534
    }
535
    VERIFY(waitForSignal(spyCollectionDeleted, 0));
536
}
537

538
void TestGuiFdoSecrets::testServiceUnlockDatabaseConcurrent()
539
{
540
    lockDatabaseInBackend();
541

542
    auto service = enableService();
543
    VERIFY(service);
544
    auto coll = getDefaultCollection(service);
545
    VERIFY(coll);
546

547
    DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
548
    auto prompt = getProxy<PromptProxy>(promptPath);
549
    VERIFY(prompt);
550
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
551
    VERIFY(spyPromptCompleted.isValid());
552
    DBUS_VERIFY(prompt->Prompt(""));
553

554
    // while the first prompt is running, another request come in
555
    DBUS_GET2(unlocked2, promptPath2, service->Unlock({QDBusObjectPath(coll->path())}));
556
    auto prompt2 = getProxy<PromptProxy>(promptPath2);
557
    VERIFY(prompt2);
558
    QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
559
    VERIFY(spyPromptCompleted2.isValid());
560
    DBUS_VERIFY(prompt2->Prompt(""));
561

562
    // there should be only one unlock dialog
563
    VERIFY(driveUnlockDialog());
564

565
    // both prompts should complete
566
    VERIFY(waitForSignal(spyPromptCompleted, 1));
567
    {
568
        auto args = spyPromptCompleted.takeFirst();
569
        COMPARE(args.size(), 2);
570
        COMPARE(args.at(0).toBool(), false);
571
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
572
    }
573
    VERIFY(waitForSignal(spyPromptCompleted2, 1));
574
    {
575
        auto args = spyPromptCompleted2.takeFirst();
576
        COMPARE(args.size(), 2);
577
        COMPARE(args.at(0).toBool(), false);
578
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
579
    }
580

581
    // check unlocked *AFTER* prompt signal
582
    DBUS_COMPARE(coll->locked(), false);
583
}
584

585
void TestGuiFdoSecrets::testServiceUnlockItems()
586
{
587
    FdoSecrets::settings()->setConfirmAccessItem(true);
588

589
    auto service = enableService();
590
    VERIFY(service);
591
    auto coll = getDefaultCollection(service);
592
    VERIFY(coll);
593
    auto item = getFirstItem(coll);
594
    VERIFY(item);
595
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
596
    VERIFY(sess);
597

598
    DBUS_COMPARE(item->locked(), true);
599

600
    {
601
        DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
602
        // nothing is unlocked immediately without user's action
603
        COMPARE(unlocked, {});
604

605
        auto prompt = getProxy<PromptProxy>(promptPath);
606
        VERIFY(prompt);
607
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
608
        VERIFY(spyPromptCompleted.isValid());
609

610
        // nothing is unlocked yet
611
        COMPARE(spyPromptCompleted.count(), 0);
612
        DBUS_COMPARE(item->locked(), true);
613

614
        // drive the prompt
615
        DBUS_VERIFY(prompt->Prompt(""));
616
        // only allow once
617
        VERIFY(driveAccessControlDialog(false));
618

619
        VERIFY(waitForSignal(spyPromptCompleted, 1));
620
        {
621
            auto args = spyPromptCompleted.takeFirst();
622
            COMPARE(args.size(), 2);
623
            COMPARE(args.at(0).toBool(), false);
624
            COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
625
        }
626

627
        // unlocked
628
        DBUS_COMPARE(item->locked(), false);
629
    }
630

631
    // access the secret should reset the locking state
632
    {
633
        DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
634
    }
635
    DBUS_COMPARE(item->locked(), true);
636

637
    // unlock again with remember
638
    {
639
        DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
640
        // nothing is unlocked immediately without user's action
641
        COMPARE(unlocked, {});
642

643
        auto prompt = getProxy<PromptProxy>(promptPath);
644
        VERIFY(prompt);
645
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
646
        VERIFY(spyPromptCompleted.isValid());
647

648
        // nothing is unlocked yet
649
        COMPARE(spyPromptCompleted.count(), 0);
650
        DBUS_COMPARE(item->locked(), true);
651

652
        // drive the prompt
653
        DBUS_VERIFY(prompt->Prompt(""));
654
        // only allow and remember
655
        VERIFY(driveAccessControlDialog(true));
656

657
        VERIFY(waitForSignal(spyPromptCompleted, 1));
658
        {
659
            auto args = spyPromptCompleted.takeFirst();
660
            COMPARE(args.size(), 2);
661
            COMPARE(args.at(0).toBool(), false);
662
            COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
663
        }
664

665
        // unlocked
666
        DBUS_COMPARE(item->locked(), false);
667
    }
668

669
    // access the secret does not reset the locking state
670
    {
671
        DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
672
    }
673
    DBUS_COMPARE(item->locked(), false);
674
}
675

676
void TestGuiFdoSecrets::testServiceUnlockItemsIncludeFutureEntries()
677
{
678
    FdoSecrets::settings()->setConfirmAccessItem(true);
679

680
    auto service = enableService();
681
    VERIFY(service);
682
    auto coll = getDefaultCollection(service);
683
    VERIFY(coll);
684
    auto item = getFirstItem(coll);
685
    VERIFY(item);
686
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
687
    VERIFY(sess);
688

689
    DBUS_COMPARE(item->locked(), true);
690

691
    {
692
        DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
693
        // nothing is unlocked immediately without user's action
694
        COMPARE(unlocked, {});
695

696
        auto prompt = getProxy<PromptProxy>(promptPath);
697
        VERIFY(prompt);
698
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
699
        VERIFY(spyPromptCompleted.isValid());
700

701
        // nothing is unlocked yet
702
        COMPARE(spyPromptCompleted.count(), 0);
703
        DBUS_COMPARE(item->locked(), true);
704

705
        // drive the prompt
706
        DBUS_VERIFY(prompt->Prompt(""));
707
        // remember and include future entries
708
        VERIFY(driveAccessControlDialog(true, true));
709

710
        VERIFY(waitForSignal(spyPromptCompleted, 1));
711
        {
712
            auto args = spyPromptCompleted.takeFirst();
713
            COMPARE(args.size(), 2);
714
            COMPARE(args.at(0).toBool(), false);
715
            COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
716
        }
717

718
        // unlocked
719
        DBUS_COMPARE(item->locked(), false);
720
    }
721

722
    // check other entries are also unlocked
723
    {
724
        DBUS_GET(itemPaths, coll->items());
725
        VERIFY(itemPaths.size() > 1);
726
        auto anotherItem = getProxy<ItemProxy>(itemPaths.last());
727
        VERIFY(anotherItem);
728
        DBUS_COMPARE(anotherItem->locked(), false);
729
    }
730
}
731

732
void TestGuiFdoSecrets::testServiceLock()
733
{
734
    auto service = enableService();
735
    VERIFY(service);
736
    auto coll = getDefaultCollection(service);
737
    VERIFY(coll);
738

739
    QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
740
    VERIFY(spyCollectionCreated.isValid());
741
    QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
742
    VERIFY(spyCollectionDeleted.isValid());
743
    QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
744
    VERIFY(spyCollectionChanged.isValid());
745

746
    // if the db is modified, prompt user
747
    m_db->markAsModified();
748
    {
749
        DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
750
        COMPARE(locked, {});
751
        auto prompt = getProxy<PromptProxy>(promptPath);
752
        VERIFY(prompt);
753
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
754
        VERIFY(spyPromptCompleted.isValid());
755

756
        // prompt and click cancel
757
        MessageBox::setNextAnswer(MessageBox::Cancel);
758
        DBUS_VERIFY(prompt->Prompt(""));
759
        processEvents();
760

761
        VERIFY(waitForSignal(spyPromptCompleted, 1));
762
        auto args = spyPromptCompleted.takeFirst();
763
        COMPARE(args.count(), 2);
764
        COMPARE(args.at(0).toBool(), true);
765
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {});
766

767
        DBUS_COMPARE(coll->locked(), false);
768
    }
769
    {
770
        DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
771
        COMPARE(locked, {});
772
        auto prompt = getProxy<PromptProxy>(promptPath);
773
        VERIFY(prompt);
774
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
775
        VERIFY(spyPromptCompleted.isValid());
776

777
        // prompt and click save
778
        MessageBox::setNextAnswer(MessageBox::Save);
779
        DBUS_VERIFY(prompt->Prompt(""));
780
        processEvents();
781

782
        VERIFY(waitForSignal(spyPromptCompleted, 1));
783
        auto args = spyPromptCompleted.takeFirst();
784
        COMPARE(args.count(), 2);
785
        COMPARE(args.at(0).toBool(), false);
786
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
787

788
        DBUS_COMPARE(coll->locked(), true);
789
    }
790

791
    VERIFY(waitForSignal(spyCollectionCreated, 0));
792
    QTRY_VERIFY(!spyCollectionChanged.isEmpty());
793
    for (const auto& args : spyCollectionChanged) {
794
        COMPARE(args.size(), 1);
795
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
796
    }
797
    VERIFY(waitForSignal(spyCollectionDeleted, 0));
798

799
    // locking item locks the whole db
800
    unlockDatabaseInBackend();
801
    {
802
        auto item = getFirstItem(coll);
803
        DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(item->path())}));
804
        COMPARE(locked, {});
805
        auto prompt = getProxy<PromptProxy>(promptPath);
806
        VERIFY(prompt);
807

808
        MessageBox::setNextAnswer(MessageBox::Save);
809
        DBUS_VERIFY(prompt->Prompt(""));
810
        processEvents();
811

812
        DBUS_COMPARE(coll->locked(), true);
813
    }
814
}
815

816
void TestGuiFdoSecrets::testServiceLockConcurrent()
817
{
818
    auto service = enableService();
819
    VERIFY(service);
820
    auto coll = getDefaultCollection(service);
821
    VERIFY(coll);
822

823
    m_db->markAsModified();
824

825
    DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
826
    auto prompt = getProxy<PromptProxy>(promptPath);
827
    VERIFY(prompt);
828
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
829
    VERIFY(spyPromptCompleted.isValid());
830

831
    DBUS_GET2(locked2, promptPath2, service->Lock({QDBusObjectPath(coll->path())}));
832
    auto prompt2 = getProxy<PromptProxy>(promptPath2);
833
    VERIFY(prompt2);
834
    QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
835
    VERIFY(spyPromptCompleted2.isValid());
836

837
    // prompt and click save
838
    MessageBox::setNextAnswer(MessageBox::Save);
839
    DBUS_VERIFY(prompt->Prompt(""));
840

841
    // second prompt should not show dialog
842
    DBUS_VERIFY(prompt2->Prompt(""));
843

844
    VERIFY(waitForSignal(spyPromptCompleted, 1));
845
    {
846
        auto args = spyPromptCompleted.takeFirst();
847
        COMPARE(args.count(), 2);
848
        COMPARE(args.at(0).toBool(), false);
849
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
850
    }
851

852
    VERIFY(waitForSignal(spyPromptCompleted2, 1));
853
    {
854
        auto args = spyPromptCompleted2.takeFirst();
855
        COMPARE(args.count(), 2);
856
        COMPARE(args.at(0).toBool(), false);
857
        COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
858
    }
859

860
    DBUS_COMPARE(coll->locked(), true);
861
}
862

863
void TestGuiFdoSecrets::testSessionOpen()
864
{
865
    auto service = enableService();
866
    VERIFY(service);
867

868
    auto sess = openSession(service, PlainCipher::Algorithm);
869
    VERIFY(sess);
870

871
    sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
872
    VERIFY(sess);
873
}
874

875
void TestGuiFdoSecrets::testSessionClose()
876
{
877
    auto service = enableService();
878
    VERIFY(service);
879

880
    auto sess = openSession(service, PlainCipher::Algorithm);
881
    VERIFY(sess);
882

883
    DBUS_VERIFY(sess->Close());
884
}
885

886
void TestGuiFdoSecrets::testCollectionCreate()
887
{
888
    auto service = enableService();
889
    VERIFY(service);
890

891
    QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
892
    VERIFY(spyCollectionCreated.isValid());
893

894
    // returns existing if alias is nonempty and exists
895
    {
896
        auto existing = getDefaultCollection(service);
897
        DBUS_GET2(collPath,
898
                  promptPath,
899
                  service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "NewDB"}}, "default"));
900
        COMPARE(promptPath, QDBusObjectPath("/"));
901
        COMPARE(collPath.path(), existing->path());
902
    }
903
    VERIFY(waitForSignal(spyCollectionCreated, 0));
904

905
    // create new one and set properties
906
    {
907
        DBUS_GET2(collPath,
908
                  promptPath,
909
                  service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb"));
910
        COMPARE(collPath, QDBusObjectPath("/"));
911
        auto prompt = getProxy<PromptProxy>(promptPath);
912
        VERIFY(prompt);
913

914
        QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
915
        VERIFY(spyPromptCompleted.isValid());
916

917
        DBUS_VERIFY(prompt->Prompt(""));
918
        VERIFY(driveNewDatabaseWizard());
919

920
        VERIFY(waitForSignal(spyPromptCompleted, 1));
921
        auto args = spyPromptCompleted.takeFirst();
922
        COMPARE(args.size(), 2);
923
        COMPARE(args.at(0).toBool(), false);
924
        auto coll = getProxy<CollectionProxy>(getSignalVariantArgument<QDBusObjectPath>(args.at(1)));
925
        VERIFY(coll);
926

927
        DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB"));
928

929
        VERIFY(waitForSignal(spyCollectionCreated, 1));
930
        {
931
            args = spyCollectionCreated.takeFirst();
932
            COMPARE(args.size(), 1);
933
            COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
934
        }
935
    }
936
}
937

938
void TestGuiFdoSecrets::testCollectionDelete()
939
{
940
    auto service = enableService();
941
    VERIFY(service);
942
    auto coll = getDefaultCollection(service);
943
    VERIFY(coll);
944
    // save the path which will be gone after the deletion.
945
    auto collPath = coll->path();
946

947
    QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
948
    VERIFY(spyCollectionDeleted.isValid());
949

950
    m_db->markAsModified();
951
    DBUS_GET(promptPath, coll->Delete());
952
    auto prompt = getProxy<PromptProxy>(promptPath);
953
    VERIFY(prompt);
954
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
955
    VERIFY(spyPromptCompleted.isValid());
956

957
    // prompt and click save
958
    MessageBox::setNextAnswer(MessageBox::Save);
959
    DBUS_VERIFY(prompt->Prompt(""));
960

961
    // closing the tab should have deleted the database if not in testing
962
    // but deleteLater is not processed in QApplication::processEvent
963
    // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
964

965
    VERIFY(waitForSignal(spyPromptCompleted, 1));
966
    auto args = spyPromptCompleted.takeFirst();
967
    COMPARE(args.count(), 2);
968
    COMPARE(args.at(0).toBool(), false);
969
    COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
970

971
    // however, the object should already be taken down from dbus
972
    {
973
        auto reply = coll->locked();
974
        VERIFY(reply.isFinished() && reply.isError());
975
        COMPARE(reply.error().type(), QDBusError::UnknownObject);
976
    }
977

978
    VERIFY(waitForSignal(spyCollectionDeleted, 1));
979
    {
980
        args = spyCollectionDeleted.takeFirst();
981
        COMPARE(args.size(), 1);
982
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), collPath);
983
    }
984
}
985

986
void TestGuiFdoSecrets::testCollectionDeleteConcurrent()
987
{
988
    auto service = enableService();
989
    VERIFY(service);
990
    auto coll = getDefaultCollection(service);
991
    VERIFY(coll);
992

993
    m_db->markAsModified();
994
    DBUS_GET(promptPath, coll->Delete());
995
    auto prompt = getProxy<PromptProxy>(promptPath);
996
    VERIFY(prompt);
997
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
998
    VERIFY(spyPromptCompleted.isValid());
999

1000
    // before interacting with the prompt, another request come in
1001
    DBUS_GET(promptPath2, coll->Delete());
1002
    auto prompt2 = getProxy<PromptProxy>(promptPath);
1003
    VERIFY(prompt2);
1004
    QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
1005
    VERIFY(spyPromptCompleted2.isValid());
1006

1007
    // prompt and click save
1008
    MessageBox::setNextAnswer(MessageBox::Save);
1009
    DBUS_VERIFY(prompt->Prompt(""));
1010

1011
    // there should be no prompt
1012
    DBUS_VERIFY(prompt2->Prompt(""));
1013

1014
    VERIFY(waitForSignal(spyPromptCompleted, 1));
1015
    {
1016
        auto args = spyPromptCompleted.takeFirst();
1017
        COMPARE(args.count(), 2);
1018
        COMPARE(args.at(0).toBool(), false);
1019
        COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
1020
    }
1021

1022
    VERIFY(waitForSignal(spyPromptCompleted2, 1));
1023
    {
1024
        auto args = spyPromptCompleted2.takeFirst();
1025
        COMPARE(args.count(), 2);
1026
        COMPARE(args.at(0).toBool(), false);
1027
        COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
1028
    }
1029

1030
    {
1031
        auto reply = coll->locked();
1032
        VERIFY(reply.isFinished() && reply.isError());
1033
        COMPARE(reply.error().type(), QDBusError::UnknownObject);
1034
    }
1035
}
1036

1037
void TestGuiFdoSecrets::testCollectionChange()
1038
{
1039
    auto service = enableService();
1040
    VERIFY(service);
1041
    auto coll = getDefaultCollection(service);
1042
    VERIFY(coll);
1043

1044
    QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
1045
    VERIFY(spyCollectionChanged.isValid());
1046

1047
    DBUS_VERIFY(coll->setLabel("anotherLabel"));
1048
    COMPARE(m_db->metadata()->name(), QStringLiteral("anotherLabel"));
1049
    QTRY_COMPARE(spyCollectionChanged.size(), 1);
1050
    {
1051
        auto args = spyCollectionChanged.takeFirst();
1052
        COMPARE(args.size(), 1);
1053
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
1054
    }
1055
}
1056

1057
void TestGuiFdoSecrets::testHiddenFilename()
1058
{
1059
    // when file name contains leading dot, all parts excepting the last should be used
1060
    // for collection name, and the registration should success
1061
    VERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx"));
1062

1063
    // reset is necessary to not hold database longer and cause connections
1064
    // not cleaned up when the database tab is closed.
1065
    m_db.reset();
1066
    VERIFY(m_tabWidget->closeAllDatabaseTabs());
1067
    m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
1068
    m_dbWidget = m_tabWidget->currentDatabaseWidget();
1069
    m_db = m_dbWidget->database();
1070

1071
    // enable the service
1072
    auto service = enableService();
1073
    VERIFY(service);
1074

1075
    // collection is properly registered
1076
    auto coll = getDefaultCollection(service);
1077
    auto collObj = m_plugin->dbus()->pathToObject<Collection>(QDBusObjectPath(coll->path()));
1078
    VERIFY(collObj);
1079
    COMPARE(collObj->name(), QStringLiteral(".Name"));
1080
}
1081

1082
void TestGuiFdoSecrets::testDuplicateName()
1083
{
1084
    QTemporaryDir dir;
1085
    VERIFY(dir.isValid());
1086
    // create another file under different path but with the same filename
1087
    QString anotherFile = dir.path() + "/" + QFileInfo(*m_dbFile).fileName();
1088
    m_dbFile->copy(anotherFile);
1089
    m_tabWidget->addDatabaseTab(anotherFile, false, "a");
1090

1091
    auto service = enableService();
1092
    VERIFY(service);
1093

1094
    // when two databases have the same name, one of it will have part of its uuid suffixed
1095
    const QString pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC");
1096
    DBUS_GET(colls, service->collections());
1097
    COMPARE(colls.size(), 2);
1098
    COMPARE(colls[0].path(), pathNoSuffix);
1099
    VERIFY(colls[1].path() != pathNoSuffix);
1100
}
1101

1102
void TestGuiFdoSecrets::testItemCreate()
1103
{
1104
    auto service = enableService();
1105
    VERIFY(service);
1106
    auto coll = getDefaultCollection(service);
1107
    VERIFY(coll);
1108
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1109
    VERIFY(sess);
1110

1111
    QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1112
    VERIFY(spyItemCreated.isValid());
1113

1114
    // create item
1115
    StringStringMap attributes{
1116
        {"application", "fdosecrets-test"},
1117
        {"attr-i[bute]", "![some] -value*"},
1118
    };
1119

1120
    auto item = createItem(sess, coll, "abc", "Password", attributes, false);
1121
    VERIFY(item);
1122

1123
    // signals
1124
    {
1125
        VERIFY(waitForSignal(spyItemCreated, 1));
1126
        auto args = spyItemCreated.takeFirst();
1127
        COMPARE(args.size(), 1);
1128
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1129
    }
1130

1131
    // attributes
1132
    {
1133
        DBUS_GET(actual, item->attributes());
1134
        for (const auto& key : attributes.keys()) {
1135
            COMPARE(actual[key], attributes[key]);
1136
        }
1137
    }
1138

1139
    // label
1140
    DBUS_COMPARE(item->label(), QStringLiteral("abc"));
1141

1142
    // secrets
1143
    {
1144
        DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
1145
        auto decrypted = m_clientCipher->decrypt(ss.unmarshal(m_plugin->dbus()));
1146
        COMPARE(decrypted.value, QByteArrayLiteral("Password"));
1147
    }
1148

1149
    // searchable
1150
    {
1151
        DBUS_GET2(unlocked, locked, service->SearchItems(attributes));
1152
        COMPARE(locked, {});
1153
        COMPARE(unlocked, {QDBusObjectPath(item->path())});
1154
    }
1155
    {
1156
        DBUS_GET(unlocked, coll->SearchItems(attributes));
1157
        VERIFY(unlocked.contains(QDBusObjectPath(item->path())));
1158
    }
1159
}
1160

1161
void TestGuiFdoSecrets::testItemCreateUnlock()
1162
{
1163
    auto service = enableService();
1164
    VERIFY(service);
1165
    auto coll = getDefaultCollection(service);
1166
    VERIFY(coll);
1167
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1168
    VERIFY(sess);
1169

1170
    // NOTE: entries are no longer valid after locking
1171
    lockDatabaseInBackend();
1172

1173
    QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1174
    VERIFY(spyItemCreated.isValid());
1175

1176
    // create item
1177
    StringStringMap attributes{
1178
        {"application", "fdosecrets-test"},
1179
        {"attr-i[bute]", "![some] -value*"},
1180
    };
1181

1182
    auto item = createItem(sess, coll, "abc", "Password", attributes, false, false, true);
1183
    VERIFY(item);
1184
}
1185

1186
void TestGuiFdoSecrets::testItemChange()
1187
{
1188
    auto service = enableService();
1189
    VERIFY(service);
1190
    auto coll = getDefaultCollection(service);
1191
    VERIFY(coll);
1192
    auto item = getFirstItem(coll);
1193
    VERIFY(item);
1194
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1195
    VERIFY(itemObj);
1196
    auto entry = itemObj->backend();
1197
    VERIFY(entry);
1198

1199
    QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
1200
    VERIFY(spyItemChanged.isValid());
1201

1202
    DBUS_VERIFY(item->setLabel("anotherLabel"));
1203
    COMPARE(entry->title(), QStringLiteral("anotherLabel"));
1204
    QTRY_VERIFY(!spyItemChanged.isEmpty());
1205
    for (const auto& args : spyItemChanged) {
1206
        COMPARE(args.size(), 1);
1207
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1208
    }
1209

1210
    spyItemChanged.clear();
1211
    DBUS_VERIFY(item->setAttributes({
1212
        {"abc", "def"},
1213
    }));
1214
    COMPARE(entry->attributes()->value("abc"), QStringLiteral("def"));
1215
    QTRY_VERIFY(!spyItemChanged.isEmpty());
1216
    for (const auto& args : spyItemChanged) {
1217
        COMPARE(args.size(), 1);
1218
        COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1219
    }
1220
}
1221

1222
void TestGuiFdoSecrets::testItemReplace()
1223
{
1224
    auto service = enableService();
1225
    VERIFY(service);
1226
    auto coll = getDefaultCollection(service);
1227
    VERIFY(coll);
1228
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1229
    VERIFY(sess);
1230

1231
    // create item
1232
    StringStringMap attr1{
1233
        {"application", "fdosecrets-test"},
1234
        {"attr-i[bute]", "![some] -value*"},
1235
        {"fdosecrets-attr", "1"},
1236
    };
1237
    StringStringMap attr2{
1238
        {"application", "fdosecrets-test"},
1239
        {"attr-i[bute]", "![some] -value*"},
1240
        {"fdosecrets-attr", "2"},
1241
    };
1242

1243
    auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false);
1244
    VERIFY(item1);
1245
    auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false);
1246
    VERIFY(item2);
1247

1248
    {
1249
        DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1250
        QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
1251
        COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1252
    }
1253

1254
    QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1255
    VERIFY(spyItemCreated.isValid());
1256
    QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
1257
    VERIFY(spyItemChanged.isValid());
1258

1259
    {
1260
        // when replace, existing item with matching attr is updated
1261
        auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true);
1262
        VERIFY(item3);
1263
        COMPARE(item2->path(), item3->path());
1264
        DBUS_COMPARE(item3->label(), QStringLiteral("abc3"));
1265
        // there are still 2 entries
1266
        DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1267
        QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
1268
        COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1269

1270
        VERIFY(waitForSignal(spyItemCreated, 0));
1271
        // there may be multiple changed signals, due to each item attribute is set separately
1272
        QTRY_VERIFY(!spyItemChanged.isEmpty());
1273
        for (const auto& args : spyItemChanged) {
1274
            COMPARE(args.size(), 1);
1275
            COMPARE(args.at(0).value<QDBusObjectPath>().path(), item3->path());
1276
        }
1277
    }
1278

1279
    spyItemCreated.clear();
1280
    spyItemChanged.clear();
1281
    {
1282
        // when NOT replace, another entry is created
1283
        auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false);
1284
        VERIFY(item4);
1285
        DBUS_COMPARE(item2->label(), QStringLiteral("abc3"));
1286
        DBUS_COMPARE(item4->label(), QStringLiteral("abc4"));
1287
        // there are 3 entries
1288
        DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1289
        QSet<QDBusObjectPath> expected{
1290
            QDBusObjectPath(item1->path()),
1291
            QDBusObjectPath(item2->path()),
1292
            QDBusObjectPath(item4->path()),
1293
        };
1294
        COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1295

1296
        VERIFY(waitForSignal(spyItemCreated, 1));
1297
        {
1298
            auto args = spyItemCreated.takeFirst();
1299
            COMPARE(args.size(), 1);
1300
            COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
1301
        }
1302
        // there may be multiple changed signals, due to each item attribute is set separately
1303
        VERIFY(!spyItemChanged.isEmpty());
1304
        for (const auto& args : spyItemChanged) {
1305
            COMPARE(args.size(), 1);
1306
            COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
1307
        }
1308
    }
1309
}
1310

1311
void TestGuiFdoSecrets::testItemReplaceExistingLocked()
1312
{
1313
    auto service = enableService();
1314
    VERIFY(service);
1315
    auto coll = getDefaultCollection(service);
1316
    VERIFY(coll);
1317
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1318
    VERIFY(sess);
1319

1320
    // create item
1321
    StringStringMap attr1{
1322
        {"application", "fdosecrets-test"},
1323
        {"attr-i[bute]", "![some] -value*"},
1324
        {"fdosecrets-attr", "1"},
1325
    };
1326

1327
    auto item = createItem(sess, coll, "abc1", "Password", attr1, false);
1328
    VERIFY(item);
1329

1330
    // make sure the item is locked
1331
    {
1332
        auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1333
        VERIFY(itemObj);
1334
        auto entry = itemObj->backend();
1335
        VERIFY(entry);
1336
        FdoSecrets::settings()->setConfirmAccessItem(true);
1337
        m_client->setItemAuthorized(entry->uuid(), AuthDecision::Undecided);
1338
        DBUS_COMPARE(item->locked(), true);
1339
    }
1340

1341
    // when replace with a locked item, there will be a prompt
1342
    auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true);
1343
    VERIFY(item2);
1344
    COMPARE(item2->path(), item->path());
1345
    DBUS_COMPARE(item2->label(), QStringLiteral("abc2"));
1346
}
1347

1348
void TestGuiFdoSecrets::testItemSecret()
1349
{
1350
    const QString TEXT_PLAIN = "text/plain";
1351
    const QString APPLICATION_OCTET_STREAM = "application/octet-stream";
1352

1353
    auto service = enableService();
1354
    VERIFY(service);
1355
    auto coll = getDefaultCollection(service);
1356
    VERIFY(coll);
1357
    auto item = getFirstItem(coll);
1358
    VERIFY(item);
1359
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1360
    VERIFY(sess);
1361

1362
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1363
    VERIFY(itemObj);
1364
    auto entry = itemObj->backend();
1365
    VERIFY(entry);
1366

1367
    // plain text secret
1368
    {
1369
        DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1370
        auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1371
        COMPARE(ss.contentType, TEXT_PLAIN);
1372
        COMPARE(ss.value, entry->password().toUtf8());
1373
    }
1374

1375
    // get secret with notification
1376
    FdoSecrets::settings()->setShowNotification(true);
1377
    {
1378
        QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int)));
1379
        VERIFY(spyShowNotification.isValid());
1380

1381
        DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1382
        auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1383
        COMPARE(ss.contentType, TEXT_PLAIN);
1384
        COMPARE(ss.value, entry->password().toUtf8());
1385

1386
        COMPARE(ss.contentType, TEXT_PLAIN);
1387
        COMPARE(ss.value, entry->password().toUtf8());
1388

1389
        VERIFY(waitForSignal(spyShowNotification, 1));
1390
    }
1391
    FdoSecrets::settings()->setShowNotification(false);
1392

1393
    // set secret with plain text
1394
    {
1395
        // first create Secret in wire format,
1396
        // then convert to internal format and encrypt
1397
        // finally convert encrypted internal format back to wire format to pass to SetSecret
1398
        const QByteArray expected = QByteArrayLiteral("NewPassword");
1399
        auto encrypted = encryptPassword(expected, TEXT_PLAIN, sess);
1400
        DBUS_VERIFY(item->SetSecret(encrypted));
1401
        COMPARE(entry->password().toUtf8(), expected);
1402
    }
1403

1404
    // set secret with something else is saved as attachment
1405
    const QByteArray expected = QByteArrayLiteral("NewPasswordBinary");
1406
    {
1407
        auto encrypted = encryptPassword(expected, APPLICATION_OCTET_STREAM, sess);
1408
        DBUS_VERIFY(item->SetSecret(encrypted));
1409
        COMPARE(entry->password(), QStringLiteral(""));
1410
    }
1411
    {
1412
        DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1413
        auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1414
        COMPARE(ss.contentType, APPLICATION_OCTET_STREAM);
1415
        COMPARE(ss.value, expected);
1416
    }
1417
}
1418

1419
void TestGuiFdoSecrets::testItemDelete()
1420
{
1421
    FdoSecrets::settings()->setConfirmDeleteItem(true);
1422

1423
    auto service = enableService();
1424
    VERIFY(service);
1425
    auto coll = getDefaultCollection(service);
1426
    VERIFY(coll);
1427
    auto item = getFirstItem(coll);
1428
    VERIFY(item);
1429
    // save the path which will be gone after the deletion.
1430
    auto itemPath = item->path();
1431

1432
    QSignalSpy spyItemDeleted(coll.data(), SIGNAL(ItemDeleted(QDBusObjectPath)));
1433
    VERIFY(spyItemDeleted.isValid());
1434

1435
    DBUS_GET(promptPath, item->Delete());
1436
    auto prompt = getProxy<PromptProxy>(promptPath);
1437
    VERIFY(prompt);
1438

1439
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
1440
    VERIFY(spyPromptCompleted.isValid());
1441

1442
    // prompt and click save
1443
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1444
    VERIFY(itemObj);
1445
    MessageBox::setNextAnswer(MessageBox::Delete);
1446
    DBUS_VERIFY(prompt->Prompt(""));
1447

1448
    VERIFY(waitForSignal(spyPromptCompleted, 1));
1449
    auto args = spyPromptCompleted.takeFirst();
1450
    COMPARE(args.count(), 2);
1451
    COMPARE(args.at(0).toBool(), false);
1452
    COMPARE(args.at(1).toString(), QStringLiteral(""));
1453

1454
    VERIFY(waitForSignal(spyItemDeleted, 1));
1455
    args = spyItemDeleted.takeFirst();
1456
    COMPARE(args.size(), 1);
1457
    COMPARE(args.at(0).value<QDBusObjectPath>().path(), itemPath);
1458
}
1459

1460
void TestGuiFdoSecrets::testItemLockState()
1461
{
1462
    auto service = enableService();
1463
    VERIFY(service);
1464
    auto coll = getDefaultCollection(service);
1465
    VERIFY(coll);
1466
    auto item = getFirstItem(coll);
1467
    VERIFY(item);
1468
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1469
    VERIFY(sess);
1470
    auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1471
    VERIFY(itemObj);
1472
    auto entry = itemObj->backend();
1473
    VERIFY(entry);
1474

1475
    auto secret =
1476
        wire::Secret{
1477
            QDBusObjectPath(sess->path()),
1478
            {},
1479
            "NewPassword",
1480
            "text/plain",
1481
        }
1482
            .unmarshal(m_plugin->dbus());
1483
    auto encrypted = m_clientCipher->encrypt(secret).marshal();
1484

1485
    // when access confirmation is disabled, item is unlocked when the collection is unlocked
1486
    FdoSecrets::settings()->setConfirmAccessItem(false);
1487
    DBUS_COMPARE(item->locked(), false);
1488

1489
    // when access confirmation is enabled, item is locked if the client has no authorization
1490
    FdoSecrets::settings()->setConfirmAccessItem(true);
1491
    DBUS_COMPARE(item->locked(), true);
1492
    // however, item properties are still accessible as long as the collection is unlocked
1493
    DBUS_VERIFY(item->attributes());
1494
    DBUS_VERIFY(item->setAttributes({}));
1495
    DBUS_VERIFY(item->label());
1496
    DBUS_VERIFY(item->setLabel("abc"));
1497
    DBUS_VERIFY(item->created());
1498
    DBUS_VERIFY(item->modified());
1499
    // except secret, which is locked
1500
    {
1501
        auto reply = item->GetSecret(QDBusObjectPath(sess->path()));
1502
        VERIFY(reply.isError());
1503
        COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
1504
    }
1505
    {
1506
        auto reply = item->SetSecret(encrypted);
1507
        VERIFY(reply.isError());
1508
        COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
1509
    }
1510

1511
    // item is unlocked if the client is authorized
1512
    m_client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed);
1513
    DBUS_COMPARE(item->locked(), false);
1514
    DBUS_VERIFY(item->GetSecret(QDBusObjectPath(sess->path())));
1515
    DBUS_VERIFY(item->SetSecret(encrypted));
1516
}
1517

1518
void TestGuiFdoSecrets::testItemRejectSetReferenceFields()
1519
{
1520
    // expose a subgroup, entries in it should not be able to retrieve data from entries outside it
1521
    auto rootEntry = m_db->rootGroup()->entries().first();
1522
    VERIFY(rootEntry);
1523
    auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
1524
    VERIFY(subgroup);
1525
    FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1526
    auto service = enableService();
1527
    VERIFY(service);
1528
    auto coll = getDefaultCollection(service);
1529
    VERIFY(coll);
1530
    auto item = getFirstItem(coll);
1531
    VERIFY(item);
1532
    auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1533
    VERIFY(sess);
1534

1535
    const auto refText = QStringLiteral("{REF:P@T:%1}").arg(rootEntry->title());
1536

1537
    // reject ref in label
1538
    {
1539
        auto reply = item->setLabel(refText);
1540
        VERIFY(reply.isFinished() && reply.isError());
1541
        COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1542
    }
1543
    // reject ref in custom attributes
1544
    {
1545
        auto reply = item->setAttributes({{"steal", refText}});
1546
        VERIFY(reply.isFinished() && reply.isError());
1547
        COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1548
    }
1549
    // reject ref in password
1550
    {
1551
        auto reply = item->SetSecret(encryptPassword(refText.toUtf8(), "text/plain", sess));
1552
        VERIFY(reply.isFinished() && reply.isError());
1553
        COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1554
    }
1555
    // reject ref in content type
1556
    {
1557
        auto reply = item->SetSecret(encryptPassword("dummy", refText, sess));
1558
        VERIFY(reply.isFinished() && reply.isError());
1559
        COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1560
    }
1561
}
1562

1563
void TestGuiFdoSecrets::testAlias()
1564
{
1565
    auto service = enableService();
1566
    VERIFY(service);
1567

1568
    // read default alias
1569
    DBUS_GET(collPath, service->ReadAlias("default"));
1570
    auto coll = getProxy<CollectionProxy>(collPath);
1571
    VERIFY(coll);
1572

1573
    // set extra alias
1574
    DBUS_VERIFY(service->SetAlias("another", QDBusObjectPath(collPath)));
1575

1576
    // get using extra alias
1577
    DBUS_GET(collPath2, service->ReadAlias("another"));
1578
    COMPARE(collPath2, collPath);
1579
}
1580

1581
void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent()
1582
{
1583
    auto service = enableService();
1584
    VERIFY(service);
1585

1586
    // one collection, which is default alias
1587
    auto coll = getDefaultCollection(service);
1588
    VERIFY(coll);
1589

1590
    // after locking, the collection is still there, but locked
1591
    lockDatabaseInBackend();
1592

1593
    coll = getDefaultCollection(service);
1594
    VERIFY(coll);
1595
    DBUS_COMPARE(coll->locked(), true);
1596

1597
    // unlock the database, the alias and collection is present
1598
    unlockDatabaseInBackend();
1599

1600
    coll = getDefaultCollection(service);
1601
    VERIFY(coll);
1602
    DBUS_COMPARE(coll->locked(), false);
1603
}
1604

1605
void TestGuiFdoSecrets::testExposeSubgroup()
1606
{
1607
    auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
1608
    VERIFY(subgroup);
1609
    FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1610
    auto service = enableService();
1611
    VERIFY(service);
1612

1613
    auto coll = getDefaultCollection(service);
1614
    VERIFY(coll);
1615

1616
    // exposing subgroup does not expose entries in other groups
1617
    DBUS_GET(itemPaths, coll->items());
1618
    QSet<Entry*> exposedEntries;
1619
    for (const auto& itemPath : itemPaths) {
1620
        exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
1621
    }
1622
    COMPARE(exposedEntries, QSet<Entry*>::fromList(subgroup->entries()));
1623
}
1624

1625
void TestGuiFdoSecrets::testModifyingExposedGroup()
1626
{
1627
    // test when exposed group is removed the collection is not exposed anymore
1628
    auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking");
1629
    VERIFY(subgroup);
1630
    FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1631
    auto service = enableService();
1632
    VERIFY(service);
1633

1634
    {
1635
        DBUS_GET(collPaths, service->collections());
1636
        COMPARE(collPaths.size(), 1);
1637
    }
1638

1639
    m_db->metadata()->setRecycleBinEnabled(true);
1640
    m_db->recycleGroup(subgroup);
1641
    processEvents();
1642

1643
    {
1644
        DBUS_GET(collPaths, service->collections());
1645
        COMPARE(collPaths, {});
1646
    }
1647

1648
    // test setting another exposed group, the collection will be exposed again
1649
    FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
1650
    processEvents();
1651
    {
1652
        DBUS_GET(collPaths, service->collections());
1653
        COMPARE(collPaths.size(), 1);
1654
    }
1655
}
1656

1657
void TestGuiFdoSecrets::testNoExposeRecycleBin()
1658
{
1659
    // when the recycle bin is underneath the exposed group
1660
    // be careful not to expose entries in there
1661

1662
    FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
1663
    m_db->metadata()->setRecycleBinEnabled(true);
1664

1665
    auto entry = m_db->rootGroup()->entries().first();
1666
    VERIFY(entry);
1667
    m_db->recycleEntry(entry);
1668
    processEvents();
1669

1670
    auto service = enableService();
1671
    VERIFY(service);
1672

1673
    auto coll = getDefaultCollection(service);
1674
    VERIFY(coll);
1675

1676
    // exposing subgroup does not expose entries in other groups
1677
    DBUS_GET(itemPaths, coll->items());
1678
    QSet<Entry*> exposedEntries;
1679
    for (const auto& itemPath : itemPaths) {
1680
        exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
1681
    }
1682
    VERIFY(!exposedEntries.contains(entry));
1683

1684
    // searching should not return the entry
1685
    DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
1686
    COMPARE(locked, {});
1687
    COMPARE(unlocked, {});
1688
}
1689

1690
void TestGuiFdoSecrets::lockDatabaseInBackend()
1691
{
1692
    m_tabWidget->lockDatabases();
1693
    m_db.reset();
1694
    processEvents();
1695
}
1696

1697
void TestGuiFdoSecrets::unlockDatabaseInBackend()
1698
{
1699
    m_dbWidget->performUnlockDatabase("a");
1700
    m_db = m_dbWidget->database();
1701
    processEvents();
1702
}
1703

1704
void TestGuiFdoSecrets::processEvents()
1705
{
1706
    // Couldn't use QApplication::processEvents, because per Qt documentation:
1707
    //     events that are posted while the function runs will be queued until a later round of event processing.
1708
    // and we may post QTimer single shot events during event handling to achieve async method.
1709
    // So we directly call event dispatcher in a loop until no events can be handled
1710
    while (QAbstractEventDispatcher::instance()->processEvents(QEventLoop::AllEvents)) {
1711
        // pass
1712
    }
1713
}
1714

1715
// the following functions have return value, switch macros to the version supporting that
1716
#undef VERIFY
1717
#undef VERIFY2
1718
#undef COMPARE
1719
#define VERIFY(stmt) VERIFY2_RET(stmt, "")
1720
#define VERIFY2 VERIFY2_RET
1721
#define COMPARE COMPARE_RET
1722

1723
QSharedPointer<ServiceProxy> TestGuiFdoSecrets::enableService()
1724
{
1725
    FdoSecrets::settings()->setEnabled(true);
1726
    VERIFY(m_plugin);
1727
    m_plugin->updateServiceState();
1728
    return getProxy<ServiceProxy>(QDBusObjectPath(DBUS_PATH_SECRETS));
1729
}
1730

1731
QSharedPointer<SessionProxy> TestGuiFdoSecrets::openSession(const QSharedPointer<ServiceProxy>& service,
1732
                                                            const QString& algo)
1733
{
1734
    VERIFY(service);
1735

1736
    if (algo == PlainCipher::Algorithm) {
1737
        DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant("")));
1738

1739
        return getProxy<SessionProxy>(sessPath);
1740
    } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) {
1741
        DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(m_clientCipher->negotiationOutput())));
1742
        m_clientCipher->updateClientPublicKey(output.variant().toByteArray());
1743
        return getProxy<SessionProxy>(sessPath);
1744
    }
1745
    QTest::qFail("Unsupported algorithm", __FILE__, __LINE__);
1746
    return {};
1747
}
1748

1749
QSharedPointer<CollectionProxy> TestGuiFdoSecrets::getDefaultCollection(const QSharedPointer<ServiceProxy>& service)
1750
{
1751
    VERIFY(service);
1752
    DBUS_GET(collPath, service->ReadAlias("default"));
1753
    return getProxy<CollectionProxy>(collPath);
1754
}
1755

1756
QSharedPointer<ItemProxy> TestGuiFdoSecrets::getFirstItem(const QSharedPointer<CollectionProxy>& coll)
1757
{
1758
    VERIFY(coll);
1759
    DBUS_GET(itemPaths, coll->items());
1760
    VERIFY(!itemPaths.isEmpty());
1761
    return getProxy<ItemProxy>(itemPaths.first());
1762
}
1763

1764
QSharedPointer<ItemProxy> TestGuiFdoSecrets::createItem(const QSharedPointer<SessionProxy>& sess,
1765
                                                        const QSharedPointer<CollectionProxy>& coll,
1766
                                                        const QString& label,
1767
                                                        const QString& pass,
1768
                                                        const StringStringMap& attr,
1769
                                                        bool replace,
1770
                                                        bool expectPrompt,
1771
                                                        bool expectUnlockPrompt)
1772
{
1773
    VERIFY(sess);
1774
    VERIFY(coll);
1775

1776
    QVariantMap properties{
1777
        {DBUS_INTERFACE_SECRET_ITEM + ".Label", QVariant::fromValue(label)},
1778
        {DBUS_INTERFACE_SECRET_ITEM + ".Attributes", QVariant::fromValue(attr)},
1779
    };
1780

1781
    wire::Secret ss;
1782
    ss.session = QDBusObjectPath(sess->path());
1783
    ss.value = pass.toLocal8Bit();
1784
    ss.contentType = "plain/text";
1785
    auto encrypted = m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
1786

1787
    DBUS_GET2(itemPath, promptPath, coll->CreateItem(properties, encrypted, replace));
1788

1789
    auto prompt = getProxy<PromptProxy>(promptPath);
1790
    VERIFY(prompt);
1791
    QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
1792
    VERIFY(spyPromptCompleted.isValid());
1793

1794
    // drive the prompt
1795
    DBUS_VERIFY(prompt->Prompt(""));
1796

1797
    bool unlockFound = driveUnlockDialog();
1798
    COMPARE(unlockFound, expectUnlockPrompt);
1799

1800
    bool found = driveAccessControlDialog();
1801
    COMPARE(found, expectPrompt);
1802

1803
    VERIFY(waitForSignal(spyPromptCompleted, 1));
1804
    auto args = spyPromptCompleted.takeFirst();
1805
    COMPARE(args.size(), 2);
1806
    COMPARE(args.at(0).toBool(), false);
1807
    itemPath = getSignalVariantArgument<QDBusObjectPath>(args.at(1));
1808

1809
    return getProxy<ItemProxy>(itemPath);
1810
}
1811

1812
FdoSecrets::wire::Secret
1813
TestGuiFdoSecrets::encryptPassword(QByteArray value, QString contentType, const QSharedPointer<SessionProxy>& sess)
1814
{
1815
    wire::Secret ss;
1816
    ss.contentType = std::move(contentType);
1817
    ss.value = std::move(value);
1818
    ss.session = QDBusObjectPath(sess->path());
1819
    return m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
1820
}
1821

1822
bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember, bool includeFutureEntries)
1823
{
1824
    processEvents();
1825
    for (auto w : QApplication::topLevelWidgets()) {
1826
        if (!w->isWindow()) {
1827
            continue;
1828
        }
1829
        auto dlg = qobject_cast<AccessControlDialog*>(w);
1830
        if (dlg && dlg->isVisible()) {
1831
            auto rememberCheck = dlg->findChild<QCheckBox*>("rememberCheck");
1832
            VERIFY(rememberCheck);
1833
            rememberCheck->setChecked(remember);
1834

1835
            if (includeFutureEntries) {
1836
                dlg->done(AccessControlDialog::AllowAll);
1837
            } else {
1838
                dlg->done(AccessControlDialog::AllowSelected);
1839
            }
1840

1841
            processEvents();
1842
            VERIFY(dlg->isHidden());
1843
            return true;
1844
        }
1845
    }
1846
    return false;
1847
}
1848

1849
bool TestGuiFdoSecrets::driveNewDatabaseWizard()
1850
{
1851
    // processEvents will block because the NewDatabaseWizard is shown using exec
1852
    // which creates a local QEventLoop.
1853
    // so we do a little trick here to get the return value back
1854
    bool ret = false;
1855
    QTimer::singleShot(0, this, [this, &ret]() {
1856
        ret = [this]() -> bool {
1857
            auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>();
1858
            VERIFY(wizard);
1859

1860
            COMPARE(wizard->currentId(), 0);
1861
            wizard->next();
1862
            wizard->next();
1863
            COMPARE(wizard->currentId(), 2);
1864

1865
            // enter password
1866
            auto* passwordEdit =
1867
                wizard->findChild<PasswordWidget*>("enterPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
1868
            auto* passwordRepeatEdit =
1869
                wizard->findChild<PasswordWidget*>("repeatPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
1870
            VERIFY(passwordEdit);
1871
            VERIFY(passwordRepeatEdit);
1872
            QTest::keyClicks(passwordEdit, "test");
1873
            QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
1874
            QTest::keyClicks(passwordRepeatEdit, "test");
1875

1876
            // save database to temporary file
1877
            TemporaryFile tmpFile;
1878
            VERIFY(tmpFile.open());
1879
            tmpFile.close();
1880
            fileDialog()->setNextFileName(tmpFile.fileName());
1881

1882
            // click Continue on the warning due to weak password
1883
            MessageBox::setNextAnswer(MessageBox::ContinueWithWeakPass);
1884
            wizard->accept();
1885

1886
            tmpFile.remove();
1887
            return true;
1888
        }();
1889
    });
1890
    processEvents();
1891
    return ret;
1892
}
1893

1894
bool TestGuiFdoSecrets::driveUnlockDialog(DatabaseWidget* target)
1895
{
1896
    processEvents();
1897
    auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
1898
    VERIFY(dbOpenDlg);
1899
    if (!dbOpenDlg->isVisible()) {
1900
        return false;
1901
    }
1902
    dbOpenDlg->setActiveDatabaseTab(target);
1903

1904
    auto editPassword = dbOpenDlg->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
1905
    VERIFY(editPassword);
1906
    editPassword->setFocus();
1907
    QTest::keyClicks(editPassword, "a");
1908
    QTest::keyClick(editPassword, Qt::Key_Enter);
1909
    processEvents();
1910
    return true;
1911
}
1912

1913
bool TestGuiFdoSecrets::waitForSignal(QSignalSpy& spy, int expectedCount)
1914
{
1915
    processEvents();
1916
    // If already expected count, do not wait and return immediately
1917
    if (spy.count() == expectedCount) {
1918
        return true;
1919
    } else if (spy.count() > expectedCount) {
1920
        return false;
1921
    }
1922
    spy.wait();
1923
    COMPARE(spy.count(), expectedCount);
1924
    return true;
1925
}
1926

1927
#undef VERIFY
1928
#define VERIFY QVERIFY
1929
#undef COMPARE
1930
#define COMPARE QCOMPARE
1931

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.