/* * SPDX-License-Identifier: GPL-3.0-or-later * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk */ #include "account/actions_p.h" #include "../../secrets/test-utils/random.h" #include "../../test-utils/spy.h" #include "../test-utils/output.h" #include #include #include #include static QString existingPasswordIniResource(QStringLiteral(":/request-account-password/existing-password.ini")); static QString newPasswordIniResource(QStringLiteral(":/request-account-password/new-password.ini")); static QString newPasswordIniResultResource(QStringLiteral(":/request-account-password/new-password-result.ini")); class RequestAccountPasswordTest : public QObject // clazy:exclude=ctor-missing-parent-argument { Q_OBJECT private Q_SLOTS: void testExistingPassword(void); void testExistingPasswordAbort(void); void testExistingPasswordRetry(void); void testNewPassword(void); void testNewPasswordAbort(void); void testAbortBeforeRun(void); }; void RequestAccountPasswordTest::testAbortBeforeRun(void) { const QString isolated(QStringLiteral("abort-before-run.ini")); QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret; QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); secret.cancelRequests(); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now"); QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts"); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 0); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(existingPasswordNeeded.count(), 0); QCOMPARE(passwordAvailable.count(), 0); QCOMPARE(keyAvailable.count(), 0); QCOMPARE(keyFailed.count(), 0); QCOMPARE(passwordRequestsCancelled.count(), 1); QCOMPARE(failed.count(), 1); QCOMPARE(unlocked.count(), 0); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResource)); } void RequestAccountPasswordTest::testNewPassword(void) { const QString isolated(QStringLiteral("supply-new-password.ini")); QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret(&test::fakeRandom); QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(newPasswordNeeded), "(new) password should be asked for"); QCOMPARE(openCounter, 1); QCOMPARE(existingPasswordNeeded.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 0); QCOMPARE(jobFinished.count(), 0); QString password(QStringLiteral("hello, world")); std::optional defaults = secrets::KeyDerivationParameters::create(); QVERIFY2(defaults, "should be able to construct default key derivation parameters"); QVERIFY2(secret.answerNewPassword(password, *defaults), "should be able to answer (new) password"); QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(new) password should be accepted"); QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived"); QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked"); QCOMPARE(openCounter, 2); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 2); QCOMPARE(newPasswordNeeded.count(), 1); QCOMPARE(existingPasswordNeeded.count(), 0); QCOMPARE(passwordAvailable.count(), 1); QCOMPARE(keyAvailable.count(), 1); QCOMPARE(keyFailed.count(), 0); QCOMPARE(passwordRequestsCancelled.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 1); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResultResource)); } void RequestAccountPasswordTest::testNewPasswordAbort(void) { const QString isolated(QStringLiteral("abort-new-password.ini")); QVERIFY2(test::copyResourceAsWritable(newPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret(&test::fakeRandom); QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(newPasswordNeeded), "(new) password should be asked for"); QCOMPARE(openCounter, 1); QCOMPARE(existingPasswordNeeded.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 0); QCOMPARE(jobFinished.count(), 0); secret.cancelRequests(); QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now"); QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts"); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 1); QCOMPARE(newPasswordNeeded.count(), 1); QCOMPARE(existingPasswordNeeded.count(), 0); QCOMPARE(passwordAvailable.count(), 0); QCOMPARE(keyAvailable.count(), 0); QCOMPARE(keyFailed.count(), 0); QCOMPARE(passwordRequestsCancelled.count(), 1); QCOMPARE(failed.count(), 1); QCOMPARE(unlocked.count(), 0); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(newPasswordIniResource)); } void RequestAccountPasswordTest::testExistingPassword(void) { const QString isolated(QStringLiteral("supply-existing-password.ini")); QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret; QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for"); QCOMPARE(openCounter, 1); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 0); QCOMPARE(jobFinished.count(), 0); QString password(QStringLiteral("hello, world")); QVERIFY2(secret.answerExistingPassword(password), "should be able to answer (existing) password"); QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password should be accepted"); QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived"); QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked"); QCOMPARE(openCounter, 2); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 2); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(existingPasswordNeeded.count(), 1); QCOMPARE(passwordAvailable.count(), 1); QCOMPARE(keyAvailable.count(), 1); QCOMPARE(keyFailed.count(), 0); QCOMPARE(passwordRequestsCancelled.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 1); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource)); } void RequestAccountPasswordTest::testExistingPasswordRetry(void) { const QString isolated(QStringLiteral("supply-existing-password.ini")); QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret; QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for"); QCOMPARE(openCounter, 1); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 0); QCOMPARE(jobFinished.count(), 0); QString incorrect(QStringLiteral("incorrect")); QVERIFY2(secret.answerExistingPassword(incorrect), "should be able to answer (existing) password"); QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password attempt should be accepted"); QVERIFY2(test::signal_eventually_emitted_once(keyFailed), "should fail to derive key for incorrect password"); QCOMPARE(openCounter, 1); QString correct(QStringLiteral("hello, world")); QVERIFY2(secret.answerExistingPassword(correct), "should be able to retry (existing) password"); QVERIFY2(test::signal_eventually_emitted_twice(passwordAvailable), "second attempt for (existing) password should be accepted"); QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived"); QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked"); QCOMPARE(openCounter, 2); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 2); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(existingPasswordNeeded.count(), 1); QCOMPARE(passwordAvailable.count(), 2); QCOMPARE(keyAvailable.count(), 1); QCOMPARE(keyFailed.count(), 1); QCOMPARE(passwordRequestsCancelled.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 1); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource)); } void RequestAccountPasswordTest::testExistingPasswordAbort(void) { const QString isolated(QStringLiteral("abort-existing-password.ini")); QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file"); int openCounter = 0; const QString actualIni = test::path(isolated); const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void { QSettings data(actualIni, QSettings::IniFormat); openCounter++; action(data); }); accounts::AccountSecret secret; QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable); QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed); QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled); accounts::RequestAccountPassword uut(settings, &secret); QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed); QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked); QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished); uut.run(); QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for"); QCOMPARE(openCounter, 1); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(failed.count(), 0); QCOMPARE(unlocked.count(), 0); QCOMPARE(jobFinished.count(), 0); secret.cancelRequests(); QVERIFY2(test::signal_eventually_emitted_once(passwordRequestsCancelled), "account secret should have signalled cancellation by now"); QVERIFY2(test::signal_eventually_emitted_once(failed), "job should signal it failed to unlock the accounts"); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QCOMPARE(openCounter, 1); QCOMPARE(newPasswordNeeded.count(), 0); QCOMPARE(existingPasswordNeeded.count(), 1); QCOMPARE(passwordAvailable.count(), 0); QCOMPARE(keyAvailable.count(), 0); QCOMPARE(keyFailed.count(), 0); QCOMPARE(passwordRequestsCancelled.count(), 1); QCOMPARE(failed.count(), 1); QCOMPARE(unlocked.count(), 0); QFile result(actualIni); QVERIFY2(result.exists(), "accounts file should still exist"); QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource)); } QTEST_MAIN(RequestAccountPasswordTest) #include "request-account-password.moc"