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"