File indexing completed on 2024-05-12 05:52:47
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 static QString testIniResource(QLatin1String("test.ini")); 0020 static QString testIniLockFile(QLatin1String("test.ini.lock")); 0021 0022 class HotpCounterUpdateTest: public QObject 0023 { 0024 Q_OBJECT 0025 private Q_SLOTS: 0026 void initTestCase(void); 0027 void testCounterUpdate(void); 0028 }; 0029 0030 void HotpCounterUpdateTest::initTestCase(void) 0031 { 0032 QVERIFY2(test::ensureOutputDirectory(), "output directory should be available"); 0033 QVERIFY2(test::copyResourceAsWritable(QStringLiteral(":/counter-update/starting.ini"), testIniResource), "test corpus INI resource should be available as file"); 0034 } 0035 0036 void HotpCounterUpdateTest::testCounterUpdate(void) 0037 { 0038 const QString iniResource = test::path(testIniResource); 0039 const QString sampleAccountName(QLatin1String("valid-hotp-sample-1")); 0040 const QString originalToken(QLatin1String("755224")); 0041 const QString updatedToken(QLatin1String("287082")); 0042 0043 const accounts::SettingsProvider settings([&iniResource](const accounts::PersistenceAction &action) -> void 0044 { 0045 QSettings data(iniResource, QSettings::IniFormat); 0046 action(data); 0047 }); 0048 0049 QThread *thread = new QThread(this); 0050 QSignalSpy threadStarted(thread, &QThread::started); 0051 QSignalSpy threadFinished(thread, &QThread::finished); 0052 QSignalSpy threadCleaned(thread, &QThread::destroyed); 0053 0054 thread->start(); 0055 QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now"); 0056 0057 accounts::AccountSecret *secret = new accounts::AccountSecret(&test::fakeRandom); 0058 QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded); 0059 QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded); 0060 QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable); 0061 QSignalSpy keyAvailable(secret, &accounts::AccountSecret::keyAvailable); 0062 QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled); 0063 QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed); 0064 0065 accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread, secret); 0066 QSignalSpy error(uut, &accounts::AccountStorage::error); 0067 QSignalSpy loaded(uut, &accounts::AccountStorage::loaded); 0068 QSignalSpy accountAdded(uut, &accounts::AccountStorage::added); 0069 QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed); 0070 QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed); 0071 QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed); 0072 0073 // first phase: check that account objects can be loaded from storage 0074 0075 // expect that unlocking is scheduled automatically, so advancing the event loop should trigger the signal 0076 QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked by now"); 0077 QCOMPARE(newPasswordNeeded.count(), 0); 0078 0079 QString password(QLatin1String("password")); 0080 secret->answerExistingPassword(password); 0081 0082 QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password should have been accepted by now"); 0083 QCOMPARE(password, QString(QLatin1String("********"))); 0084 0085 QVERIFY2(test::signal_eventually_emitted_once(keyAvailable, 2500), "key should have been derived by now"); 0086 0087 // expect that loading is scheduled automatically, so advancing the event loop should trigger the signal 0088 QVERIFY2(test::signal_eventually_emitted_once(loaded), "sample account should be loaded by now"); 0089 QCOMPARE(uut->isLoaded(), true); 0090 QCOMPARE(uut->hasError(), false); 0091 QCOMPARE(error.count(), 0); 0092 QCOMPARE(accountAdded.count(), 1); 0093 QCOMPARE(accountAdded.at(0).at(0), sampleAccountName); 0094 0095 accounts::Account *sampleAccount = uut->get(sampleAccountName); 0096 QVERIFY2(sampleAccount != nullptr, "get() should return the sample account"); 0097 0098 QSignalSpy sampleAccountUpdated(sampleAccount, &accounts::Account::updated); 0099 QSignalSpy sampleAccountRemoved(sampleAccount, &accounts::Account::removed); 0100 QSignalSpy sampleAccountCleaned(sampleAccount, &accounts::Account::destroyed); 0101 QSignalSpy sampleAccountTokenUpdated(sampleAccount, &accounts::Account::tokenChanged); 0102 0103 QCOMPARE(sampleAccount->name(), sampleAccountName); 0104 QCOMPARE(sampleAccount->issuer(), QString()); 0105 QCOMPARE(sampleAccount->algorithm(), accounts::Account::Hotp); 0106 QCOMPARE(sampleAccount->token(), QString()); 0107 0108 QCOMPARE(sampleAccount->counter(), 0ULL); 0109 QCOMPARE(sampleAccount->tokenLength(), 6); 0110 QCOMPARE(sampleAccount->offset(), std::nullopt); 0111 QCOMPARE(sampleAccount->checksum(), false); 0112 0113 QFile initialLockFile(test::path(testIniLockFile)); 0114 QVERIFY2(!initialLockFile.exists(), "initial: lock file should not be present anymore"); 0115 0116 QFile initialIni(iniResource); 0117 QVERIFY2(initialIni.exists(), "initial: accounts file should still exist"); 0118 QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/counter-update/starting.ini"))); 0119 0120 // second phase: check that hotp tokens can be (re)computed 0121 sampleAccount->recompute(); 0122 0123 QVERIFY2(test::signal_eventually_emitted_once(sampleAccountTokenUpdated), "sample account token should be recomputed by now"); 0124 QCOMPARE(sampleAccountTokenUpdated.at(0).at(0), originalToken); 0125 0126 QFile afterComputingTokenLockFile(test::path(testIniLockFile)); 0127 QVERIFY2(!afterComputingTokenLockFile.exists(), "after computing token: lock file should still not be present"); 0128 0129 QFile afterComputingTokenIni(iniResource); 0130 QVERIFY2(afterComputingTokenIni.exists(), "after computing token: accounts file should still exist"); 0131 QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/counter-update/starting.ini"))); 0132 0133 // third phase: check that hotp counters can be updated in storage 0134 sampleAccount->advanceCounter(); 0135 0136 QVERIFY2(test::signal_eventually_emitted_once(sampleAccountUpdated), "sample account should be updated in storage by now"); 0137 QVERIFY2(test::signal_eventually_emitted_twice(sampleAccountTokenUpdated), "sample account token should be updated to the new counter by now"); 0138 QCOMPARE(sampleAccountTokenUpdated.at(1).at(0).toString(), updatedToken); 0139 QCOMPARE(uut->hasError(), false); 0140 QCOMPARE(error.count(), 0); 0141 0142 QFile afterUpdatingCounterLockFile(test::path(testIniLockFile)); 0143 QVERIFY2(!afterUpdatingCounterLockFile.exists(), "after updating counter: lock file should not be present anymore"); 0144 0145 QFile afterUpdatingCounterIni(iniResource); 0146 QVERIFY2(afterUpdatingCounterIni.exists(), "after updating counter: accounts file should still exist"); 0147 QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/counter-update/after-updating-counter.ini"))); 0148 0149 // fourth phase: check that disposing storage cleans up objects properly 0150 uut->dispose(); 0151 0152 QVERIFY2(test::signal_eventually_emitted_once(storageDisposed), "storage should be disposed of by now"); 0153 QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now"); 0154 QVERIFY2(test::signal_eventually_emitted_once(sampleAccountCleaned), "sample account should be cleaned up by now"); 0155 QVERIFY2(test::signal_eventually_emitted_once(secretCleaned), "account secret should be cleaned up by now"); 0156 0157 // fifth phase: check the sum-total effects 0158 0159 QCOMPARE(error.count(), 0); 0160 QCOMPARE(loaded.count(), 1); 0161 QCOMPARE(accountAdded.count(), 1); 0162 QCOMPARE(accountRemoved.count(), 0); 0163 QCOMPARE(sampleAccountRemoved.count(), 0); 0164 QCOMPARE(sampleAccountCleaned.count(), 1); 0165 0166 QFile finalIni(iniResource); 0167 QVERIFY2(finalIni.exists(), "final: accounts file should still exist"); 0168 QCOMPARE(test::slurp(iniResource), test::slurp(QLatin1String(":/counter-update/after-updating-counter.ini"))); 0169 0170 // sixth phase: wind down test 0171 QCOMPARE(threadFinished.count(), 0); 0172 0173 thread->quit(); 0174 QVERIFY2(test::signal_eventually_emitted_once(threadFinished), "thread should be finished by now"); 0175 0176 thread->deleteLater(); 0177 QVERIFY2(test::signal_eventually_emitted_once(threadCleaned), "thread should be cleaned up by now"); 0178 0179 uut->deleteLater(); 0180 QVERIFY2(test::signal_eventually_emitted_once(storageCleaned), "storage should be cleaned up by now"); 0181 } 0182 0183 QTEST_MAIN(HotpCounterUpdateTest) 0184 0185 #include "hotp-counter-update.moc"