File indexing completed on 2024-05-12 05:52:48

0001 /*
0002  * SPDX-License-Identifier: GPL-3.0-or-later
0003  * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
0004  */
0005 #include "account/account.h"
0006 
0007 #include "../test-utils/output.h"
0008 #include "../../test-utils/spy.h"
0009 #include "../../secrets/test-utils/random.h"
0010 
0011 #include <QDateTime>
0012 #include <QFile>
0013 #include <QSignalSpy>
0014 #include <QString>
0015 #include <QTest>
0016 #include <QVector>
0017 #include <QtDebug>
0018 
0019 #include <string.h>
0020 
0021 static QString testIniResource(QLatin1String("test.ini"));
0022 static QString testIniLockFile(QLatin1String("test.ini.lock"));
0023 
0024 class StorageLifeCyclesTest: public QObject
0025 {
0026     Q_OBJECT
0027 private Q_SLOTS:
0028     void initTestCase(void);
0029     void testLifecycle(void);
0030 };
0031 
0032 void StorageLifeCyclesTest::initTestCase(void)
0033 {
0034     QVERIFY2(test::ensureOutputDirectory(), "output directory should be available");
0035     QVERIFY2(test::copyResourceAsWritable(QStringLiteral(":/storage-lifecycles/starting.ini"), testIniResource), "test corpus INI resource should be available as file");
0036 }
0037 
0038 void StorageLifeCyclesTest::testLifecycle(void)
0039 {
0040     const QString iniResource = test::path(testIniResource);
0041     const QString initialAccountFullName(QLatin1String("autotests:valid-hotp-sample-1"));
0042     const QString addedAccountFullName(QLatin1String("autotests:valid-totp-sample-1"));
0043     const QString accountIssuer(QLatin1String("autotests"));
0044     const QString initialAccountName(QLatin1String("valid-hotp-sample-1"));
0045     const QString addedAccountName(QLatin1String("valid-totp-sample-1"));
0046 
0047     const accounts::SettingsProvider settings([&iniResource](const accounts::PersistenceAction &action) -> void
0048     {
0049         QSettings data(iniResource, QSettings::IniFormat);
0050         action(data);
0051     });
0052 
0053     QThread *thread = new QThread(this);
0054     QSignalSpy threadStarted(thread, &QThread::started);
0055     QSignalSpy threadFinished(thread, &QThread::finished);
0056     QSignalSpy threadCleaned(thread, &QThread::destroyed);
0057 
0058     thread->start();
0059     QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now");
0060 
0061     accounts::AccountSecret *secret = new accounts::AccountSecret(&test::fakeRandom);
0062     QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded);
0063     QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded);
0064     QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable);
0065     QSignalSpy keyAvailable(secret, &accounts::AccountSecret::keyAvailable);
0066     QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled);
0067     QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed);
0068 
0069     accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread, secret);
0070     QSignalSpy error(uut, &accounts::AccountStorage::error);
0071     QSignalSpy loaded(uut, &accounts::AccountStorage::loaded);
0072     QSignalSpy accountAdded(uut, &accounts::AccountStorage::added);
0073     QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed);
0074     QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed);
0075     QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed);
0076 
0077     // first phase: check that account objects can be loaded from storage
0078     QCOMPARE(uut->isLoaded(), false);
0079     QCOMPARE(uut->hasError(), false);
0080     QCOMPARE(accountAdded.count(), 0);
0081     QCOMPARE(loaded.count(), 0);
0082     QCOMPARE(error.count(), 0);
0083     QVERIFY2(uut->isAccountStillAvailable(initialAccountName, accountIssuer),  "sample account, issuer should still be available");
0084     QVERIFY2(uut->isAccountStillAvailable(initialAccountFullName),  "sample account (full name) should still be available");
0085     QVERIFY2(uut->isAccountStillAvailable(addedAccountName, accountIssuer),  "new account, issuer should still be available");
0086     QVERIFY2(uut->isAccountStillAvailable(addedAccountFullName),  "new account (full name) should still be available");
0087     QCOMPARE(uut->accounts(), QVector<QString>());
0088 
0089     // expect that unlocking is scheduled automatically, so advancing the event loop should trigger the signal
0090     QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked by now");
0091     QCOMPARE(newPasswordNeeded.count(), 0);
0092 
0093     QString password(QLatin1String("password"));
0094     secret->answerExistingPassword(password);
0095 
0096     QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password should have been accepted by now");
0097     QCOMPARE(password, QString(QLatin1String("********")));
0098 
0099     QVERIFY2(test::signal_eventually_emitted_once(keyAvailable, 2500), "key should have been derived by now");
0100 
0101     // expect that loading is scheduled automatically, so advancing the event loop should trigger the signal
0102     QVERIFY2(test::signal_eventually_emitted_once(loaded), "sample account should be loaded by now");
0103     QCOMPARE(uut->isLoaded(), true);
0104     QCOMPARE(uut->hasError(), false);
0105     QCOMPARE(error.count(), 0);
0106     QCOMPARE(accountAdded.count(), 1);
0107     QCOMPARE(accountAdded.at(0).at(0), initialAccountFullName);
0108 
0109     QVERIFY2(!uut->isAccountStillAvailable(initialAccountName, accountIssuer),  "sample account, issuer should no longer be available");
0110     QVERIFY2(!uut->isAccountStillAvailable(initialAccountFullName),  "sample account (full name) should no longer be available");
0111     QVERIFY2(uut->isAccountStillAvailable(addedAccountName, accountIssuer),  "new account, issuer should still be available");
0112     QVERIFY2(uut->isAccountStillAvailable(addedAccountFullName),  "new account (full name) should still be available");
0113     QCOMPARE(uut->accounts(), QVector<QString>() << initialAccountFullName);
0114 
0115     QVERIFY2(uut->contains(initialAccountName, accountIssuer), "contains(name, issuer) should report the sample account");
0116     QVERIFY2(uut->contains(initialAccountFullName), "contains(full name) should report the sample account");
0117 
0118     accounts::Account *initialAccount = uut->get(initialAccountFullName);
0119     QVERIFY2(initialAccount != nullptr, "get(full name) should return the sample account");
0120     QCOMPARE(uut->get(initialAccountName, accountIssuer), initialAccount);
0121 
0122     QSignalSpy initialAccountRemoved(initialAccount, &accounts::Account::removed);
0123     QSignalSpy initialAccountCleaned(initialAccount, &accounts::Account::destroyed);
0124 
0125     QCOMPARE(initialAccount->name(), initialAccountName);
0126     QCOMPARE(initialAccount->issuer(), accountIssuer);
0127     QCOMPARE(initialAccount->algorithm(), accounts::Account::Hotp);
0128     QCOMPARE(initialAccount->token(), QString());
0129 
0130     QCOMPARE(initialAccount->counter(), 42ULL);
0131     QCOMPARE(initialAccount->tokenLength(), 7);
0132     QCOMPARE(initialAccount->offset(), std::nullopt);
0133     QCOMPARE(initialAccount->checksum(), false);
0134 
0135     QFile initialLockFile(test::path(testIniLockFile));
0136     QVERIFY2(!initialLockFile.exists(), "initial: lock file should not be present anymore");
0137 
0138     QFile initialIni(iniResource);
0139     QVERIFY2(initialIni.exists(), "initial: accounts file should still exist");
0140     QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/storage-lifecycles/starting.ini")));
0141 
0142     // second phase: check that account objects can be removed from storage
0143     QCOMPARE(accountRemoved.count(), 0);
0144 
0145     initialAccount->remove();
0146 
0147     QVERIFY2(test::signal_eventually_emitted_once(accountRemoved), "sample account should be removed from storage by now");
0148     QCOMPARE(accountRemoved.at(0).at(0), initialAccountFullName);
0149 
0150     QVERIFY2(uut->isAccountStillAvailable(initialAccountName, accountIssuer),  "sample account, issuer should again be available");
0151     QVERIFY2(uut->isAccountStillAvailable(initialAccountFullName),  "sample account (full name) should again be available");
0152     QVERIFY2(uut->isAccountStillAvailable(addedAccountName, accountIssuer),  "new account, issuer should still be available");
0153     QVERIFY2(uut->isAccountStillAvailable(addedAccountFullName),  "new account (full name) should still be available");
0154     QCOMPARE(uut->accounts(), QVector<QString>());
0155 
0156     QVERIFY2(!uut->contains(initialAccountName, accountIssuer), "contains(name, issuer) should no longer report the sample account");
0157     QVERIFY2(!uut->contains(initialAccountFullName), "contains(full name) should no longer report the sample account");
0158     QVERIFY2(uut->get(initialAccountFullName) == nullptr, "get(full name) should no longer return the sample account");
0159     QVERIFY2(uut->get(initialAccountName, accountIssuer) == nullptr, "get(name, issuer) should no longer return the sample account");
0160 
0161     QFile afterRemovingLockFile(test::path(testIniLockFile));
0162     QVERIFY2(!afterRemovingLockFile.exists(), "after removing: lock file should not be present anymore");
0163 
0164     QFile afterRemovingIni(iniResource);
0165     QVERIFY2(afterRemovingIni.exists(), "after removing: accounts file should still exist");
0166     QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/storage-lifecycles/after-removing.ini")));
0167 
0168     QVERIFY2(test::signal_eventually_emitted_once(initialAccountRemoved), "sample account should have signalled its own removal by now");
0169     QVERIFY2(test::signal_eventually_emitted_once(initialAccountCleaned), "sample account should be cleaned up by now");
0170 
0171     // third phase: check that new account objects can be added to storage
0172     uut->addTotp(addedAccountName, accountIssuer, QLatin1String("NBSWY3DPFQQHO33SNRSCC==="), 8U, 42U);
0173 
0174     QVERIFY2(test::signal_eventually_emitted_twice(accountAdded), "new account should be added to storage by now");
0175     QCOMPARE(error.count(), 0);
0176     QCOMPARE(accountAdded.at(1).at(0), addedAccountFullName);
0177 
0178     QVERIFY2(uut->isAccountStillAvailable(initialAccountName, accountIssuer),  "sample account, issuer should again still be available");
0179     QVERIFY2(uut->isAccountStillAvailable(initialAccountFullName),  "sample account (full name) should again still be available");
0180     QVERIFY2(!uut->isAccountStillAvailable(addedAccountName, accountIssuer),  "new account, issuer should no longer be available");
0181     QVERIFY2(!uut->isAccountStillAvailable(addedAccountFullName),  "new account (full name) should no longer be available");
0182     QCOMPARE(uut->accounts(), QVector<QString>() << addedAccountFullName);
0183 
0184     QVERIFY2(uut->contains(addedAccountName, accountIssuer), "contains(name, issuer) should report the new account");
0185     QVERIFY2(uut->contains(addedAccountFullName), "contains(full name) should report the new account");
0186 
0187     accounts::Account *addedAccount = uut->get(addedAccountFullName);
0188     QVERIFY2(addedAccount != nullptr, "get(full name) should return the new account");
0189     QCOMPARE(uut->get(addedAccountName, accountIssuer), addedAccount);
0190 
0191     QSignalSpy addedAccountRemoved(addedAccount, &accounts::Account::removed);
0192     QSignalSpy addedAccountCleaned(addedAccount, &accounts::Account::destroyed);
0193 
0194     QCOMPARE(addedAccount->name(), addedAccountName);
0195     QCOMPARE(addedAccount->issuer(), accountIssuer);
0196     QCOMPARE(addedAccount->algorithm(), accounts::Account::Totp);
0197     QCOMPARE(addedAccount->token(), QString());
0198 
0199     QCOMPARE(addedAccount->timeStep(), 42U);
0200     QCOMPARE(addedAccount->tokenLength(), 8);
0201     QCOMPARE(addedAccount->epoch(), QDateTime::fromMSecsSinceEpoch(0));
0202     QCOMPARE(addedAccount->hash(), accounts::Account::Sha1);
0203 
0204     QFile afterAddingLockFile(test::path(testIniLockFile));
0205     QVERIFY2(!afterAddingLockFile.exists(), "after adding: lock file should not be present anymore");
0206 
0207     QFile afterAddingIni(iniResource);
0208     QVERIFY2(afterAddingIni.exists(), "after adding: accounts file should still exist");
0209     QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/storage-lifecycles/after-adding.ini")));
0210 
0211     // fourth phase: check that disposing storage cleans up objects properly
0212     uut->dispose();
0213 
0214     QVERIFY2(!uut->isAccountStillAvailable(initialAccountName, accountIssuer),  "sample account, issuer should again no longer be available");
0215     QVERIFY2(!uut->isAccountStillAvailable(initialAccountFullName),  "sample account (full name) should again no longer be available");
0216     QVERIFY2(!uut->isAccountStillAvailable(addedAccountName, accountIssuer),  "new account, issuer should no longer be available still");
0217     QVERIFY2(!uut->isAccountStillAvailable(addedAccountFullName),  "new account (full name) should no longer be available still");
0218     QCOMPARE(uut->accounts(), QVector<QString>());
0219 
0220     QVERIFY2(!uut->contains(addedAccountName, accountIssuer), "contains(name, issuer) should no longer report the new account");
0221     QVERIFY2(!uut->contains(addedAccountFullName), "contains(full name) should no longer report the new account");
0222     QVERIFY2(uut->get(addedAccountFullName) == nullptr, "get(full name) should no longer return the new account");
0223     QVERIFY2(uut->get(addedAccountName, accountIssuer) == nullptr, "get(name, issuer) should no longer return the new account");
0224 
0225     QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now");
0226 
0227     /*
0228      * The disposed() signal is the hook for consuming code to know when to drop objects.
0229      * Check that it is emitted *before* account objects are actually destroyed, i.e that the signal arrives before, and not after the fact.
0230      */
0231     QVERIFY2(test::signal_eventually_emitted_once(storageDisposed), "storage should be disposed of by now");
0232     QCOMPARE(addedAccountCleaned.count(), 0);
0233 
0234     QVERIFY2(test::signal_eventually_emitted_once(addedAccountCleaned), "new account should be disposed of by now");
0235     QVERIFY2(test::signal_eventually_emitted_once(secretCleaned), "account secret should be cleaned up by now");
0236 
0237     // fifth phase: check the sum-total effects
0238 
0239     QCOMPARE(error.count(), 0);
0240     QCOMPARE(loaded.count(), 1);
0241     QCOMPARE(addedAccountRemoved.count(), 0);
0242     QCOMPARE(accountAdded.count(), 2);
0243     QCOMPARE(accountRemoved.count(), 1);
0244     QCOMPARE(initialAccountRemoved.count(), 1);
0245     QCOMPARE(initialAccountCleaned.count(), 1);
0246     QCOMPARE(addedAccountRemoved.count(), 0);
0247     QCOMPARE(addedAccountCleaned.count(), 1);
0248 
0249     QFile finalIni(iniResource);
0250     QVERIFY2(finalIni.exists(), "final: accounts file should still exist");
0251     QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/storage-lifecycles/after-adding.ini")));
0252 
0253     // sixth phase: wind down test
0254     QCOMPARE(threadFinished.count(), 0);
0255 
0256     thread->quit();
0257     QVERIFY2(test::signal_eventually_emitted_once(threadFinished), "thread should be finished by now");
0258 
0259     thread->deleteLater();
0260     QVERIFY2(test::signal_eventually_emitted_once(threadCleaned), "thread should be cleaned up by now");
0261 
0262     uut->deleteLater();
0263     QVERIFY2(test::signal_eventually_emitted_once(storageCleaned), "storage should be cleaned up by now");
0264 }
0265 
0266 QTEST_MAIN(StorageLifeCyclesTest)
0267 
0268 #include "storage-object-lifecycles.moc"