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"