3#include <QCoreApplication>
8#include <QProcessEnvironment>
10#include <QTemporaryDir>
44 struct PassStoreGuard {
46 explicit PassStoreGuard(
const QString &orig) : original(orig) {}
61 void callSetEnvVar(
const QString &key,
const QString &value) {
64 QStringList environment() {
66 return exec.environment();
68 void callFinished(
int id,
int exitCode,
const QString &out,
72 void callPassFinished(
int id,
int exitCode,
const QString &out,
78 template <
typename T,
void (*Setter)(const T &)>
struct SettingGuard {
80 SettingGuard(T orig,
const T &newVal) : original(std::move(orig)) {
83 ~SettingGuard() { Setter(original); }
84 SettingGuard(
const SettingGuard &) =
delete;
85 SettingGuard &operator=(
const SettingGuard &) =
delete;
89 void cleanupTestCase();
90 void normalizeFolderPath();
91 void normalizeFolderPathEdgeCases();
93 void fileContentEdgeCases();
94 void namedValuesTakeValue();
95 void namedValuesEdgeCases();
96 void totpHiddenFromDisplay();
99 void regexPatternEdgeCases();
100 void endsWithGpgEdgeCases();
101 void userInfoValidity();
102 void userInfoValidityEdgeCases();
103 void passwordConfigurationCharacters();
104 void simpleTransactionBasic();
105 void simpleTransactionNested();
106 void createGpgIdFile();
107 void createGpgIdFileEmptyKeys();
108 void generateRandomPassword();
109 void boundedRandom();
110 void findBinaryInPath();
111 void findPasswordStore();
112 void configIsValid();
114 void getDirWithIndex();
115 void findBinaryInPathNotFound();
116 void findPasswordStoreEnvVar();
117 void normalizeFolderPathMultipleCalls();
118 void userInfoFullyValid();
119 void userInfoMarginallyValid();
120 void userInfoIsValid();
121 void userInfoCreatedAndExpiry();
122 void qProgressIndicatorBasic();
123 void qProgressIndicatorStartStop();
124 void namedValueBasic();
125 void namedValueMultiple();
126 void buildClipboardMimeDataLinux();
127 void buildClipboardMimeDataWindows();
128 void buildClipboardMimeDataMac();
129 void utilRegexEnsuresGpg();
130 void utilRegexProtocol();
131 void utilRegexNewLines();
132 void reencryptPathNormalization();
133 void reencryptPathAbsolutePath();
134 void buildClipboardMimeDataDword();
135 void imitatePassResolveMoveDestination();
136 void imitatePassResolveMoveDestinationForce();
137 void imitatePassResolveMoveDestinationDestExistsNoForce();
138 void imitatePassResolveMoveDestinationDir();
139 void imitatePassResolveMoveDestinationNonExistent();
140 void imitatePassRemoveDir();
141 void getRecipientListBasic();
142 void getRecipientListEmpty();
143 void getRecipientListWithComments();
144 void getRecipientListInvalidKeyId();
145 void isValidKeyIdBasic();
146 void isValidKeyIdWith0xPrefix();
147 void isValidKeyIdWithEmail();
148 void isValidKeyIdInvalid();
149 void getRecipientStringCount();
150 void getGpgIdPathBasic();
151 void getGpgIdPathSubfolder();
152 void getGpgIdPathNotFound();
153 void findBinaryInPathReturnedPathIsAbsolute();
154 void findBinaryInPathReturnedPathIsExecutable();
155 void findBinaryInPathMultipleKnownBinaries();
156 void findBinaryInPathConsistency();
157 void findBinaryInPathResultContainsBinaryName();
158 void findBinaryInPathTempExecutableInTempDir();
159 void findBinaryInPathWithConstQStringRef();
160 void findBinaryInPathEmptyString();
161 void findBinaryInPathStringLiteral();
162 void setEnvVarAdds();
163 void setEnvVarUpdates();
164 void setEnvVarRemoves();
165 void setEnvVarNoopOnMissingRemove();
166 void updateEnvSetsExpectedVars();
167 void updateEnvEmptyCustomCharsetFallsBackToAllChars();
168 void updateEnvWslenvContainsRequiredVars();
169 void gpgErrorMessageKeyExpiredStatusToken();
170 void gpgErrorMessageKeyRevokedStatusToken();
171 void gpgErrorMessageNoPubkeyStatusToken();
172 void gpgErrorMessageInvRecpStatusToken();
173 void gpgErrorMessageFailureStatusToken();
174 void gpgErrorMessageKeyExpiredFallback();
175 void gpgErrorMessageRevokedFallback();
176 void gpgErrorMessageNoPubkeyFallback();
177 void gpgErrorMessageEncryptionFailedFallback();
178 void gpgErrorMessageUnknownReturnsEmpty();
179 void gpgErrorMessageStatusTokenTakesPriorityOverFallback();
181 void parseGrepOutputEmpty();
182 void parseGrepOutputSingleEntry();
183 void parseGrepOutputMultipleEntries();
184 void parseGrepOutputAnsiStripped();
185 void parseGrepOutputHeaderColonStripped();
186 void parseGrepOutputCrlfHandled();
187 void parseGrepOutputOrphanMatchesIgnored();
188 void parseGrepOutputEmptyMatchLinesIgnored();
189 void parseGrepOutputLastEntryIncluded();
190 void parseGrepOutputEmbeddedBlueNotHeader();
191 void parseGrepOutputPlainTextHeaders();
193 void passFinishedGrepNoMatchEmitsEmpty();
194 void passFinishedGrepErrorEmitsProcessError();
195 void passFinishedGrepSuccessEmitsResults();
197 void grepMatchFileFailedDecryptReturnsEmpty();
198 void grepScanStoreEmptyDirReturnsEmpty();
199 void grepImitatePassEmptyStoreEmitsEmpty();
200 void grepImitatePassInvalidRegexEmitsEmpty();
217 qRegisterMetaType<GrepResults>(
"GrepResults");
219 qRegisterMetaType<GrepResults>(
"QList<QPair<QString,QStringList>>");
232void tst_util::cleanupTestCase() {
240void tst_util::normalizeFolderPath() {
242 QString sep = QDir::separator();
246 QVERIFY(result.endsWith(sep));
248 QVERIFY(result.endsWith(sep));
251 QVERIFY2(result ==
"test" + sep,
252 qPrintable(QString(
"Expected 'test%1', got '%2'").arg(sep, result)));
254 QVERIFY2(result ==
"test" + sep,
255 qPrintable(QString(
"Expected 'test%1', got '%2'").arg(sep, result)));
258 if (QDir::separator() ==
'\\') {
260 QVERIFY(result.endsWith(
"\\"));
261 QVERIFY(result.contains(
"test"));
262 QVERIFY(result.contains(
"subdir"));
265 result ==
"test\\subdir\\",
267 QString(
"Expected 'test\\\\subdir\\\\', got '%1'").arg(result)));
272 QVERIFY(result.endsWith(sep));
273 QVERIFY(result.contains(
"test"));
274 QVERIFY(result.contains(
"subdir"));
275 QVERIFY(result.contains(
"folder"));
278void tst_util::fileContent() {
279 NamedValue key = {
"key",
"val"};
280 NamedValue key2 = {
"key2",
"val2"};
281 QString password =
"password";
309void tst_util::namedValuesTakeValue() {
310 NamedValues nv = {{
"key1",
"value1"}, {
"key2",
"value2"}, {
"key3",
"value3"}};
313 QCOMPARE(val, QString(
"value2"));
314 QCOMPARE(nv.length(), 2);
315 QVERIFY(!nv.contains({
"key2",
"value2"}));
318 QVERIFY(val.isEmpty());
321 QCOMPARE(val, QString(
"value1"));
323 QCOMPARE(val, QString(
"value3"));
324 QVERIFY(nv.isEmpty());
327void tst_util::totpHiddenFromDisplay() {
329 "password\notpauth://totp/Test?secret=JBSWY3DPEHPK3PXP\nkey: value\n", {},
333 QVERIFY(remaining.contains(
"otpauth://"));
334 QVERIFY(remaining.contains(
"key: value"));
337 QVERIFY(!display.contains(
"otpauth"));
338 QVERIFY(display.contains(
"key: value"));
341 "password\nOTPAUTH://TOTP/Test?secret=JBSWY3DPEHPK3PXP\n", {},
false);
345void tst_util::testAwsUrl() {
348 QRegularExpressionMatch match1 =
349 proto.match(
"https://rh-dev.signin.aws.amazon.com/console");
350 QVERIFY2(match1.hasMatch(),
"Should match AWS console URL");
351 QString captured1 = match1.captured(1);
352 QVERIFY2(captured1.contains(
"amazon.com"),
"Should include full URL");
354 QRegularExpressionMatch match2 = proto.match(
"https://test-example.com/path");
355 QVERIFY2(match2.hasMatch(),
"Should match URL with dash");
356 QString captured2 = match2.captured(1);
357 QVERIFY2(captured2.contains(
"test-example.com"),
358 "Should include full domain");
361void tst_util::regexPatterns() {
363 QVERIFY(gpg.match(
"test.gpg").hasMatch());
364 QVERIFY(gpg.match(
"folder/test.gpg").hasMatch());
365 QVERIFY(!gpg.match(
"test.gpg~").hasMatch());
366 QVERIFY(!gpg.match(
"test.gpg.bak").hasMatch());
369 QVERIFY(proto.match(
"https://example.com").hasMatch());
370 QVERIFY(proto.match(
"ssh://user@host/path").hasMatch());
371 QVERIFY(proto.match(
"ftp://server/file").hasMatch());
372 QVERIFY(proto.match(
"webdav://localhost/share").hasMatch());
373 QVERIFY(!proto.match(
"not a url").hasMatch());
374 QRegularExpressionMatch urlWithTrailingTextMatch =
375 proto.match(
"https://example.com/ is the address");
376 QVERIFY(urlWithTrailingTextMatch.hasMatch());
377 QString captured = urlWithTrailingTextMatch.captured(1);
378 QVERIFY2(!captured.contains(
" "),
"URL should not include space");
379 QVERIFY2(!captured.contains(
"<"),
"URL should not include <");
380 QVERIFY2(captured ==
"https://example.com/",
"URL should stop at space");
382 QRegularExpressionMatch urlWithFragmentMatch =
383 proto.match(
"Link: https://test.org/path?q=1#frag");
384 QVERIFY(urlWithFragmentMatch.hasMatch());
385 captured = urlWithFragmentMatch.captured(1);
386 QVERIFY2(captured.contains(
"?"),
"URL should include query params");
387 QVERIFY2(captured.contains(
"#"),
"URL should include fragment");
388 QVERIFY2(!captured.contains(
" now"),
"URL should not include trailing text");
391 QVERIFY(nl.match(
"\n").hasMatch());
392 QVERIFY(nl.match(
"\r").hasMatch());
393 QVERIFY(nl.match(
"\r\n").hasMatch());
396void tst_util::normalizeFolderPathEdgeCases() {
398 QVERIFY(result.endsWith(QDir::separator()));
399 QVERIFY2(result == QDir::separator() || result.endsWith(QDir::separator()),
400 "Empty path should become separator");
403 QVERIFY(result.endsWith(QDir::separator()));
406 QVERIFY(result.endsWith(QDir::separator()));
409 QVERIFY(nativeResult.endsWith(QDir::separator()));
412void tst_util::fileContentEdgeCases() {
417 "secret\nurl: https://login.com\n",
418 {
"username",
"password",
"url"},
false);
423 QCOMPARE(nv.length(), 1);
424 QCOMPARE(nv.at(0).name, QString(
"key"));
425 QVERIFY(nv.at(0).value.contains(
"spaces"));
441void tst_util::namedValuesEdgeCases() {
443 QVERIFY(nv.isEmpty());
444 QVERIFY(nv.
takeValue(
"nonexistent").isEmpty());
446 NamedValue n1 = {
"key",
"value"};
448 QCOMPARE(nv.length(), 1);
449 NamedValue n2 = {
"key2",
"value2"};
451 QCOMPARE(nv.length(), 2);
454 QVERIFY(nv.isEmpty());
455 QVERIFY(nv.
takeValue(
"anything").isEmpty());
458void tst_util::regexPatternEdgeCases() {
460 QVERIFY(gpg.match(
".gpg").hasMatch());
461 QVERIFY(gpg.match(
"a.gpg").hasMatch());
462 QVERIFY(!gpg.match(
"test.gpgx").hasMatch());
465 QVERIFY(proto.match(
"webdavs://secure.example.com").hasMatch());
466 QVERIFY(proto.match(
"ftps://ftp.server.org").hasMatch());
467 QVERIFY(proto.match(
"sftp://user:pass@host").hasMatch());
469 QVERIFY(!proto.match(
"file:///path/to/file").hasMatch());
472 QVERIFY(nl.match(
"\n").hasMatch());
473 QVERIFY(nl.match(
"\r").hasMatch());
474 QVERIFY(nl.match(
"\r\n").hasMatch());
477void tst_util::endsWithGpgEdgeCases() {
479 QVERIFY(!gpg.match(
".gpgx").hasMatch());
480 QVERIFY(!gpg.match(
"test.gpg.bak").hasMatch());
481 QVERIFY(!gpg.match(
"test.gpg~").hasMatch());
482 QVERIFY(!gpg.match(
"test.gpg.orig").hasMatch());
483 QVERIFY(gpg.match(
"test.gpg").hasMatch());
484 QVERIFY(gpg.match(
"test/path/file.gpg").hasMatch());
485 QVERIFY(gpg.match(
"/absolute/path/file.gpg").hasMatch());
486 QVERIFY(gpg.match(
"file name with spaces.gpg").hasMatch());
489void tst_util::userInfoValidity() {
515void tst_util::userInfoValidityEdgeCases() {
523 char nullChar =
'\0';
531void tst_util::passwordConfigurationCharacters() {
532 PasswordConfiguration config;
533 QCOMPARE(config.
length, 16);
548void tst_util::simpleTransactionBasic() {
549 simpleTransaction transaction;
555void tst_util::simpleTransactionNested() {
556 simpleTransaction transaction;
566void tst_util::createGpgIdFile() {
567 QTemporaryDir tempDir;
568 QString newDir = tempDir.path() +
"/testfolder";
569 QVERIFY(QDir().mkdir(newDir));
571 QString gpgIdFile = newDir +
"/.gpg-id";
572 QStringList keyIds = {
"ABCDEF12",
"34567890"};
574 QFile gpgId(gpgIdFile);
575 QVERIFY(gpgId.open(QIODevice::WriteOnly));
576 for (
const QString &keyId : keyIds) {
577 gpgId.write((keyId +
"\n").toUtf8());
581 QVERIFY(QFile::exists(gpgIdFile));
583 QFile readFile(gpgIdFile);
584 QVERIFY(readFile.open(QIODevice::ReadOnly));
585 QString content = QString::fromUtf8(readFile.readAll());
588 QStringList lines = content.trimmed().split(
'\n');
589 QCOMPARE(lines.size(), 2);
590 QCOMPARE(lines[0], QString(
"ABCDEF12"));
591 QCOMPARE(lines[1], QString(
"34567890"));
594void tst_util::createGpgIdFileEmptyKeys() {
595 QTemporaryDir tempDir;
596 QString newDir = tempDir.path() +
"/testfolder";
597 QVERIFY(QDir().mkdir(newDir));
599 QString gpgIdFile = newDir +
"/.gpg-id";
601 QFile gpgId(gpgIdFile);
602 QVERIFY(gpgId.open(QIODevice::WriteOnly));
605 QVERIFY(QFile::exists(gpgIdFile));
607 QFile readFile(gpgIdFile);
608 QVERIFY(readFile.open(QIODevice::ReadOnly));
609 QString content = QString::fromUtf8(readFile.readAll());
612 QVERIFY(content.isEmpty());
615void tst_util::generateRandomPassword() {
616 SettingGuard<bool, QtPassSettings::setUsePwgen> pwgenGuard{
620 QString charset =
"abcdefghijklmnopqrstuvwxyz";
623 QCOMPARE(result.length(), 10);
624 for (
const QChar &ch : result) {
626 charset.contains(ch),
627 "Generated password contains character outside the specified charset");
631 QCOMPARE(result.length(), 100);
632 for (
const QChar &ch : result) {
634 QStringLiteral(
"abcd").contains(ch),
635 "Generated password contains character outside the specified charset");
639 QVERIFY(result.isEmpty());
642 QCOMPARE(result.length(), 50);
643 for (
const QChar &ch : result) {
645 QStringLiteral(
"ABC").contains(ch),
646 "Generated password contains character outside the specified charset");
649 const QString randomCharset = QStringLiteral(
"abcd");
651 QCOMPARE(first.length(), 32);
652 bool foundDifferent =
false;
653 for (
int i = 0; i < 20; ++i) {
655 QCOMPARE(candidate.length(), 32);
656 for (
const QChar &ch : candidate) {
657 QVERIFY2(randomCharset.contains(ch),
658 "Generated password contains character outside the specified "
661 if (candidate != first) {
662 foundDifferent =
true;
668 "Multiple generated passwords were identical; randomness may be broken");
671void tst_util::boundedRandom() {
672 SettingGuard<bool, QtPassSettings::setUsePwgen> pwgenGuard{
677 QVector<quint32> counts(10, 0);
678 const int iterations = 1000;
680 for (
int i = 0; i < iterations; ++i) {
682 quint32 val = result.at(0).digitValue();
683 QVERIFY2(val < 10,
"generatePassword should only return digit characters");
687 for (
int i = 0; i < 10; ++i) {
688 QVERIFY2(counts[i] > 0,
"Each digit should appear at least once");
691 const double expected =
static_cast<double>(iterations) / 10.0;
693 for (
int i = 0; i < 10; ++i) {
694 const double count =
static_cast<double>(counts[i]);
695 chi2 += (count - expected) * (count - expected) / expected;
697 const double chi2Critical = 25.0;
698 QVERIFY2(chi2 < chi2Critical,
700 QStringLiteral(
"Chi-square %1 exceeds critical value %2 (df=9)")
702 .arg(chi2Critical)));
705void tst_util::findBinaryInPath() {
707 const QString binaryName = QStringLiteral(
"cmd.exe");
709 const QString binaryName = QStringLiteral(
"sh");
712 QVERIFY2(!result.isEmpty(),
"Should find a standard shell in PATH");
713 QVERIFY(result.contains(binaryName));
716 QVERIFY(result.isEmpty());
719void tst_util::findPasswordStore() {
721 QVERIFY(!result.isEmpty());
722 QVERIFY(result.endsWith(QDir::separator()));
725void tst_util::configIsValid() {
726 QTemporaryDir tempDir;
727 QVERIFY2(tempDir.isValid(),
"Temporary directory should be created");
731 SettingGuard<bool, QtPassSettings::setUsePass> usePassGuard{
737 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id is missing");
740 QFile gpgIdFile(tempDir.path() + QDir::separator() +
741 QStringLiteral(
".gpg-id"));
742 QVERIFY2(gpgIdFile.open(QIODevice::WriteOnly | QIODevice::Truncate),
743 "Should be able to create .gpg-id");
744 gpgIdFile.write(
"test@example.com\n");
747 SettingGuard<QString, QtPassSettings::setGpgExecutable> gpgGuard{
751 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id exists but gpg "
752 "executable is missing");
755 QStringLiteral(
"definitely_nonexistent_gpg_binary_12345"));
757 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id exists but gpg "
758 "executable is invalid");
761void tst_util::getDirBasic() {
762 QTemporaryDir tempDir;
763 QVERIFY2(tempDir.isValid(),
764 "Temporary directory should be created successfully");
766 QFileSystemModel fileSystemModel;
767 fileSystemModel.setRootPath(tempDir.path());
768 StoreModel storeModel;
770 QVERIFY(storeModel.sourceModel() !=
nullptr);
771 QVERIFY2(storeModel.
getStore() == tempDir.path(),
772 "Store path should match the set value");
773 QModelIndex rootIndex = fileSystemModel.index(tempDir.path());
774 QVERIFY2(rootIndex.isValid(),
"Filesystem model root index should be valid");
779 Util::getDir(QModelIndex(),
false, fileSystemModel, storeModel);
780 QString expectedDir = QDir(tempDir.path()).absolutePath();
781 if (!expectedDir.endsWith(QDir::separator())) {
782 expectedDir += QDir::separator();
785 result == expectedDir,
786 qPrintable(QString(
"Expected '%1', got '%2'").arg(expectedDir, result)));
790void tst_util::getDirWithIndex() {
791 QTemporaryDir tempDir;
792 QVERIFY2(tempDir.isValid(),
793 "Temporary directory should be created successfully");
795 const QString dirPath = tempDir.path();
796 const QString filePath =
797 QDir(dirPath).filePath(QStringLiteral(
"testfile.txt"));
799 QFile file(filePath);
800 QVERIFY2(file.open(QIODevice::WriteOnly),
801 "Failed to create test file in temporary directory");
802 const char testData[] =
"dummy";
803 const qint64 bytesWritten = file.write(testData,
sizeof(testData) - 1);
804 QVERIFY2(bytesWritten ==
static_cast<qint64
>(
sizeof(testData) - 1),
805 "Failed to write test data to file in temporary directory");
809 PassStoreGuard passStoreGuard(originalPassStore);
812 QFileSystemModel fileSystemModel;
813 fileSystemModel.setRootPath(dirPath);
815 StoreModel storeModel;
817 QVERIFY2(storeModel.
getStore() == dirPath,
818 "Store path should match the set value");
820 QModelIndex sourceIndex = fileSystemModel.index(filePath);
821 QVERIFY2(sourceIndex.isValid(),
822 "Source index should be valid for the test file");
823 QModelIndex fileIndex = storeModel.mapFromSource(sourceIndex);
824 QVERIFY2(fileIndex.isValid(),
825 "Proxy index should be valid for the test file");
827 QString result =
Util::getDir(fileIndex,
false, fileSystemModel, storeModel);
828 QVERIFY2(!result.isEmpty(),
829 "getDir should return a non-empty directory for a valid index");
830 QVERIFY(result.endsWith(QDir::separator()));
832 QString expectedPath = dirPath;
833 if (!expectedPath.endsWith(QDir::separator())) {
834 expectedPath += QDir::separator();
837 result == expectedPath,
839 QStringLiteral(
"Expected '%1', got '%2'").arg(expectedPath, result)));
841 QModelIndex invalidIndex;
842 QString invalidResult =
843 Util::getDir(invalidIndex,
false, fileSystemModel, storeModel);
844 QString expectedForInvalid = dirPath;
845 if (!expectedForInvalid.endsWith(QDir::separator())) {
846 expectedForInvalid += QDir::separator();
848 QVERIFY2(invalidResult == expectedForInvalid,
849 qPrintable(QStringLiteral(
"getDir should return pass store for "
850 "invalid index. Expected '%1', got '%2'")
851 .arg(expectedForInvalid, invalidResult)));
854void tst_util::findBinaryInPathNotFound() {
856 QVERIFY(result.isEmpty());
859void tst_util::findPasswordStoreEnvVar() {
861 QVERIFY(!result.isEmpty());
864void tst_util::normalizeFolderPathMultipleCalls() {
867 QVERIFY(result1.endsWith(QDir::separator()));
868 QVERIFY(result2.endsWith(QDir::separator()));
871void tst_util::userInfoFullyValid() {
881void tst_util::userInfoMarginallyValid() {
889void tst_util::userInfoIsValid() {
899void tst_util::userInfoCreatedAndExpiry() {
901 ui.
name =
"Test User";
904 QVERIFY(!ui.
created.isValid());
905 QVERIFY(!ui.
expiry.isValid());
907 QDateTime future = QDateTime::currentDateTime().addYears(1);
909 QVERIFY(ui.
expiry.isValid());
910 QVERIFY(ui.
expiry.toSecsSinceEpoch() > 0);
912 QDateTime past = QDateTime::currentDateTime().addYears(-1);
915 QVERIFY(ui.
created.toSecsSinceEpoch() > 0);
918void tst_util::qProgressIndicatorBasic() {
919 QProgressIndicator pi;
923void tst_util::qProgressIndicatorStartStop() {
924 QProgressIndicator pi;
931void tst_util::namedValueBasic() {
935 QCOMPARE(nv.
name, QString(
"key"));
936 QCOMPARE(nv.
value, QString(
"value"));
939void tst_util::namedValueMultiple() {
945 QCOMPARE(nvs.size(), 1);
948void tst_util::imitatePassResolveMoveDestination() {
950 QTemporaryDir tmpDir;
951 QString srcPath = tmpDir.path() +
"/test.gpg";
952 QFile srcFile(srcPath);
953 QVERIFY(srcFile.open(QFile::WriteOnly));
954 srcFile.write(
"test");
957 QString destPath = tmpDir.path() +
"/dest.gpg";
959 QString expected = destPath;
960 QVERIFY2(result == expected,
"Destination should have .gpg extension");
963void tst_util::imitatePassResolveMoveDestinationForce() {
965 QTemporaryDir tmpDir;
966 QString srcPath = tmpDir.path() +
"/test.gpg";
967 QFile srcFile(srcPath);
968 QVERIFY(srcFile.open(QFile::WriteOnly));
969 srcFile.write(
"test");
972 QString destPath = tmpDir.path() +
"/existing.gpg";
973 QFile destFile(destPath);
974 QVERIFY(destFile.open(QFile::WriteOnly));
975 destFile.write(
"old");
979 QVERIFY2(result == destPath,
"Should return dest path when force=true");
982void tst_util::imitatePassResolveMoveDestinationDestExistsNoForce() {
984 QTemporaryDir tmpDir;
985 QString srcPath = tmpDir.path() +
"/test.gpg";
986 QFile srcFile(srcPath);
987 QVERIFY(srcFile.open(QFile::WriteOnly));
988 srcFile.write(
"test");
991 QString destPath = tmpDir.path() +
"/existing.gpg";
992 QFile destFile(destPath);
993 QVERIFY(destFile.open(QFile::WriteOnly));
994 destFile.write(
"old");
998 QVERIFY2(result.isEmpty(),
999 "Should return empty when dest exists and force=false");
1002void tst_util::imitatePassResolveMoveDestinationDir() {
1004 QTemporaryDir tmpDir;
1005 QString srcPath = tmpDir.path() +
"/test.gpg";
1006 QFile srcFile(srcPath);
1007 QVERIFY(srcFile.open(QFile::WriteOnly));
1008 srcFile.write(
"test");
1012 QVERIFY2(result == tmpDir.path() +
"/test.gpg",
1013 "Should append filename when dest is dir");
1016void tst_util::imitatePassResolveMoveDestinationNonExistent() {
1018 QTemporaryDir tmpDir;
1019 QString destPath = tmpDir.path() +
"/dest.gpg";
1022 QVERIFY2(result.isEmpty(),
"Should return empty for non-existent source");
1025void tst_util::imitatePassRemoveDir() {
1027 QTemporaryDir tmpDir;
1028 QString subDir = tmpDir.path() +
"/testdir";
1029 QVERIFY(QDir().mkpath(subDir));
1030 QVERIFY(QDir(subDir).exists());
1033 QVERIFY(!QDir(subDir).exists());
1036void tst_util::getRecipientListBasic() {
1037 QTemporaryDir tempDir;
1038 QString passStore = tempDir.path();
1039 QString gpgIdFile = passStore +
"/.gpg-id";
1041 QFile file(gpgIdFile);
1042 QVERIFY(file.open(QIODevice::WriteOnly));
1043 file.write(
"ABCDEF12\n34567890\n");
1049 QCOMPARE(recipients.size(), 2);
1050 QCOMPARE(recipients[0], QString(
"ABCDEF12"));
1051 QCOMPARE(recipients[1], QString(
"34567890"));
1054void tst_util::getRecipientListEmpty() {
1055 QTemporaryDir tempDir;
1056 QString passStore = tempDir.path();
1057 QString gpgIdFile = passStore +
"/.gpg-id";
1059 QFile file(gpgIdFile);
1060 QVERIFY(file.open(QIODevice::WriteOnly));
1066 QVERIFY(recipients.isEmpty());
1069void tst_util::getRecipientListWithComments() {
1070 QTemporaryDir tempDir;
1071 QString passStore = tempDir.path();
1072 QString gpgIdFile = passStore +
"/.gpg-id";
1074 QFile file(gpgIdFile);
1075 QVERIFY(file.open(QIODevice::WriteOnly));
1076 file.write(
"ABCDEF12\n# comment\n34567890\n");
1082 QCOMPARE(recipients.size(), 2);
1083 QVERIFY(!recipients.contains(
"# comment"));
1084 QVERIFY(!recipients.contains(
"comment"));
1087void tst_util::getRecipientListInvalidKeyId() {
1088 QTemporaryDir tempDir;
1089 QString passStore = tempDir.path();
1090 QString gpgIdFile = passStore +
"/.gpg-id";
1092 QFile file(gpgIdFile);
1093 QVERIFY(file.open(QIODevice::WriteOnly));
1094 file.write(
"ABCDEF12\ninvalid\n0xABCDEF123456789012\n<a@b>\nuser@qtpass@"
1099 PassStoreGuard originalGuard(originalPassStore);
1102 QVERIFY(!recipients.contains(
"invalid"));
1103 QVERIFY(recipients.contains(
"ABCDEF12"));
1104 QVERIFY(recipients.contains(
"0xABCDEF123456789012"));
1105 QVERIFY(recipients.contains(
"user@qtpass@example.org"));
1108void tst_util::isValidKeyIdBasic() {
1115void tst_util::isValidKeyIdWith0xPrefix() {
1123void tst_util::isValidKeyIdWithEmail() {
1131void tst_util::isValidKeyIdInvalid() {
1139void tst_util::getRecipientStringCount() {
1140 QTemporaryDir tempDir;
1141 QString passStore = tempDir.path();
1142 QString gpgIdFile = passStore +
"/.gpg-id";
1144 QFile file(gpgIdFile);
1145 QVERIFY(file.open(QIODevice::WriteOnly));
1146 file.write(
"ABCDEF12\n34567890\n");
1150 PassStoreGuard originalGuard(originalPassStore);
1153 QStringList parsedRecipients =
1157 QStringList expectedRecipients = {
"ABCDEF12",
"34567890"};
1160 QCOMPARE(count, (
int)expectedRecipients.size());
1162 QCOMPARE(parsedRecipients, recipientsNoCount);
1164 QVERIFY(parsedRecipients.contains(
"ABCDEF12"));
1165 QVERIFY(parsedRecipients.contains(
"34567890"));
1168 QVERIFY(recipientsNoCount.contains(
"ABCDEF12"));
1169 QVERIFY(recipientsNoCount.contains(
"34567890"));
1172void tst_util::getGpgIdPathBasic() {
1173 QTemporaryDir tempDir;
1174 QString passStore = tempDir.path();
1175 QString gpgIdFile = passStore +
"/.gpg-id";
1177 QFile file(gpgIdFile);
1178 QVERIFY(file.open(QIODevice::WriteOnly));
1179 file.write(
"ABCDEF12\n");
1185 QString expected = QDir::cleanPath(gpgIdFile);
1186 QVERIFY2(path == expected,
1187 qPrintable(QString(
"Expected %1, got %2").arg(expected, path)));
1190void tst_util::getGpgIdPathSubfolder() {
1191 QTemporaryDir tempDir;
1192 QString passStore = tempDir.path();
1193 QString subfolder = passStore +
"/subfolder";
1194 QString gpgIdFile = subfolder +
"/.gpg-id";
1196 QVERIFY(QDir().mkdir(subfolder));
1197 QFile file(gpgIdFile);
1198 QVERIFY(file.open(QIODevice::WriteOnly));
1199 file.write(
"ABCDEF12\n");
1205 QVERIFY2(path == gpgIdFile,
1206 qPrintable(QString(
"Expected %1, got %2").arg(gpgIdFile, path)));
1209void tst_util::getGpgIdPathNotFound() {
1210 QTemporaryDir tempDir;
1211 QString passStore = tempDir.path();
1217 QString expected = QDir::cleanPath(passStore +
"/.gpg-id");
1218 QVERIFY2(path == expected,
1219 qPrintable(QString(
"Expected %1, got %2").arg(expected, path)));
1225void tst_util::findBinaryInPathReturnedPathIsAbsolute() {
1228 const QString binaryName = QStringLiteral(
"cmd.exe");
1230 const QString binaryName = QStringLiteral(
"sh");
1233 QVERIFY2(!result.isEmpty(),
"Should find a standard shell");
1234 QFileInfo fi(result);
1238 QStringLiteral(
"Returned path '%1' must be absolute").arg(result)));
1241void tst_util::findBinaryInPathReturnedPathIsExecutable() {
1245 const QString binaryName = QStringLiteral(
"cmd.exe");
1247 const QString binaryName = QStringLiteral(
"sh");
1250 QVERIFY2(!result.isEmpty(),
"Should find a standard shell");
1251 QFileInfo fi(result);
1255 QStringLiteral(
"Returned path '%1' must be executable").arg(result)));
1258void tst_util::findBinaryInPathMultipleKnownBinaries() {
1261 const QStringList binaries = {QStringLiteral(
"sh"), QStringLiteral(
"ls"),
1262 QStringLiteral(
"cat")};
1263 for (
const QString &bin : binaries) {
1265 QVERIFY2(!result.isEmpty(),
1266 qPrintable(QStringLiteral(
"Should find '%1' in PATH").arg(bin)));
1267 QVERIFY2(result.contains(bin),
1268 qPrintable(QStringLiteral(
"Result '%1' should contain '%2'")
1269 .arg(result, bin)));
1271 QFileInfo(result).isExecutable(),
1273 QStringLiteral(
"Result '%1' should be executable").arg(result)));
1276 QSKIP(
"Non-Windows binary list not applicable on Windows");
1280void tst_util::findBinaryInPathConsistency() {
1284 const QString binaryName = QStringLiteral(
"cmd.exe");
1286 const QString binaryName = QStringLiteral(
"sh");
1290 QVERIFY2(!first.isEmpty(),
"First call should find the binary");
1291 QCOMPARE(first, second);
1294void tst_util::findBinaryInPathResultContainsBinaryName() {
1298 const QString binaryName = QStringLiteral(
"cmd");
1300 const QString binaryName = QStringLiteral(
"sh");
1303 QVERIFY2(!result.isEmpty(),
"Should find the binary");
1305 result.endsWith(binaryName) ||
1306 result.endsWith(binaryName + QStringLiteral(
".exe")),
1307 qPrintable(QStringLiteral(
"Path '%1' should end with binary name '%2'")
1308 .arg(result, binaryName)));
1311void tst_util::findBinaryInPathTempExecutableInTempDir() {
1320 if (shPath.isEmpty()) {
1321 QSKIP(
"Cannot find 'sh' to determine a writable PATH directory");
1323 const QString pathDir = QFileInfo(shPath).absolutePath();
1324 const QString uniqueName = QStringLiteral(
"qtpass_test_exec_") +
1325 QUuid::createUuid().toString(QUuid::WithoutBraces);
1326 const QString uniquePath = pathDir + QDir::separator() + uniqueName;
1328 QFile exec(uniquePath);
1329 if (!exec.open(QIODevice::WriteOnly)) {
1330 QSKIP(
"Cannot write to the PATH directory containing 'sh' (need write "
1333 QVERIFY2(exec.exists(),
"File should exist after opening for writing");
1334 exec.write(
"#!/bin/sh\n");
1336 exec.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
1337 QFileDevice::ExeOwner);
1342 const bool removed = QFile::remove(uniquePath);
1346 QStringLiteral(
"Failed to clean up test file '%1'").arg(uniquePath)));
1348 QVERIFY2(!result.isEmpty(),
1349 "findBinaryInPath should locate the executable placed in a PATH "
1351 QVERIFY2(result.endsWith(uniqueName),
1352 qPrintable(QStringLiteral(
"Result '%1' should end with '%2'")
1353 .arg(result, uniqueName)));
1354 QVERIFY2(QFileInfo(result).isAbsolute(),
"Result must be an absolute path");
1356 QSKIP(
"Temp-executable test is Unix-only");
1360void tst_util::buildClipboardMimeDataLinux() {
1363 QVERIFY(mime !=
nullptr);
1364 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1365 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1366 QVERIFY2(mime->data(
"x-kde-passwordManagerHint") == QByteArray(
"secret"),
1367 "Linux should set password hint");
1370 QSKIP(
"Linux-only test");
1374void tst_util::buildClipboardMimeDataWindows() {
1377 QVERIFY(mime !=
nullptr);
1378 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1379 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1380 QByteArray excl = mime->data(
"ExcludeClipboardContentFromMonitorProcessing");
1381 QVERIFY2(excl.size() == 4,
"Windows ExcludeClipboard should be 4 bytes");
1382 QVERIFY2(excl == dwordBytes(1),
"Windows ExcludeClipboard should be DWORD 1");
1383 QVERIFY(mime->hasFormat(
"ExcludeClipboardContentFromMonitorProcessing"));
1384 QVERIFY(mime->hasFormat(
"CanIncludeInClipboardHistory"));
1385 QVERIFY(mime->hasFormat(
"CanUploadToCloudClipboard"));
1386 QByteArray canHistory = mime->data(
"CanIncludeInClipboardHistory");
1387 QVERIFY2(canHistory.size() == 4,
1388 "CanIncludeInClipboardHistory should be 4 bytes");
1389 QVERIFY2(canHistory == dwordBytes(0),
1390 "CanIncludeInClipboardHistory should be DWORD 0");
1391 QByteArray cloudClip = mime->data(
"CanUploadToCloudClipboard");
1392 QVERIFY2(cloudClip.size() == 4,
1393 "CanUploadToCloudClipboard should be 4 bytes");
1394 QVERIFY2(cloudClip == dwordBytes(0),
1395 "CanUploadToCloudClipboard should be DWORD 0");
1398 QSKIP(
"Windows-only test");
1402void tst_util::buildClipboardMimeDataDword() {
1404 QByteArray zero = dwordBytes(0);
1405 QVERIFY2(zero.size() == 4,
"DWORD should be 4 bytes");
1406 QVERIFY2(zero.at(0) ==
char(0),
"DWORD 0 should be 0x00");
1407 QVERIFY2(zero.at(1) ==
char(0),
"DWORD 0 should be 0x00");
1408 QVERIFY2(zero.at(2) ==
char(0),
"DWORD 0 should be 0x00");
1409 QVERIFY2(zero.at(3) ==
char(0),
"DWORD 0 should be 0x00");
1411 QByteArray one = dwordBytes(1);
1412 QVERIFY2(one.size() == 4,
"DWORD should be 4 bytes");
1413 QVERIFY2(one.at(0) ==
char(1),
"DWORD 1 should be 0x01");
1414 QVERIFY2(one.at(1) ==
char(0),
"DWORD 1 should be 0x00");
1415 QVERIFY2(one.at(2) ==
char(0),
"DWORD 1 should be 0x00");
1416 QVERIFY2(one.at(3) ==
char(0),
"DWORD 1 should be 0x00");
1418 QSKIP(
"Windows-only test");
1422void tst_util::buildClipboardMimeDataMac() {
1425 QVERIFY(mime !=
nullptr);
1426 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1427 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1428 QVERIFY2(mime->hasFormat(
"application/x-nspasteboard-concealed-type"),
1429 "macOS should have concealed type format");
1430 QVERIFY2(mime->data(
"application/x-nspasteboard-concealed-type") ==
1432 "macOS concealed type should be empty");
1435 QSKIP(
"macOS-only test");
1439void tst_util::utilRegexEnsuresGpg() {
1441 QVERIFY2(rex.isValid(),
"Regex should be valid");
1442 QVERIFY2(rex.match(
"file.gpg").hasMatch(),
"Should match .gpg extension");
1443 QVERIFY2(!rex.match(
"file.txt").hasMatch(),
"Should not match .txt");
1444 QVERIFY2(!rex.match(
"test.gpgx").hasMatch(),
"Should not match .gpgx");
1447void tst_util::utilRegexProtocol() {
1449 QVERIFY2(rex.isValid(),
"Protocol regex should be valid");
1450 QVERIFY2(rex.match(
"http://example.com").hasMatch(),
"Should match http://");
1451 QVERIFY2(rex.match(
"https://secure.com").hasMatch(),
"Should match https://");
1452 QVERIFY2(rex.match(
"ssh://host").hasMatch(),
"Should match ssh://");
1453 QVERIFY2(!rex.match(
"://no-protocol").hasMatch(),
"Should not match invalid");
1456void tst_util::utilRegexNewLines() {
1458 QVERIFY2(rex.isValid(),
"Newlines regex should be valid");
1459 QVERIFY2(rex.match(
"\n").hasMatch(),
"Should match newline");
1460 QVERIFY2(rex.match(
"line1\nline2").hasMatch(),
1461 "Should match embedded newline");
1464void tst_util::reencryptPathNormalization() {
1465 QTemporaryDir tempDir;
1466 QVERIFY2(tempDir.isValid(),
"Temporary directory should be created");
1468 QString basePath = tempDir.path();
1469 QString withExtraSlashes = basePath +
"/./subdir/../";
1470 QString cleaned = QDir::cleanPath(withExtraSlashes);
1471 QString normalized = QDir::cleanPath(basePath);
1472 QVERIFY2(cleaned == normalized,
1473 qPrintable(QString(
"cleanPath should normalize: expected %1, got %2")
1474 .arg(normalized, cleaned)));
1477void tst_util::reencryptPathAbsolutePath() {
1478 QTemporaryDir tempDir;
1479 QVERIFY2(tempDir.isValid(),
"Temporary directory should be created");
1481 QString tempPath = tempDir.path();
1482 QDir(tempPath).mkdir(
"testdir");
1483 QString relativePathFromTemp = tempPath +
"/testdir";
1485 QString result = QDir::cleanPath(QDir(relativePathFromTemp).absolutePath());
1486 QString expected = QDir::cleanPath(tempPath +
"/testdir");
1490 QString(
"Absolute path: expected %1, got %2").arg(expected, result)));
1498void tst_util::findBinaryInPathWithConstQStringRef() {
1502 const QString binaryName = QStringLiteral(
"cmd.exe");
1504 const QString binaryName = QStringLiteral(
"sh");
1507 QVERIFY2(!result.isEmpty(),
1508 "findBinaryInPath should find shell with const QString& arg");
1509 QVERIFY2(result.contains(binaryName),
1510 "Result should contain the binary name");
1511 QVERIFY2(QFileInfo(result).isAbsolute(),
"Returned path should be absolute");
1514void tst_util::findBinaryInPathEmptyString() {
1518 QVERIFY2(result.isEmpty(),
1519 "findBinaryInPath(\"\") should return empty QString");
1522void tst_util::findBinaryInPathStringLiteral() {
1527 const QString binaryName = QStringLiteral(
"sh");
1529 QVERIFY2(!resultDirect.isEmpty(),
1530 "findBinaryInPath with string literal should succeed");
1531 QCOMPARE(resultDirect, resultNamed);
1533 QSKIP(
"Unix-only test");
1537void tst_util::setEnvVarAdds() {
1539 pass.callSetEnvVar(QStringLiteral(
"TEST_KEY="), QStringLiteral(
"hello"));
1540 QStringList env = pass.environment();
1541 QVERIFY2(env.contains(QStringLiteral(
"TEST_KEY=hello")),
1542 "setEnvVar should append key=value when absent");
1545void tst_util::setEnvVarUpdates() {
1547 pass.callSetEnvVar(QStringLiteral(
"TEST_KEY="), QStringLiteral(
"first"));
1548 pass.callSetEnvVar(QStringLiteral(
"TEST_KEY="), QStringLiteral(
"second"));
1549 QStringList env = pass.environment();
1550 QVERIFY2(env.contains(QStringLiteral(
"TEST_KEY=second")),
1551 "setEnvVar should update existing entry");
1552 QVERIFY2(!env.contains(QStringLiteral(
"TEST_KEY=first")),
1553 "setEnvVar should remove old value");
1554 QCOMPARE(env.filter(QStringLiteral(
"TEST_KEY=")).size(), 1);
1557void tst_util::setEnvVarRemoves() {
1559 pass.callSetEnvVar(QStringLiteral(
"TEST_KEY="), QStringLiteral(
"value"));
1560 pass.callSetEnvVar(QStringLiteral(
"TEST_KEY="), QString());
1561 QStringList env = pass.environment();
1562 QVERIFY2(env.filter(QStringLiteral(
"TEST_KEY=")).isEmpty(),
1563 "setEnvVar with empty value should remove the entry");
1566void tst_util::setEnvVarNoopOnMissingRemove() {
1568 QStringList before = pass.environment();
1569 pass.callSetEnvVar(QStringLiteral(
"NONEXISTENT_KEY="), QString());
1570 QStringList after = pass.environment();
1571 QCOMPARE(before, after);
1574void tst_util::updateEnvSetsExpectedVars() {
1576 QTemporaryDir tmpDir;
1577 QVERIFY(tmpDir.isValid());
1581 QStringList env = pass.environment();
1582 QVERIFY2(env.filter(QStringLiteral(
"PASSWORD_STORE_DIR=")).size() == 1,
1583 "updateEnv should set PASSWORD_STORE_DIR");
1584 QVERIFY2(env.filter(QStringLiteral(
"PASSWORD_STORE_DIR="))
1586 .contains(tmpDir.path()),
1587 "updateEnv should set PASSWORD_STORE_DIR to configured store path");
1589 env.filter(QStringLiteral(
"PASSWORD_STORE_GENERATED_LENGTH=")).size() ==
1591 "updateEnv should set PASSWORD_STORE_GENERATED_LENGTH");
1592 QVERIFY2(env.filter(QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET=")).size() ==
1594 "updateEnv should set PASSWORD_STORE_CHARACTER_SET");
1597void tst_util::updateEnvEmptyCustomCharsetFallsBackToAllChars() {
1600 struct ConfigRollback {
1601 PasswordConfiguration value;
1603 } rollback{original};
1605 PasswordConfiguration config = original;
1610 QStringList env = pass.environment();
1611 QStringList charsetEntries =
1612 env.filter(QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET="));
1614 charsetEntries.size() == 1,
1615 "PASSWORD_STORE_CHARACTER_SET should be set even when CUSTOM is empty");
1616 QString val = charsetEntries.first().mid(
1617 QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET=").size());
1618 QVERIFY2(!val.isEmpty(),
1619 "charset should fall back to ALLCHARS, not be empty");
1622void tst_util::updateEnvWslenvContainsRequiredVars() {
1624 const QStringList env = pass.environment();
1626 const QStringList wslenvEntries =
1627 env.filter(QRegularExpression(QStringLiteral(
"^WSLENV=")));
1628 QVERIFY2(!wslenvEntries.isEmpty(),
1629 "At least one WSLENV entry expected after Pass construction");
1631 const QString wslenv = wslenvEntries.first();
1632 QVERIFY2(wslenv.contains(QStringLiteral(
"PASSWORD_STORE_DIR/p")),
1633 "WSLENV should include PASSWORD_STORE_DIR/p");
1634 QVERIFY2(wslenv.contains(QStringLiteral(
"PASSWORD_STORE_GENERATED_LENGTH/w")),
1635 "WSLENV should include PASSWORD_STORE_GENERATED_LENGTH/w");
1636 QVERIFY2(wslenv.contains(QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET/w")),
1637 "WSLENV should include PASSWORD_STORE_CHARACTER_SET/w");
1642void tst_util::gpgErrorMessageKeyExpiredStatusToken() {
1643 QString err =
"[GNUPG:] KEYEXPIRED 1234567890\n"
1644 "gpg: key DEADBEEF: key has expired\n"
1645 "gpg: [stdin]: encryption failed: Unusable public key";
1647 QVERIFY2(!msg.isEmpty(),
"Should recognise KEYEXPIRED status token");
1648 QVERIFY2(msg.contains(
"expired", Qt::CaseInsensitive),
1649 qPrintable(
"Expected 'expired' in: " + msg));
1652void tst_util::gpgErrorMessageKeyRevokedStatusToken() {
1653 QString err =
"[GNUPG:] KEYREVOKED\n"
1654 "gpg: [stdin]: encryption failed: Unusable public key";
1656 QVERIFY2(!msg.isEmpty(),
"Should recognise KEYREVOKED status token");
1657 QVERIFY2(msg.contains(
"revoked", Qt::CaseInsensitive),
1658 qPrintable(
"Expected 'revoked' in: " + msg));
1661void tst_util::gpgErrorMessageNoPubkeyStatusToken() {
1662 QString err =
"[GNUPG:] NO_PUBKEY DEADBEEFDEADBEEF\n"
1663 "gpg: DEADBEEF: skipped: No public key";
1665 QVERIFY2(!msg.isEmpty(),
"Should recognise NO_PUBKEY status token");
1666 QVERIFY2(msg.contains(
"not found", Qt::CaseInsensitive) ||
1667 msg.contains(
"invalid", Qt::CaseInsensitive),
1668 qPrintable(
"Expected 'not found' or 'invalid' in: " + msg));
1671void tst_util::gpgErrorMessageInvRecpStatusToken() {
1673 QString errExpired =
"[GNUPG:] INV_RECP 5 DEADBEEF\n"
1674 "gpg: [stdin]: encryption failed: Unusable public key";
1676 QVERIFY2(!msgExpired.isEmpty(),
"Should recognise INV_RECP 5 (expired)");
1677 QVERIFY2(msgExpired.contains(
"expired", Qt::CaseInsensitive),
1678 qPrintable(
"Expected 'expired' for INV_RECP 5: " + msgExpired));
1681 QString errRevoked =
"[GNUPG:] INV_RECP 4 DEADBEEF\n"
1682 "gpg: [stdin]: encryption failed: Unusable public key";
1684 QVERIFY2(!msgRevoked.isEmpty(),
"Should recognise INV_RECP 4 (revoked)");
1685 QVERIFY2(msgRevoked.contains(
"revoked", Qt::CaseInsensitive),
1686 qPrintable(
"Expected 'revoked' for INV_RECP 4: " + msgRevoked));
1689 QString errGeneric =
"[GNUPG:] INV_RECP 10 DEADBEEF\n"
1690 "gpg: [stdin]: encryption failed: Unusable public key";
1692 QVERIFY2(!msgGeneric.isEmpty(),
"Should recognise INV_RECP status token");
1693 QVERIFY2(msgGeneric.contains(
"not found", Qt::CaseInsensitive) ||
1694 msgGeneric.contains(
"invalid", Qt::CaseInsensitive),
1695 qPrintable(
"Expected 'not found' or 'invalid' in: " + msgGeneric));
1698void tst_util::gpgErrorMessageFailureStatusToken() {
1699 QString err =
"[GNUPG:] FAILURE encrypt 67108949";
1701 QVERIFY2(!msg.isEmpty(),
"Should recognise FAILURE status token");
1702 QVERIFY2(msg.contains(
"failed", Qt::CaseInsensitive),
1703 qPrintable(
"Expected 'failed' in: " + msg));
1706void tst_util::gpgErrorMessageKeyExpiredFallback() {
1707 QString err =
"gpg: key DEADBEEF: key has expired\n"
1708 "gpg: [stdin]: encryption failed: Unusable public key";
1710 QVERIFY2(!msg.isEmpty(),
"Should recognise 'key has expired' fallback");
1711 QVERIFY2(msg.contains(
"expired", Qt::CaseInsensitive),
1712 qPrintable(
"Expected 'expired' in: " + msg));
1715void tst_util::gpgErrorMessageRevokedFallback() {
1716 QString err =
"gpg: key DEADBEEF: key has been revoked\n"
1717 "gpg: [stdin]: encryption failed: Unusable public key";
1719 QVERIFY2(!msg.isEmpty(),
"Should recognise 'key has been revoked' fallback");
1720 QVERIFY2(msg.contains(
"revoked", Qt::CaseInsensitive),
1721 qPrintable(
"Expected 'revoked' in: " + msg));
1724void tst_util::gpgErrorMessageNoPubkeyFallback() {
1725 QString err =
"gpg: DEADBEEF: skipped: No public key\n"
1726 "gpg: [stdin]: encryption failed: No public key";
1728 QVERIFY2(!msg.isEmpty(),
"Should recognise 'No public key' fallback");
1729 QVERIFY2(msg.contains(
"not found", Qt::CaseInsensitive) ||
1730 msg.contains(
"invalid", Qt::CaseInsensitive),
1731 qPrintable(
"Expected 'not found' or 'invalid' in: " + msg));
1734void tst_util::gpgErrorMessageEncryptionFailedFallback() {
1735 QString err =
"gpg: [stdin]: encryption failed: Unusable public key";
1737 QVERIFY2(!msg.isEmpty(),
1738 "Should recognise generic 'encryption failed' fallback");
1739 QVERIFY2(msg.contains(
"failed", Qt::CaseInsensitive),
1740 qPrintable(
"Expected 'failed' in: " + msg));
1743void tst_util::gpgErrorMessageUnknownReturnsEmpty() {
1744 QString err =
"some unrelated process error output";
1746 QVERIFY2(msg.isEmpty(),
1747 "Should return empty string for unrecognised GPG output");
1750void tst_util::gpgErrorMessageStatusTokenTakesPriorityOverFallback() {
1753 QString err =
"[GNUPG:] KEYEXPIRED 1234567890\n"
1754 "gpg: [stdin]: encryption failed: Unusable public key";
1756 QVERIFY2(msg.contains(
"expired", Qt::CaseInsensitive),
1757 "KEYEXPIRED token should take priority and mention 'expired'");
1762void tst_util::parseGrepOutputEmpty() {
1764 QVERIFY(results.isEmpty());
1767void tst_util::parseGrepOutputSingleEntry() {
1769 const QString raw = QStringLiteral(
"\x1B[94mmy/entry\x1B[0m:\nsome match\n");
1771 QCOMPARE(results.size(), 1);
1772 QCOMPARE(results[0].first, QStringLiteral(
"my/entry"));
1773 QCOMPARE(results[0].second.size(), 1);
1774 QCOMPARE(results[0].second[0], QStringLiteral(
"some match"));
1777void tst_util::parseGrepOutputMultipleEntries() {
1778 const QString raw = QStringLiteral(
"\x1B[94mentry/a\x1B[0m:\nline1\nline2\n"
1779 "\x1B[94mentry/b\x1B[0m:\nlineX\n");
1781 QCOMPARE(results.size(), 2);
1782 QCOMPARE(results[0].first, QStringLiteral(
"entry/a"));
1783 QCOMPARE(results[0].second.size(), 2);
1784 QCOMPARE(results[1].first, QStringLiteral(
"entry/b"));
1785 QCOMPARE(results[1].second.size(), 1);
1788void tst_util::parseGrepOutputAnsiStripped() {
1791 QStringLiteral(
"\x1B[94mfoo\x1B[0m:\n\x1B[1mhi\x1B[0m there\n");
1793 QCOMPARE(results.size(), 1);
1794 QCOMPARE(results[0].second[0], QStringLiteral(
"hi there"));
1797void tst_util::parseGrepOutputHeaderColonStripped() {
1798 const QString raw = QStringLiteral(
"\x1B[94msome/path:\x1B[0m\nvalue\n");
1800 QCOMPARE(results.size(), 1);
1803 !results[0].first.endsWith(
':'),
1804 qPrintable(
"Entry should not end with ':', got: " + results[0].first));
1807void tst_util::parseGrepOutputCrlfHandled() {
1808 const QString raw = QStringLiteral(
"\x1B[94mentry\x1B[0m:\r\nmatch line\r\n");
1810 QCOMPARE(results.size(), 1);
1811 QVERIFY2(!results[0].second[0].contains(
'\r'),
1812 "Match line must not contain CR");
1815void tst_util::parseGrepOutputOrphanMatchesIgnored() {
1818 QStringLiteral(
"orphan line\n\x1B[94mentry\x1B[0m:\nreal match\n");
1820 QCOMPARE(results.size(), 1);
1821 QCOMPARE(results[0].second.size(), 1);
1822 QCOMPARE(results[0].second[0], QStringLiteral(
"real match"));
1825void tst_util::parseGrepOutputEmptyMatchLinesIgnored() {
1826 const QString raw = QStringLiteral(
"\x1B[94mentry\x1B[0m:\n\n \nmatch\n");
1828 QCOMPARE(results.size(), 1);
1829 QCOMPARE(results[0].second.size(), 1);
1832void tst_util::parseGrepOutputLastEntryIncluded() {
1834 const QString raw = QStringLiteral(
"\x1B[94mfinal\x1B[0m:\nvalue");
1836 QCOMPARE(results.size(), 1);
1837 QCOMPARE(results[0].first, QStringLiteral(
"final"));
1840void tst_util::parseGrepOutputEmbeddedBlueNotHeader() {
1844 const QString raw = QStringLiteral(
1845 "\x1B[94mentry/a\x1B[0m:\nfirst match\ncontains \x1B[94mblue\x1B[0m "
1846 "highlight\n\x1B[94mentry/b\x1B[0m:\nother\n");
1848 QCOMPARE(results.size(), 2);
1849 QCOMPARE(results[0].first, QStringLiteral(
"entry/a"));
1850 QCOMPARE(results[0].second.size(), 2);
1851 QCOMPARE(results[0].second[1], QStringLiteral(
"contains blue highlight"));
1854void tst_util::parseGrepOutputPlainTextHeaders() {
1857 QStringLiteral(
"entry/a:\n match one\n match two\nentry/b:\n other\n");
1859 QCOMPARE(results.size(), 2);
1860 QCOMPARE(results[0].first, QStringLiteral(
"entry/a"));
1861 QCOMPARE(results[0].second.size(), 2);
1862 QCOMPARE(results[1].first, QStringLiteral(
"entry/b"));
1863 QCOMPARE(results[1].second.size(), 1);
1868void tst_util::passFinishedGrepNoMatchEmitsEmpty() {
1875 QCOMPARE(spy.count(), 1);
1876 QCOMPARE(errSpy.count(), 0);
1877 const auto results = spy[0][0].value<
GrepResults>();
1878 QVERIFY(results.isEmpty());
1881void tst_util::passFinishedGrepErrorEmitsProcessError() {
1887 QStringLiteral(
"some gpg error"));
1888 QCOMPARE(errSpy.count(), 1);
1889 QCOMPARE(spy.count(), 1);
1890 QVERIFY(spy[0][0].value<GrepResults>().isEmpty());
1893void tst_util::passFinishedGrepSuccessEmitsResults() {
1898 QStringLiteral(
"\x1B[94mwork/github\x1B[0m:\ntoken: abc123\n");
1899 pass.callPassFinished(
static_cast<int>(
Enums::PASS_GREP), 0, out, QString());
1901 QVERIFY2(spy.count() == 1,
"finishedGrep should be emitted exactly once");
1902 const auto results = spy[0][0].value<
GrepResults>();
1903 QVERIFY2(results.size() == 1,
"Should have one matching entry");
1904 const auto &entry = results[0];
1905 QVERIFY2(entry.first ==
"work/github",
"Entry path should be 'work/github'");
1906 QVERIFY2(entry.second.size() == 1,
"Should have one matched line");
1907 QVERIFY2(entry.second[0] ==
"token: abc123",
1908 "Matched line should be 'token: abc123'");
1913void tst_util::grepMatchFileFailedDecryptReturnsEmpty() {
1916 QRegularExpression rx(QStringLiteral(
".*"));
1917 const QStringList env;
1918 const QStringList matches =
1919 ImitatePass::grepMatchFile(env, QStringLiteral(
"/nonexistent/gpg"),
1920 QStringLiteral(
"/no/such.gpg"), rx);
1921 QVERIFY(matches.isEmpty());
1924void tst_util::grepScanStoreEmptyDirReturnsEmpty() {
1926 QVERIFY(tmp.isValid());
1927 QRegularExpression rx(QStringLiteral(
".*"));
1928 const QStringList env;
1929 const auto results = ImitatePass::grepScanStore(
1930 env, QStringLiteral(
"/nonexistent/gpg"), tmp.path(), rx);
1931 QVERIFY(results.isEmpty());
1934void tst_util::grepImitatePassEmptyStoreEmitsEmpty() {
1936 QVERIFY(tmp.isValid());
1942 pass.Grep(QStringLiteral(
"anything"));
1944 QVERIFY(spy.wait(3000));
1945 QCOMPARE(spy.count(), 1);
1946 const auto results = spy[0][0].value<
GrepResults>();
1947 QVERIFY(results.isEmpty());
1950void tst_util::grepImitatePassInvalidRegexEmitsEmpty() {
1952 QVERIFY(tmp.isValid());
1959 pass.Grep(QStringLiteral(
"[invalid"));
1960 QVERIFY(spy.wait(3000));
1961 QCOMPARE(spy.count(), 1);
1962 const auto results = spy[0][0].value<
GrepResults>();
1963 QVERIFY(results.isEmpty());
1967#include "tst_util.moc"
auto getRemainingData() const -> QString
Gets remaining data not in named values.
auto getNamedValues() const -> NamedValues
Gets named value pairs from the parsed file.
auto getRemainingDataForDisplay() const -> QString
Gets remaining data for display (excludes hidden fields like OTP).
auto getPassword() const -> QString
Gets the password from the parsed file.
static auto parse(const QString &fileContent, const QStringList &templateFields, bool allFields) -> FileContent
parse parses the given fileContent in a FileContent object. The password is accessible through getPas...
Implementation that imitates 'pass' when the real tool is unavailable.
auto removeDir(const QString &dirName) -> bool
Remove directory recursively.
void finished(int id, int exitCode, const QString &out, const QString &err) override
Handle process completion.
auto resolveMoveDestination(const QString &src, const QString &dest, bool force) -> QString
Resolve destination for move operation.
auto takeValue(const QString &name) -> QString
Finds and removes a named value by name.
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
void setEnvVar(const QString &key, const QString &value)
Set or remove an environment variable.
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
Emitted when grep finishes with matching results.
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
void processErrorExit(int exitCode, const QString &err)
Emitted on process error exit.
void updateEnv()
Update environment for subprocesses.
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
static auto getRecipientString(const QString &for_file, const QString &separator=" ", int *count=nullptr) -> QStringList
Get recipients as string.
void stopAnimation()
Stops the spin animation.
auto isAnimated() const -> bool
Returns a Boolean value indicating whether the component is currently animated.
void startAnimation()
Starts the spin animation.
static void setPassStore(const QString &passStore)
Save password store path.
static void setPasswordConfiguration(const PasswordConfiguration &config)
Save complete password generation configuration.
static auto getPasswordConfiguration() -> PasswordConfiguration
Get complete password generation configuration.
static auto isUsePass(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to use pass (true) or GPG (false).
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto getGpgExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG executable path.
static void setGpgExecutable(const QString &gpgExecutable)
Save GPG executable path.
static auto isUsePwgen(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether pwgen support is enabled.
auto getStore() const -> QString
Get the password store root path.
void setModelAndStore(QFileSystemModel *sourceModel, const QString &passStore)
Initialize model with source model and store path.
static auto protocolRegex() -> const QRegularExpression &
Returns a regex to match URL protocols.
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
static auto findPasswordStore() -> QString
Locate the password store directory.
static auto getDir(const QModelIndex &index, bool forPass, const QFileSystemModel &model, const StoreModel &storeModel) -> QString
Get the selected folder path, either relative to the configured pass store or absolute.
static auto isValidKeyId(const QString &keyId) -> bool
Check if a string looks like a valid GPG key ID. Accepts:
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with the native directory separator.
static auto findBinaryInPath(const QString &binary) -> QString
Locate an executable by searching the process PATH and (on Windows) falling back to WSL.
static auto configIsValid() -> bool
Verify that the required configuration is complete.
auto transactionIsOver(Enums::PROCESS) -> Enums::PROCESS
transactionIsOver checks wheather currently finished process is last in current transaction
void transactionAdd(Enums::PROCESS)
transactionAdd If called after call to transactionStart() and before transactionEnd(),...
The tst_util class is our first unit test.
void cleanup()
tst_util::cleanup unit test cleanup method
tst_util()
tst_util::tst_util basic constructor
void init()
tst_util::init unit test init method
~tst_util() override
tst_util::~tst_util basic destructor
PROCESS
Identifies different subprocess operations used in QtPass.
auto gpgErrorMessage(const QString &err) -> QString
Maps GPG stderr (which may include –status-fd 2 tokens) to a translated user-friendly encryption erro...
auto parseGrepOutput(const QString &rawOut) -> QList< QPair< QString, QStringList > >
Parses 'pass grep' raw output into (entry, matches) pairs.
auto buildClipboardMimeData(const QString &text) -> QMimeData *
Build clipboard MIME data with platform-specific security hints.
int length
Length of the password.
enum PasswordConfiguration::characterSet selected
QString Characters[CHARSETS_COUNT]
The different character sets.
bool have_secret
UserInfo::have_secret whether secret key is available (can decrypt with this key).
bool enabled
UserInfo::enabled Whether this user/key is enabled for normal use. True when the key should be treate...
auto marginallyValid() const -> bool
UserInfo::marginallyValid when validity is m. http://git.gnupg.org/cgi-bin/gitweb....
QString key_id
UserInfo::key_id hexadecimal representation of the GnuPG key identifier.
auto fullyValid() const -> bool
UserInfo::fullyValid when validity is f or u. http://git.gnupg.org/cgi-bin/gitweb....
auto isValid() const -> bool
UserInfo::isValid when fullyValid or marginallyValid.
QDateTime created
UserInfo::created date/time when key was created.
QString name
UserInfo::name GPG user ID / full name.
char validity
UserInfo::validity GnuPG representation of validity http://git.gnupg.org/cgi-bin/gitweb....
QDateTime expiry
UserInfo::expiry date/time when key expires.
QList< QPair< QString, QStringList > > GrepResults
Integration tests for ImitatePass and RealPass backends.
QList< QPair< QString, QStringList > > GrepResults