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/actions_p.h"
0006 
0007 #include "../test-utils/output.h"
0008 #include "../../secrets/test-utils/random.h"
0009 #include "../../test-utils/spy.h"
0010 
0011 #include <QSignalSpy>
0012 #include <QString>
0013 #include <QTest>
0014 #include <QtDebug>
0015 
0016 static QString existingPasswordIniResource(QStringLiteral(":/request-account-password/existing-password.ini"));
0017 static QString newPasswordIniResource(QStringLiteral(":/request-account-password/new-password.ini"));
0018 static QString newPasswordIniResultResource(QStringLiteral(":/request-account-password/new-password-result.ini"));
0019 
0020 class RequestAccountPasswordTest: public QObject // clazy:exclude=ctor-missing-parent-argument
0021 {
0022     Q_OBJECT
0023 private Q_SLOTS:
0024     void testExistingPassword(void);
0025     void testExistingPasswordAbort(void);
0026     void testExistingPasswordRetry(void);
0027     void testNewPassword(void);
0028     void testNewPasswordAbort(void);
0029     void testAbortBeforeRun(void);
0030 };
0031 
0032 void RequestAccountPasswordTest::testAbortBeforeRun(void)
0033 {
0034     const QString isolated(QStringLiteral("abort-before-run.ini"));
0035     QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file");
0036 
0037     int openCounter = 0;
0038     const QString actualIni = test::path(isolated);
0039     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0040     {
0041         QSettings data(actualIni, QSettings::IniFormat);
0042         openCounter++;
0043         action(data);
0044     });
0045 
0046     accounts::AccountSecret secret;
0047     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0048     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0049     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0050     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0051     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0052     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0053 
0054     accounts::RequestAccountPassword uut(settings, &secret);
0055 
0056     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0057     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0058     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0059 
0060     secret.cancelRequests();
0061     uut.run();
0062 
0063     QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now");
0064     QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts");
0065     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0066 
0067     QCOMPARE(openCounter, 0);
0068     QCOMPARE(newPasswordNeeded.count(), 0);
0069     QCOMPARE(existingPasswordNeeded.count(), 0);
0070     QCOMPARE(passwordAvailable.count(), 0);
0071     QCOMPARE(keyAvailable.count(), 0);
0072     QCOMPARE(keyFailed.count(), 0);
0073     QCOMPARE(passwordRequestsCancelled.count(), 1);
0074     QCOMPARE(failed.count(), 1);
0075     QCOMPARE(unlocked.count(), 0);
0076 
0077     QFile result(actualIni);
0078     QVERIFY2(result.exists(), "accounts file should still exist");
0079     QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResource));
0080 }
0081 
0082 void RequestAccountPasswordTest::testNewPassword(void)
0083 {
0084     const QString isolated(QStringLiteral("supply-new-password.ini"));
0085     QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file");
0086 
0087     int openCounter = 0;
0088     const QString actualIni = test::path(isolated);
0089     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0090     {
0091         QSettings data(actualIni, QSettings::IniFormat);
0092         openCounter++;
0093         action(data);
0094     });
0095 
0096     accounts::AccountSecret secret(&test::fakeRandom);
0097     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0098     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0099     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0100     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0101     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0102     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0103 
0104     accounts::RequestAccountPassword uut(settings, &secret);
0105 
0106     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0107     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0108     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0109 
0110     uut.run();
0111 
0112     QVERIFY2(test::signal_eventually_emitted_once(newPasswordNeeded), "(new) password should be asked for");
0113     QCOMPARE(openCounter, 1);
0114     QCOMPARE(existingPasswordNeeded.count(), 0);
0115     QCOMPARE(failed.count(), 0);
0116     QCOMPARE(unlocked.count(), 0);
0117     QCOMPARE(jobFinished.count(), 0);
0118 
0119     QString password(QStringLiteral("hello, world"));
0120     std::optional<secrets::KeyDerivationParameters> defaults = secrets::KeyDerivationParameters::create();
0121     QVERIFY2(defaults, "should be able to construct default key derivation parameters");
0122     QVERIFY2(secret.answerNewPassword(password, *defaults), "should be able to answer (new) password");
0123 
0124     QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(new) password should be accepted");
0125     QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived");
0126     QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked");
0127     QCOMPARE(openCounter, 2);
0128 
0129     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0130 
0131     QCOMPARE(openCounter, 2);
0132     QCOMPARE(newPasswordNeeded.count(), 1);
0133     QCOMPARE(existingPasswordNeeded.count(), 0);
0134     QCOMPARE(passwordAvailable.count(), 1);
0135     QCOMPARE(keyAvailable.count(), 1);
0136     QCOMPARE(keyFailed.count(), 0);
0137     QCOMPARE(passwordRequestsCancelled.count(), 0);
0138     QCOMPARE(failed.count(), 0);
0139     QCOMPARE(unlocked.count(), 1);
0140 
0141     QFile result(actualIni);
0142     QVERIFY2(result.exists(), "accounts file should still exist");
0143     QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResultResource));
0144 }
0145 
0146 void RequestAccountPasswordTest::testNewPasswordAbort(void)
0147 {
0148     const QString isolated(QStringLiteral("abort-new-password.ini"));
0149     QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file");
0150 
0151     int openCounter = 0;
0152     const QString actualIni = test::path(isolated);
0153     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0154     {
0155         QSettings data(actualIni, QSettings::IniFormat);
0156         openCounter++;
0157         action(data);
0158     });
0159 
0160     accounts::AccountSecret secret(&test::fakeRandom);
0161     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0162     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0163     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0164     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0165     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0166     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0167 
0168     accounts::RequestAccountPassword uut(settings, &secret);
0169 
0170     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0171     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0172     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0173 
0174     uut.run();
0175 
0176     QVERIFY2(test::signal_eventually_emitted_once(newPasswordNeeded), "(new) password should be asked for");
0177     QCOMPARE(openCounter, 1);
0178     QCOMPARE(existingPasswordNeeded.count(), 0);
0179     QCOMPARE(failed.count(), 0);
0180     QCOMPARE(unlocked.count(), 0);
0181     QCOMPARE(jobFinished.count(), 0);
0182 
0183      secret.cancelRequests();
0184 
0185     QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now");
0186     QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts");
0187     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0188 
0189     QCOMPARE(openCounter, 1);
0190     QCOMPARE(newPasswordNeeded.count(), 1);
0191     QCOMPARE(existingPasswordNeeded.count(), 0);
0192     QCOMPARE(passwordAvailable.count(), 0);
0193     QCOMPARE(keyAvailable.count(), 0);
0194     QCOMPARE(keyFailed.count(), 0);
0195     QCOMPARE(passwordRequestsCancelled.count(), 1);
0196     QCOMPARE(failed.count(), 1);
0197     QCOMPARE(unlocked.count(), 0);
0198 
0199     QFile result(actualIni);
0200     QVERIFY2(result.exists(), "accounts file should still exist");
0201     QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResource));
0202 }
0203 
0204 void RequestAccountPasswordTest::testExistingPassword(void)
0205 {
0206     const QString isolated(QStringLiteral("supply-existing-password.ini"));
0207     QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file");
0208 
0209     int openCounter = 0;
0210     const QString actualIni = test::path(isolated);
0211     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0212     {
0213         QSettings data(actualIni, QSettings::IniFormat);
0214         openCounter++;
0215         action(data);
0216     });
0217 
0218     accounts::AccountSecret secret;
0219     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0220     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0221     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0222     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0223     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0224     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0225 
0226     accounts::RequestAccountPassword uut(settings, &secret);
0227 
0228     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0229     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0230     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0231 
0232     uut.run();
0233 
0234     QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for");
0235     QCOMPARE(openCounter, 1);
0236     QCOMPARE(newPasswordNeeded.count(), 0);
0237     QCOMPARE(failed.count(), 0);
0238     QCOMPARE(unlocked.count(), 0);
0239     QCOMPARE(jobFinished.count(), 0);
0240 
0241     QString password(QStringLiteral("hello, world"));
0242     QVERIFY2(secret.answerExistingPassword(password), "should be able to answer (existing) password");
0243 
0244     QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password should be accepted");
0245     QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived");
0246     QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked");
0247     QCOMPARE(openCounter, 2);
0248 
0249     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0250 
0251     QCOMPARE(openCounter, 2);
0252     QCOMPARE(newPasswordNeeded.count(), 0);
0253     QCOMPARE(existingPasswordNeeded.count(), 1);
0254     QCOMPARE(passwordAvailable.count(), 1);
0255     QCOMPARE(keyAvailable.count(), 1);
0256     QCOMPARE(keyFailed.count(), 0);
0257     QCOMPARE(passwordRequestsCancelled.count(), 0);
0258     QCOMPARE(failed.count(), 0);
0259     QCOMPARE(unlocked.count(), 1);
0260 
0261     QFile result(actualIni);
0262     QVERIFY2(result.exists(), "accounts file should still exist");
0263     QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource));
0264 }
0265 
0266 void RequestAccountPasswordTest::testExistingPasswordRetry(void)
0267 {
0268     const QString isolated(QStringLiteral("supply-existing-password.ini"));
0269     QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file");
0270 
0271     int openCounter = 0;
0272     const QString actualIni = test::path(isolated);
0273     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0274     {
0275         QSettings data(actualIni, QSettings::IniFormat);
0276         openCounter++;
0277         action(data);
0278     });
0279 
0280     accounts::AccountSecret secret;
0281     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0282     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0283     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0284     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0285     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0286     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0287 
0288     accounts::RequestAccountPassword uut(settings, &secret);
0289 
0290     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0291     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0292     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0293 
0294     uut.run();
0295 
0296     QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for");
0297     QCOMPARE(openCounter, 1);
0298     QCOMPARE(newPasswordNeeded.count(), 0);
0299     QCOMPARE(failed.count(), 0);
0300     QCOMPARE(unlocked.count(), 0);
0301     QCOMPARE(jobFinished.count(), 0);
0302 
0303     QString incorrect(QStringLiteral("incorrect"));
0304     QVERIFY2(secret.answerExistingPassword(incorrect), "should be able to answer (existing) password");
0305 
0306     QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password attempt should be accepted");
0307     QVERIFY2(test::signal_eventually_emitted_once(keyFailed), "should fail to derive key for incorrect password");
0308     QCOMPARE(openCounter, 1);
0309 
0310     QString correct(QStringLiteral("hello, world"));
0311     QVERIFY2(secret.answerExistingPassword(correct), "should be able to retry (existing) password");
0312 
0313     QVERIFY2(test::signal_eventually_emitted_twice(passwordAvailable), "second attempt for (existing) password should be accepted");
0314     QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived");
0315     QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked");
0316     QCOMPARE(openCounter, 2);
0317 
0318     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0319 
0320     QCOMPARE(openCounter, 2);
0321     QCOMPARE(newPasswordNeeded.count(), 0);
0322     QCOMPARE(existingPasswordNeeded.count(), 1);
0323     QCOMPARE(passwordAvailable.count(), 2);
0324     QCOMPARE(keyAvailable.count(), 1);
0325     QCOMPARE(keyFailed.count(), 1);
0326     QCOMPARE(passwordRequestsCancelled.count(), 0);
0327     QCOMPARE(failed.count(), 0);
0328     QCOMPARE(unlocked.count(), 1);
0329 
0330     QFile result(actualIni);
0331     QVERIFY2(result.exists(), "accounts file should still exist");
0332     QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource));
0333 }
0334 
0335 void RequestAccountPasswordTest::testExistingPasswordAbort(void)
0336 {
0337     const QString isolated(QStringLiteral("abort-existing-password.ini"));
0338     QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file");
0339 
0340     int openCounter = 0;
0341     const QString actualIni = test::path(isolated);
0342     const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
0343     {
0344         QSettings data(actualIni, QSettings::IniFormat);
0345         openCounter++;
0346         action(data);
0347     });
0348 
0349     accounts::AccountSecret secret;
0350     QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
0351     QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
0352     QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
0353     QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
0354     QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
0355     QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
0356 
0357     accounts::RequestAccountPassword uut(settings, &secret);
0358 
0359     QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
0360     QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
0361     QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
0362 
0363     uut.run();
0364 
0365     QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for");
0366     QCOMPARE(openCounter, 1);
0367     QCOMPARE(newPasswordNeeded.count(), 0);
0368     QCOMPARE(failed.count(), 0);
0369     QCOMPARE(unlocked.count(), 0);
0370     QCOMPARE(jobFinished.count(), 0);
0371 
0372     secret.cancelRequests();
0373 
0374     QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now");
0375     QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts");
0376     QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
0377 
0378     QCOMPARE(openCounter, 1);
0379     QCOMPARE(newPasswordNeeded.count(), 0);
0380     QCOMPARE(existingPasswordNeeded.count(), 1);
0381     QCOMPARE(passwordAvailable.count(), 0);
0382     QCOMPARE(keyAvailable.count(), 0);
0383     QCOMPARE(keyFailed.count(), 0);
0384     QCOMPARE(passwordRequestsCancelled.count(), 1);
0385     QCOMPARE(failed.count(), 1);
0386     QCOMPARE(unlocked.count(), 0);
0387 
0388     QFile result(actualIni);
0389     QVERIFY2(result.exists(), "accounts file should still exist");
0390     QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource));
0391 }
0392 
0393 QTEST_MAIN(RequestAccountPasswordTest)
0394 
0395 #include "request-account-password.moc"