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"