github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/config/config_test.go (about) 1 package config // import "github.com/Prakhar-Agarwal-byte/moby/daemon/config" 2 3 import ( 4 "os" 5 "path/filepath" 6 "reflect" 7 "strings" 8 "testing" 9 10 "github.com/Prakhar-Agarwal-byte/moby/libnetwork/ipamutils" 11 "github.com/Prakhar-Agarwal-byte/moby/opts" 12 "github.com/google/go-cmp/cmp" 13 "github.com/google/go-cmp/cmp/cmpopts" 14 "github.com/imdario/mergo" 15 "github.com/spf13/pflag" 16 "golang.org/x/text/encoding" 17 "golang.org/x/text/encoding/unicode" 18 "gotest.tools/v3/assert" 19 is "gotest.tools/v3/assert/cmp" 20 "gotest.tools/v3/skip" 21 ) 22 23 func makeConfigFile(t *testing.T, content string) string { 24 t.Helper() 25 name := filepath.Join(t.TempDir(), "daemon.json") 26 err := os.WriteFile(name, []byte(content), 0o666) 27 assert.NilError(t, err) 28 return name 29 } 30 31 func TestDaemonConfigurationNotFound(t *testing.T) { 32 _, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker") 33 assert.Check(t, os.IsNotExist(err), "got: %[1]T: %[1]v", err) 34 } 35 36 func TestDaemonBrokenConfiguration(t *testing.T) { 37 configFile := makeConfigFile(t, `{"Debug": tru`) 38 39 _, err := MergeDaemonConfigurations(&Config{}, nil, configFile) 40 assert.ErrorContains(t, err, `invalid character ' ' in literal true`) 41 } 42 43 // TestDaemonConfigurationUnicodeVariations feeds various variations of Unicode into the JSON parser, ensuring that we 44 // respect a BOM and otherwise default to UTF-8. 45 func TestDaemonConfigurationUnicodeVariations(t *testing.T) { 46 jsonData := `{"debug": true}` 47 48 testCases := []struct { 49 name string 50 encoding encoding.Encoding 51 }{ 52 { 53 name: "UTF-8", 54 encoding: unicode.UTF8, 55 }, 56 { 57 name: "UTF-8 (with BOM)", 58 encoding: unicode.UTF8BOM, 59 }, 60 { 61 name: "UTF-16 (BE with BOM)", 62 encoding: unicode.UTF16(unicode.BigEndian, unicode.UseBOM), 63 }, 64 { 65 name: "UTF-16 (LE with BOM)", 66 encoding: unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), 67 }, 68 } 69 for _, tc := range testCases { 70 t.Run(tc.name, func(t *testing.T) { 71 encodedJson, err := tc.encoding.NewEncoder().String(jsonData) 72 assert.NilError(t, err) 73 configFile := makeConfigFile(t, encodedJson) 74 _, err = MergeDaemonConfigurations(&Config{}, nil, configFile) 75 assert.NilError(t, err) 76 }) 77 } 78 } 79 80 // TestDaemonConfigurationInvalidUnicode ensures that the JSON parser returns a useful error message if malformed UTF-8 81 // is provided. 82 func TestDaemonConfigurationInvalidUnicode(t *testing.T) { 83 configFileBOM := makeConfigFile(t, "\xef\xbb\xbf{\"debug\": true}\xff") 84 _, err := MergeDaemonConfigurations(&Config{}, nil, configFileBOM) 85 assert.ErrorIs(t, err, encoding.ErrInvalidUTF8) 86 87 configFileNoBOM := makeConfigFile(t, "{\"debug\": true}\xff") 88 _, err = MergeDaemonConfigurations(&Config{}, nil, configFileNoBOM) 89 assert.ErrorIs(t, err, encoding.ErrInvalidUTF8) 90 } 91 92 func TestFindConfigurationConflicts(t *testing.T) { 93 config := map[string]interface{}{"authorization-plugins": "foobar"} 94 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 95 96 flags.String("authorization-plugins", "", "") 97 assert.Check(t, flags.Set("authorization-plugins", "asdf")) 98 assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "authorization-plugins: (from flag: asdf, from file: foobar)")) 99 } 100 101 func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) { 102 config := map[string]interface{}{"hosts": []string{"qwer"}} 103 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 104 105 var hosts []string 106 flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), "host", "H", "Daemon socket(s) to connect to") 107 assert.Check(t, flags.Set("host", "tcp://127.0.0.1:4444")) 108 assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock")) 109 assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "hosts")) 110 } 111 112 func TestDaemonConfigurationMergeConflicts(t *testing.T) { 113 configFile := makeConfigFile(t, `{"debug": true}`) 114 115 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 116 flags.Bool("debug", false, "") 117 assert.Check(t, flags.Set("debug", "false")) 118 119 _, err := MergeDaemonConfigurations(&Config{}, flags, configFile) 120 if err == nil { 121 t.Fatal("expected error, got nil") 122 } 123 if !strings.Contains(err.Error(), "debug") { 124 t.Fatalf("expected debug conflict, got %v", err) 125 } 126 } 127 128 func TestDaemonConfigurationMergeConcurrent(t *testing.T) { 129 configFile := makeConfigFile(t, `{"max-concurrent-downloads": 1}`) 130 131 _, err := MergeDaemonConfigurations(&Config{}, nil, configFile) 132 assert.NilError(t, err) 133 } 134 135 func TestDaemonConfigurationMergeConcurrentError(t *testing.T) { 136 configFile := makeConfigFile(t, `{"max-concurrent-downloads": -1}`) 137 138 _, err := MergeDaemonConfigurations(&Config{}, nil, configFile) 139 assert.ErrorContains(t, err, `invalid max concurrent downloads: -1`) 140 } 141 142 func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) { 143 configFile := makeConfigFile(t, `{"tlscacert": "/etc/certificates/ca.pem"}`) 144 145 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 146 flags.String("tlscacert", "", "") 147 assert.Check(t, flags.Set("tlscacert", "~/.docker/ca.pem")) 148 149 _, err := MergeDaemonConfigurations(&Config{}, flags, configFile) 150 assert.ErrorContains(t, err, `the following directives are specified both as a flag and in the configuration file: tlscacert`) 151 } 152 153 // TestDaemonConfigurationMergeDefaultAddressPools is a regression test for #40711. 154 func TestDaemonConfigurationMergeDefaultAddressPools(t *testing.T) { 155 emptyConfigFile := makeConfigFile(t, `{}`) 156 configFile := makeConfigFile(t, `{"default-address-pools":[{"base": "10.123.0.0/16", "size": 24 }]}`) 157 158 expected := []*ipamutils.NetworkToSplit{{Base: "10.123.0.0/16", Size: 24}} 159 160 t.Run("empty config file", func(t *testing.T) { 161 conf := Config{} 162 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 163 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 164 assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24")) 165 166 config, err := MergeDaemonConfigurations(&conf, flags, emptyConfigFile) 167 assert.NilError(t, err) 168 assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected) 169 }) 170 171 t.Run("config file", func(t *testing.T) { 172 conf := Config{} 173 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 174 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 175 176 config, err := MergeDaemonConfigurations(&conf, flags, configFile) 177 assert.NilError(t, err) 178 assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected) 179 }) 180 181 t.Run("with conflicting options", func(t *testing.T) { 182 conf := Config{} 183 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 184 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 185 assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24")) 186 187 _, err := MergeDaemonConfigurations(&conf, flags, configFile) 188 assert.ErrorContains(t, err, "the following directives are specified both as a flag and in the configuration file") 189 assert.ErrorContains(t, err, "default-address-pools") 190 }) 191 } 192 193 func TestFindConfigurationConflictsWithUnknownKeys(t *testing.T) { 194 config := map[string]interface{}{"tls-verify": "true"} 195 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 196 197 flags.Bool("tlsverify", false, "") 198 err := findConfigurationConflicts(config, flags) 199 assert.ErrorContains(t, err, "the following directives don't match any configuration option: tls-verify") 200 } 201 202 func TestFindConfigurationConflictsWithMergedValues(t *testing.T) { 203 var hosts []string 204 config := map[string]interface{}{"hosts": "tcp://127.0.0.1:2345"} 205 flags := pflag.NewFlagSet("base", pflag.ContinueOnError) 206 flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, nil), "host", "H", "") 207 208 err := findConfigurationConflicts(config, flags) 209 assert.NilError(t, err) 210 211 assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock")) 212 err = findConfigurationConflicts(config, flags) 213 assert.ErrorContains(t, err, "hosts: (from flag: [unix:///var/run/docker.sock], from file: tcp://127.0.0.1:2345)") 214 } 215 216 func TestValidateConfigurationErrors(t *testing.T) { 217 testCases := []struct { 218 name string 219 field string 220 config *Config 221 expectedErr string 222 }{ 223 { 224 name: "single label without value", 225 config: &Config{ 226 CommonConfig: CommonConfig{ 227 Labels: []string{"one"}, 228 }, 229 }, 230 expectedErr: "bad attribute format: one", 231 }, 232 { 233 name: "multiple label without value", 234 config: &Config{ 235 CommonConfig: CommonConfig{ 236 Labels: []string{"foo=bar", "one"}, 237 }, 238 }, 239 expectedErr: "bad attribute format: one", 240 }, 241 { 242 name: "single DNS, invalid IP-address", 243 config: &Config{ 244 CommonConfig: CommonConfig{ 245 DNSConfig: DNSConfig{ 246 DNS: []string{"1.1.1.1o"}, 247 }, 248 }, 249 }, 250 expectedErr: "1.1.1.1o is not an ip address", 251 }, 252 { 253 name: "multiple DNS, invalid IP-address", 254 config: &Config{ 255 CommonConfig: CommonConfig{ 256 DNSConfig: DNSConfig{ 257 DNS: []string{"2.2.2.2", "1.1.1.1o"}, 258 }, 259 }, 260 }, 261 expectedErr: "1.1.1.1o is not an ip address", 262 }, 263 { 264 name: "single DNSSearch", 265 config: &Config{ 266 CommonConfig: CommonConfig{ 267 DNSConfig: DNSConfig{ 268 DNSSearch: []string{"123456"}, 269 }, 270 }, 271 }, 272 expectedErr: "123456 is not a valid domain", 273 }, 274 { 275 name: "multiple DNSSearch", 276 config: &Config{ 277 CommonConfig: CommonConfig{ 278 DNSConfig: DNSConfig{ 279 DNSSearch: []string{"a.b.c", "123456"}, 280 }, 281 }, 282 }, 283 expectedErr: "123456 is not a valid domain", 284 }, 285 { 286 name: "negative MTU", 287 config: &Config{ 288 CommonConfig: CommonConfig{ 289 BridgeConfig: BridgeConfig{MTU: -10}, 290 }, 291 }, 292 expectedErr: "invalid default MTU: -10", 293 }, 294 { 295 name: "negative max-concurrent-downloads", 296 config: &Config{ 297 CommonConfig: CommonConfig{ 298 MaxConcurrentDownloads: -10, 299 }, 300 }, 301 expectedErr: "invalid max concurrent downloads: -10", 302 }, 303 { 304 name: "negative max-concurrent-uploads", 305 config: &Config{ 306 CommonConfig: CommonConfig{ 307 MaxConcurrentUploads: -10, 308 }, 309 }, 310 expectedErr: "invalid max concurrent uploads: -10", 311 }, 312 { 313 name: "negative max-download-attempts", 314 config: &Config{ 315 CommonConfig: CommonConfig{ 316 MaxDownloadAttempts: -10, 317 }, 318 }, 319 expectedErr: "invalid max download attempts: -10", 320 }, 321 // TODO(thaJeztah) temporarily excluding this test as it assumes defaults are set before validating and applying updated configs 322 /* 323 { 324 name: "zero max-download-attempts", 325 field: "MaxDownloadAttempts", 326 config: &Config{ 327 CommonConfig: CommonConfig{ 328 MaxDownloadAttempts: 0, 329 }, 330 }, 331 expectedErr: "invalid max download attempts: 0", 332 }, 333 */ 334 { 335 name: "generic resource without =", 336 config: &Config{ 337 CommonConfig: CommonConfig{ 338 NodeGenericResources: []string{"foo"}, 339 }, 340 }, 341 expectedErr: "could not parse GenericResource: incorrect term foo, missing '=' or malformed expression", 342 }, 343 { 344 name: "generic resource mixed named and discrete", 345 config: &Config{ 346 CommonConfig: CommonConfig{ 347 NodeGenericResources: []string{"foo=bar", "foo=1"}, 348 }, 349 }, 350 expectedErr: "could not parse GenericResource: mixed discrete and named resources in expression 'foo=[bar 1]'", 351 }, 352 { 353 name: "with invalid hosts", 354 config: &Config{ 355 CommonConfig: CommonConfig{ 356 Hosts: []string{"127.0.0.1:2375/path"}, 357 }, 358 }, 359 expectedErr: "invalid bind address (127.0.0.1:2375/path): should not contain a path element", 360 }, 361 { 362 name: "with invalid log-level", 363 config: &Config{ 364 CommonConfig: CommonConfig{ 365 LogLevel: "foobar", 366 }, 367 }, 368 expectedErr: "invalid logging level: foobar", 369 }, 370 } 371 for _, tc := range testCases { 372 t.Run(tc.name, func(t *testing.T) { 373 cfg, err := New() 374 assert.NilError(t, err) 375 if tc.field != "" { 376 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride, withForceOverwrite(tc.field))) 377 } else { 378 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride)) 379 } 380 err = Validate(cfg) 381 assert.Error(t, err, tc.expectedErr) 382 }) 383 } 384 } 385 386 func withForceOverwrite(fieldName string) func(config *mergo.Config) { 387 return mergo.WithTransformers(overwriteTransformer{fieldName: fieldName}) 388 } 389 390 type overwriteTransformer struct { 391 fieldName string 392 } 393 394 func (tf overwriteTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { 395 if typ == reflect.TypeOf(CommonConfig{}) { 396 return func(dst, src reflect.Value) error { 397 dst.FieldByName(tf.fieldName).Set(src.FieldByName(tf.fieldName)) 398 return nil 399 } 400 } 401 return nil 402 } 403 404 func TestValidateConfiguration(t *testing.T) { 405 testCases := []struct { 406 name string 407 field string 408 config *Config 409 }{ 410 { 411 name: "with label", 412 field: "Labels", 413 config: &Config{ 414 CommonConfig: CommonConfig{ 415 Labels: []string{"one=two"}, 416 }, 417 }, 418 }, 419 { 420 name: "with dns", 421 field: "DNSConfig", 422 config: &Config{ 423 CommonConfig: CommonConfig{ 424 DNSConfig: DNSConfig{ 425 DNS: []string{"1.1.1.1"}, 426 }, 427 }, 428 }, 429 }, 430 { 431 name: "with dns-search", 432 field: "DNSConfig", 433 config: &Config{ 434 CommonConfig: CommonConfig{ 435 DNSConfig: DNSConfig{ 436 DNSSearch: []string{"a.b.c"}, 437 }, 438 }, 439 }, 440 }, 441 { 442 name: "with mtu", 443 field: "MTU", 444 config: &Config{ 445 CommonConfig: CommonConfig{ 446 BridgeConfig: BridgeConfig{MTU: 1234}, 447 }, 448 }, 449 }, 450 { 451 name: "with max-concurrent-downloads", 452 field: "MaxConcurrentDownloads", 453 config: &Config{ 454 CommonConfig: CommonConfig{ 455 MaxConcurrentDownloads: 4, 456 }, 457 }, 458 }, 459 { 460 name: "with max-concurrent-uploads", 461 field: "MaxConcurrentUploads", 462 config: &Config{ 463 CommonConfig: CommonConfig{ 464 MaxConcurrentUploads: 4, 465 }, 466 }, 467 }, 468 { 469 name: "with max-download-attempts", 470 field: "MaxDownloadAttempts", 471 config: &Config{ 472 CommonConfig: CommonConfig{ 473 MaxDownloadAttempts: 4, 474 }, 475 }, 476 }, 477 { 478 name: "with multiple node generic resources", 479 field: "NodeGenericResources", 480 config: &Config{ 481 CommonConfig: CommonConfig{ 482 NodeGenericResources: []string{"foo=bar", "foo=baz"}, 483 }, 484 }, 485 }, 486 { 487 name: "with node generic resources", 488 field: "NodeGenericResources", 489 config: &Config{ 490 CommonConfig: CommonConfig{ 491 NodeGenericResources: []string{"foo=1"}, 492 }, 493 }, 494 }, 495 { 496 name: "with hosts", 497 field: "Hosts", 498 config: &Config{ 499 CommonConfig: CommonConfig{ 500 Hosts: []string{"tcp://127.0.0.1:2375"}, 501 }, 502 }, 503 }, 504 { 505 name: "with log-level warn", 506 field: "LogLevel", 507 config: &Config{ 508 CommonConfig: CommonConfig{ 509 LogLevel: "warn", 510 }, 511 }, 512 }, 513 } 514 for _, tc := range testCases { 515 t.Run(tc.name, func(t *testing.T) { 516 // Start with a config with all defaults set, so that we only 517 cfg, err := New() 518 assert.NilError(t, err) 519 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride)) 520 521 // Check that the override happened :) 522 assert.Check(t, is.DeepEqual(cfg, tc.config, field(tc.field))) 523 err = Validate(cfg) 524 assert.NilError(t, err) 525 }) 526 } 527 } 528 529 func field(field string) cmp.Option { 530 tmp := reflect.TypeOf(Config{}) 531 ignoreFields := make([]string, 0, tmp.NumField()) 532 for i := 0; i < tmp.NumField(); i++ { 533 if tmp.Field(i).Name != field { 534 ignoreFields = append(ignoreFields, tmp.Field(i).Name) 535 } 536 } 537 return cmpopts.IgnoreFields(Config{}, ignoreFields...) 538 } 539 540 // TestReloadSetConfigFileNotExist tests that when `--config-file` is set, and it doesn't exist the `Reload` function 541 // returns an error. 542 func TestReloadSetConfigFileNotExist(t *testing.T) { 543 configFile := "/tmp/blabla/not/exists/config.json" 544 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 545 flags.String("config-file", "", "") 546 assert.Check(t, flags.Set("config-file", configFile)) 547 548 err := Reload(configFile, flags, func(c *Config) {}) 549 assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file")) 550 } 551 552 // TestReloadDefaultConfigNotExist tests that if the default configuration file doesn't exist the daemon still will 553 // still be reloaded. 554 func TestReloadDefaultConfigNotExist(t *testing.T) { 555 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 556 defaultConfigFile := "/tmp/blabla/not/exists/daemon.json" 557 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 558 flags.String("config-file", defaultConfigFile, "") 559 reloaded := false 560 err := Reload(defaultConfigFile, flags, func(c *Config) { 561 reloaded = true 562 }) 563 assert.Check(t, err) 564 assert.Check(t, reloaded) 565 } 566 567 // TestReloadBadDefaultConfig tests that when `--config-file` is not set and the default configuration file exists and 568 // is bad, an error is returned. 569 func TestReloadBadDefaultConfig(t *testing.T) { 570 configFile := makeConfigFile(t, `{wrong: "configuration"}`) 571 572 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 573 flags.String("config-file", configFile, "") 574 reloaded := false 575 err := Reload(configFile, flags, func(c *Config) { 576 reloaded = true 577 }) 578 assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file")) 579 assert.Check(t, reloaded == false) 580 } 581 582 func TestReloadWithConflictingLabels(t *testing.T) { 583 configFile := makeConfigFile(t, `{"labels": ["foo=bar", "foo=baz"]}`) 584 585 var lbls []string 586 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 587 flags.String("config-file", configFile, "") 588 flags.StringSlice("labels", lbls, "") 589 reloaded := false 590 err := Reload(configFile, flags, func(c *Config) { 591 reloaded = true 592 }) 593 assert.Check(t, is.ErrorContains(err, "conflict labels for foo=baz and foo=bar")) 594 assert.Check(t, reloaded == false) 595 } 596 597 func TestReloadWithDuplicateLabels(t *testing.T) { 598 configFile := makeConfigFile(t, `{"labels": ["foo=the-same", "foo=the-same"]}`) 599 600 var lbls []string 601 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 602 flags.String("config-file", configFile, "") 603 flags.StringSlice("labels", lbls, "") 604 reloaded := false 605 err := Reload(configFile, flags, func(c *Config) { 606 reloaded = true 607 assert.Check(t, is.DeepEqual(c.Labels, []string{"foo=the-same"})) 608 }) 609 assert.Check(t, err) 610 assert.Check(t, reloaded) 611 } 612 613 func TestMaskURLCredentials(t *testing.T) { 614 tests := []struct { 615 rawURL string 616 maskedURL string 617 }{ 618 { 619 rawURL: "", 620 maskedURL: "", 621 }, { 622 rawURL: "invalidURL", 623 maskedURL: "invalidURL", 624 }, { 625 rawURL: "http://proxy.example.com:80/", 626 maskedURL: "http://proxy.example.com:80/", 627 }, { 628 rawURL: "http://USER:PASSWORD@proxy.example.com:80/", 629 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 630 }, { 631 rawURL: "http://PASSWORD:PASSWORD@proxy.example.com:80/", 632 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 633 }, { 634 rawURL: "http://USER:@proxy.example.com:80/", 635 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 636 }, { 637 rawURL: "http://:PASSWORD@proxy.example.com:80/", 638 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 639 }, { 640 rawURL: "http://USER@docker:password@proxy.example.com:80/", 641 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 642 }, { 643 rawURL: "http://USER%40docker:password@proxy.example.com:80/", 644 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 645 }, { 646 rawURL: "http://USER%40docker:pa%3Fsword@proxy.example.com:80/", 647 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 648 }, { 649 rawURL: "http://USER%40docker:pa%3Fsword@proxy.example.com:80/hello%20world", 650 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/hello%20world", 651 }, 652 } 653 for _, test := range tests { 654 maskedURL := MaskCredentials(test.rawURL) 655 assert.Equal(t, maskedURL, test.maskedURL) 656 } 657 }