QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
tst_util.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
3#include <QCoreApplication>
4#include <QDir>
5#include <QFile>
6#include <QFileInfo>
7#include <QList>
8#include <QProcessEnvironment>
9#include <QTemporaryDir>
10#include <QUuid>
11#include <QtTest>
12
13#include "../../../src/enums.h"
16#include "../../../src/pass.h"
19#include "../../../src/qtpass.h"
23#include "../../../src/util.h"
24
28class tst_util : public QObject {
29 Q_OBJECT
30
31public:
33 ~tst_util() override;
34
35public Q_SLOTS:
36 void init();
37 void cleanup();
38
39private:
40 struct PassStoreGuard {
41 QString original;
42 explicit PassStoreGuard(const QString &orig) : original(orig) {}
43 ~PassStoreGuard() { QtPassSettings::setPassStore(original); }
44 };
45
46private Q_SLOTS:
47 void cleanupTestCase();
48 void normalizeFolderPath();
49 void normalizeFolderPathEdgeCases();
50 void fileContent();
51 void fileContentEdgeCases();
52 void namedValuesTakeValue();
53 void namedValuesEdgeCases();
54 void totpHiddenFromDisplay();
55 void testAwsUrl();
56 void regexPatterns();
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();
67 void boundedRandom();
68 void findBinaryInPath();
69 void findPasswordStore();
70 void configIsValid();
71 void getDirBasic();
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();
115};
116
120tst_util::tst_util() = default;
121
125tst_util::~tst_util() = default;
126
131 // Intentionally left empty: no per-test setup required.
132}
133
138 // Intentionally left empty: no per-test cleanup required.
139}
140
144void tst_util::cleanupTestCase() {
145 // No test case cleanup required; function intentionally left empty.
146}
147
152void tst_util::normalizeFolderPath() {
153 QString result;
154 QString sep = QDir::separator();
155
156 // Forward slash path
157 result = Util::normalizeFolderPath("test");
158 QVERIFY(result.endsWith(sep));
159 result = Util::normalizeFolderPath("test/");
160 QVERIFY(result.endsWith(sep));
161 // Verify exact normalized path content
162 result = Util::normalizeFolderPath("test");
163 QVERIFY2(result == "test" + sep,
164 qPrintable(QString("Expected 'test%1', got '%2'").arg(sep, result)));
165 result = Util::normalizeFolderPath("test/");
166 QVERIFY2(result == "test" + sep,
167 qPrintable(QString("Expected 'test%1', got '%2'").arg(sep, result)));
168
169 // Windows-style backslash path (only on Windows)
170 if (QDir::separator() == '\\') {
171 result = Util::normalizeFolderPath("test\\subdir");
172 QVERIFY(result.endsWith("\\"));
173 QVERIFY(result.contains("test"));
174 QVERIFY(result.contains("subdir"));
175 // Verify exact normalized path content
176 QVERIFY2(
177 result == "test\\subdir\\",
178 qPrintable(
179 QString("Expected 'test\\\\subdir\\\\', got '%1'").arg(result)));
180 }
181
182 // Mixed separators test
183 result = Util::normalizeFolderPath("test/subdir\\folder");
184 QVERIFY(result.endsWith(sep));
185 QVERIFY(result.contains("test"));
186 QVERIFY(result.contains("subdir"));
187 QVERIFY(result.contains("folder"));
188}
189
190void tst_util::fileContent() {
191 NamedValue key = {"key", "val"};
192 NamedValue key2 = {"key2", "val2"};
193 QString password = "password";
194
195 FileContent fc = FileContent::parse("password\n", {}, false);
196 QCOMPARE(fc.getPassword(), password);
197 QCOMPARE(fc.getNamedValues(), {});
198 QCOMPARE(fc.getRemainingData(), QString());
199
200 fc = FileContent::parse("password", {}, false);
201 QCOMPARE(fc.getPassword(), password);
202 QCOMPARE(fc.getNamedValues(), {});
203 QCOMPARE(fc.getRemainingData(), QString());
204
205 fc = FileContent::parse("password\nfoobar\n", {}, false);
206 QCOMPARE(fc.getPassword(), password);
207 QCOMPARE(fc.getNamedValues(), {});
208 QCOMPARE(fc.getRemainingData(), QString("foobar\n"));
209
210 fc = FileContent::parse("password\nkey: val\nkey2: val2", {"key2"}, false);
211 QCOMPARE(fc.getPassword(), password);
212 QCOMPARE(fc.getNamedValues(), NamedValues({key2}));
213 QCOMPARE(fc.getRemainingData(), QString("key: val"));
214
215 fc = FileContent::parse("password\nkey: val\nkey2: val2", {"key2"}, true);
216 QCOMPARE(fc.getPassword(), password);
217 QCOMPARE(fc.getNamedValues(), NamedValues({key, key2}));
218 QCOMPARE(fc.getRemainingData(), QString());
219}
220
221void tst_util::namedValuesTakeValue() {
222 NamedValues nv = {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}};
223
224 QString val = nv.takeValue("key2");
225 QCOMPARE(val, QString("value2"));
226 QCOMPARE(nv.length(), 2);
227 QVERIFY(!nv.contains({"key2", "value2"}));
228
229 val = nv.takeValue("nonexistent");
230 QVERIFY(val.isEmpty());
231
232 val = nv.takeValue("key1");
233 QCOMPARE(val, QString("value1"));
234 val = nv.takeValue("key3");
235 QCOMPARE(val, QString("value3"));
236 QVERIFY(nv.isEmpty());
237}
238
239void tst_util::totpHiddenFromDisplay() {
240 FileContent fc = FileContent::parse(
241 "password\notpauth://totp/Test?secret=JBSWY3DPEHPK3PXP\nkey: value\n", {},
242 false);
243
244 QString remaining = fc.getRemainingData();
245 QVERIFY(remaining.contains("otpauth://"));
246 QVERIFY(remaining.contains("key: value"));
247
248 QString display = fc.getRemainingDataForDisplay();
249 QVERIFY(!display.contains("otpauth"));
250 QVERIFY(display.contains("key: value"));
251
253 "password\nOTPAUTH://TOTP/Test?secret=JBSWY3DPEHPK3PXP\n", {}, false);
254 QVERIFY(fc.getRemainingDataForDisplay().isEmpty());
255}
256
257void tst_util::testAwsUrl() {
258 QRegularExpression proto = Util::protocolRegex();
259
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");
265
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");
271}
272
273void tst_util::regexPatterns() {
274 QRegularExpression gpg = Util::endsWithGpg();
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());
279
280 QRegularExpression proto = Util::protocolRegex();
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");
293
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");
301
302 QRegularExpression nl = Util::newLinesRegex();
303 QVERIFY(nl.match("\n").hasMatch());
304 QVERIFY(nl.match("\r").hasMatch());
305 QVERIFY(nl.match("\r\n").hasMatch());
306}
307
308void tst_util::normalizeFolderPathEdgeCases() {
309 QString result = Util::normalizeFolderPath("");
310 QVERIFY(result.endsWith(QDir::separator()));
311 QVERIFY2(result == QDir::separator() || result.endsWith(QDir::separator()),
312 "Empty path should become separator");
313
314 result = Util::normalizeFolderPath(QDir::separator());
315 QVERIFY(result.endsWith(QDir::separator()));
316
317 result = Util::normalizeFolderPath("path/to/dir/");
318 QVERIFY(result.endsWith(QDir::separator()));
319
320 QString nativeResult = Util::normalizeFolderPath("path/to/dir");
321 QVERIFY(nativeResult.endsWith(QDir::separator()));
322}
323
324void tst_util::fileContentEdgeCases() {
325 FileContent fc = FileContent::parse("", {}, false);
326 QVERIFY(fc.getPassword().isEmpty());
327
328 fc = FileContent::parse("pass\nusername: user@example.com\npassword: "
329 "secret\nurl: https://login.com\n",
330 {"username", "password", "url"}, false);
331 QVERIFY(fc.getNamedValues().length() >= 3);
332
333 fc = FileContent::parse("pass\nkey: value with spaces\n", {"key"}, true);
334 NamedValues nv = fc.getNamedValues();
335 QCOMPARE(nv.length(), 1);
336 QCOMPARE(nv.at(0).name, QString("key"));
337 QVERIFY(nv.at(0).value.contains("spaces"));
338
339 fc = FileContent::parse("pass\n://something\n", {}, false);
340 QVERIFY(fc.getRemainingData().contains("://"));
341
342 fc = FileContent::parse("pass\nno colon line\n", {}, false);
343 QVERIFY(fc.getRemainingData().contains("no colon line"));
344
345 fc = FileContent::parse("pass\nkey: value\nkey2: duplicate\n", {}, true);
346 QVERIFY(fc.getNamedValues().length() >= 2);
347
348 fc = FileContent::parse("pass\n", {}, false);
349 QCOMPARE(fc.getPassword(), QString("pass"));
350 QVERIFY(fc.getNamedValues().isEmpty());
351}
352
353void tst_util::namedValuesEdgeCases() {
354 NamedValues nv;
355 QVERIFY(nv.isEmpty());
356 QVERIFY(nv.takeValue("nonexistent").isEmpty());
357
358 NamedValue n1 = {"key", "value"};
359 nv.append(n1);
360 QCOMPARE(nv.length(), 1);
361 NamedValue n2 = {"key2", "value2"};
362 nv.append(n2);
363 QCOMPARE(nv.length(), 2);
364
365 nv.clear();
366 QVERIFY(nv.isEmpty());
367 QVERIFY(nv.takeValue("anything").isEmpty());
368}
369
370void tst_util::regexPatternEdgeCases() {
371 const QRegularExpression &gpg = Util::endsWithGpg();
372 QVERIFY(gpg.match(".gpg").hasMatch());
373 QVERIFY(gpg.match("a.gpg").hasMatch());
374 QVERIFY(!gpg.match("test.gpgx").hasMatch());
375
376 const QRegularExpression &proto = Util::protocolRegex();
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());
380 // file:/// URLs are not matched - see Util::protocolRegex()
381 QVERIFY(!proto.match("file:///path/to/file").hasMatch());
382
383 const QRegularExpression &nl = Util::newLinesRegex();
384 QVERIFY(nl.match("\n").hasMatch());
385 QVERIFY(nl.match("\r").hasMatch());
386 QVERIFY(nl.match("\r\n").hasMatch());
387}
388
389void tst_util::endsWithGpgEdgeCases() {
390 const QRegularExpression &gpg = Util::endsWithGpg();
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());
399}
400
401void tst_util::userInfoValidity() {
402 UserInfo info;
403 info.validity = 'f';
404 QVERIFY(info.fullyValid());
405 QVERIFY(!info.marginallyValid());
406 QVERIFY(info.isValid());
407
408 info.validity = 'u';
409 QVERIFY(info.fullyValid());
410 QVERIFY(!info.marginallyValid());
411 QVERIFY(info.isValid());
412
413 info.validity = 'm';
414 QVERIFY(!info.fullyValid());
415 QVERIFY(info.marginallyValid());
416 QVERIFY(info.isValid());
417
418 info.validity = 'n';
419 QVERIFY(!info.fullyValid());
420 QVERIFY(!info.marginallyValid());
421 QVERIFY(!info.isValid());
422
423 info.validity = 'e';
424 QVERIFY(!info.isValid());
425}
426
427void tst_util::userInfoValidityEdgeCases() {
428 UserInfo info;
429 info.validity = '-';
430 QVERIFY(!info.isValid());
431
432 info.validity = 'q';
433 QVERIFY(!info.isValid());
434
435 char nullChar = '\0';
436 info.validity = nullChar;
437 QVERIFY(!info.isValid());
438
439 QVERIFY(!info.have_secret);
440 QVERIFY(!info.enabled);
441}
442
443void tst_util::passwordConfigurationCharacters() {
444 PasswordConfiguration config;
445 QCOMPARE(config.length, 16);
447
448 QVERIFY(!config.Characters[PasswordConfiguration::ALLCHARS].isEmpty());
449 QVERIFY(!config.Characters[PasswordConfiguration::ALPHABETICAL].isEmpty());
450 QVERIFY(!config.Characters[PasswordConfiguration::ALPHANUMERIC].isEmpty());
451 QVERIFY(!config.Characters[PasswordConfiguration::CUSTOM].isEmpty());
452
453 QVERIFY(config.Characters[PasswordConfiguration::ALLCHARS].length() >
455
456 QVERIFY(config.Characters[PasswordConfiguration::ALPHANUMERIC].length() >
458}
459
460void tst_util::simpleTransactionBasic() {
461 simpleTransaction trans;
464 QCOMPARE(result, Enums::PASS_INSERT);
465}
466
467void tst_util::simpleTransactionNested() {
468 simpleTransaction trans;
471 Enums::PROCESS passInsertResult = trans.transactionIsOver(Enums::PASS_INSERT);
472 QCOMPARE(passInsertResult, Enums::PASS_INSERT);
473 Enums::PROCESS gitPushResult = trans.transactionIsOver(Enums::GIT_PUSH);
474 QCOMPARE(gitPushResult, Enums::GIT_PUSH);
475}
476
477void tst_util::createGpgIdFile() {
478 QTemporaryDir tempDir;
479 QString newDir = tempDir.path() + "/testfolder";
480 QVERIFY(QDir().mkdir(newDir));
481
482 QString gpgIdFile = newDir + "/.gpg-id";
483 QStringList keyIds = {"ABCDEF12", "34567890"};
484
485 QFile gpgId(gpgIdFile);
486 QVERIFY(gpgId.open(QIODevice::WriteOnly));
487 for (const QString &keyId : keyIds) {
488 gpgId.write((keyId + "\n").toUtf8());
489 }
490 gpgId.close();
491
492 QVERIFY(QFile::exists(gpgIdFile));
493
494 QFile readFile(gpgIdFile);
495 QVERIFY(readFile.open(QIODevice::ReadOnly));
496 QString content = QString::fromUtf8(readFile.readAll());
497 readFile.close();
498
499 QStringList lines = content.trimmed().split('\n');
500 QCOMPARE(lines.size(), 2);
501 QCOMPARE(lines[0], QString("ABCDEF12"));
502 QCOMPARE(lines[1], QString("34567890"));
503}
504
505void tst_util::createGpgIdFileEmptyKeys() {
506 QTemporaryDir tempDir;
507 QString newDir = tempDir.path() + "/testfolder";
508 QVERIFY(QDir().mkdir(newDir));
509
510 QString gpgIdFile = newDir + "/.gpg-id";
511
512 QFile gpgId(gpgIdFile);
513 QVERIFY(gpgId.open(QIODevice::WriteOnly));
514 gpgId.close();
515
516 QVERIFY(QFile::exists(gpgIdFile));
517
518 QFile readFile(gpgIdFile);
519 QVERIFY(readFile.open(QIODevice::ReadOnly));
520 QString content = QString::fromUtf8(readFile.readAll());
521 readFile.close();
522
523 QVERIFY(content.isEmpty());
524}
525
526void tst_util::generateRandomPassword() {
527 ImitatePass pass;
528 QString charset = "abcdefghijklmnopqrstuvwxyz";
529 QString result = pass.generatePassword(10, charset);
530
531 QCOMPARE(result.length(), 10);
532 for (const QChar &ch : result) {
533 QVERIFY2(
534 charset.contains(ch),
535 "Generated password contains character outside the specified charset");
536 }
537
538 result = pass.generatePassword(100, "abcd");
539 QString charset2 = "abcd";
540 QCOMPARE(result.length(), 100);
541 for (const QChar &ch : result) {
542 QVERIFY2(
543 charset2.contains(ch),
544 "Generated password contains character outside the specified charset");
545 }
546
547 result = pass.generatePassword(0, "");
548 QVERIFY(result.isEmpty());
549
550 result = pass.generatePassword(50, "ABC");
551 QString charset3 = "ABC";
552 QCOMPARE(result.length(), 50);
553 for (const QChar &ch : result) {
554 QVERIFY2(
555 charset3.contains(ch),
556 "Generated password contains character outside the specified charset");
557 }
558}
559
560void tst_util::boundedRandom() {
561 ImitatePass pass;
562
563 QVector<quint32> counts(10, 0);
564 const int iterations = 1000;
565
566 for (int i = 0; i < iterations; ++i) {
567 QString result = pass.generatePassword(1, "0123456789");
568 quint32 val = result.at(0).digitValue();
569 QVERIFY2(val < 10, "generatePassword should only return digit characters");
570 counts[val]++;
571 }
572
573 for (int i = 0; i < 10; ++i) {
574 QVERIFY2(counts[i] > 0, "Each digit should appear at least once");
575 }
576}
577
578void tst_util::findBinaryInPath() {
579#ifdef Q_OS_WIN
580 const QString binaryName = QStringLiteral("cmd.exe");
581#else
582 const QString binaryName = QStringLiteral("sh");
583#endif
584 QString result = Util::findBinaryInPath(binaryName);
585 QVERIFY2(!result.isEmpty(), "Should find a standard shell in PATH");
586 QVERIFY(result.contains(binaryName));
587
588 result = Util::findBinaryInPath("nonexistentbinary12345");
589 QVERIFY(result.isEmpty());
590}
591
592void tst_util::findPasswordStore() {
593 QString result = Util::findPasswordStore();
594 QVERIFY(!result.isEmpty());
595 QVERIFY(result.endsWith(QDir::separator()));
596}
597
598void tst_util::configIsValid() {
599 QTemporaryDir tempDir;
600 QVERIFY2(tempDir.isValid(), "Temporary directory should be created");
601
602 PassStoreGuard guard(QtPassSettings::getPassStore());
603
604 // No .gpg-id in this store => config must be invalid.
605 QtPassSettings::setPassStore(tempDir.path());
606 bool isValid = Util::configIsValid();
607 QVERIFY2(!isValid, "Expected invalid config when .gpg-id is missing");
608
609 // Create .gpg-id, then force invalid executable configuration.
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");
615 gpgIdFile.close();
616
617 QString originalGpgExecutable = QtPassSettings::getGpgExecutable();
618 struct GpgRollback {
619 QString value;
620 ~GpgRollback() { QtPassSettings::setGpgExecutable(value); }
621 } gpgRollback{originalGpgExecutable};
622
624 isValid = Util::configIsValid();
625 QVERIFY2(!isValid, "Expected invalid config when .gpg-id exists but gpg "
626 "executable is missing");
627
629 QStringLiteral("definitely_nonexistent_gpg_binary_12345"));
630 isValid = Util::configIsValid();
631 QVERIFY2(!isValid, "Expected invalid config when .gpg-id exists but gpg "
632 "executable is invalid");
633}
634
635void tst_util::getDirBasic() {
636 QTemporaryDir tempDir;
637 QVERIFY2(tempDir.isValid(),
638 "Temporary directory should be created successfully");
639
640 QFileSystemModel fsm;
641 fsm.setRootPath(tempDir.path());
642 StoreModel sm;
643 sm.setModelAndStore(&fsm, 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");
649 const QString originalStore = QtPassSettings::getPassStore();
650 QtPassSettings::setPassStore(tempDir.path());
651
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();
656 }
657 QVERIFY2(
658 result == expectedDir,
659 qPrintable(QString("Expected '%1', got '%2'").arg(expectedDir, result)));
660 QtPassSettings::setPassStore(originalStore);
661}
662
663void tst_util::getDirWithIndex() {
664 QTemporaryDir tempDir;
665 QVERIFY2(tempDir.isValid(),
666 "Temporary directory should be created successfully");
667
668 const QString dirPath = tempDir.path();
669 const QString filePath =
670 QDir(dirPath).filePath(QStringLiteral("testfile.txt"));
671
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");
679 file.close();
680
681 const QString originalPassStore = QtPassSettings::getPassStore();
682 PassStoreGuard passStoreGuard(originalPassStore);
684
685 QFileSystemModel fsm;
686 fsm.setRootPath(dirPath);
687
688 StoreModel sm;
689 sm.setModelAndStore(&fsm, dirPath);
690 QVERIFY2(sm.getStore() == dirPath, "Store path should match the set value");
691
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");
698
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()));
703
704 QString expectedPath = dirPath;
705 if (!expectedPath.endsWith(QDir::separator())) {
706 expectedPath += QDir::separator();
707 }
708 QVERIFY2(
709 result == expectedPath,
710 qPrintable(
711 QStringLiteral("Expected '%1', got '%2'").arg(expectedPath, result)));
712
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();
718 }
719 QVERIFY2(invalidResult == expectedForInvalid,
720 qPrintable(QStringLiteral("getDir should return pass store for "
721 "invalid index. Expected '%1', got '%2'")
722 .arg(expectedForInvalid, invalidResult)));
723}
724
725void tst_util::findBinaryInPathNotFound() {
726 QString result = Util::findBinaryInPath("this-binary-does-not-exist-12345");
727 QVERIFY(result.isEmpty());
728}
729
730void tst_util::findPasswordStoreEnvVar() {
731 QString result = Util::findPasswordStore();
732 QVERIFY(!result.isEmpty());
733}
734
735void tst_util::normalizeFolderPathMultipleCalls() {
736 QString result1 = Util::normalizeFolderPath("test1");
737 QString result2 = Util::normalizeFolderPath("test2");
738 QVERIFY(result1.endsWith(QDir::separator()));
739 QVERIFY(result2.endsWith(QDir::separator()));
740}
741
742void tst_util::userInfoFullyValid() {
743 UserInfo ui;
744 ui.validity = 'f';
745 QVERIFY(ui.fullyValid());
746 ui.validity = 'u';
747 QVERIFY(ui.fullyValid());
748 ui.validity = '-';
749 QVERIFY(!ui.fullyValid());
750}
751
752void tst_util::userInfoMarginallyValid() {
753 UserInfo ui;
754 ui.validity = 'm';
755 QVERIFY(ui.marginallyValid());
756 ui.validity = 'f';
757 QVERIFY(!ui.marginallyValid());
758}
759
760void tst_util::userInfoIsValid() {
761 UserInfo ui;
762 ui.validity = 'f';
763 QVERIFY(ui.isValid());
764 ui.validity = 'm';
765 QVERIFY(ui.isValid());
766 ui.validity = '-';
767 QVERIFY(!ui.isValid());
768}
769
770void tst_util::userInfoCreatedAndExpiry() {
771 UserInfo ui;
772 ui.name = "Test User";
773 ui.key_id = "ABCDEF12";
774
775 QVERIFY(!ui.created.isValid());
776 QVERIFY(!ui.expiry.isValid());
777
778 QDateTime future = QDateTime::currentDateTime().addYears(1);
779 ui.expiry = future;
780 QVERIFY(ui.expiry.isValid());
781 QVERIFY(ui.expiry.toSecsSinceEpoch() > 0);
782
783 QDateTime past = QDateTime::currentDateTime().addYears(-1);
784 ui.created = past;
785 QVERIFY(ui.created.isValid());
786 QVERIFY(ui.created.toSecsSinceEpoch() > 0);
787}
788
789void tst_util::qProgressIndicatorBasic() {
790 QProgressIndicator pi;
791 QVERIFY(!pi.isAnimated());
792}
793
794void tst_util::qProgressIndicatorStartStop() {
795 QProgressIndicator pi;
796 pi.startAnimation();
797 QVERIFY(pi.isAnimated());
798 pi.stopAnimation();
799 QVERIFY(!pi.isAnimated());
800}
801
802void tst_util::namedValueBasic() {
803 NamedValue nv;
804 nv.name = "key";
805 nv.value = "value";
806 QCOMPARE(nv.name, QString("key"));
807 QCOMPARE(nv.value, QString("value"));
808}
809
810void tst_util::namedValueMultiple() {
811 NamedValues nvs;
812 NamedValue nv1;
813 nv1.name = "user1";
814 nv1.value = "pass1";
815 nvs.append(nv1);
816 QCOMPARE(nvs.size(), 1);
817}
818
819void tst_util::imitatePassResolveMoveDestination() {
820 ImitatePass pass;
821 QTemporaryDir tmpDir;
822 QString srcPath = tmpDir.path() + "/test.gpg";
823 QFile srcFile(srcPath);
824 QVERIFY(srcFile.open(QFile::WriteOnly));
825 srcFile.write("test");
826 srcFile.close();
827
828 QString destPath = tmpDir.path() + "/dest.gpg";
829 QString result = pass.resolveMoveDestination(srcPath, destPath, false);
830 QString expected = destPath;
831 QVERIFY2(result == expected, "Destination should have .gpg extension");
832}
833
834void tst_util::imitatePassResolveMoveDestinationForce() {
835 ImitatePass pass;
836 QTemporaryDir tmpDir;
837 QString srcPath = tmpDir.path() + "/test.gpg";
838 QFile srcFile(srcPath);
839 QVERIFY(srcFile.open(QFile::WriteOnly));
840 srcFile.write("test");
841 srcFile.close();
842
843 QString destPath = tmpDir.path() + "/existing.gpg";
844 QFile destFile(destPath);
845 QVERIFY(destFile.open(QFile::WriteOnly));
846 destFile.write("old");
847 destFile.close();
848
849 QString result = pass.resolveMoveDestination(srcPath, destPath, true);
850 QVERIFY2(result == destPath, "Should return dest path when force=true");
851}
852
853void tst_util::imitatePassResolveMoveDestinationDestExistsNoForce() {
854 ImitatePass pass;
855 QTemporaryDir tmpDir;
856 QString srcPath = tmpDir.path() + "/test.gpg";
857 QFile srcFile(srcPath);
858 QVERIFY(srcFile.open(QFile::WriteOnly));
859 srcFile.write("test");
860 srcFile.close();
861
862 QString destPath = tmpDir.path() + "/existing.gpg";
863 QFile destFile(destPath);
864 QVERIFY(destFile.open(QFile::WriteOnly));
865 destFile.write("old");
866 destFile.close();
867
868 QString result = pass.resolveMoveDestination(srcPath, destPath, false);
869 QVERIFY2(result.isEmpty(),
870 "Should return empty when dest exists and force=false");
871}
872
873void tst_util::imitatePassResolveMoveDestinationDir() {
874 ImitatePass pass;
875 QTemporaryDir tmpDir;
876 QString srcPath = tmpDir.path() + "/test.gpg";
877 QFile srcFile(srcPath);
878 QVERIFY(srcFile.open(QFile::WriteOnly));
879 srcFile.write("test");
880 srcFile.close();
881
882 QString result = pass.resolveMoveDestination(srcPath, tmpDir.path(), false);
883 QVERIFY2(result == tmpDir.path() + "/test.gpg",
884 "Should append filename when dest is dir");
885}
886
887void tst_util::imitatePassResolveMoveDestinationNonExistent() {
888 ImitatePass pass;
889 QTemporaryDir tmpDir;
890 QString destPath = tmpDir.path() + "/dest.gpg";
891 QString result =
892 pass.resolveMoveDestination("/non/existent/path.gpg", destPath, false);
893 QVERIFY2(result.isEmpty(), "Should return empty for non-existent source");
894}
895
896void tst_util::imitatePassRemoveDir() {
897 ImitatePass pass;
898 QTemporaryDir tmpDir;
899 QString subDir = tmpDir.path() + "/testdir";
900 QVERIFY(QDir().mkpath(subDir));
901 QVERIFY(QDir(subDir).exists());
902 bool result = pass.removeDir(subDir);
903 QVERIFY(result);
904 QVERIFY(!QDir(subDir).exists());
905}
906
907void tst_util::getRecipientListBasic() {
908 QTemporaryDir tempDir;
909 QString passStore = tempDir.path();
910 QString gpgIdFile = passStore + "/.gpg-id";
911
912 QFile file(gpgIdFile);
913 QVERIFY(file.open(QIODevice::WriteOnly));
914 file.write("ABCDEF12\n34567890\n");
915 file.close();
916
917 PassStoreGuard guard(QtPassSettings::getPassStore());
919 QStringList recipients = Pass::getRecipientList(passStore);
920 QCOMPARE(recipients.size(), 2);
921 QCOMPARE(recipients[0], QString("ABCDEF12"));
922 QCOMPARE(recipients[1], QString("34567890"));
923}
924
925void tst_util::getRecipientListEmpty() {
926 QTemporaryDir tempDir;
927 QString passStore = tempDir.path();
928 QString gpgIdFile = passStore + "/.gpg-id";
929
930 QFile file(gpgIdFile);
931 QVERIFY(file.open(QIODevice::WriteOnly));
932 file.close();
933
934 PassStoreGuard guard(QtPassSettings::getPassStore());
936 QStringList recipients = Pass::getRecipientList(passStore);
937 QVERIFY(recipients.isEmpty());
938}
939
940void tst_util::getRecipientListWithComments() {
941 QTemporaryDir tempDir;
942 QString passStore = tempDir.path();
943 QString gpgIdFile = passStore + "/.gpg-id";
944
945 QFile file(gpgIdFile);
946 QVERIFY(file.open(QIODevice::WriteOnly));
947 file.write("ABCDEF12\n# comment\n34567890\n");
948 file.close();
949
950 PassStoreGuard guard(QtPassSettings::getPassStore());
952 QStringList recipients = Pass::getRecipientList(passStore);
953 QCOMPARE(recipients.size(), 2);
954 QVERIFY(!recipients.contains("# comment"));
955 QVERIFY(!recipients.contains("comment"));
956}
957
958void tst_util::getRecipientListInvalidKeyId() {
959 QTemporaryDir tempDir;
960 QString passStore = tempDir.path();
961 QString gpgIdFile = passStore + "/.gpg-id";
962
963 QFile file(gpgIdFile);
964 QVERIFY(file.open(QIODevice::WriteOnly));
965 file.write(
966 "ABCDEF12\ninvalid\n0xABCDEF123456789012\n<a@b>\nuser@domain.org\n");
967 file.close();
968
969 const QString originalPassStore = QtPassSettings::getPassStore();
970 PassStoreGuard originalGuard(originalPassStore);
972 QStringList recipients = Pass::getRecipientList(passStore);
973 QVERIFY(!recipients.contains("invalid"));
974 QVERIFY(recipients.contains("ABCDEF12"));
975}
976
977void tst_util::isValidKeyIdBasic() {
978 QVERIFY(Util::isValidKeyId("ABCDEF12"));
979 QVERIFY(Util::isValidKeyId("abcdef12"));
980 QVERIFY(Util::isValidKeyId("0123456789ABCDEF"));
981 QVERIFY(Util::isValidKeyId("0123456789abcdef"));
982}
983
984void tst_util::isValidKeyIdWith0xPrefix() {
985 QVERIFY(Util::isValidKeyId("0xABCDEF12"));
986 QVERIFY(Util::isValidKeyId("0XABCDEF12"));
987 QVERIFY(Util::isValidKeyId("0xabcdef12"));
988 QVERIFY(Util::isValidKeyId("0Xabcdef12"));
989 QVERIFY(Util::isValidKeyId("0x0123456789ABCDEF"));
990}
991
992void tst_util::isValidKeyIdWithEmail() {
993 QVERIFY(Util::isValidKeyId("<a@b>"));
994 QVERIFY(Util::isValidKeyId("user@domain.org"));
995 QVERIFY(Util::isValidKeyId("/any/text/here"));
996 QVERIFY(Util::isValidKeyId("#anything"));
997 QVERIFY(Util::isValidKeyId("&anything"));
998}
999
1000void tst_util::isValidKeyIdInvalid() {
1001 QVERIFY(!Util::isValidKeyId(""));
1002 QVERIFY(!Util::isValidKeyId("short"));
1003 QVERIFY(!Util::isValidKeyId(QString(41, 'a')));
1004 QVERIFY(!Util::isValidKeyId("invalidchars!"));
1005 QVERIFY(!Util::isValidKeyId("space in key"));
1006}
1007
1008void tst_util::getRecipientStringCount() {
1009 QTemporaryDir tempDir;
1010 QString passStore = tempDir.path();
1011 QString gpgIdFile = passStore + "/.gpg-id";
1012
1013 QFile file(gpgIdFile);
1014 QVERIFY(file.open(QIODevice::WriteOnly));
1015 file.write("ABCDEF12\n34567890\n");
1016 file.close();
1017
1018 const QString originalPassStore = QtPassSettings::getPassStore();
1019 PassStoreGuard originalGuard(originalPassStore);
1021 int count = 0;
1022 QStringList parsedRecipients =
1023 Pass::getRecipientString(passStore, " ", &count);
1024 QStringList recipientsNoCount = Pass::getRecipientString(passStore, " ");
1025
1026 QStringList expectedRecipients = {"ABCDEF12", "34567890"};
1027 // Verify count matches the expected number of parsed recipients.
1028 QVERIFY(count > 0);
1029 QCOMPARE(count, (int)expectedRecipients.size());
1030 // Verify both overloads return the same result
1031 QCOMPARE(parsedRecipients, recipientsNoCount);
1032 // Verify that the parsed recipients match the expected values.
1033 QVERIFY(parsedRecipients.contains("ABCDEF12"));
1034 QVERIFY(parsedRecipients.contains("34567890"));
1035 // Also verify that the recipients returned without count match the expected
1036 // values.
1037 QVERIFY(recipientsNoCount.contains("ABCDEF12"));
1038 QVERIFY(recipientsNoCount.contains("34567890"));
1039}
1040
1041void tst_util::getGpgIdPathBasic() {
1042 QTemporaryDir tempDir;
1043 QString passStore = tempDir.path();
1044 QString gpgIdFile = passStore + "/.gpg-id";
1045
1046 QFile file(gpgIdFile);
1047 QVERIFY(file.open(QIODevice::WriteOnly));
1048 file.write("ABCDEF12\n");
1049 file.close();
1050
1051 PassStoreGuard guard(QtPassSettings::getPassStore());
1053 QString path = QDir::cleanPath(Pass::getGpgIdPath(passStore));
1054 QString expected = QDir::cleanPath(gpgIdFile);
1055 QVERIFY2(path == expected,
1056 qPrintable(QString("Expected %1, got %2").arg(expected, path)));
1057}
1058
1059void tst_util::getGpgIdPathSubfolder() {
1060 QTemporaryDir tempDir;
1061 QString passStore = tempDir.path();
1062 QString subfolder = passStore + "/subfolder";
1063 QString gpgIdFile = subfolder + "/.gpg-id";
1064
1065 QVERIFY(QDir().mkdir(subfolder));
1066 QFile file(gpgIdFile);
1067 QVERIFY(file.open(QIODevice::WriteOnly));
1068 file.write("ABCDEF12\n");
1069 file.close();
1070
1071 PassStoreGuard guard(QtPassSettings::getPassStore());
1073 QString path = Pass::getGpgIdPath(subfolder + "/password.gpg");
1074 QVERIFY2(path == gpgIdFile,
1075 qPrintable(QString("Expected %1, got %2").arg(gpgIdFile, path)));
1076}
1077
1078void tst_util::getGpgIdPathNotFound() {
1079 QTemporaryDir tempDir;
1080 QString passStore = tempDir.path();
1081
1082 PassStoreGuard guard(QtPassSettings::getPassStore());
1084 QString path =
1085 QDir::cleanPath(Pass::getGpgIdPath(passStore + "/nonexistent"));
1086 QString expected = QDir::cleanPath(passStore + "/.gpg-id");
1087 QVERIFY2(path == expected,
1088 qPrintable(QString("Expected %1, got %2").arg(expected, path)));
1089}
1090
1091// Tests for findBinaryInPath - verifies it correctly locates executables in
1092// PATH.
1093
1094void tst_util::findBinaryInPathReturnedPathIsAbsolute() {
1095 // Verify that the returned path is absolute, not a relative fragment.
1096#ifdef Q_OS_WIN
1097 const QString binaryName = QStringLiteral("cmd.exe");
1098#else
1099 const QString binaryName = QStringLiteral("sh");
1100#endif
1101 QString result = Util::findBinaryInPath(binaryName);
1102 QVERIFY2(!result.isEmpty(), "Should find a standard shell");
1103 QFileInfo fi(result);
1104 QVERIFY2(
1105 fi.isAbsolute(),
1106 qPrintable(
1107 QStringLiteral("Returned path '%1' must be absolute").arg(result)));
1108}
1109
1110void tst_util::findBinaryInPathReturnedPathIsExecutable() {
1111 // Verify the returned path satisfies the isExecutable() check that guards
1112 // the assignment inside the loop.
1113#ifdef Q_OS_WIN
1114 const QString binaryName = QStringLiteral("cmd.exe");
1115#else
1116 const QString binaryName = QStringLiteral("sh");
1117#endif
1118 QString result = Util::findBinaryInPath(binaryName);
1119 QVERIFY2(!result.isEmpty(), "Should find a standard shell");
1120 QFileInfo fi(result);
1121 QVERIFY2(
1122 fi.isExecutable(),
1123 qPrintable(
1124 QStringLiteral("Returned path '%1' must be executable").arg(result)));
1125}
1126
1127void tst_util::findBinaryInPathMultipleKnownBinaries() {
1128 // Test finding multiple common binaries in PATH.
1129#ifndef Q_OS_WIN
1130 const QStringList binaries = {QStringLiteral("sh"), QStringLiteral("ls"),
1131 QStringLiteral("cat")};
1132 for (const QString &bin : binaries) {
1133 QString result = Util::findBinaryInPath(bin);
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)));
1139 QVERIFY2(
1140 QFileInfo(result).isExecutable(),
1141 qPrintable(
1142 QStringLiteral("Result '%1' should be executable").arg(result)));
1143 }
1144#else
1145 QSKIP("Non-Windows binary list not applicable on Windows");
1146#endif
1147}
1148
1149void tst_util::findBinaryInPathConsistency() {
1150 // Calling findBinaryInPath twice for the same binary must return the same
1151 // result, confirming the loop does not corrupt state across calls.
1152#ifdef Q_OS_WIN
1153 const QString binaryName = QStringLiteral("cmd.exe");
1154#else
1155 const QString binaryName = QStringLiteral("sh");
1156#endif
1157 QString first = Util::findBinaryInPath(binaryName);
1158 QString second = Util::findBinaryInPath(binaryName);
1159 QVERIFY2(!first.isEmpty(), "First call should find the binary");
1160 QCOMPARE(first, second);
1161}
1162
1163void tst_util::findBinaryInPathResultContainsBinaryName() {
1164 // The returned absolute path must end with (or at least contain) the
1165 // binary name, ruling out any off-by-one concatenation artefact.
1166#ifdef Q_OS_WIN
1167 const QString binaryName = QStringLiteral("cmd");
1168#else
1169 const QString binaryName = QStringLiteral("sh");
1170#endif
1171 QString result = Util::findBinaryInPath(binaryName);
1172 QVERIFY2(!result.isEmpty(), "Should find the binary");
1173 QVERIFY2(
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)));
1178}
1179
1180void tst_util::findBinaryInPathTempExecutableInTempDir() {
1181 // Place a real executable in the same directory as "sh" (which is on the
1182 // cached PATH) and verify findBinaryInPath locates it.
1183 //
1184 // This test is skipped in restricted environments where writing to the "sh"
1185 // directory is not allowed. An alternative approach (QTemporaryDir + PATH
1186 // manipulation) doesn't work because Util::_env is cached on first use.
1187#ifndef Q_OS_WIN
1188 QString shPath = Util::findBinaryInPath(QStringLiteral("sh"));
1189 if (shPath.isEmpty()) {
1190 QSKIP("Cannot find 'sh' to determine a writable PATH directory");
1191 }
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;
1196
1197 QFile exec(uniquePath);
1198 if (!exec.open(QIODevice::WriteOnly)) {
1199 QSKIP("Cannot write to the PATH directory containing 'sh' (need write "
1200 "access)");
1201 }
1202 QVERIFY2(exec.exists(), "File should exist after opening for writing");
1203 exec.write("#!/bin/sh\n");
1204 exec.close();
1205 exec.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner |
1206 QFileDevice::ExeOwner);
1207
1208 QString result = Util::findBinaryInPath(uniqueName);
1209
1210 // Remove file before assertions so it is always cleaned up.
1211 const bool removed = QFile::remove(uniquePath);
1212 QVERIFY2(
1213 removed,
1214 qPrintable(
1215 QStringLiteral("Failed to clean up test file '%1'").arg(uniquePath)));
1216
1217 QVERIFY2(!result.isEmpty(),
1218 "findBinaryInPath should locate the executable placed in a PATH "
1219 "directory");
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");
1224#else
1225 QSKIP("Temp-executable test is Unix-only");
1226#endif
1227}
1228
1229void tst_util::buildClipboardMimeDataLinux() {
1230#ifdef Q_OS_LINUX
1231 QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
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");
1237 delete mime;
1238#else
1239 QSKIP("Linux-only test");
1240#endif
1241}
1242
1243void tst_util::buildClipboardMimeDataWindows() {
1244#ifdef Q_OS_WIN
1245 QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
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");
1265 delete mime;
1266#else
1267 QSKIP("Windows-only test");
1268#endif
1269}
1270
1271void tst_util::buildClipboardMimeDataDword() {
1272#ifdef Q_OS_WIN
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");
1279
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");
1286#else
1287 QSKIP("Windows-only test");
1288#endif
1289}
1290
1291void tst_util::buildClipboardMimeDataMac() {
1292#ifdef Q_OS_MAC
1293 QMimeData *mime = buildClipboardMimeData(QStringLiteral("testpassword"));
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") ==
1300 QByteArray(),
1301 "macOS concealed type should be empty");
1302 delete mime;
1303#else
1304 QSKIP("macOS-only test");
1305#endif
1306}
1307
1308void tst_util::utilRegexEnsuresGpg() {
1309 const QRegularExpression &rex = Util::endsWithGpg();
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");
1314}
1315
1316void tst_util::utilRegexProtocol() {
1317 const QRegularExpression &rex = Util::protocolRegex();
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");
1323}
1324
1325void tst_util::utilRegexNewLines() {
1326 const QRegularExpression &rex = Util::newLinesRegex();
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");
1331}
1332
1333QTEST_MAIN(tst_util)
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.
Definition pass.cpp:100
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
Definition pass.cpp:550
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
Definition pass.cpp:520
static auto getRecipientString(const QString &for_file, const QString &separator=" ", int *count=nullptr) -> QStringList
Get recipients as string.
Definition pass.cpp:573
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.
Definition storemodel.h:90
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.
Definition util.cpp:266
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
Definition util.cpp:249
static auto findPasswordStore() -> QString
Locate the password store directory.
Definition util.cpp:77
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.
Definition util.cpp:231
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:
Definition util.cpp:277
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
Definition util.cpp:272
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with the native directory separator.
Definition util.cpp:94
static auto findBinaryInPath(QString binary) -> QString
Locate an executable by searching the process PATH and (on Windows) falling back to WSL.
Definition util.cpp:120
static auto configIsValid() -> bool
Verify that the required configuration is complete.
Definition util.cpp:188
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.
Definition tst_util.cpp:28
void cleanup()
tst_util::cleanup unit test cleanup method
Definition tst_util.cpp:137
tst_util()
tst_util::tst_util basic constructor
void init()
tst_util::init unit test init method
Definition tst_util.cpp:130
~tst_util() override
tst_util::~tst_util basic destructor
PROCESS
Identifies different subprocess operations used in QtPass.
Definition enums.h:26
@ PASS_INSERT
Definition enums.h:34
@ GIT_PUSH
Definition enums.h:32
auto buildClipboardMimeData(const QString &text) -> QMimeData *
Build clipboard MIME data with platform-specific security hints.
Definition qtpass.cpp:477
QString name
Definition filecontent.h:11
QString value
Definition filecontent.h:12
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).
Definition userinfo.h:51
bool enabled
UserInfo::enabled Whether this user/key is enabled for normal use. True when the key should be treate...
Definition userinfo.h:58
auto marginallyValid() const -> bool
UserInfo::marginallyValid when validity is m. http://git.gnupg.org/cgi-bin/gitweb....
Definition userinfo.h:27
QString key_id
UserInfo::key_id hexadecimal representation of the GnuPG key identifier.
Definition userinfo.h:41
auto fullyValid() const -> bool
UserInfo::fullyValid when validity is f or u. http://git.gnupg.org/cgi-bin/gitweb....
Definition userinfo.h:20
auto isValid() const -> bool
UserInfo::isValid when fullyValid or marginallyValid.
Definition userinfo.h:31
QDateTime created
UserInfo::created date/time when key was created.
Definition userinfo.h:66
QString name
UserInfo::name GPG user ID / full name.
Definition userinfo.h:36
char validity
UserInfo::validity GnuPG representation of validity http://git.gnupg.org/cgi-bin/gitweb....
Definition userinfo.h:46
QDateTime expiry
UserInfo::expiry date/time when key expires.
Definition userinfo.h:62