| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCE2ENetworkListTests.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains end-to-end tests for WSLC. |
| 12 | --*/ |
| 13 | |
| 14 | #include "precomp.h" |
| 15 | #include "windows/Common.h" |
| 16 | #include "WSLCExecutor.h" |
| 17 | #include "WSLCE2EHelpers.h" |
| 18 | |
| 19 | namespace WSLCE2ETests { |
| 20 | using namespace wsl::shared; |
| 21 | using namespace wsl::shared::string; |
| 22 | |
| 23 | class WSLCE2ENetworkListTests |
| 24 | { |
| 25 | WSLC_TEST_CLASS(WSLCE2ENetworkListTests) |
| 26 | |
| 27 | TEST_METHOD_SETUP(MethodSetup) |
| 28 | { |
| 29 | EnsureNetworkDoesNotExist(TestNetworkName); |
| 30 | EnsureNetworkDoesNotExist(TestNetworkName2); |
| 31 | for (const auto& name : FilterTestNetworkNames) |
| 32 | { |
| 33 | EnsureNetworkDoesNotExist(name); |
| 34 | } |
| 35 | return true; |
| 36 | } |
| 37 | |
| 38 | TEST_CLASS_CLEANUP(ClassCleanup) |
| 39 | { |
| 40 | EnsureNetworkDoesNotExist(TestNetworkName); |
| 41 | EnsureNetworkDoesNotExist(TestNetworkName2); |
| 42 | for (const auto& name : FilterTestNetworkNames) |
| 43 | { |
| 44 | EnsureNetworkDoesNotExist(name); |
| 45 | } |
| 46 | return true; |
| 47 | } |
| 48 | |
| 49 | WSLC_TEST_METHOD(WSLCE2E_Network_List_HelpCommand) |
| 50 | { |
| 51 | auto result = RunWslc(L"network list --help"); |
| 52 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 53 | VERIFY_IS_FALSE(result.Stdout.value().empty()); |
| 54 | } |
| 55 | |
| 56 | WSLC_TEST_METHOD(WSLCE2E_Network_List_InvalidFormatOption) |
| 57 | { |
| 58 | auto result = RunWslc(L"network list --format invalid"); |
| 59 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 60 | VERIFY_IS_TRUE(result.StderrContainsSubstring( |
| 61 | L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table.")); |
| 62 | } |
| 63 | |
| 64 | WSLC_TEST_METHOD(WSLCE2E_Network_List_QuietOption_OutputsIdsOnly) |
| 65 | { |
| 66 | auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); |
| 67 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 68 | result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); |
| 69 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 70 | |
| 71 | result = RunWslc(L"network list --format json"); |
| 72 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 73 | |
| 74 | std::vector<std::wstring> expectedIds; |
| 75 | for (const auto& network : ParseNdjsonOutput(result)) |
| 76 | { |
| 77 | expectedIds.push_back(MultiByteToWide(network.at("ID").get<std::string>())); |
| 78 | } |
| 79 | |
| 80 | result = RunWslc(L"network list --quiet"); |
| 81 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 82 | |
| 83 | auto lines = result.GetStdoutLines(); |
| 84 | VERIFY_ARE_EQUAL(expectedIds.size(), lines.size()); |
| 85 | for (const auto& id : expectedIds) |
| 86 | { |
| 87 | VerifyIdOutput(id, true); |
| 88 | VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), id)); |
| 89 | } |
| 90 | |
| 91 | VERIFY_ARE_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestNetworkName)); |
| 92 | VERIFY_ARE_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), TestNetworkName2)); |
| 93 | |
| 94 | result = RunWslc(L"network list --format json --no-trunc"); |
| 95 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 96 | |
| 97 | expectedIds.clear(); |
| 98 | for (const auto& network : ParseNdjsonOutput(result)) |
| 99 | { |
| 100 | expectedIds.push_back(MultiByteToWide(network.at("ID").get<std::string>())); |
| 101 | } |
| 102 | |
| 103 | result = RunWslc(L"network list --quiet --no-trunc"); |
| 104 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 105 | |
| 106 | lines = result.GetStdoutLines(); |
| 107 | VERIFY_ARE_EQUAL(expectedIds.size(), lines.size()); |
| 108 | for (const auto& id : expectedIds) |
| 109 | { |
| 110 | VerifyIdOutput(id, false); |
| 111 | VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), id)); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | WSLC_TEST_METHOD(WSLCE2E_Network_List_JsonFormat) |
| 116 | { |
| 117 | auto result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName)); |
| 118 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 119 | result = RunWslc(std::format(L"network create --driver bridge {}", TestNetworkName2)); |
| 120 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 121 | |
| 122 | result = RunWslc(L"network list --format json"); |
| 123 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 124 | auto networks = ParseNdjsonOutput(result); |
| 125 | |
| 126 | std::vector<std::string> names; |
| 127 | names.reserve(networks.size()); |
| 128 | for (const auto& network : networks) |
| 129 | { |
| 130 | VERIFY_ARE_EQUAL(9u, network.size()); |
| 131 | VERIFY_IS_TRUE(network.contains("ID")); |
| 132 | VERIFY_IS_FALSE(network.contains("Id")); |
| 133 | VERIFY_IS_TRUE(network.contains("Name")); |
| 134 | VERIFY_IS_TRUE(network.contains("Driver")); |
| 135 | VERIFY_IS_TRUE(network.contains("Scope")); |
| 136 | VERIFY_IS_TRUE(network.contains("CreatedAt")); |
| 137 | VERIFY_IS_TRUE(network.contains("IPv4")); |
| 138 | VERIFY_IS_TRUE(network.contains("IPv6")); |
| 139 | VERIFY_IS_TRUE(network.contains("Internal")); |
| 140 | VERIFY_IS_TRUE(network.contains("Labels")); |
| 141 | VERIFY_IS_TRUE(network.at("CreatedAt").get<std::string>().ends_with(" +0000 UTC")); |
| 142 | VerifyIdOutput(MultiByteToWide(network.at("ID").get<std::string>()), true); |
| 143 | names.push_back(network.at("Name").get<std::string>()); |
| 144 | } |
| 145 | |
| 146 | VERIFY_ARE_EQUAL(1u, static_cast<size_t>(std::count(names.begin(), names.end(), WideToMultiByte(TestNetworkName)))); |
| 147 | VERIFY_ARE_EQUAL(1u, static_cast<size_t>(std::count(names.begin(), names.end(), WideToMultiByte(TestNetworkName2)))); |
| 148 | |
| 149 | auto quietResult = RunWslc(L"network list --format json --quiet"); |
| 150 | quietResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 151 | VERIFY_ARE_EQUAL(result.Stdout.value(), quietResult.Stdout.value()); |
| 152 | |
| 153 | auto fullResult = RunWslc(L"network list --format json --no-trunc"); |
| 154 | fullResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 155 | for (const auto& network : ParseNdjsonOutput(fullResult)) |
| 156 | { |
| 157 | VerifyIdOutput(MultiByteToWide(network.at("ID").get<std::string>()), false); |
| 158 | } |
| 159 | |
| 160 | auto fullQuietResult = RunWslc(L"network list --format json --quiet --no-trunc"); |
| 161 | fullQuietResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 162 | VERIFY_ARE_EQUAL(fullResult.Stdout.value(), fullQuietResult.Stdout.value()); |
| 163 | |
| 164 | auto fullTableResult = RunWslc(L"network list --no-trunc"); |
| 165 | fullTableResult.Verify({.Stderr = L"", .ExitCode = 0}); |
| 166 | for (const auto& network : ParseNdjsonOutput(fullResult)) |
| 167 | { |
| 168 | const auto id = MultiByteToWide(network.at("ID").get<std::string>()); |
| 169 | VERIFY_IS_TRUE(fullTableResult.StdoutContainsSubstring(id)); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | WSLC_TEST_METHOD(WSLCE2E_Network_List_IncludesPredefinedNetworks) |
| 174 | { |
| 175 | // The predefined networks are not created by wslc but are still reported by list. |
| 176 | auto result = RunWslc(L"network list --format json"); |
| 177 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 178 | |
| 179 | std::set<std::string> names; |
| 180 | for (const auto& network : ParseNdjsonOutputAs<NetworkListOutput>(result)) |
| 181 | { |
| 182 | names.insert(network.Name); |
| 183 | } |
| 184 | |
| 185 | VERIFY_IS_TRUE(names.contains("bridge")); |
| 186 | VERIFY_IS_TRUE(names.contains("host")); |
| 187 | VERIFY_IS_TRUE(names.contains("none")); |
| 188 | |
| 189 | result = RunWslc(L"network list"); |
| 190 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 191 | VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"NETWORK ID")); |
| 192 | VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"SCOPE")); |
| 193 | } |
| 194 | |
| 195 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_MalformedValue) |
| 196 | { |
| 197 | // Filter values must be of the form key=value; bare keys are rejected by the CLI. |
| 198 | const auto result = RunWslc(L"network list --filter label"); |
| 199 | result.Verify({.Stdout = L"", .ExitCode = 1}); |
| 200 | VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_InvalidFilterError(L"label"))); |
| 201 | } |
| 202 | |
| 203 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_InvalidKey) |
| 204 | { |
| 205 | // Unknown filter keys are rejected by the Docker daemon. |
| 206 | const auto result = RunWslc(L"network list --filter color=blue"); |
| 207 | VERIFY_ARE_EQUAL(1, result.ExitCode); |
| 208 | VERIFY_IS_TRUE(result.Stderr.has_value()); |
| 209 | VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter")); |
| 210 | } |
| 211 | |
| 212 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Driver) |
| 213 | { |
| 214 | const std::wstring alpha = L"wslc-flt-list-driver-alpha"; |
| 215 | const std::wstring beta = L"wslc-flt-list-driver-beta"; |
| 216 | auto cleanup = wil::scope_exit([&]() { |
| 217 | EnsureNetworkDoesNotExist(alpha); |
| 218 | EnsureNetworkDoesNotExist(beta); |
| 219 | }); |
| 220 | |
| 221 | auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); |
| 222 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 223 | result = RunWslc(std::format(L"network create --driver bridge {}", beta)); |
| 224 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 225 | |
| 226 | auto listNames = [&](const std::wstring& filterArgs) { |
| 227 | auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); |
| 228 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 229 | const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r); |
| 230 | std::set<std::string> names; |
| 231 | for (const auto& n : networks) |
| 232 | { |
| 233 | names.insert(n.Name); |
| 234 | } |
| 235 | return names; |
| 236 | }; |
| 237 | |
| 238 | { |
| 239 | const auto names = listNames(L"--filter driver=bridge"); |
| 240 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 241 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); |
| 242 | } |
| 243 | |
| 244 | // overlay networks require swarm mode; none exist in the test session. |
| 245 | { |
| 246 | const auto names = listNames(L"--filter driver=overlay"); |
| 247 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(alpha))); |
| 248 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Label) |
| 253 | { |
| 254 | const std::wstring alpha = L"wslc-flt-list-label-alpha"; |
| 255 | const std::wstring beta = L"wslc-flt-list-label-beta"; |
| 256 | const std::wstring scopeKey = L"wslc.e2e.list_filter_label"; |
| 257 | const std::wstring scopeValue = L"1"; |
| 258 | |
| 259 | auto cleanup = wil::scope_exit([&]() { |
| 260 | EnsureNetworkDoesNotExist(alpha); |
| 261 | EnsureNetworkDoesNotExist(beta); |
| 262 | }); |
| 263 | |
| 264 | // alpha carries both scope-key=1 and env=prod; beta carries only scope-key=1. |
| 265 | auto result = |
| 266 | RunWslc(std::format(L"network create --driver bridge --label {}={} --label env=prod {}", scopeKey, scopeValue, alpha)); |
| 267 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 268 | |
| 269 | result = RunWslc(std::format(L"network create --driver bridge --label {}={} {}", scopeKey, scopeValue, beta)); |
| 270 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 271 | |
| 272 | auto listNames = [&](const std::wstring& filterArgs) { |
| 273 | auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); |
| 274 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 275 | const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r); |
| 276 | std::set<std::string> names; |
| 277 | for (const auto& n : networks) |
| 278 | { |
| 279 | names.insert(n.Name); |
| 280 | } |
| 281 | return names; |
| 282 | }; |
| 283 | |
| 284 | // label=<key> (key-only) matches any value. |
| 285 | { |
| 286 | const auto names = listNames(std::format(L"--filter label={}", scopeKey)); |
| 287 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 288 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); |
| 289 | } |
| 290 | |
| 291 | // label=<key>=<value> narrows to alpha. |
| 292 | { |
| 293 | const auto names = listNames(std::format(L"--filter label={}={} --filter label=env=prod", scopeKey, scopeValue)); |
| 294 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 295 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); |
| 296 | } |
| 297 | |
| 298 | // Multiple --filter label= entries are AND'd. |
| 299 | { |
| 300 | const auto names = listNames(std::format(L"--filter label={} --filter label=env=prod", scopeKey)); |
| 301 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 302 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_JsonEmptyIsExactlyEmpty) |
| 307 | { |
| 308 | const std::wstring alpha = L"wslc-flt-list-empty-alpha"; |
| 309 | auto cleanup = wil::scope_exit([&]() { EnsureNetworkDoesNotExist(alpha); }); |
| 310 | |
| 311 | auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); |
| 312 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 313 | |
| 314 | // NDJSON with zero rows must be exactly empty stdout — not "[]", not "\n". |
| 315 | result = RunWslc(L"network list --format json --filter name=wslc-flt-list-no-such-network-zzz"); |
| 316 | result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| 317 | } |
| 318 | |
| 319 | WSLC_TEST_METHOD(WSLCE2E_Network_List_Filter_Name) |
| 320 | { |
| 321 | const std::wstring alpha = L"wslc-flt-list-name-alpha"; |
| 322 | const std::wstring beta = L"wslc-flt-list-name-beta"; |
| 323 | auto cleanup = wil::scope_exit([&]() { |
| 324 | EnsureNetworkDoesNotExist(alpha); |
| 325 | EnsureNetworkDoesNotExist(beta); |
| 326 | }); |
| 327 | |
| 328 | auto result = RunWslc(std::format(L"network create --driver bridge {}", alpha)); |
| 329 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 330 | result = RunWslc(std::format(L"network create --driver bridge {}", beta)); |
| 331 | result.Verify({.Stderr = L"", .ExitCode = 0}); |
| 332 | |
| 333 | auto listNames = [&](const std::wstring& filterArgs) { |
| 334 | auto r = RunWslc(std::format(L"network list --format json {}", filterArgs)); |
| 335 | r.Verify({.Stderr = L"", .ExitCode = 0}); |
| 336 | const auto networks = ParseNdjsonOutputAs<NetworkListOutput>(r); |
| 337 | std::set<std::string> names; |
| 338 | for (const auto& n : networks) |
| 339 | { |
| 340 | names.insert(n.Name); |
| 341 | } |
| 342 | return names; |
| 343 | }; |
| 344 | |
| 345 | // Docker's `name` filter is a substring match; the shared prefix picks up both networks. |
| 346 | { |
| 347 | const auto names = listNames(L"--filter name=wslc-flt-list-name-"); |
| 348 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 349 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(beta))); |
| 350 | } |
| 351 | |
| 352 | // A narrower substring selects only the matching one. |
| 353 | { |
| 354 | const auto names = listNames(L"--filter name=name-alpha"); |
| 355 | VERIFY_IS_TRUE(names.contains(WideToMultiByte(alpha))); |
| 356 | VERIFY_IS_FALSE(names.contains(WideToMultiByte(beta))); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | private: |
| 361 | const std::wstring TestNetworkName = L"wslc-e2e-network-list"; |
| 362 | const std::wstring TestNetworkName2 = L"wslc-e2e-network-list-2"; |
| 363 | const std::vector<std::wstring> FilterTestNetworkNames = { |
| 364 | L"wslc-flt-list-driver-alpha", |
| 365 | L"wslc-flt-list-driver-beta", |
| 366 | L"wslc-flt-list-label-alpha", |
| 367 | L"wslc-flt-list-label-beta", |
| 368 | L"wslc-flt-list-empty-alpha", |
| 369 | L"wslc-flt-list-name-alpha", |
| 370 | L"wslc-flt-list-name-beta", |
| 371 | }; |
| 372 | }; |
| 373 | } // namespace WSLCE2ETests |