master
cpp 567 lines 21.9 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLITerminalUnitTests.cpp
8
9 Abstract:
10
11 Unit tests for OutputChannel, InputChannel, and Terminal.
12
13 --*/
14
15 #include "precomp.h"
16 #include "windows/Common.h"
17 #include "WSLCCLITestHelpers.h"
18
19 #include "InputChannel.h"
20 #include "OutputChannel.h"
21 #include "Terminal.h"
22
23 using namespace wsl::windows::wslc;
24 using namespace wsl::windows::common::vt;
25 using namespace WSLCTestHelpers;
26 using namespace WEX::Logging;
27 using namespace WEX::Common;
28 using namespace WEX::TestExecution;
29
30 namespace WSLCCLITerminalUnitTests {
31
32 // Dual-pipe Terminal so stdout and stderr can be asserted independently.
33 struct SplitCaptureTerminal
34 {
35 CapturePipe outPipe;
36 CapturePipe errPipe;
37 Terminal terminal;
38
39 explicit SplitCaptureTerminal(bool vtEnabled = false) : terminal(outPipe.file(), vtEnabled, errPipe.file(), vtEnabled)
40 {
41 }
42 };
43
44 // Terminal wired with a preloaded input pipe plus split output capture, so prompt
45 // input and the label/newline it writes can be asserted together.
46 struct InputCaptureTerminal
47 {
48 CapturePipe outPipe;
49 CapturePipe errPipe;
50 InputPipe inPipe;
51 Terminal terminal;
52
53 explicit InputCaptureTerminal(const std::wstring& input, bool interactive = false) :
54 inPipe(input), terminal(outPipe.file(), false, errPipe.file(), false, inPipe.file(), interactive)
55 {
56 }
57 };
58
59 class WSLCCLITerminalUnitTests
60 {
61 WSLC_TEST_CLASS(WSLCCLITerminalUnitTests)
62
63 TEST_CLASS_SETUP(TestClassSetup)
64 {
65 return true;
66 }
67
68 TEST_CLASS_CLEANUP(TestClassCleanup)
69 {
70 return true;
71 }
72
73 TEST_METHOD(OutputChannel_WriteStringWritesText)
74 {
75 CapturePipe pipe;
76 const OutputChannel channel{pipe.file(), false};
77 channel.WriteString(L"hello");
78 VERIFY_ARE_EQUAL(std::wstring{L"hello"}, pipe.captured());
79 }
80
81 TEST_METHOD(OutputChannel_WriteStringIsNoOpOnEmpty)
82 {
83 CapturePipe pipe;
84 const OutputChannel channel{pipe.file(), false};
85 channel.WriteString(L"");
86 VERIFY_ARE_EQUAL(std::wstring{L""}, pipe.captured());
87 }
88
89 TEST_METHOD(OutputChannel_FromHandleFallsBackToFileForNonConsole)
90 {
91 CapturePipe pipe;
92 const OutputChannel channel{INVALID_HANDLE_VALUE, pipe.file()};
93 channel.WriteString(L"fallback");
94 VERIFY_ARE_EQUAL(std::wstring{L"fallback"}, pipe.captured());
95 VERIFY_IS_FALSE(channel.GetConsoleWidth().has_value());
96 }
97
98 TEST_METHOD(OutputChannel_GetConsoleWidth_FileChannelReturnsNullopt)
99 {
100 CapturePipe pipe;
101 const OutputChannel channel{pipe.file(), false};
102 VERIFY_IS_FALSE(channel.GetConsoleWidth().has_value());
103 }
104
105 TEST_METHOD(Terminal_WriteEmitsExactText)
106 {
107 CaptureTerminal cap;
108 cap.terminal.Output(L"hello\n");
109 VERIFY_ARE_EQUAL(std::wstring{L"hello\n"}, cap.captured());
110 }
111
112 TEST_METHOD(Terminal_WriteWithoutNewline)
113 {
114 CaptureTerminal cap;
115 cap.terminal.Write(Terminal::Level::Output, L"hello");
116 VERIFY_ARE_EQUAL(std::wstring{L"hello"}, cap.captured());
117 }
118
119 TEST_METHOD(Terminal_FormatStringSubstitutesArgs)
120 {
121 CaptureTerminal cap;
122 cap.terminal.Output(L"value={}, name={}\n", 42, L"alice");
123 VERIFY_ARE_EQUAL(std::wstring{L"value=42, name=alice\n"}, cap.captured());
124 }
125
126 TEST_METHOD(Terminal_PlainStringNeedsNoArgs)
127 {
128 CaptureTerminal cap;
129 cap.terminal.Output(L"plain literal\n");
130 VERIFY_ARE_EQUAL(std::wstring{L"plain literal\n"}, cap.captured());
131 }
132
133 TEST_METHOD(Terminal_SequenceEmittedWhenVTEnabled)
134 {
135 CaptureTerminal cap{/*vtEnabled*/ true};
136 cap.terminal.Output(L"{}highlighted{}\n", Format::Fg::BrightYellow, Format::Default);
137
138 const auto result = cap.captured();
139 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"highlighted"));
140 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(Format::Fg::BrightYellow.Get()));
141 }
142
143 TEST_METHOD(Terminal_SequenceStrippedWhenVTDisabled)
144 {
145 CaptureTerminal cap{/*vtEnabled*/ false};
146 cap.terminal.Output(L"{}plain{}\n", Format::Fg::BrightYellow, Format::Default);
147 VERIFY_ARE_EQUAL(std::wstring{L"plain\n"}, cap.captured());
148 }
149
150 TEST_METHOD(Terminal_ColorSequenceStrippedWhenNoColor)
151 {
152 CaptureTerminal cap{/*vtEnabled*/ true};
153 cap.terminal.SetNoColor(true);
154
155 // Color sequence (SGR) stripped; cursor moves (non-color) still pass.
156 cap.terminal.Output(L"{}{}plain{}\n", Cursor::Up(1), Format::Fg::BrightRed, Format::Default);
157
158 const auto result = cap.captured();
159 VERIFY_ARE_EQUAL(std::wstring::npos, result.find(Format::Fg::BrightRed.Get()));
160 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(Cursor::Up(1).Get()));
161 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"plain"));
162 }
163
164 TEST_METHOD(Terminal_ConstructedSequenceHandledLikeSequence)
165 {
166 CaptureTerminal cap{/*vtEnabled*/ true};
167 const auto cursor = Cursor::Up(3);
168 cap.terminal.Output(L"{}done\n", cursor);
169
170 const auto result = cap.captured();
171 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(cursor.Get()));
172 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.find(L"done"));
173 }
174
175 TEST_METHOD(Terminal_LevelColorWrapsOutputWhenVTEnabled)
176 {
177 CaptureTerminal cap{/*vtEnabled*/ true};
178
179 cap.terminal.Output(L"starting\n");
180 cap.terminal.Info(L"pulling\n");
181 cap.terminal.Warn(L"careful\n");
182 cap.terminal.Error(L"failed\n");
183
184 const std::wstring def{Format::Default.Get()};
185 const std::wstring yellow{Format::Fg::BrightYellow.Get()};
186 const std::wstring red{Format::Fg::BrightRed.Get()};
187
188 const auto expected = std::wstring{L"starting\npulling\n"} + yellow + L"careful\n" + def + red + L"failed\n" + def;
189
190 VERIFY_ARE_EQUAL(expected, cap.captured());
191 }
192
193 TEST_METHOD(Terminal_LevelColorSuppressedWhenVTDisabled)
194 {
195 CaptureTerminal cap{/*vtEnabled*/ false};
196 cap.terminal.Error(L"failed\n");
197 VERIFY_ARE_EQUAL(std::wstring{L"failed\n"}, cap.captured());
198 }
199
200 TEST_METHOD(Terminal_LevelColorSuppressedWhenNoColor)
201 {
202 CaptureTerminal cap{/*vtEnabled*/ true};
203 cap.terminal.SetNoColor(true);
204 cap.terminal.Warn(L"careful\n");
205 VERIFY_ARE_EQUAL(std::wstring{L"careful\n"}, cap.captured());
206 }
207
208 TEST_METHOD(Terminal_RoutingByLevel)
209 {
210 SplitCaptureTerminal cap;
211
212 cap.terminal.Output(L"output text\n");
213 cap.terminal.Info(L"info text\n");
214 cap.terminal.Warn(L"warn text\n");
215 cap.terminal.Error(L"error text\n");
216
217 VERIFY_ARE_EQUAL(std::wstring{L"output text\n"}, cap.outPipe.captured());
218 VERIFY_ARE_EQUAL(std::wstring{L"info text\nwarn text\nerror text\n"}, cap.errPipe.captured());
219 }
220
221 TEST_METHOD(Terminal_SetNoColorTogglesIsNoColor)
222 {
223 CaptureTerminal cap;
224 VERIFY_IS_FALSE(cap.terminal.IsNoColor());
225 cap.terminal.SetNoColor(true);
226 VERIFY_IS_TRUE(cap.terminal.IsNoColor());
227 cap.terminal.SetNoColor(false);
228 VERIFY_IS_FALSE(cap.terminal.IsNoColor());
229 }
230
231 TEST_METHOD(Terminal_IsVTEnabledReflectsPerChannelState)
232 {
233 {
234 SplitCaptureTerminal cap{/*vt*/ false};
235 VERIFY_IS_FALSE(cap.terminal.IsVTEnabled(Terminal::Level::Output));
236 VERIFY_IS_FALSE(cap.terminal.IsVTEnabled(Terminal::Level::Error));
237 }
238 {
239 SplitCaptureTerminal cap{/*vt*/ true};
240 VERIFY_IS_TRUE(cap.terminal.IsVTEnabled(Terminal::Level::Output));
241 VERIFY_IS_TRUE(cap.terminal.IsVTEnabled(Terminal::Level::Error));
242 }
243 {
244 CapturePipe outPipe;
245 CapturePipe errPipe;
246 Terminal terminal{outPipe.file(), /*outVt*/ true, errPipe.file(), /*errVt*/ false};
247 VERIFY_IS_TRUE(terminal.IsVTEnabled(Terminal::Level::Output));
248 VERIFY_IS_FALSE(terminal.IsVTEnabled(Terminal::Level::Info));
249 VERIFY_IS_FALSE(terminal.IsVTEnabled(Terminal::Level::Warning));
250 VERIFY_IS_FALSE(terminal.IsVTEnabled(Terminal::Level::Error));
251 }
252 }
253
254 TEST_METHOD(Terminal_IsColorEnabledPerLevelHonorsBothVTAndNoColor)
255 {
256 SplitCaptureTerminal cap{/*vt*/ true};
257 VERIFY_IS_TRUE(cap.terminal.IsColorEnabled(Terminal::Level::Output));
258 VERIFY_IS_TRUE(cap.terminal.IsColorEnabled(Terminal::Level::Error));
259
260 cap.terminal.SetNoColor(true);
261 VERIFY_IS_FALSE(cap.terminal.IsColorEnabled(Terminal::Level::Output));
262 VERIFY_IS_FALSE(cap.terminal.IsColorEnabled(Terminal::Level::Error));
263 }
264
265 TEST_METHOD(Terminal_GetConsoleWidthReturnsNulloptForFileChannels)
266 {
267 SplitCaptureTerminal cap;
268 VERIFY_IS_FALSE(cap.terminal.GetConsoleWidth(Terminal::Level::Output).has_value());
269 VERIFY_IS_FALSE(cap.terminal.GetConsoleWidth(Terminal::Level::Info).has_value());
270 VERIFY_IS_FALSE(cap.terminal.GetConsoleWidth(Terminal::Level::Warning).has_value());
271 VERIFY_IS_FALSE(cap.terminal.GetConsoleWidth(Terminal::Level::Error).has_value());
272 }
273
274 TEST_METHOD(Terminal_Write_MixesSequencesWithStandardFormatArgs)
275 {
276 // Terminal.Write is std::format under the hood — any formattable type works
277 // alongside Sequences. Sequences are stripped when color is off; everything
278 // else formats normally through std::format machinery.
279 //
280 // This test exercises four sequence categories in a single format call:
281 // SGR color (Format::Fg::BrightRed) — color, stripped by NoColor
282 // Non-color (Erase::LineForward) — not color, survives NoColor
283 // Hyperlink (ConstructedSequence OSC8) — color, stripped by NoColor
284 // SGR reset (Format::Default) — color, stripped by NoColor
285 //
286 // Hyperlink open/close are separate sequences so the visible link text
287 // degrades gracefully when sequences are stripped.
288
289 const auto& eraseLine = Erase::LineForward; // \x1b[K — non-color CSI
290 const auto linkOpen = Format::LinkOpen(L"https://example.com");
291 const auto& linkClose = Format::LinkClose;
292
293 // Format: <color>Count: <int>, hex: <hex>, <erase><linkOpen>click here<linkClose><reset>
294 constexpr auto fmt = L"{}Count: {}, hex: {:04x}, {}{}click here{}{}\n";
295
296 // VT + color enabled: equivalent to std::format with all sequence bytes.
297 {
298 CaptureTerminal cap{/*vtEnabled*/ true};
299 cap.terminal.Output(fmt, Format::Fg::BrightRed, 42, 255u, eraseLine, linkOpen, linkClose, Format::Default);
300
301 const auto expected = std::format(
302 fmt, Format::Fg::BrightRed.Get(), 42, 255u, eraseLine.Get(), linkOpen.Get(), linkClose.Get(), Format::Default.Get());
303 VERIFY_ARE_EQUAL(expected, cap.captured());
304 }
305
306 // NoColor (VT enabled, color disabled): non-color sequences pass through,
307 // color sequences (SGR, hyperlink) replaced with empty string.
308 {
309 CaptureTerminal cap{/*vtEnabled*/ true};
310 cap.terminal.SetNoColor(true);
311 cap.terminal.Output(fmt, Format::Fg::BrightRed, 42, 255u, eraseLine, linkOpen, linkClose, Format::Default);
312
313 const std::wstring_view empty;
314 const auto expected = std::format(fmt, empty, 42, 255u, eraseLine.Get(), empty, empty, empty);
315 VERIFY_ARE_EQUAL(expected, cap.captured());
316 }
317
318 // VT disabled: all sequences replaced with empty string.
319 {
320 CaptureTerminal cap{/*vtEnabled*/ false};
321 cap.terminal.Output(fmt, Format::Fg::BrightRed, 42, 255u, eraseLine, linkOpen, linkClose, Format::Default);
322
323 const std::wstring_view empty;
324 const auto expected = std::format(fmt, empty, 42, 255u, empty, empty, empty, empty);
325 VERIFY_ARE_EQUAL(expected, cap.captured());
326 }
327 }
328
329 TEST_METHOD(InputChannel_ReadLineReturnsNulloptAtEof)
330 {
331 InputPipe pipe{L""};
332 const InputChannel channel{pipe.file(), false};
333 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
334 }
335
336 TEST_METHOD(InputChannel_ReadLineSplitsOnNewline)
337 {
338 InputPipe pipe{L"user\npass\n"};
339 const InputChannel channel{pipe.file(), false};
340
341 auto first = channel.ReadLine(false);
342 VERIFY_IS_TRUE(first.has_value());
343 VERIFY_ARE_EQUAL(std::wstring{L"user"}, first.value());
344
345 auto second = channel.ReadLine(false);
346 VERIFY_IS_TRUE(second.has_value());
347 VERIFY_ARE_EQUAL(std::wstring{L"pass"}, second.value());
348
349 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
350 }
351
352 TEST_METHOD(InputChannel_ReadLineStripsCarriageReturn)
353 {
354 InputPipe pipe{L"user\r\npass\r\n"};
355 const InputChannel channel{pipe.file(), false};
356
357 VERIFY_ARE_EQUAL(std::wstring{L"user"}, channel.ReadLine(false).value_or(L"<eof>"));
358 VERIFY_ARE_EQUAL(std::wstring{L"pass"}, channel.ReadLine(false).value_or(L"<eof>"));
359 }
360
361 TEST_METHOD(InputChannel_ReadLineReturnsEmptyStringForBlankLine)
362 {
363 // A bare empty line is distinct from EOF: value present but empty.
364 InputPipe pipe{L"\nsecond\n"};
365 const InputChannel channel{pipe.file(), false};
366
367 auto blank = channel.ReadLine(false);
368 VERIFY_IS_TRUE(blank.has_value());
369 VERIFY_ARE_EQUAL(std::wstring{L""}, blank.value());
370
371 VERIFY_ARE_EQUAL(std::wstring{L"second"}, channel.ReadLine(false).value_or(L"<eof>"));
372 }
373
374 TEST_METHOD(InputChannel_ReadLineReturnsFinalLineWithoutTrailingNewline)
375 {
376 InputPipe pipe{L"only"};
377 const InputChannel channel{pipe.file(), false};
378
379 VERIFY_ARE_EQUAL(std::wstring{L"only"}, channel.ReadLine(false).value_or(L"<eof>"));
380 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
381 }
382
383 TEST_METHOD(InputChannel_IsInteractiveReflectsOverrideForNonConsole)
384 {
385 InputPipe pipe{L"x\n"};
386 const InputChannel notInteractive{pipe.file(), false};
387 VERIFY_IS_FALSE(notInteractive.IsInteractive());
388
389 InputPipe pipe2{L"x\n"};
390 const InputChannel interactive{pipe2.file(), true};
391 VERIFY_IS_TRUE(interactive.IsInteractive());
392 }
393
394 TEST_METHOD(InputChannel_ReadLineWithMaskReadsWhenNoConsole)
395 {
396 // Masking is a no-op without a real console; the read still succeeds.
397 InputPipe pipe{L"secret\n"};
398 const InputChannel channel{pipe.file(), false};
399 VERIFY_ARE_EQUAL(std::wstring{L"secret"}, channel.ReadLine(true).value_or(L"<eof>"));
400 }
401
402 TEST_METHOD(InputChannel_ReadLineOnNullFileReturnsNullopt)
403 {
404 const InputChannel channel{static_cast<FILE*>(nullptr), false};
405 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
406 }
407
408 TEST_METHOD(InputChannel_ReadLinePreservesInteriorAndSurroundingWhitespace)
409 {
410 // Only the trailing CR/LF is stripped. Leading, interior, and trailing spaces
411 // and tabs are preserved verbatim (WSLC does not trim, unlike Docker's prompt).
412 InputPipe pipe{L" spaced \t value \n"};
413 const InputChannel channel{pipe.file(), false};
414 VERIFY_ARE_EQUAL(std::wstring{L" spaced \t value "}, channel.ReadLine(false).value_or(L"<eof>"));
415 }
416
417 TEST_METHOD(InputChannel_ReadLineStripsLoneTrailingCarriageReturnAtEof)
418 {
419 // A lone trailing CR (no following LF) is not collapsed by the stream's CRLF
420 // translation, so it reaches ReadLine and exercises the trailing-CR strip.
421 InputPipe pipe{L"value\r"};
422 const InputChannel channel{pipe.file(), false};
423 VERIFY_ARE_EQUAL(std::wstring{L"value"}, channel.ReadLine(false).value_or(L"<eof>"));
424 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
425 }
426
427 TEST_METHOD(InputChannel_ReadLinePreservesEmbeddedCarriageReturn)
428 {
429 // Only a trailing CR is stripped; a CR in the middle of a line is preserved.
430 InputPipe pipe{L"a\rb\n"};
431 const InputChannel channel{pipe.file(), false};
432 VERIFY_ARE_EQUAL(std::wstring{L"a\rb"}, channel.ReadLine(false).value_or(L"<eof>"));
433 }
434
435 TEST_METHOD(InputChannel_ReadLineDecodesUnicode)
436 {
437 // UTF-8 bytes on the wire decode back to the original wide characters.
438 const std::wstring expected = L"\u00e9\u4e2d\u6587\u2013user";
439 InputPipe pipe{expected + L"\n"};
440 const InputChannel channel{pipe.file(), false};
441 VERIFY_ARE_EQUAL(expected, channel.ReadLine(false).value_or(L"<eof>"));
442 }
443
444 TEST_METHOD(InputChannel_ReadLineHandlesLongLine)
445 {
446 // Lines longer than any internal buffer are read in full (fgetwc loop).
447 const std::wstring expected(8192, L'z');
448 InputPipe pipe{expected + L"\n"};
449 const InputChannel channel{pipe.file(), false};
450 VERIFY_ARE_EQUAL(expected, channel.ReadLine(false).value_or(L"<eof>"));
451 }
452
453 TEST_METHOD(InputChannel_ReadLineReturnsNulloptAfterAllLinesConsumed)
454 {
455 InputPipe pipe{L"one\ntwo\n"};
456 const InputChannel channel{pipe.file(), false};
457 VERIFY_ARE_EQUAL(std::wstring{L"one"}, channel.ReadLine(false).value_or(L"<eof>"));
458 VERIFY_ARE_EQUAL(std::wstring{L"two"}, channel.ReadLine(false).value_or(L"<eof>"));
459 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
460 // Further reads keep returning nullopt (idempotent at EOF).
461 VERIFY_IS_FALSE(channel.ReadLine(false).has_value());
462 }
463
464 TEST_METHOD(Terminal_ReadLineReturnsInput)
465 {
466 InputCaptureTerminal cap{L"line1\nline2\n"};
467 VERIFY_ARE_EQUAL(std::wstring{L"line1"}, cap.terminal.ReadLine().value_or(L"<eof>"));
468 VERIFY_ARE_EQUAL(std::wstring{L"line2"}, cap.terminal.ReadLine().value_or(L"<eof>"));
469 VERIFY_IS_FALSE(cap.terminal.ReadLine().has_value());
470 }
471
472 TEST_METHOD(Terminal_IsInputInteractiveReflectsChannel)
473 {
474 InputCaptureTerminal pipeInput{L"x\n", /*interactive*/ false};
475 VERIFY_IS_FALSE(pipeInput.terminal.IsInputInteractive());
476
477 InputCaptureTerminal consoleInput{L"x\n", /*interactive*/ true};
478 VERIFY_IS_TRUE(consoleInput.terminal.IsInputInteractive());
479 }
480
481 TEST_METHOD(Terminal_PromptForLineWritesLabelToStdoutAndReturnsInput)
482 {
483 InputCaptureTerminal cap{L"myuser\n"};
484
485 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Username: ", false);
486 VERIFY_ARE_EQUAL(std::wstring{L"myuser"}, result);
487
488 // Label lands on stdout (Docker convention); nothing on stderr; no trailing
489 // newline because the input was not masked.
490 VERIFY_ARE_EQUAL(std::wstring{L"Username: "}, cap.outPipe.captured());
491 VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured());
492 }
493
494 TEST_METHOD(Terminal_PromptForLineMaskedInteractiveEmitsTrailingNewline)
495 {
496 // Interactive override makes willMask true, so the un-echoed Enter is advanced
497 // with a trailing newline after the label.
498 InputCaptureTerminal cap{L"secret\n", /*interactive*/ true};
499
500 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Password: ", true);
501 VERIFY_ARE_EQUAL(std::wstring{L"secret"}, result);
502 VERIFY_ARE_EQUAL(std::wstring{L"Password: \n"}, cap.outPipe.captured());
503 }
504
505 TEST_METHOD(Terminal_PromptForLineMaskedNonInteractiveEmitsNoTrailingNewline)
506 {
507 // Redirected input is not interactive, so no masking and no trailing newline.
508 InputCaptureTerminal cap{L"secret\n", /*interactive*/ false};
509
510 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Password: ", true);
511 VERIFY_ARE_EQUAL(std::wstring{L"secret"}, result);
512 VERIFY_ARE_EQUAL(std::wstring{L"Password: "}, cap.outPipe.captured());
513 }
514
515 TEST_METHOD(Terminal_PromptForLineReturnsEmptyStringAtEof)
516 {
517 InputCaptureTerminal cap{L""};
518 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Username: ", false);
519 VERIFY_ARE_EQUAL(std::wstring{L""}, result);
520 VERIFY_ARE_EQUAL(std::wstring{L"Username: "}, cap.outPipe.captured());
521 }
522
523 TEST_METHOD(Terminal_PromptForLineEmitsLabelVerbatimWithFormatCharacters)
524 {
525 // The label is passed as a formatting argument, not a format string, so brace
526 // and percent characters in it must never be interpreted (no format injection).
527 InputCaptureTerminal cap{L"answer\n"};
528 const std::wstring label = L"Value {} {0} {name} 100% ${var}: ";
529
530 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, label, false);
531 VERIFY_ARE_EQUAL(std::wstring{L"answer"}, result);
532 VERIFY_ARE_EQUAL(label, cap.outPipe.captured());
533 }
534
535 TEST_METHOD(Terminal_PromptForLineDoesNotTrimPasswordWhitespace)
536 {
537 // Secrets are opaque: interior and surrounding whitespace is preserved so a
538 // password like " a b " is returned exactly as typed.
539 InputCaptureTerminal cap{L" a b \n", /*interactive*/ true};
540
541 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Password: ", true);
542 VERIFY_ARE_EQUAL(std::wstring{L" a b "}, result);
543 VERIFY_ARE_EQUAL(std::wstring{L"Password: \n"}, cap.outPipe.captured());
544 }
545
546 TEST_METHOD(Terminal_PromptForLineReturnsUnicodeInput)
547 {
548 const std::wstring expected = L"\u00fcser\u00f1ame";
549 InputCaptureTerminal cap{expected + L"\n"};
550
551 const auto result = cap.terminal.PromptForLine(Terminal::Level::Output, L"Username: ", false);
552 VERIFY_ARE_EQUAL(expected, result);
553 }
554
555 TEST_METHOD(Terminal_ReadLineMaskDefaultsToUnmasked)
556 {
557 // ReadLine(bool mask = false): the default reads without masking and returns
558 // the line, used by the --password-stdin path.
559 InputCaptureTerminal cap{L"piped-secret\n"};
560 VERIFY_ARE_EQUAL(std::wstring{L"piped-secret"}, cap.terminal.ReadLine().value_or(L"<eof>"));
561 // Nothing is written for a bare ReadLine (no prompt label).
562 VERIFY_ARE_EQUAL(std::wstring{L""}, cap.outPipe.captured());
563 VERIFY_ARE_EQUAL(std::wstring{L""}, cap.errPipe.captured());
564 }
565 };
566
567 } // namespace WSLCCLITerminalUnitTests