QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
tst_executor.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
3#include <QtTest>
4
6#include "../../../src/pass.h"
8
9class tst_executor : public QObject {
10 Q_OBJECT
11
12private Q_SLOTS:
13#ifndef Q_OS_WIN
14 void executeBlockingEcho();
15 void executeBlockingWithArgs();
16 void executeBlockingWithInput();
17 void executeBlockingExitCode();
18 void executeBlockingStderr();
19 void executeBlockingEmptyArgs();
20 void executeBlockingEchoMultiple();
21 void executeBlockingSpecialChars();
22 void executeBlockingWithEnv();
23 void executeBlockingWithEnvEmpty();
24 void executeBlockingWithEnvSetsVariable();
25 void executeBlockingTwoArgOverload();
26 void executeBlockingConstQStringRef();
27#endif
28 void executeBlockingNotFound();
29 void executeBlockingGpgVersion();
30 void gpgSupportsEd25519();
31 void getDefaultKeyTemplate();
32 void executeBlockingGpgKillAgent();
33 void resolveGpgconfCommand();
34 void executeBlockingWithEnvNotFound();
35};
36
37#ifndef Q_OS_WIN
38void tst_executor::executeBlockingEcho() {
39 QString output;
40 int result = Executor::executeBlocking("echo", {"hello"}, QString(), &output);
41 QVERIFY2(result == 0, "echo should exit successfully");
42 QVERIFY2(output.contains("hello"), "output should contain 'hello'");
43}
44
45void tst_executor::executeBlockingWithArgs() {
46 QString output;
47 int result =
48 Executor::executeBlocking("echo", {"hello", "world"}, QString(), &output);
49 QVERIFY2(result == 0, "echo should exit successfully");
50 QVERIFY2(output.contains("hello world"), "output should contain both args");
51}
52
53void tst_executor::executeBlockingWithInput() {
54 QString output;
55 QString input = "test input";
56 int result = Executor::executeBlocking("cat", {}, input, &output);
57 QVERIFY2(result == 0, "cat should exit successfully");
58 QVERIFY2(output.contains("test input"), "output should echo input");
59}
60
61void tst_executor::executeBlockingExitCode() {
62 QString output;
63 int result = Executor::executeBlocking("false", {}, QString(), &output);
64 QVERIFY2(result != 0, "false should exit with non-zero");
65}
66
67void tst_executor::executeBlockingStderr() {
68 QString output;
69 QString err;
70 Executor::executeBlocking("sh", {"-c", "echo error >&2"}, QString(), &output,
71 &err);
72 QVERIFY2(err.contains("error"), "stderr should contain error");
73}
74
75void tst_executor::executeBlockingEmptyArgs() {
76 QString output;
77 int result = Executor::executeBlocking("echo", {}, QString(), &output);
78 QVERIFY2(result == 0, "echo with empty args should succeed");
79}
80
81void tst_executor::executeBlockingEchoMultiple() {
82 QString output;
83 int result = Executor::executeBlocking("sh", {"-c", "echo a && echo b"},
84 QString(), &output);
85 QVERIFY2(result == 0, "shell should exit successfully");
86 QVERIFY(output.contains("a"));
87 QVERIFY(output.contains("b"));
88}
89
90void tst_executor::executeBlockingSpecialChars() {
91 QString output;
92 int result = Executor::executeBlocking("echo", {"$PATH"}, QString(), &output);
93 QVERIFY2(result == 0, "echo should succeed");
94 QVERIFY2(output.trimmed() == "$PATH", "literal $PATH should be preserved");
95}
96
97// Tests for the third executeBlocking overload:
98// executeBlocking(const QStringList &env, const QString &app, ...)
99// This overload was changed from (QString app) to (const QString &app).
100
101void tst_executor::executeBlockingWithEnv() {
102 // Basic: custom env, echo succeeds, output captured.
103 QStringList env = {"MY_VAR=hello_env"};
104 QString output;
105 int result = Executor::executeBlocking(env, "sh", {"-c", "echo ok"}, &output);
106 QVERIFY2(result == 0, "sh with custom env should exit 0");
107 QVERIFY2(output.contains("ok"), "output should contain 'ok'");
108}
109
110void tst_executor::executeBlockingWithEnvEmpty() {
111 // Empty env list: process starts with an empty environment.
112 // On POSIX, running 'env' with empty environment should succeed.
113 QStringList env;
114 QString output;
115 QString err;
116 int result =
117 Executor::executeBlocking(env, "sh", {"-c", "echo empty"}, &output, &err);
118 QVERIFY2(result == 0, "sh with empty env should start");
119 QVERIFY2(output.contains("empty"), "output should contain 'empty'");
120}
121
122void tst_executor::executeBlockingWithEnvSetsVariable() {
123 // Verify that a variable injected via the env parameter is visible inside
124 // the process.
125 QStringList env = {"QTPASS_TEST_VAR=injected_value"};
126 QString output;
127 int result = Executor::executeBlocking(
128 env, "sh", {"-c", "echo $QTPASS_TEST_VAR"}, &output);
129 QVERIFY2(result == 0, "sh should exit 0");
130 QVERIFY2(output.contains("injected_value"),
131 "env variable should be visible in child process");
132}
133
134void tst_executor::executeBlockingTwoArgOverload() {
135 // The two-argument overload (app, args, out, err) delegates to the
136 // three-argument overload with empty input. Verify it captures stdout.
137 QString output;
138 QString err;
139 int result =
140 Executor::executeBlocking("echo", {"two-arg-overload"}, &output, &err);
141 QVERIFY2(result == 0, "echo two-arg-overload should succeed");
142 QVERIFY2(output.contains("two-arg-overload"),
143 "output should contain 'two-arg-overload'");
144}
145
146void tst_executor::executeBlockingConstQStringRef() {
147 // Explicitly verify that the refactored const QString & parameter
148 // accepts a const-qualified variable without copies or issues.
149 const QString app = QStringLiteral("echo");
150 const QStringList args = {QStringLiteral("const-ref-ok")};
151 QString output;
152 const int result = Executor::executeBlocking(app, args, QString(), &output);
153 QVERIFY2(result == 0, "const QString& app should be accepted");
154 QVERIFY2(output.contains("const-ref-ok"),
155 "output should contain 'const-ref-ok'");
156}
157
158#endif
159
160void tst_executor::executeBlockingNotFound() {
161 QString output;
162 int result = Executor::executeBlocking("nonexistent_command_xyz", {},
163 QString(), &output);
164 QVERIFY2(result != 0, "nonexistent should fail");
165}
166
167void tst_executor::executeBlockingWithEnvNotFound() {
168 // The env-based overload should also fail gracefully for a missing binary.
169 QStringList env = {"MY_VAR=irrelevant"};
170 QString output;
171 int result = Executor::executeBlocking(env, "nonexistent_command_env_xyz", {},
172 &output);
173 QVERIFY2(result != 0,
174 "env-overload with nonexistent command should return non-zero");
175}
176
177void tst_executor::executeBlockingGpgVersion() {
178 QString output;
179 QString err;
180 int result =
181 Executor::executeBlocking("gpg", {"--version"}, QString(), &output, &err);
182 if (result != 0) {
183 QSKIP("gpg not available");
184 }
185 QVERIFY2(output.contains("gpg"), "output should contain gpg");
186}
187
188void tst_executor::gpgSupportsEd25519() {
189 QString output;
190 QString err;
191 int result =
192 Executor::executeBlocking("gpg", {"--version"}, QString(), &output, &err);
193 if (result != 0) {
194 QSKIP("gpg not available");
195 }
196 bool supported = Pass::gpgSupportsEd25519();
197 QRegularExpression versionRegex(R"(gpg \‍(GnuPG\) (\d+)\.(\d+))");
198 QRegularExpressionMatch match = versionRegex.match(output);
199 QVERIFY2(match.hasMatch(), "Could not parse gpg version output");
200 int major = match.captured(1).toInt();
201 int minor = match.captured(2).toInt();
202 bool expectedSupported = major > 2 || (major == 2 && minor >= 1);
203 if (supported != expectedSupported) {
204 QSKIP("GPG version mismatch between test and Pass::gpgSupportsEd25519");
205 }
206}
207
208void tst_executor::getDefaultKeyTemplate() {
209 QString templateStr = Pass::getDefaultKeyTemplate();
210 QVERIFY2(!templateStr.isEmpty(), "Default key template should not be empty");
211 QVERIFY2(templateStr.contains("Key-Type"),
212 "Template should contain Key-Type");
213}
214
215void tst_executor::executeBlockingGpgKillAgent() {
216#ifndef Q_OS_WIN
217 QString output;
218 QString err;
219 int result = Executor::executeBlocking("gpgconf", {"--kill", "gpg-agent"},
220 QString(), &output, &err);
221 if (result != 0) {
222 QSKIP("gpgconf not available in PATH");
223 }
224 QVERIFY2(result == 0, "gpgconf --kill gpg-agent should succeed");
225#else
226 QSKIP("gpgconf not available on Windows");
227#endif
228}
229
230void tst_executor::resolveGpgconfCommand() {
231 // Empty input
232 {
233 auto result = Pass::resolveGpgconfCommand("");
234 QVERIFY2(result.program == "gpgconf",
235 "Empty input should fallback to gpgconf");
236 }
237
238 // WSL simple
239 {
240 auto result = Pass::resolveGpgconfCommand("wsl gpg2");
241 QStringList expectedArgs = {"gpgconf"};
242 QVERIFY2(result.program == "wsl" && result.arguments == expectedArgs,
243 "WSL simple should replace gpg with gpgconf");
244 }
245
246 // WSL with distro
247 {
248 auto result = Pass::resolveGpgconfCommand("wsl --distro Debian gpg2");
249 QVERIFY2(result.program == "wsl", "WSL distro preserves wsl");
250 QVERIFY2(result.arguments.contains("--distro") &&
251 result.arguments.contains("Debian"),
252 "WSL distro arguments should be preserved");
253 }
254
255 // WSL with full path
256 {
257 auto result = Pass::resolveGpgconfCommand("wsl /usr/bin/gpg2");
258 QStringList expectedArgs = {"/usr/bin/gpgconf"};
259 QVERIFY2(result.program == "wsl" && result.arguments == expectedArgs,
260 "WSL with full path should preserve directory");
261 }
262
263 // WSL complex (should fallback)
264 {
265 auto result = Pass::resolveGpgconfCommand("wsl sh -c \"gpg2 --version\"");
266 QVERIFY2(result.program == "gpgconf", "Complex WSL shell should fallback");
267 }
268
269 // WSL malformed (only "wsl")
270 {
271 auto result = Pass::resolveGpgconfCommand("wsl");
272 QVERIFY2(result.program == "gpgconf", "Malformed WSL should fallback");
273 }
274
275 // PATH-only
276 {
277 auto result = Pass::resolveGpgconfCommand("gpg2");
278 QVERIFY2(result.program == "gpgconf" && result.arguments.isEmpty(),
279 "PATH-only should fallback with no extra arguments");
280 }
281
282 // Unix absolute - use temp directory for filesystem-independent test
283 {
284 QTemporaryDir tempDir;
285 QVERIFY2(tempDir.isValid(), "Temp directory should be valid");
286 QString absPath = tempDir.path() + "/gpg2";
287 QFile gpg2File(absPath);
288 QVERIFY2(gpg2File.open(QIODevice::WriteOnly),
289 "Should be able to create temporary gpg2 file");
290 gpg2File.close();
291 auto result = Pass::resolveGpgconfCommand(absPath);
292 QVERIFY2(result.program == "gpgconf", "Absolute path should fallback");
293 }
294}
295
296QTEST_MAIN(tst_executor)
297#include "tst_executor.moc"
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
Definition executor.cpp:223
static bool gpgSupportsEd25519()
Check if GPG supports Ed25519 encryption.
Definition pass.cpp:199
static auto resolveGpgconfCommand(const QString &gpgPath) -> ResolvedGpgconfCommand
Resolve the gpgconf command to kill agents.
Definition pass.cpp:367
static QString getDefaultKeyTemplate()
Get default key template for new GPG keys.
Definition pass.cpp:220