3#include <QCoreApplication>
8#include <QProcessEnvironment>
9#include <QTemporaryDir>
40 struct PassStoreGuard {
42 explicit PassStoreGuard(
const QString &orig) : original(orig) {}
47 void cleanupTestCase();
48 void normalizeFolderPath();
49 void normalizeFolderPathEdgeCases();
51 void fileContentEdgeCases();
52 void namedValuesTakeValue();
53 void namedValuesEdgeCases();
54 void totpHiddenFromDisplay();
57 void regexPatternEdgeCases();
58 void endsWithGpgEdgeCases();
59 void userInfoValidity();
60 void userInfoValidityEdgeCases();
61 void passwordConfigurationCharacters();
62 void simpleTransactionBasic();
63 void simpleTransactionNested();
64 void createGpgIdFile();
65 void createGpgIdFileEmptyKeys();
66 void generateRandomPassword();
68 void findBinaryInPath();
69 void findPasswordStore();
72 void getDirWithIndex();
73 void findBinaryInPathNotFound();
74 void findPasswordStoreEnvVar();
75 void normalizeFolderPathMultipleCalls();
76 void userInfoFullyValid();
77 void userInfoMarginallyValid();
78 void userInfoIsValid();
79 void userInfoCreatedAndExpiry();
80 void qProgressIndicatorBasic();
81 void qProgressIndicatorStartStop();
82 void namedValueBasic();
83 void namedValueMultiple();
84 void buildClipboardMimeDataLinux();
85 void buildClipboardMimeDataWindows();
86 void buildClipboardMimeDataMac();
87 void utilRegexEnsuresGpg();
88 void utilRegexProtocol();
89 void utilRegexNewLines();
90 void buildClipboardMimeDataDword();
91 void imitatePassResolveMoveDestination();
92 void imitatePassResolveMoveDestinationForce();
93 void imitatePassResolveMoveDestinationDestExistsNoForce();
94 void imitatePassResolveMoveDestinationDir();
95 void imitatePassResolveMoveDestinationNonExistent();
96 void imitatePassRemoveDir();
97 void getRecipientListBasic();
98 void getRecipientListEmpty();
99 void getRecipientListWithComments();
100 void getRecipientListInvalidKeyId();
101 void isValidKeyIdBasic();
102 void isValidKeyIdWith0xPrefix();
103 void isValidKeyIdWithEmail();
104 void isValidKeyIdInvalid();
105 void getRecipientStringCount();
106 void getGpgIdPathBasic();
107 void getGpgIdPathSubfolder();
108 void getGpgIdPathNotFound();
109 void findBinaryInPathReturnedPathIsAbsolute();
110 void findBinaryInPathReturnedPathIsExecutable();
111 void findBinaryInPathMultipleKnownBinaries();
112 void findBinaryInPathConsistency();
113 void findBinaryInPathResultContainsBinaryName();
114 void findBinaryInPathTempExecutableInTempDir();
144void tst_util::cleanupTestCase() {
152void tst_util::normalizeFolderPath() {
154 QString sep = QDir::separator();
158 QVERIFY(result.endsWith(sep));
160 QVERIFY(result.endsWith(sep));
163 QVERIFY2(result ==
"test" + sep,
164 qPrintable(QString(
"Expected 'test%1', got '%2'").arg(sep, result)));
166 QVERIFY2(result ==
"test" + sep,
167 qPrintable(QString(
"Expected 'test%1', got '%2'").arg(sep, result)));
170 if (QDir::separator() ==
'\\') {
172 QVERIFY(result.endsWith(
"\\"));
173 QVERIFY(result.contains(
"test"));
174 QVERIFY(result.contains(
"subdir"));
177 result ==
"test\\subdir\\",
179 QString(
"Expected 'test\\\\subdir\\\\', got '%1'").arg(result)));
184 QVERIFY(result.endsWith(sep));
185 QVERIFY(result.contains(
"test"));
186 QVERIFY(result.contains(
"subdir"));
187 QVERIFY(result.contains(
"folder"));
190void tst_util::fileContent() {
191 NamedValue key = {
"key",
"val"};
192 NamedValue key2 = {
"key2",
"val2"};
193 QString password =
"password";
221void tst_util::namedValuesTakeValue() {
222 NamedValues nv = {{
"key1",
"value1"}, {
"key2",
"value2"}, {
"key3",
"value3"}};
225 QCOMPARE(val, QString(
"value2"));
226 QCOMPARE(nv.length(), 2);
227 QVERIFY(!nv.contains({
"key2",
"value2"}));
230 QVERIFY(val.isEmpty());
233 QCOMPARE(val, QString(
"value1"));
235 QCOMPARE(val, QString(
"value3"));
236 QVERIFY(nv.isEmpty());
239void tst_util::totpHiddenFromDisplay() {
241 "password\notpauth://totp/Test?secret=JBSWY3DPEHPK3PXP\nkey: value\n", {},
245 QVERIFY(remaining.contains(
"otpauth://"));
246 QVERIFY(remaining.contains(
"key: value"));
249 QVERIFY(!display.contains(
"otpauth"));
250 QVERIFY(display.contains(
"key: value"));
253 "password\nOTPAUTH://TOTP/Test?secret=JBSWY3DPEHPK3PXP\n", {},
false);
257void tst_util::testAwsUrl() {
260 QRegularExpressionMatch match1 =
261 proto.match(
"https://rh-dev.signin.aws.amazon.com/console");
262 QVERIFY2(match1.hasMatch(),
"Should match AWS console URL");
263 QString captured1 = match1.captured(1);
264 QVERIFY2(captured1.contains(
"amazon.com"),
"Should include full URL");
266 QRegularExpressionMatch match2 = proto.match(
"https://test-example.com/path");
267 QVERIFY2(match2.hasMatch(),
"Should match URL with dash");
268 QString captured2 = match2.captured(1);
269 QVERIFY2(captured2.contains(
"test-example.com"),
270 "Should include full domain");
273void tst_util::regexPatterns() {
275 QVERIFY(gpg.match(
"test.gpg").hasMatch());
276 QVERIFY(gpg.match(
"folder/test.gpg").hasMatch());
277 QVERIFY(!gpg.match(
"test.gpg~").hasMatch());
278 QVERIFY(!gpg.match(
"test.gpg.bak").hasMatch());
281 QVERIFY(proto.match(
"https://example.com").hasMatch());
282 QVERIFY(proto.match(
"ssh://user@host/path").hasMatch());
283 QVERIFY(proto.match(
"ftp://server/file").hasMatch());
284 QVERIFY(proto.match(
"webdav://localhost/share").hasMatch());
285 QVERIFY(!proto.match(
"not a url").hasMatch());
286 QRegularExpressionMatch urlWithTrailingTextMatch =
287 proto.match(
"https://example.com/ is the address");
288 QVERIFY(urlWithTrailingTextMatch.hasMatch());
289 QString captured = urlWithTrailingTextMatch.captured(1);
290 QVERIFY2(!captured.contains(
" "),
"URL should not include space");
291 QVERIFY2(!captured.contains(
"<"),
"URL should not include <");
292 QVERIFY2(captured ==
"https://example.com/",
"URL should stop at space");
294 QRegularExpressionMatch urlWithFragmentMatch =
295 proto.match(
"Link: https://test.org/path?q=1#frag");
296 QVERIFY(urlWithFragmentMatch.hasMatch());
297 captured = urlWithFragmentMatch.captured(1);
298 QVERIFY2(captured.contains(
"?"),
"URL should include query params");
299 QVERIFY2(captured.contains(
"#"),
"URL should include fragment");
300 QVERIFY2(!captured.contains(
" now"),
"URL should not include trailing text");
303 QVERIFY(nl.match(
"\n").hasMatch());
304 QVERIFY(nl.match(
"\r").hasMatch());
305 QVERIFY(nl.match(
"\r\n").hasMatch());
308void tst_util::normalizeFolderPathEdgeCases() {
310 QVERIFY(result.endsWith(QDir::separator()));
311 QVERIFY2(result == QDir::separator() || result.endsWith(QDir::separator()),
312 "Empty path should become separator");
315 QVERIFY(result.endsWith(QDir::separator()));
318 QVERIFY(result.endsWith(QDir::separator()));
321 QVERIFY(nativeResult.endsWith(QDir::separator()));
324void tst_util::fileContentEdgeCases() {
329 "secret\nurl: https://login.com\n",
330 {
"username",
"password",
"url"},
false);
335 QCOMPARE(nv.length(), 1);
336 QCOMPARE(nv.at(0).name, QString(
"key"));
337 QVERIFY(nv.at(0).value.contains(
"spaces"));
353void tst_util::namedValuesEdgeCases() {
355 QVERIFY(nv.isEmpty());
356 QVERIFY(nv.
takeValue(
"nonexistent").isEmpty());
358 NamedValue n1 = {
"key",
"value"};
360 QCOMPARE(nv.length(), 1);
361 NamedValue n2 = {
"key2",
"value2"};
363 QCOMPARE(nv.length(), 2);
366 QVERIFY(nv.isEmpty());
367 QVERIFY(nv.
takeValue(
"anything").isEmpty());
370void tst_util::regexPatternEdgeCases() {
372 QVERIFY(gpg.match(
".gpg").hasMatch());
373 QVERIFY(gpg.match(
"a.gpg").hasMatch());
374 QVERIFY(!gpg.match(
"test.gpgx").hasMatch());
377 QVERIFY(proto.match(
"webdavs://secure.example.com").hasMatch());
378 QVERIFY(proto.match(
"ftps://ftp.server.org").hasMatch());
379 QVERIFY(proto.match(
"sftp://user:pass@host").hasMatch());
381 QVERIFY(!proto.match(
"file:///path/to/file").hasMatch());
384 QVERIFY(nl.match(
"\n").hasMatch());
385 QVERIFY(nl.match(
"\r").hasMatch());
386 QVERIFY(nl.match(
"\r\n").hasMatch());
389void tst_util::endsWithGpgEdgeCases() {
391 QVERIFY(!gpg.match(
".gpgx").hasMatch());
392 QVERIFY(!gpg.match(
"test.gpg.bak").hasMatch());
393 QVERIFY(!gpg.match(
"test.gpg~").hasMatch());
394 QVERIFY(!gpg.match(
"test.gpg.orig").hasMatch());
395 QVERIFY(gpg.match(
"test.gpg").hasMatch());
396 QVERIFY(gpg.match(
"test/path/file.gpg").hasMatch());
397 QVERIFY(gpg.match(
"/absolute/path/file.gpg").hasMatch());
398 QVERIFY(gpg.match(
"file name with spaces.gpg").hasMatch());
401void tst_util::userInfoValidity() {
427void tst_util::userInfoValidityEdgeCases() {
435 char nullChar =
'\0';
443void tst_util::passwordConfigurationCharacters() {
444 PasswordConfiguration config;
445 QCOMPARE(config.
length, 16);
460void tst_util::simpleTransactionBasic() {
461 simpleTransaction trans;
467void tst_util::simpleTransactionNested() {
468 simpleTransaction trans;
477void tst_util::createGpgIdFile() {
478 QTemporaryDir tempDir;
479 QString newDir = tempDir.path() +
"/testfolder";
480 QVERIFY(QDir().mkdir(newDir));
482 QString gpgIdFile = newDir +
"/.gpg-id";
483 QStringList keyIds = {
"ABCDEF12",
"34567890"};
485 QFile gpgId(gpgIdFile);
486 QVERIFY(gpgId.open(QIODevice::WriteOnly));
487 for (
const QString &keyId : keyIds) {
488 gpgId.write((keyId +
"\n").toUtf8());
492 QVERIFY(QFile::exists(gpgIdFile));
494 QFile readFile(gpgIdFile);
495 QVERIFY(readFile.open(QIODevice::ReadOnly));
496 QString content = QString::fromUtf8(readFile.readAll());
499 QStringList lines = content.trimmed().split(
'\n');
500 QCOMPARE(lines.size(), 2);
501 QCOMPARE(lines[0], QString(
"ABCDEF12"));
502 QCOMPARE(lines[1], QString(
"34567890"));
505void tst_util::createGpgIdFileEmptyKeys() {
506 QTemporaryDir tempDir;
507 QString newDir = tempDir.path() +
"/testfolder";
508 QVERIFY(QDir().mkdir(newDir));
510 QString gpgIdFile = newDir +
"/.gpg-id";
512 QFile gpgId(gpgIdFile);
513 QVERIFY(gpgId.open(QIODevice::WriteOnly));
516 QVERIFY(QFile::exists(gpgIdFile));
518 QFile readFile(gpgIdFile);
519 QVERIFY(readFile.open(QIODevice::ReadOnly));
520 QString content = QString::fromUtf8(readFile.readAll());
523 QVERIFY(content.isEmpty());
526void tst_util::generateRandomPassword() {
528 QString charset =
"abcdefghijklmnopqrstuvwxyz";
531 QCOMPARE(result.length(), 10);
532 for (
const QChar &ch : result) {
534 charset.contains(ch),
535 "Generated password contains character outside the specified charset");
539 QString charset2 =
"abcd";
540 QCOMPARE(result.length(), 100);
541 for (
const QChar &ch : result) {
543 charset2.contains(ch),
544 "Generated password contains character outside the specified charset");
548 QVERIFY(result.isEmpty());
551 QString charset3 =
"ABC";
552 QCOMPARE(result.length(), 50);
553 for (
const QChar &ch : result) {
555 charset3.contains(ch),
556 "Generated password contains character outside the specified charset");
560void tst_util::boundedRandom() {
563 QVector<quint32> counts(10, 0);
564 const int iterations = 1000;
566 for (
int i = 0; i < iterations; ++i) {
568 quint32 val = result.at(0).digitValue();
569 QVERIFY2(val < 10,
"generatePassword should only return digit characters");
573 for (
int i = 0; i < 10; ++i) {
574 QVERIFY2(counts[i] > 0,
"Each digit should appear at least once");
578void tst_util::findBinaryInPath() {
580 const QString binaryName = QStringLiteral(
"cmd.exe");
582 const QString binaryName = QStringLiteral(
"sh");
585 QVERIFY2(!result.isEmpty(),
"Should find a standard shell in PATH");
586 QVERIFY(result.contains(binaryName));
589 QVERIFY(result.isEmpty());
592void tst_util::findPasswordStore() {
594 QVERIFY(!result.isEmpty());
595 QVERIFY(result.endsWith(QDir::separator()));
598void tst_util::configIsValid() {
599 QTemporaryDir tempDir;
600 QVERIFY2(tempDir.isValid(),
"Temporary directory should be created");
607 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id is missing");
610 QFile gpgIdFile(tempDir.path() + QDir::separator() +
611 QStringLiteral(
".gpg-id"));
612 QVERIFY2(gpgIdFile.open(QIODevice::WriteOnly | QIODevice::Truncate),
613 "Should be able to create .gpg-id");
614 gpgIdFile.write(
"test@example.com\n");
621 } gpgRollback{originalGpgExecutable};
625 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id exists but gpg "
626 "executable is missing");
629 QStringLiteral(
"definitely_nonexistent_gpg_binary_12345"));
631 QVERIFY2(!isValid,
"Expected invalid config when .gpg-id exists but gpg "
632 "executable is invalid");
635void tst_util::getDirBasic() {
636 QTemporaryDir tempDir;
637 QVERIFY2(tempDir.isValid(),
638 "Temporary directory should be created successfully");
640 QFileSystemModel fsm;
641 fsm.setRootPath(tempDir.path());
644 QVERIFY(sm.sourceModel() !=
nullptr);
645 QVERIFY2(sm.
getStore() == tempDir.path(),
646 "Store path should match the set value");
647 QModelIndex rootIndex = fsm.index(tempDir.path());
648 QVERIFY2(rootIndex.isValid(),
"Filesystem model root index should be valid");
652 QString result =
Util::getDir(QModelIndex(),
false, fsm, sm);
653 QString expectedDir = QDir(tempDir.path()).absolutePath();
654 if (!expectedDir.endsWith(QDir::separator())) {
655 expectedDir += QDir::separator();
658 result == expectedDir,
659 qPrintable(QString(
"Expected '%1', got '%2'").arg(expectedDir, result)));
663void tst_util::getDirWithIndex() {
664 QTemporaryDir tempDir;
665 QVERIFY2(tempDir.isValid(),
666 "Temporary directory should be created successfully");
668 const QString dirPath = tempDir.path();
669 const QString filePath =
670 QDir(dirPath).filePath(QStringLiteral(
"testfile.txt"));
672 QFile file(filePath);
673 QVERIFY2(file.open(QIODevice::WriteOnly),
674 "Failed to create test file in temporary directory");
675 const char testData[] =
"dummy";
676 const qint64 bytesWritten = file.write(testData,
sizeof(testData) - 1);
677 QVERIFY2(bytesWritten ==
static_cast<qint64
>(
sizeof(testData) - 1),
678 "Failed to write test data to file in temporary directory");
682 PassStoreGuard passStoreGuard(originalPassStore);
685 QFileSystemModel fsm;
686 fsm.setRootPath(dirPath);
690 QVERIFY2(sm.
getStore() == dirPath,
"Store path should match the set value");
692 QModelIndex sourceIndex = fsm.index(filePath);
693 QVERIFY2(sourceIndex.isValid(),
694 "Source index should be valid for the test file");
695 QModelIndex fileIndex = sm.mapFromSource(sourceIndex);
696 QVERIFY2(fileIndex.isValid(),
697 "Proxy index should be valid for the test file");
699 QString result =
Util::getDir(fileIndex,
false, fsm, sm);
700 QVERIFY2(!result.isEmpty(),
701 "getDir should return a non-empty directory for a valid index");
702 QVERIFY(result.endsWith(QDir::separator()));
704 QString expectedPath = dirPath;
705 if (!expectedPath.endsWith(QDir::separator())) {
706 expectedPath += QDir::separator();
709 result == expectedPath,
711 QStringLiteral(
"Expected '%1', got '%2'").arg(expectedPath, result)));
713 QModelIndex invalidIndex;
714 QString invalidResult =
Util::getDir(invalidIndex,
false, fsm, sm);
715 QString expectedForInvalid = dirPath;
716 if (!expectedForInvalid.endsWith(QDir::separator())) {
717 expectedForInvalid += QDir::separator();
719 QVERIFY2(invalidResult == expectedForInvalid,
720 qPrintable(QStringLiteral(
"getDir should return pass store for "
721 "invalid index. Expected '%1', got '%2'")
722 .arg(expectedForInvalid, invalidResult)));
725void tst_util::findBinaryInPathNotFound() {
727 QVERIFY(result.isEmpty());
730void tst_util::findPasswordStoreEnvVar() {
732 QVERIFY(!result.isEmpty());
735void tst_util::normalizeFolderPathMultipleCalls() {
738 QVERIFY(result1.endsWith(QDir::separator()));
739 QVERIFY(result2.endsWith(QDir::separator()));
742void tst_util::userInfoFullyValid() {
752void tst_util::userInfoMarginallyValid() {
760void tst_util::userInfoIsValid() {
770void tst_util::userInfoCreatedAndExpiry() {
772 ui.
name =
"Test User";
775 QVERIFY(!ui.
created.isValid());
776 QVERIFY(!ui.
expiry.isValid());
778 QDateTime future = QDateTime::currentDateTime().addYears(1);
780 QVERIFY(ui.
expiry.isValid());
781 QVERIFY(ui.
expiry.toSecsSinceEpoch() > 0);
783 QDateTime past = QDateTime::currentDateTime().addYears(-1);
786 QVERIFY(ui.
created.toSecsSinceEpoch() > 0);
789void tst_util::qProgressIndicatorBasic() {
790 QProgressIndicator pi;
794void tst_util::qProgressIndicatorStartStop() {
795 QProgressIndicator pi;
802void tst_util::namedValueBasic() {
806 QCOMPARE(nv.
name, QString(
"key"));
807 QCOMPARE(nv.
value, QString(
"value"));
810void tst_util::namedValueMultiple() {
816 QCOMPARE(nvs.size(), 1);
819void tst_util::imitatePassResolveMoveDestination() {
821 QTemporaryDir tmpDir;
822 QString srcPath = tmpDir.path() +
"/test.gpg";
823 QFile srcFile(srcPath);
824 QVERIFY(srcFile.open(QFile::WriteOnly));
825 srcFile.write(
"test");
828 QString destPath = tmpDir.path() +
"/dest.gpg";
830 QString expected = destPath;
831 QVERIFY2(result == expected,
"Destination should have .gpg extension");
834void tst_util::imitatePassResolveMoveDestinationForce() {
836 QTemporaryDir tmpDir;
837 QString srcPath = tmpDir.path() +
"/test.gpg";
838 QFile srcFile(srcPath);
839 QVERIFY(srcFile.open(QFile::WriteOnly));
840 srcFile.write(
"test");
843 QString destPath = tmpDir.path() +
"/existing.gpg";
844 QFile destFile(destPath);
845 QVERIFY(destFile.open(QFile::WriteOnly));
846 destFile.write(
"old");
850 QVERIFY2(result == destPath,
"Should return dest path when force=true");
853void tst_util::imitatePassResolveMoveDestinationDestExistsNoForce() {
855 QTemporaryDir tmpDir;
856 QString srcPath = tmpDir.path() +
"/test.gpg";
857 QFile srcFile(srcPath);
858 QVERIFY(srcFile.open(QFile::WriteOnly));
859 srcFile.write(
"test");
862 QString destPath = tmpDir.path() +
"/existing.gpg";
863 QFile destFile(destPath);
864 QVERIFY(destFile.open(QFile::WriteOnly));
865 destFile.write(
"old");
869 QVERIFY2(result.isEmpty(),
870 "Should return empty when dest exists and force=false");
873void tst_util::imitatePassResolveMoveDestinationDir() {
875 QTemporaryDir tmpDir;
876 QString srcPath = tmpDir.path() +
"/test.gpg";
877 QFile srcFile(srcPath);
878 QVERIFY(srcFile.open(QFile::WriteOnly));
879 srcFile.write(
"test");
883 QVERIFY2(result == tmpDir.path() +
"/test.gpg",
884 "Should append filename when dest is dir");
887void tst_util::imitatePassResolveMoveDestinationNonExistent() {
889 QTemporaryDir tmpDir;
890 QString destPath = tmpDir.path() +
"/dest.gpg";
893 QVERIFY2(result.isEmpty(),
"Should return empty for non-existent source");
896void tst_util::imitatePassRemoveDir() {
898 QTemporaryDir tmpDir;
899 QString subDir = tmpDir.path() +
"/testdir";
900 QVERIFY(QDir().mkpath(subDir));
901 QVERIFY(QDir(subDir).exists());
904 QVERIFY(!QDir(subDir).exists());
907void tst_util::getRecipientListBasic() {
908 QTemporaryDir tempDir;
909 QString passStore = tempDir.path();
910 QString gpgIdFile = passStore +
"/.gpg-id";
912 QFile file(gpgIdFile);
913 QVERIFY(file.open(QIODevice::WriteOnly));
914 file.write(
"ABCDEF12\n34567890\n");
920 QCOMPARE(recipients.size(), 2);
921 QCOMPARE(recipients[0], QString(
"ABCDEF12"));
922 QCOMPARE(recipients[1], QString(
"34567890"));
925void tst_util::getRecipientListEmpty() {
926 QTemporaryDir tempDir;
927 QString passStore = tempDir.path();
928 QString gpgIdFile = passStore +
"/.gpg-id";
930 QFile file(gpgIdFile);
931 QVERIFY(file.open(QIODevice::WriteOnly));
937 QVERIFY(recipients.isEmpty());
940void tst_util::getRecipientListWithComments() {
941 QTemporaryDir tempDir;
942 QString passStore = tempDir.path();
943 QString gpgIdFile = passStore +
"/.gpg-id";
945 QFile file(gpgIdFile);
946 QVERIFY(file.open(QIODevice::WriteOnly));
947 file.write(
"ABCDEF12\n# comment\n34567890\n");
953 QCOMPARE(recipients.size(), 2);
954 QVERIFY(!recipients.contains(
"# comment"));
955 QVERIFY(!recipients.contains(
"comment"));
958void tst_util::getRecipientListInvalidKeyId() {
959 QTemporaryDir tempDir;
960 QString passStore = tempDir.path();
961 QString gpgIdFile = passStore +
"/.gpg-id";
963 QFile file(gpgIdFile);
964 QVERIFY(file.open(QIODevice::WriteOnly));
966 "ABCDEF12\ninvalid\n0xABCDEF123456789012\n<a@b>\nuser@domain.org\n");
970 PassStoreGuard originalGuard(originalPassStore);
973 QVERIFY(!recipients.contains(
"invalid"));
974 QVERIFY(recipients.contains(
"ABCDEF12"));
977void tst_util::isValidKeyIdBasic() {
984void tst_util::isValidKeyIdWith0xPrefix() {
992void tst_util::isValidKeyIdWithEmail() {
1000void tst_util::isValidKeyIdInvalid() {
1008void tst_util::getRecipientStringCount() {
1009 QTemporaryDir tempDir;
1010 QString passStore = tempDir.path();
1011 QString gpgIdFile = passStore +
"/.gpg-id";
1013 QFile file(gpgIdFile);
1014 QVERIFY(file.open(QIODevice::WriteOnly));
1015 file.write(
"ABCDEF12\n34567890\n");
1019 PassStoreGuard originalGuard(originalPassStore);
1022 QStringList parsedRecipients =
1026 QStringList expectedRecipients = {
"ABCDEF12",
"34567890"};
1029 QCOMPARE(count, (
int)expectedRecipients.size());
1031 QCOMPARE(parsedRecipients, recipientsNoCount);
1033 QVERIFY(parsedRecipients.contains(
"ABCDEF12"));
1034 QVERIFY(parsedRecipients.contains(
"34567890"));
1037 QVERIFY(recipientsNoCount.contains(
"ABCDEF12"));
1038 QVERIFY(recipientsNoCount.contains(
"34567890"));
1041void tst_util::getGpgIdPathBasic() {
1042 QTemporaryDir tempDir;
1043 QString passStore = tempDir.path();
1044 QString gpgIdFile = passStore +
"/.gpg-id";
1046 QFile file(gpgIdFile);
1047 QVERIFY(file.open(QIODevice::WriteOnly));
1048 file.write(
"ABCDEF12\n");
1054 QString expected = QDir::cleanPath(gpgIdFile);
1055 QVERIFY2(path == expected,
1056 qPrintable(QString(
"Expected %1, got %2").arg(expected, path)));
1059void tst_util::getGpgIdPathSubfolder() {
1060 QTemporaryDir tempDir;
1061 QString passStore = tempDir.path();
1062 QString subfolder = passStore +
"/subfolder";
1063 QString gpgIdFile = subfolder +
"/.gpg-id";
1065 QVERIFY(QDir().mkdir(subfolder));
1066 QFile file(gpgIdFile);
1067 QVERIFY(file.open(QIODevice::WriteOnly));
1068 file.write(
"ABCDEF12\n");
1074 QVERIFY2(path == gpgIdFile,
1075 qPrintable(QString(
"Expected %1, got %2").arg(gpgIdFile, path)));
1078void tst_util::getGpgIdPathNotFound() {
1079 QTemporaryDir tempDir;
1080 QString passStore = tempDir.path();
1086 QString expected = QDir::cleanPath(passStore +
"/.gpg-id");
1087 QVERIFY2(path == expected,
1088 qPrintable(QString(
"Expected %1, got %2").arg(expected, path)));
1094void tst_util::findBinaryInPathReturnedPathIsAbsolute() {
1097 const QString binaryName = QStringLiteral(
"cmd.exe");
1099 const QString binaryName = QStringLiteral(
"sh");
1102 QVERIFY2(!result.isEmpty(),
"Should find a standard shell");
1103 QFileInfo fi(result);
1107 QStringLiteral(
"Returned path '%1' must be absolute").arg(result)));
1110void tst_util::findBinaryInPathReturnedPathIsExecutable() {
1114 const QString binaryName = QStringLiteral(
"cmd.exe");
1116 const QString binaryName = QStringLiteral(
"sh");
1119 QVERIFY2(!result.isEmpty(),
"Should find a standard shell");
1120 QFileInfo fi(result);
1124 QStringLiteral(
"Returned path '%1' must be executable").arg(result)));
1127void tst_util::findBinaryInPathMultipleKnownBinaries() {
1130 const QStringList binaries = {QStringLiteral(
"sh"), QStringLiteral(
"ls"),
1131 QStringLiteral(
"cat")};
1132 for (
const QString &bin : binaries) {
1134 QVERIFY2(!result.isEmpty(),
1135 qPrintable(QStringLiteral(
"Should find '%1' in PATH").arg(bin)));
1136 QVERIFY2(result.contains(bin),
1137 qPrintable(QStringLiteral(
"Result '%1' should contain '%2'")
1138 .arg(result, bin)));
1140 QFileInfo(result).isExecutable(),
1142 QStringLiteral(
"Result '%1' should be executable").arg(result)));
1145 QSKIP(
"Non-Windows binary list not applicable on Windows");
1149void tst_util::findBinaryInPathConsistency() {
1153 const QString binaryName = QStringLiteral(
"cmd.exe");
1155 const QString binaryName = QStringLiteral(
"sh");
1159 QVERIFY2(!first.isEmpty(),
"First call should find the binary");
1160 QCOMPARE(first, second);
1163void tst_util::findBinaryInPathResultContainsBinaryName() {
1167 const QString binaryName = QStringLiteral(
"cmd");
1169 const QString binaryName = QStringLiteral(
"sh");
1172 QVERIFY2(!result.isEmpty(),
"Should find the binary");
1174 result.endsWith(binaryName) ||
1175 result.endsWith(binaryName + QStringLiteral(
".exe")),
1176 qPrintable(QStringLiteral(
"Path '%1' should end with binary name '%2'")
1177 .arg(result, binaryName)));
1180void tst_util::findBinaryInPathTempExecutableInTempDir() {
1189 if (shPath.isEmpty()) {
1190 QSKIP(
"Cannot find 'sh' to determine a writable PATH directory");
1192 const QString pathDir = QFileInfo(shPath).absolutePath();
1193 const QString uniqueName = QStringLiteral(
"qtpass_test_exec_") +
1194 QUuid::createUuid().toString(QUuid::WithoutBraces);
1195 const QString uniquePath = pathDir + QDir::separator() + uniqueName;
1197 QFile exec(uniquePath);
1198 if (!exec.open(QIODevice::WriteOnly)) {
1199 QSKIP(
"Cannot write to the PATH directory containing 'sh' (need write "
1202 QVERIFY2(exec.exists(),
"File should exist after opening for writing");
1203 exec.write(
"#!/bin/sh\n");
1205 exec.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
1206 QFileDevice::ExeOwner);
1211 const bool removed = QFile::remove(uniquePath);
1215 QStringLiteral(
"Failed to clean up test file '%1'").arg(uniquePath)));
1217 QVERIFY2(!result.isEmpty(),
1218 "findBinaryInPath should locate the executable placed in a PATH "
1220 QVERIFY2(result.endsWith(uniqueName),
1221 qPrintable(QStringLiteral(
"Result '%1' should end with '%2'")
1222 .arg(result, uniqueName)));
1223 QVERIFY2(QFileInfo(result).isAbsolute(),
"Result must be an absolute path");
1225 QSKIP(
"Temp-executable test is Unix-only");
1229void tst_util::buildClipboardMimeDataLinux() {
1232 QVERIFY(mime !=
nullptr);
1233 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1234 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1235 QVERIFY2(mime->data(
"x-kde-passwordManagerHint") == QByteArray(
"secret"),
1236 "Linux should set password hint");
1239 QSKIP(
"Linux-only test");
1243void tst_util::buildClipboardMimeDataWindows() {
1246 QVERIFY(mime !=
nullptr);
1247 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1248 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1249 QByteArray excl = mime->data(
"ExcludeClipboardContentFromMonitorProcessing");
1250 QVERIFY2(excl.size() == 4,
"Windows ExcludeClipboard should be 4 bytes");
1251 QVERIFY2(excl == dwordBytes(1),
"Windows ExcludeClipboard should be DWORD 1");
1252 QVERIFY(mime->hasFormat(
"ExcludeClipboardContentFromMonitorProcessing"));
1253 QVERIFY(mime->hasFormat(
"CanIncludeInClipboardHistory"));
1254 QVERIFY(mime->hasFormat(
"CanUploadToCloudClipboard"));
1255 QByteArray canHistory = mime->data(
"CanIncludeInClipboardHistory");
1256 QVERIFY2(canHistory.size() == 4,
1257 "CanIncludeInClipboardHistory should be 4 bytes");
1258 QVERIFY2(canHistory == dwordBytes(0),
1259 "CanIncludeInClipboardHistory should be DWORD 0");
1260 QByteArray cloudClip = mime->data(
"CanUploadToCloudClipboard");
1261 QVERIFY2(cloudClip.size() == 4,
1262 "CanUploadToCloudClipboard should be 4 bytes");
1263 QVERIFY2(cloudClip == dwordBytes(0),
1264 "CanUploadToCloudClipboard should be DWORD 0");
1267 QSKIP(
"Windows-only test");
1271void tst_util::buildClipboardMimeDataDword() {
1273 QByteArray zero = dwordBytes(0);
1274 QVERIFY2(zero.size() == 4,
"DWORD should be 4 bytes");
1275 QVERIFY2(zero.at(0) ==
char(0),
"DWORD 0 should be 0x00");
1276 QVERIFY2(zero.at(1) ==
char(0),
"DWORD 0 should be 0x00");
1277 QVERIFY2(zero.at(2) ==
char(0),
"DWORD 0 should be 0x00");
1278 QVERIFY2(zero.at(3) ==
char(0),
"DWORD 0 should be 0x00");
1280 QByteArray one = dwordBytes(1);
1281 QVERIFY2(one.size() == 4,
"DWORD should be 4 bytes");
1282 QVERIFY2(one.at(0) ==
char(1),
"DWORD 1 should be 0x01");
1283 QVERIFY2(one.at(1) ==
char(0),
"DWORD 1 should be 0x00");
1284 QVERIFY2(one.at(2) ==
char(0),
"DWORD 1 should be 0x00");
1285 QVERIFY2(one.at(3) ==
char(0),
"DWORD 1 should be 0x00");
1287 QSKIP(
"Windows-only test");
1291void tst_util::buildClipboardMimeDataMac() {
1294 QVERIFY(mime !=
nullptr);
1295 QVERIFY2(mime->hasText(),
"Mime data should contain text");
1296 QVERIFY2(mime->text() ==
"testpassword",
"Text should match");
1297 QVERIFY2(mime->hasFormat(
"application/x-nspasteboard-concealed-type"),
1298 "macOS should have concealed type format");
1299 QVERIFY2(mime->data(
"application/x-nspasteboard-concealed-type") ==
1301 "macOS concealed type should be empty");
1304 QSKIP(
"macOS-only test");
1308void tst_util::utilRegexEnsuresGpg() {
1310 QVERIFY2(rex.isValid(),
"Regex should be valid");
1311 QVERIFY2(rex.match(
"file.gpg").hasMatch(),
"Should match .gpg extension");
1312 QVERIFY2(!rex.match(
"file.txt").hasMatch(),
"Should not match .txt");
1313 QVERIFY2(!rex.match(
"test.gpgx").hasMatch(),
"Should not match .gpgx");
1316void tst_util::utilRegexProtocol() {
1318 QVERIFY2(rex.isValid(),
"Protocol regex should be valid");
1319 QVERIFY2(rex.match(
"http://example.com").hasMatch(),
"Should match http://");
1320 QVERIFY2(rex.match(
"https://secure.com").hasMatch(),
"Should match https://");
1321 QVERIFY2(rex.match(
"ssh://host").hasMatch(),
"Should match ssh://");
1322 QVERIFY2(!rex.match(
"://no-protocol").hasMatch(),
"Should not match invalid");
1325void tst_util::utilRegexNewLines() {
1327 QVERIFY2(rex.isValid(),
"Newlines regex should be valid");
1328 QVERIFY2(rex.match(
"\n").hasMatch(),
"Should match newline");
1329 QVERIFY2(rex.match(
"line1\nline2").hasMatch(),
1330 "Should match embedded newline");
1334#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...
auto removeDir(const QString &dirName) -> bool
Remove directory recursively.
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.
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
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 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.
auto getStore() const -> QString
Get the password store root path.
void setModelAndStore(QFileSystemModel *sourceModel, 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. Validates a GPG key ID after normalization:
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(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 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.