master
cpp 778 lines 30 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLISettingsUnitTests.cpp
8
9 Abstract:
10
11 Unit tests for the wslc UserSettings system: SettingsMap, YAML loading,
12 per-setting validation, fallback logic, and UserSettingsType detection.
13
14 --*/
15
16 #include "precomp.h"
17 #include "windows/Common.h"
18 #include "WSLCCLITestHelpers.h"
19 #include "WSLCUserSettings.h"
20
21 #include <atomic>
22 #include <fstream>
23
24 using namespace wsl::windows::wslc::settings;
25 using namespace WSLCTestHelpers;
26 using namespace WEX::Logging;
27 using namespace WEX::Common;
28 using namespace WEX::TestExecution;
29 using Loc = wsl::shared::Localization;
30
31 namespace WSLCCLISettingsUnitTests {
32
33 // Thin subclass that makes the protected constructor publicly accessible,
34 // allowing tests to load settings from an arbitrary directory without
35 // going through the singleton.
36 class UserSettingsTest : public UserSettings
37 {
38 public:
39 explicit UserSettingsTest(const std::filesystem::path& settingsDir) : UserSettings(settingsDir)
40 {
41 }
42 };
43
44 // ---------------------------------------------------------------------------
45 // Helpers
46 // ---------------------------------------------------------------------------
47
48 static std::atomic<int> s_dirCounter{0};
49
50 static std::filesystem::path UniqueTempDir()
51 {
52 auto dir = std::filesystem::temp_directory_path() / L"WSLCSettingsTests" / std::to_wstring(GetCurrentProcessId()) /
53 std::to_wstring(++s_dirCounter);
54 std::filesystem::create_directories(dir);
55 return dir;
56 }
57
58 static void WriteFile(const std::filesystem::path& path, std::string_view content)
59 {
60 std::filesystem::create_directories(path.parent_path());
61 std::ofstream f(path, std::ios::binary);
62 VERIFY_IS_TRUE(f.is_open());
63 f.write(content.data(), static_cast<std::streamsize>(content.size()));
64 }
65
66 // ---------------------------------------------------------------------------
67 // Test class
68 // ---------------------------------------------------------------------------
69
70 class WSLCCLISettingsUnitTests
71 {
72 WSLC_TEST_CLASS(WSLCCLISettingsUnitTests)
73
74 TEST_CLASS_SETUP(TestClassSetup)
75 {
76 return true;
77 }
78
79 TEST_CLASS_CLEANUP(TestClassCleanup)
80 {
81 std::error_code ec;
82 std::filesystem::remove_all(std::filesystem::temp_directory_path() / L"WSLCSettingsTests", ec);
83 return true;
84 }
85
86 // -----------------------------------------------------------------------
87 // SettingsMap — pure unit tests, no I/O
88 // -----------------------------------------------------------------------
89
90 // All four settings should return their compile-time defaults when the map
91 // is empty (no values have been inserted).
92 TEST_METHOD(SettingsMap_GetOrDefault_ReturnsBuiltInWhenAbsent)
93 {
94 SettingsMap map;
95 VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionCpuCount>());
96 VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionMemoryMb>());
97 VERIFY_ARE_EQUAL(1048576u, map.GetOrDefault<Setting::SessionStorageSizeMb>());
98 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(map.GetOrDefault<Setting::CredentialStore>()));
99 }
100
101 // After inserting a value, GetOrDefault must return it rather than the default.
102 TEST_METHOD(SettingsMap_GetOrDefault_ReturnsStoredWhenPresent)
103 {
104 SettingsMap map;
105 map.Add<Setting::SessionCpuCount>(16u);
106 VERIFY_ARE_EQUAL(16u, map.GetOrDefault<Setting::SessionCpuCount>());
107 VERIFY_ARE_EQUAL(0u, map.GetOrDefault<Setting::SessionMemoryMb>());
108 }
109
110 // -----------------------------------------------------------------------
111 // Default (setting file missing)
112 // -----------------------------------------------------------------------
113
114 // When settings file missing, the type must be Default, there
115 // must be no warnings, and all values must be at their built-in defaults.
116 TEST_METHOD(LoadSettings_NoFiles_YieldsDefaultTypeAndNoWarnings)
117 {
118 UserSettingsTest s{UniqueTempDir()};
119
120 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Default), static_cast<int>(s.GetType()));
121 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
122 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
123 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
124 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
125 VERIFY_ARE_EQUAL(std::string("host.wslc.internal"), s.Get<Setting::SessionHostLoopback>());
126 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
127 }
128
129 // -----------------------------------------------------------------------
130 // Standard (valid settings)
131 // -----------------------------------------------------------------------
132
133 // A well-formed settings file must set the type to Standard with no
134 // warnings and the specified values loaded.
135 TEST_METHOD(LoadSettings_ValidSettings_YieldsStandardTypeAndValues)
136 {
137 auto dir = UniqueTempDir();
138 WriteFile(
139 dir / L"settings.yaml",
140 "session:\n"
141 " cpuCount: 8\n"
142 " memorySize: 4GB\n"
143 " maxStorageSize: 20000MB\n"
144 "credentialStore: file\n");
145
146 UserSettingsTest s{dir};
147
148 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
149 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
150 VERIFY_ARE_EQUAL(8u, s.Get<Setting::SessionCpuCount>());
151 VERIFY_ARE_EQUAL(4096u, s.Get<Setting::SessionMemoryMb>());
152 VERIFY_ARE_EQUAL(20000u, s.Get<Setting::SessionStorageSizeMb>());
153 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::File), static_cast<int>(s.Get<Setting::CredentialStore>()));
154 }
155
156 TEST_METHOD(LoadSettings_StorageSizeFormats_YieldExpectedValues)
157 {
158 auto dir = UniqueTempDir();
159 WriteFile(
160 dir / L"settings.yaml",
161 "session:\n"
162 " memorySize: \" 1.5GiB\"\n"
163 " maxStorageSize: 20.5GB\n");
164
165 UserSettingsTest s{dir};
166
167 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
168 VERIFY_ARE_EQUAL(1536u, s.Get<Setting::SessionMemoryMb>());
169 VERIFY_ARE_EQUAL(20992u, s.Get<Setting::SessionStorageSizeMb>());
170 }
171
172 // An empty settings file is valid YAML (null document) but not a mapping;
173 // a structure warning is emitted and all settings use defaults.
174 TEST_METHOD(LoadSettings_EmptySettings_WarnsInvalidStructure)
175 {
176 auto dir = UniqueTempDir();
177 WriteFile(dir / L"settings.yaml", "");
178
179 UserSettingsTest s{dir};
180
181 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
182 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
183 VERIFY_ARE_EQUAL(Loc::WSLCUserSettings_Warning_InvalidStructure(s.SettingsFilePath().wstring()), s.GetWarnings().front().Message);
184 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
185 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
186 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
187 }
188
189 // A non-map root (e.g. bare scalar) is valid YAML but invalid structure;
190 // a warning is emitted and all settings use defaults.
191 TEST_METHOD(LoadSettings_NonMapRoot_WarnsInvalidStructure)
192 {
193 auto dir = UniqueTempDir();
194 WriteFile(dir / L"settings.yaml", "just a string\n");
195
196 UserSettingsTest s{dir};
197
198 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
199 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
200 VERIFY_ARE_EQUAL(Loc::WSLCUserSettings_Warning_InvalidStructure(s.SettingsFilePath().wstring()), s.GetWarnings().front().Message);
201 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
202 }
203
204 // When the settings file fails to parse, the type is Default and a warning is emitted.
205 TEST_METHOD(LoadSettings_InvalidSettings_YieldsDefaultTypeWithWarning)
206 {
207 auto dir = UniqueTempDir();
208 WriteFile(dir / L"settings.yaml", "session: [\n"); // broken YAML (unclosed flow seq)
209
210 UserSettingsTest s{dir};
211
212 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Default), static_cast<int>(s.GetType()));
213 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
214 // Parse errors include yaml-cpp details, so check prefix including the file path.
215 VERIFY_IS_TRUE(s.GetWarnings().front().Message.starts_with(
216 L"Warning: Settings file at " + s.SettingsFilePath().wstring() + L" could not be parsed."));
217 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
218 }
219
220 // -----------------------------------------------------------------------
221 // Per-setting validation
222 // -----------------------------------------------------------------------
223
224 // cpuCount: 0 is rejected by validation; the default (0) is used and a warning emitted.
225 TEST_METHOD(Validation_CpuCount_Zero_UsesDefaultAndWarns)
226 {
227 auto dir = UniqueTempDir();
228 WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 0\n");
229
230 UserSettingsTest s{dir};
231
232 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
233 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
234 VERIFY_ARE_EQUAL(
235 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.cpuCount", s.SettingsFilePath().wstring(), 2),
236 s.GetWarnings().front().Message);
237 VERIFY_ARE_EQUAL(std::wstring(L"session.cpuCount"), s.GetWarnings().front().SettingPath);
238 }
239
240 // memorySize: 0 is rejected by validation; the default (0) is used.
241 TEST_METHOD(Validation_MemoryMb_Zero_UsesDefaultAndWarns)
242 {
243 auto dir = UniqueTempDir();
244 WriteFile(dir / L"settings.yaml", "session:\n memorySize: 0\n");
245
246 UserSettingsTest s{dir};
247
248 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
249 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
250 VERIFY_ARE_EQUAL(
251 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.memorySize", s.SettingsFilePath().wstring(), 2),
252 s.GetWarnings().front().Message);
253 }
254
255 TEST_METHOD(Validation_MemoryMb_Limits_AreEnforced)
256 {
257 struct TestCase
258 {
259 std::string Value;
260 uint32_t Expected;
261 bool IsValid;
262 };
263
264 const std::vector<TestCase> TestCases{
265 {"0.5MiB", 0, false},
266 {"4294967295MiB", std::numeric_limits<uint32_t>::max(), true},
267 {"4294967296MiB", 0, false},
268 };
269
270 for (const auto& TestCase : TestCases)
271 {
272 auto dir = UniqueTempDir();
273 WriteFile(dir / L"settings.yaml", std::format("session:\n memorySize: {}\n", TestCase.Value));
274
275 UserSettingsTest s{dir};
276
277 VERIFY_ARE_EQUAL(TestCase.Expected, s.Get<Setting::SessionMemoryMb>());
278 VERIFY_ARE_EQUAL(TestCase.IsValid ? 0u : 1u, s.GetWarnings().size());
279 if (!TestCase.IsValid)
280 {
281 VERIFY_ARE_EQUAL(
282 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.memorySize", s.SettingsFilePath().wstring(), 2),
283 s.GetWarnings().front().Message);
284 }
285 }
286 }
287
288 // maxStorageSize: 0 must be rejected; the default is used.
289 TEST_METHOD(Validation_StorageSizeMb_Zero_UsesDefaultAndWarns)
290 {
291 auto dir = UniqueTempDir();
292 WriteFile(dir / L"settings.yaml", "session:\n maxStorageSize: 0\n");
293
294 UserSettingsTest s{dir};
295
296 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
297 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
298 VERIFY_ARE_EQUAL(
299 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.maxStorageSize", s.SettingsFilePath().wstring(), 2),
300 s.GetWarnings().front().Message);
301 }
302
303 // A string where a uint32_t is expected must emit a type warning and fall
304 // back to the default.
305 TEST_METHOD(Validation_WrongType_UsesDefaultAndWarns)
306 {
307 auto dir = UniqueTempDir();
308 WriteFile(dir / L"settings.yaml", "session:\n cpuCount: \"not-a-number\"\n");
309
310 UserSettingsTest s{dir};
311
312 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
313 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
314 VERIFY_ARE_EQUAL(
315 Loc::WSLCUserSettings_Warning_InvalidType(L"session.cpuCount", s.SettingsFilePath().wstring(), 2),
316 s.GetWarnings().front().Message);
317 }
318
319 // Absent keys must silently use defaults — no warnings emitted.
320 TEST_METHOD(Validation_AbsentKeys_NoWarningsAndDefaults)
321 {
322 auto dir = UniqueTempDir();
323 WriteFile(dir / L"settings.yaml", "session:\n");
324
325 UserSettingsTest s{dir};
326
327 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
328 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
329 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
330 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
331 }
332
333 // The string "default" for any setting must silently use the built-in
334 // default, same as if the key were absent.
335 TEST_METHOD(Validation_DefaultString_UsesBuiltInDefaultsNoWarnings)
336 {
337 auto dir = UniqueTempDir();
338 WriteFile(
339 dir / L"settings.yaml",
340 "session:\n"
341 " cpuCount: default\n"
342 " memorySize: default\n"
343 " maxStorageSize: default\n"
344 " networkingMode: default\n"
345 " hostFileShareMode: default\n"
346 " dnsTunneling: default\n"
347 " hostLoopback: default\n"
348 "experimental:\n"
349 " portRelay: default\n"
350 "credentialStore: default\n");
351
352 UserSettingsTest s{dir};
353
354 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
355 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
356 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
357 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
358 VERIFY_ARE_EQUAL(1048576u, s.Get<Setting::SessionStorageSizeMb>());
359 VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeConsomme), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
360 VERIFY_ARE_EQUAL(static_cast<int>(HostFileShareMode::VirtioFs), static_cast<int>(s.Get<Setting::SessionHostFileShareMode>()));
361 VERIFY_IS_TRUE(s.Get<Setting::SessionDnsTunneling>());
362 VERIFY_ARE_EQUAL(std::string("host.wslc.internal"), s.Get<Setting::SessionHostLoopback>());
363 VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::VirtioNet), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
364 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
365 }
366
367 // "default" on a single setting uses the built-in default for that setting
368 // while explicit values on other settings are preserved.
369 TEST_METHOD(Validation_DefaultString_MixedWithExplicitValues)
370 {
371 auto dir = UniqueTempDir();
372 WriteFile(
373 dir / L"settings.yaml",
374 "session:\n"
375 " cpuCount: 8\n"
376 " memorySize: default\n"
377 " maxStorageSize: 50000MB\n"
378 "credentialStore: default\n");
379
380 UserSettingsTest s{dir};
381
382 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
383 VERIFY_ARE_EQUAL(8u, s.Get<Setting::SessionCpuCount>());
384 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
385 VERIFY_ARE_EQUAL(50000u, s.Get<Setting::SessionStorageSizeMb>());
386 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
387 }
388
389 // Quoted "default" string must behave the same as unquoted default.
390 TEST_METHOD(Validation_DefaultString_QuotedIsAlsoValid)
391 {
392 auto dir = UniqueTempDir();
393 WriteFile(
394 dir / L"settings.yaml",
395 "session:\n"
396 " cpuCount: \"default\"\n"
397 " memorySize: \"default\"\n");
398
399 UserSettingsTest s{dir};
400
401 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
402 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionCpuCount>());
403 VERIFY_ARE_EQUAL(0u, s.Get<Setting::SessionMemoryMb>());
404 }
405
406 // "Default" (capitalized) is NOT the magic string — it must be treated as
407 // an invalid value and fall back to the built-in default with a warning.
408 TEST_METHOD(Validation_DefaultString_IsCaseSensitive)
409 {
410 auto dir = UniqueTempDir();
411 WriteFile(
412 dir / L"settings.yaml",
413 "session:\n"
414 " networkingMode: Default\n"
415 "credentialStore: DEFAULT\n");
416
417 UserSettingsTest s{dir};
418
419 // Both should be rejected by their validators and produce warnings.
420 VERIFY_ARE_EQUAL(2u, s.GetWarnings().size());
421 VERIFY_ARE_EQUAL(
422 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.networkingMode", s.SettingsFilePath().wstring(), 2),
423 s.GetWarnings()[0].Message);
424 VERIFY_ARE_EQUAL(
425 Loc::WSLCUserSettings_Warning_InvalidValue(L"credentialStore", s.SettingsFilePath().wstring(), 3), s.GetWarnings()[1].Message);
426 // Values still fall back to built-in defaults.
427 VERIFY_ARE_EQUAL(static_cast<int>(WSLCNetworkingModeConsomme), static_cast<int>(s.Get<Setting::SessionNetworkingMode>()));
428 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
429 }
430
431 // credentialStore: invalid value must fall back to default and warn.
432 TEST_METHOD(Validation_CredentialStore_Invalid_UsesDefaultAndWarns)
433 {
434 auto dir = UniqueTempDir();
435 WriteFile(dir / L"settings.yaml", "credentialStore: badvalue\n");
436
437 UserSettingsTest s{dir};
438
439 VERIFY_ARE_EQUAL(static_cast<int>(CredentialStoreType::WinCred), static_cast<int>(s.Get<Setting::CredentialStore>()));
440 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
441 VERIFY_ARE_EQUAL(
442 Loc::WSLCUserSettings_Warning_InvalidValue(L"credentialStore", s.SettingsFilePath().wstring(), 1),
443 s.GetWarnings().front().Message);
444 }
445
446 // -----------------------------------------------------------------------
447 // Unknown key warnings
448 // -----------------------------------------------------------------------
449
450 // Unknown keys in a known section and unknown root sections both produce warnings.
451 TEST_METHOD(Validation_UnknownKeys_WarnsAboutUnknownKeys)
452 {
453 auto dir = UniqueTempDir();
454 WriteFile(
455 dir / L"settings.yaml",
456 "session:\n"
457 " cpuCount: 4\n"
458 " unknownSetting: 99\n"
459 "unknownSection:\n"
460 " foo: bar\n");
461
462 UserSettingsTest s{dir};
463
464 VERIFY_ARE_EQUAL(static_cast<int>(UserSettingsType::Standard), static_cast<int>(s.GetType()));
465 VERIFY_ARE_EQUAL(4u, s.Get<Setting::SessionCpuCount>());
466 VERIFY_ARE_EQUAL(2u, s.GetWarnings().size());
467 // Root-level keys are processed before nested keys due to stack-based traversal.
468 VERIFY_ARE_EQUAL(
469 Loc::WSLCUserSettings_Warning_UnknownSection(L"unknownSection", s.SettingsFilePath().wstring(), 4), s.GetWarnings()[0].Message);
470 VERIFY_ARE_EQUAL(
471 Loc::WSLCUserSettings_Warning_UnknownKey(L"session.unknownSetting", s.SettingsFilePath().wstring(), 3),
472 s.GetWarnings()[1].Message);
473 }
474
475 // An unknown key under a known section produces a warning with the full path.
476 TEST_METHOD(Validation_UnknownKeys_UnknownInKnownSection)
477 {
478 auto dir = UniqueTempDir();
479 WriteFile(
480 dir / L"settings.yaml",
481 "session:\n"
482 " cpuCount: 4\n"
483 " typoSetting: true\n");
484
485 UserSettingsTest s{dir};
486
487 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
488 VERIFY_ARE_EQUAL(
489 Loc::WSLCUserSettings_Warning_UnknownKey(L"session.typoSetting", s.SettingsFilePath().wstring(), 3),
490 s.GetWarnings().front().Message);
491 VERIFY_ARE_EQUAL(std::wstring(L"session.typoSetting"), s.GetWarnings().front().SettingPath);
492 }
493
494 // An unknown root-level section produces a warning.
495 TEST_METHOD(Validation_UnknownKeys_UnknownRootSection)
496 {
497 auto dir = UniqueTempDir();
498 WriteFile(
499 dir / L"settings.yaml",
500 "badSection:\n"
501 " key: value\n");
502
503 UserSettingsTest s{dir};
504
505 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
506 VERIFY_ARE_EQUAL(
507 Loc::WSLCUserSettings_Warning_UnknownSection(L"badSection", s.SettingsFilePath().wstring(), 1),
508 s.GetWarnings().front().Message);
509 VERIFY_ARE_EQUAL(std::wstring(L"badSection"), s.GetWarnings().front().SettingPath);
510 }
511
512 // An unknown root-level scalar key produces a warning.
513 TEST_METHOD(Validation_UnknownKeys_UnknownRootScalar)
514 {
515 auto dir = UniqueTempDir();
516 WriteFile(
517 dir / L"settings.yaml",
518 "session:\n"
519 " cpuCount: 4\n"
520 "badKey: hello\n");
521
522 UserSettingsTest s{dir};
523
524 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
525 VERIFY_ARE_EQUAL(
526 Loc::WSLCUserSettings_Warning_UnknownKey(L"badKey", s.SettingsFilePath().wstring(), 3), s.GetWarnings().front().Message);
527 VERIFY_ARE_EQUAL(std::wstring(L"badKey"), s.GetWarnings().front().SettingPath);
528 }
529
530 // A complex YAML key (sequence) cannot be converted to string;
531 // a non-string key warning is emitted.
532 TEST_METHOD(Validation_UnknownKeys_ComplexKey_WarnsNonStringKey)
533 {
534 auto dir = UniqueTempDir();
535 WriteFile(
536 dir / L"settings.yaml",
537 "session:\n"
538 " cpuCount: 4\n"
539 " [1, 2]: value\n");
540
541 UserSettingsTest s{dir};
542
543 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
544 VERIFY_ARE_EQUAL(
545 Loc::WSLCUserSettings_Warning_NonStringKey(L"session", s.SettingsFilePath().wstring(), 3), s.GetWarnings().front().Message);
546 VERIFY_ARE_EQUAL(std::wstring(L"session"), s.GetWarnings().front().SettingPath);
547 }
548
549 // A complex YAML key (map) at root level warns with "root" location.
550 TEST_METHOD(Validation_UnknownKeys_ComplexKeyAtRoot_WarnsNonStringKey)
551 {
552 auto dir = UniqueTempDir();
553 WriteFile(
554 dir / L"settings.yaml",
555 "{a: b}: value\n"
556 "credentialStore: wincred\n");
557
558 UserSettingsTest s{dir};
559
560 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
561 VERIFY_ARE_EQUAL(
562 Loc::WSLCUserSettings_Warning_NonStringKey(L"root", s.SettingsFilePath().wstring(), 1), s.GetWarnings().front().Message);
563 VERIFY_ARE_EQUAL(std::wstring(L"root"), s.GetWarnings().front().SettingPath);
564 }
565
566 // A file with only valid known keys produces no warnings.
567 TEST_METHOD(Validation_UnknownKeys_AllKnownKeys_NoWarnings)
568 {
569 auto dir = UniqueTempDir();
570 WriteFile(
571 dir / L"settings.yaml",
572 "session:\n"
573 " cpuCount: 8\n"
574 " memorySize: 4GB\n"
575 " maxStorageSize: 50000MB\n"
576 " networkingMode: nat\n"
577 " hostFileShareMode: virtiofs\n"
578 " dnsTunneling: true\n"
579 " storagePath: C:\\wslc-data\n"
580 "experimental:\n"
581 " portRelay: wslrelay\n"
582 "credentialStore: wincred\n");
583
584 UserSettingsTest s{dir};
585
586 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
587 }
588
589 TEST_METHOD(Validation_PortRelay_ExplicitValue)
590 {
591 auto dir = UniqueTempDir();
592 WriteFile(
593 dir / L"settings.yaml",
594 "experimental:\n"
595 " portRelay: wslrelay\n");
596
597 UserSettingsTest s{dir};
598
599 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
600 VERIFY_ARE_EQUAL(static_cast<int>(PortRelayType::WslRelay), static_cast<int>(s.Get<Setting::SessionPortRelay>()));
601 }
602
603 // -----------------------------------------------------------------------
604 // session.hostLoopback
605 // -----------------------------------------------------------------------
606
607 TEST_METHOD(Validation_HostLoopback_CustomName)
608 {
609 auto dir = UniqueTempDir();
610 WriteFile(dir / L"settings.yaml", "session:\n hostLoopback: host.containers.internal\n");
611
612 UserSettingsTest s{dir};
613
614 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
615 VERIFY_ARE_EQUAL(std::string("host.containers.internal"), s.Get<Setting::SessionHostLoopback>());
616 }
617
618 TEST_METHOD(Validation_HostLoopback_NoneDisablesEntry)
619 {
620 auto dir = UniqueTempDir();
621 WriteFile(dir / L"settings.yaml", "session:\n hostLoopback: none\n");
622
623 UserSettingsTest s{dir};
624
625 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
626 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionHostLoopback>());
627 }
628
629 // -----------------------------------------------------------------------
630 // session.defaultBindingAddress
631 // -----------------------------------------------------------------------
632
633 // When unset, the default binding address falls back to the empty built-in default.
634 TEST_METHOD(Validation_DefaultBindingAddress_Absent_UsesEmptyDefault)
635 {
636 auto dir = UniqueTempDir();
637 WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n");
638
639 UserSettingsTest s{dir};
640
641 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
642 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
643 }
644
645 // A valid IPv4 binding address loads without warnings.
646 TEST_METHOD(Validation_DefaultBindingAddress_ValidValue)
647 {
648 auto dir = UniqueTempDir();
649 WriteFile(
650 dir / L"settings.yaml",
651 "session:\n"
652 " defaultBindingAddress: 0.0.0.0\n");
653
654 UserSettingsTest s{dir};
655
656 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
657 VERIFY_ARE_EQUAL(std::string("0.0.0.0"), s.Get<Setting::SessionDefaultBindingAddress>());
658 }
659
660 // "default" magic string uses the built-in (empty) default with no warning.
661 TEST_METHOD(Validation_DefaultBindingAddress_DefaultString)
662 {
663 auto dir = UniqueTempDir();
664 WriteFile(
665 dir / L"settings.yaml",
666 "session:\n"
667 " defaultBindingAddress: default\n");
668
669 UserSettingsTest s{dir};
670
671 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
672 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
673 }
674
675 // An invalid address is rejected: the default is used and an invalid-value warning emitted.
676 TEST_METHOD(Validation_DefaultBindingAddress_InvalidValue_UsesDefaultAndWarns)
677 {
678 auto dir = UniqueTempDir();
679 WriteFile(
680 dir / L"settings.yaml",
681 "session:\n"
682 " defaultBindingAddress: not-an-ip\n");
683
684 UserSettingsTest s{dir};
685
686 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
687 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
688 VERIFY_ARE_EQUAL(
689 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2),
690 s.GetWarnings().front().Message);
691 VERIFY_ARE_EQUAL(std::wstring(L"session.defaultBindingAddress"), s.GetWarnings().front().SettingPath);
692 }
693
694 // An IPv6 literal is rejected (the default binding address is IPv4 only) and warns.
695 TEST_METHOD(Validation_DefaultBindingAddress_IPv6Rejected_UsesDefaultAndWarns)
696 {
697 auto dir = UniqueTempDir();
698 WriteFile(
699 dir / L"settings.yaml",
700 "session:\n"
701 " defaultBindingAddress: \"::1\"\n");
702
703 UserSettingsTest s{dir};
704
705 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionDefaultBindingAddress>());
706 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
707 VERIFY_ARE_EQUAL(
708 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2),
709 s.GetWarnings().front().Message);
710 }
711
712 // -----------------------------------------------------------------------
713 // session.storagePath
714 // -----------------------------------------------------------------------
715
716 // When unset, the storage path falls back to the empty built-in default (caller uses %LOCALAPPDATA%).
717 TEST_METHOD(Validation_StoragePath_Absent_UsesEmptyDefault)
718 {
719 auto dir = UniqueTempDir();
720 WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n");
721
722 UserSettingsTest s{dir};
723
724 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
725 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
726 }
727
728 // A valid absolute path loads without warnings.
729 TEST_METHOD(Validation_StoragePath_AbsoluteValue)
730 {
731 auto dir = UniqueTempDir();
732 WriteFile(
733 dir / L"settings.yaml",
734 "session:\n"
735 " storagePath: D:\\wslc\n");
736
737 UserSettingsTest s{dir};
738
739 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
740 VERIFY_ARE_EQUAL(std::string("D:\\wslc"), s.Get<Setting::SessionStoragePath>());
741 }
742
743 // "default" magic string uses the built-in (empty) default with no warning.
744 TEST_METHOD(Validation_StoragePath_DefaultString)
745 {
746 auto dir = UniqueTempDir();
747 WriteFile(
748 dir / L"settings.yaml",
749 "session:\n"
750 " storagePath: default\n");
751
752 UserSettingsTest s{dir};
753
754 VERIFY_ARE_EQUAL(0u, s.GetWarnings().size());
755 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
756 }
757
758 // A relative path is rejected: the default is used and an invalid-value warning emitted.
759 TEST_METHOD(Validation_StoragePath_RelativeValue_UsesDefaultAndWarns)
760 {
761 auto dir = UniqueTempDir();
762 WriteFile(
763 dir / L"settings.yaml",
764 "session:\n"
765 " storagePath: wslc\\data\n");
766
767 UserSettingsTest s{dir};
768
769 VERIFY_ARE_EQUAL(std::string{}, s.Get<Setting::SessionStoragePath>());
770 VERIFY_ARE_EQUAL(1u, s.GetWarnings().size());
771 VERIFY_ARE_EQUAL(
772 Loc::WSLCUserSettings_Warning_InvalidValue(L"session.storagePath", s.SettingsFilePath().wstring(), 2),
773 s.GetWarnings().front().Message);
774 VERIFY_ARE_EQUAL(std::wstring(L"session.storagePath"), s.GetWarnings().front().SettingPath);
775 }
776 };
777
778 } // namespace WSLCCLISettingsUnitTests