sigs.k8s.io/external-dns@v0.14.1/endpoint/domain_filter_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package endpoint 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "regexp" 23 "testing" 24 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 ) 28 29 type domainFilterTest struct { 30 domainFilter []string 31 exclusions []string 32 domains []string 33 expected bool 34 expectedSerialization map[string][]string 35 } 36 37 type regexDomainFilterTest struct { 38 regex *regexp.Regexp 39 regexExclusion *regexp.Regexp 40 domains []string 41 expected bool 42 expectedSerialization map[string]string 43 } 44 45 var domainFilterTests = []domainFilterTest{ 46 { 47 []string{"google.com.", "exaring.de", "inovex.de"}, 48 []string{}, 49 []string{"google.com", "exaring.de", "inovex.de"}, 50 true, 51 map[string][]string{ 52 "include": {"exaring.de", "google.com", "inovex.de"}, 53 }, 54 }, 55 { 56 []string{"google.com.", "exaring.de", "inovex.de"}, 57 []string{}, 58 []string{"google.com", "exaring.de", "inovex.de"}, 59 true, 60 map[string][]string{ 61 "include": {"exaring.de", "google.com", "inovex.de"}, 62 }, 63 }, 64 { 65 []string{"google.com.", "exaring.de.", "inovex.de"}, 66 []string{}, 67 []string{"google.com", "exaring.de", "inovex.de"}, 68 true, 69 map[string][]string{ 70 "include": {"exaring.de", "google.com", "inovex.de"}, 71 }, 72 }, 73 { 74 []string{"foo.org. "}, 75 []string{}, 76 []string{"foo.org"}, 77 true, 78 map[string][]string{ 79 "include": {"foo.org"}, 80 }, 81 }, 82 { 83 []string{" foo.org"}, 84 []string{}, 85 []string{"foo.org"}, 86 true, 87 map[string][]string{ 88 "include": {"foo.org"}, 89 }, 90 }, 91 { 92 []string{"foo.org."}, 93 []string{}, 94 []string{"foo.org"}, 95 true, 96 map[string][]string{ 97 "include": {"foo.org"}, 98 }, 99 }, 100 { 101 []string{"foo.org."}, 102 []string{}, 103 []string{"baz.org"}, 104 false, 105 map[string][]string{ 106 "include": {"foo.org"}, 107 }, 108 }, 109 { 110 []string{"baz.foo.org."}, 111 []string{}, 112 []string{"foo.org"}, 113 false, 114 map[string][]string{ 115 "include": {"baz.foo.org"}, 116 }, 117 }, 118 { 119 []string{"", "foo.org."}, 120 []string{}, 121 []string{"foo.org"}, 122 true, 123 map[string][]string{ 124 "include": {"foo.org"}, 125 }, 126 }, 127 { 128 []string{"", "foo.org."}, 129 []string{}, 130 []string{}, 131 true, 132 map[string][]string{ 133 "include": {"foo.org"}, 134 }, 135 }, 136 { 137 []string{""}, 138 []string{}, 139 []string{"foo.org"}, 140 true, 141 map[string][]string{}, 142 }, 143 { 144 []string{""}, 145 []string{}, 146 []string{}, 147 true, 148 map[string][]string{}, 149 }, 150 { 151 []string{" "}, 152 []string{}, 153 []string{}, 154 true, 155 map[string][]string{}, 156 }, 157 { 158 []string{"bar.sub.example.org"}, 159 []string{}, 160 []string{"foo.bar.sub.example.org"}, 161 true, 162 map[string][]string{ 163 "include": {"bar.sub.example.org"}, 164 }, 165 }, 166 { 167 []string{"example.org"}, 168 []string{}, 169 []string{"anexample.org", "test.anexample.org"}, 170 false, 171 map[string][]string{ 172 "include": {"example.org"}, 173 }, 174 }, 175 { 176 []string{".example.org"}, 177 []string{}, 178 []string{"anexample.org", "test.anexample.org"}, 179 false, 180 map[string][]string{ 181 "include": {".example.org"}, 182 }, 183 }, 184 { 185 []string{".example.org"}, 186 []string{}, 187 []string{"example.org"}, 188 false, 189 map[string][]string{ 190 "include": {".example.org"}, 191 }, 192 }, 193 { 194 []string{".example.org"}, 195 []string{}, 196 []string{"test.example.org"}, 197 true, 198 map[string][]string{ 199 "include": {".example.org"}, 200 }, 201 }, 202 { 203 []string{"anexample.org"}, 204 []string{}, 205 []string{"example.org", "test.example.org"}, 206 false, 207 map[string][]string{ 208 "include": {"anexample.org"}, 209 }, 210 }, 211 { 212 []string{".org"}, 213 []string{}, 214 []string{"example.org", "test.example.org", "foo.test.example.org"}, 215 true, 216 map[string][]string{ 217 "include": {".org"}, 218 }, 219 }, 220 { 221 []string{"example.org"}, 222 []string{"api.example.org"}, 223 []string{"example.org", "test.example.org", "foo.test.example.org"}, 224 true, 225 map[string][]string{ 226 "include": {"example.org"}, 227 "exclude": {"api.example.org"}, 228 }, 229 }, 230 { 231 []string{"example.org"}, 232 []string{"api.example.org"}, 233 []string{"foo.api.example.org", "api.example.org"}, 234 false, 235 map[string][]string{ 236 "include": {"example.org"}, 237 "exclude": {"api.example.org"}, 238 }, 239 }, 240 { 241 []string{" example.org. "}, 242 []string{" .api.example.org "}, 243 []string{"foo.api.example.org", "bar.baz.api.example.org."}, 244 false, 245 map[string][]string{ 246 "include": {"example.org"}, 247 "exclude": {".api.example.org"}, 248 }, 249 }, 250 { 251 []string{"example.org."}, 252 []string{"api.example.org"}, 253 []string{"dev-api.example.org", "qa-api.example.org"}, 254 true, 255 map[string][]string{ 256 "include": {"example.org"}, 257 "exclude": {"api.example.org"}, 258 }, 259 }, 260 { 261 []string{"example.org."}, 262 []string{"api.example.org"}, 263 []string{"dev.api.example.org", "qa.api.example.org"}, 264 false, 265 map[string][]string{ 266 "include": {"example.org"}, 267 "exclude": {"api.example.org"}, 268 }, 269 }, 270 { 271 []string{"example.org", "api.example.org"}, 272 []string{"internal.api.example.org"}, 273 []string{"foo.api.example.org"}, 274 true, 275 map[string][]string{ 276 "include": {"api.example.org", "example.org"}, 277 "exclude": {"internal.api.example.org"}, 278 }, 279 }, 280 { 281 []string{"example.org", "api.example.org"}, 282 []string{"internal.api.example.org"}, 283 []string{"foo.internal.api.example.org"}, 284 false, 285 map[string][]string{ 286 "include": {"api.example.org", "example.org"}, 287 "exclude": {"internal.api.example.org"}, 288 }, 289 }, 290 { 291 []string{"eXaMPle.ORG", "API.example.ORG"}, 292 []string{"Foo-Bar.Example.Org"}, 293 []string{"FoOoo.Api.Example.Org"}, 294 true, 295 map[string][]string{ 296 "include": {"api.example.org", "example.org"}, 297 "exclude": {"foo-bar.example.org"}, 298 }, 299 }, 300 { 301 []string{"eXaMPle.ORG", "API.example.ORG"}, 302 []string{"api.example.org"}, 303 []string{"foobar.Example.Org"}, 304 true, 305 map[string][]string{ 306 "include": {"api.example.org", "example.org"}, 307 "exclude": {"api.example.org"}, 308 }, 309 }, 310 { 311 []string{"eXaMPle.ORG", "API.example.ORG"}, 312 []string{"api.example.org"}, 313 []string{"foobar.API.Example.Org"}, 314 false, 315 map[string][]string{ 316 "include": {"api.example.org", "example.org"}, 317 "exclude": {"api.example.org"}, 318 }, 319 }, 320 } 321 322 var regexDomainFilterTests = []regexDomainFilterTest{ 323 { 324 regexp.MustCompile("\\.org$"), 325 regexp.MustCompile(""), 326 []string{"foo.org", "bar.org", "foo.bar.org"}, 327 true, 328 map[string]string{ 329 "regexInclude": "\\.org$", 330 }, 331 }, 332 { 333 regexp.MustCompile("\\.bar\\.org$"), 334 regexp.MustCompile(""), 335 []string{"foo.org", "bar.org", "example.com"}, 336 false, 337 map[string]string{ 338 "regexInclude": "\\.bar\\.org$", 339 }, 340 }, 341 { 342 regexp.MustCompile("(?:foo|bar)\\.org$"), 343 regexp.MustCompile(""), 344 []string{"foo.org", "bar.org", "example.foo.org", "example.bar.org", "a.example.foo.org", "a.example.bar.org"}, 345 true, 346 map[string]string{ 347 "regexInclude": "(?:foo|bar)\\.org$", 348 }, 349 }, 350 { 351 regexp.MustCompile("(?:foo|bar)\\.org$"), 352 regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"), 353 []string{"foo.org", "bar.org", "a.example.foo.org", "a.example.bar.org"}, 354 true, 355 map[string]string{ 356 "regexInclude": "(?:foo|bar)\\.org$", 357 "regexExclude": "^example\\.(?:foo|bar)\\.org$", 358 }, 359 }, 360 { 361 regexp.MustCompile("(?:foo|bar)\\.org$"), 362 regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"), 363 []string{"example.foo.org", "example.bar.org"}, 364 false, 365 map[string]string{ 366 "regexInclude": "(?:foo|bar)\\.org$", 367 "regexExclude": "^example\\.(?:foo|bar)\\.org$", 368 }, 369 }, 370 { 371 regexp.MustCompile("(?:foo|bar)\\.org$"), 372 regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"), 373 []string{"foo.org", "bar.org", "a.example.foo.org", "a.example.bar.org"}, 374 true, 375 map[string]string{ 376 "regexInclude": "(?:foo|bar)\\.org$", 377 "regexExclude": "^example\\.(?:foo|bar)\\.org$", 378 }, 379 }, 380 } 381 382 func TestDomainFilterMatch(t *testing.T) { 383 for i, tt := range domainFilterTests { 384 if len(tt.exclusions) > 0 { 385 t.Logf("NewDomainFilter() doesn't support exclusions - skipping test %+v", tt) 386 continue 387 } 388 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 389 domainFilter := NewDomainFilter(tt.domainFilter) 390 391 assertSerializes(t, domainFilter, tt.expectedSerialization) 392 deserialized := deserialize(t, map[string][]string{ 393 "include": tt.domainFilter, 394 }) 395 396 for _, domain := range tt.domains { 397 assert.Equal(t, tt.expected, domainFilter.Match(domain), "%v", domain) 398 assert.Equal(t, tt.expected, domainFilter.Match(domain+"."), "%v", domain+".") 399 400 assert.Equal(t, tt.expected, deserialized.Match(domain), "deserialized %v", domain) 401 assert.Equal(t, tt.expected, deserialized.Match(domain+"."), "deserialized %v", domain+".") 402 } 403 }) 404 } 405 } 406 407 func TestDomainFilterWithExclusions(t *testing.T) { 408 for i, tt := range domainFilterTests { 409 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 410 if len(tt.exclusions) == 0 { 411 tt.exclusions = append(tt.exclusions, "") 412 } 413 domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions) 414 415 assertSerializes(t, domainFilter, tt.expectedSerialization) 416 deserialized := deserialize(t, map[string][]string{ 417 "include": tt.domainFilter, 418 "exclude": tt.exclusions, 419 }) 420 421 for _, domain := range tt.domains { 422 assert.Equal(t, tt.expected, domainFilter.Match(domain), "%v", domain) 423 assert.Equal(t, tt.expected, domainFilter.Match(domain+"."), "%v", domain+".") 424 425 assert.Equal(t, tt.expected, deserialized.Match(domain), "deserialized %v", domain) 426 assert.Equal(t, tt.expected, deserialized.Match(domain+"."), "deserialized %v", domain+".") 427 } 428 }) 429 } 430 } 431 432 func TestDomainFilterMatchWithEmptyFilter(t *testing.T) { 433 for _, tt := range domainFilterTests { 434 domainFilter := DomainFilter{} 435 for i, domain := range tt.domains { 436 assert.True(t, domainFilter.Match(domain), "should not fail: %v in test-case #%v", domain, i) 437 assert.True(t, domainFilter.Match(domain+"."), "should not fail: %v in test-case #%v", domain+".", i) 438 } 439 } 440 } 441 442 func TestRegexDomainFilter(t *testing.T) { 443 for i, tt := range regexDomainFilterTests { 444 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 445 domainFilter := NewRegexDomainFilter(tt.regex, tt.regexExclusion) 446 447 assertSerializes(t, domainFilter, tt.expectedSerialization) 448 deserialized := deserialize(t, map[string]string{ 449 "regexInclude": tt.regex.String(), 450 "regexExclude": tt.regexExclusion.String(), 451 }) 452 453 for _, domain := range tt.domains { 454 assert.Equal(t, tt.expected, domainFilter.Match(domain), "%v", domain) 455 assert.Equal(t, tt.expected, domainFilter.Match(domain+"."), "%v", domain+".") 456 457 assert.Equal(t, tt.expected, deserialized.Match(domain), "deserialized %v", domain) 458 assert.Equal(t, tt.expected, deserialized.Match(domain+"."), "deserialized %v", domain+".") 459 } 460 }) 461 } 462 } 463 464 func TestPrepareFiltersStripsWhitespaceAndDotSuffix(t *testing.T) { 465 for _, tt := range []struct { 466 input []string 467 output []string 468 }{ 469 { 470 []string{}, 471 nil, 472 }, 473 { 474 []string{""}, 475 nil, 476 }, 477 { 478 []string{" ", " ", ""}, 479 nil, 480 }, 481 { 482 []string{" foo ", " bar. ", "baz."}, 483 []string{"foo", "bar", "baz"}, 484 }, 485 { 486 []string{"foo.bar", " foo.bar. ", " foo.bar.baz ", " foo.bar.baz. "}, 487 []string{"foo.bar", "foo.bar", "foo.bar.baz", "foo.bar.baz"}, 488 }, 489 } { 490 t.Run("test string", func(t *testing.T) { 491 assert.Equal(t, tt.output, prepareFilters(tt.input)) 492 }) 493 } 494 } 495 496 func TestMatchFilterReturnsProperEmptyVal(t *testing.T) { 497 emptyFilters := []string{} 498 assert.Equal(t, true, matchFilter(emptyFilters, "somedomain.com", true)) 499 assert.Equal(t, false, matchFilter(emptyFilters, "somedomain.com", false)) 500 } 501 502 func TestDomainFilterIsConfigured(t *testing.T) { 503 for i, tt := range []struct { 504 filters []string 505 exclude []string 506 expected bool 507 }{ 508 { 509 []string{""}, 510 []string{""}, 511 false, 512 }, 513 { 514 []string{" "}, 515 []string{" "}, 516 false, 517 }, 518 { 519 []string{"", ""}, 520 []string{""}, 521 false, 522 }, 523 { 524 []string{" . "}, 525 []string{" . "}, 526 false, 527 }, 528 { 529 []string{" notempty.com "}, 530 []string{" "}, 531 true, 532 }, 533 { 534 []string{" notempty.com "}, 535 []string{" thisdoesntmatter.com "}, 536 true, 537 }, 538 { 539 []string{""}, 540 []string{" thisdoesntmatter.com "}, 541 true, 542 }, 543 } { 544 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 545 df := NewDomainFilterWithExclusions(tt.filters, tt.exclude) 546 assert.Equal(t, tt.expected, df.IsConfigured()) 547 }) 548 } 549 } 550 551 func TestRegexDomainFilterIsConfigured(t *testing.T) { 552 for i, tt := range []struct { 553 regex string 554 regexExclude string 555 expected bool 556 }{ 557 { 558 "", 559 "", 560 false, 561 }, 562 { 563 "(?:foo|bar)\\.org$", 564 "", 565 true, 566 }, 567 { 568 "", 569 "\\.org$", 570 true, 571 }, 572 { 573 "(?:foo|bar)\\.org$", 574 "\\.org$", 575 true, 576 }, 577 } { 578 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 579 df := NewRegexDomainFilter(regexp.MustCompile(tt.regex), regexp.MustCompile(tt.regexExclude)) 580 assert.Equal(t, tt.expected, df.IsConfigured()) 581 }) 582 } 583 } 584 585 func TestDomainFilterDeserializeError(t *testing.T) { 586 for _, tt := range []struct { 587 name string 588 serialized map[string]interface{} 589 expectedError string 590 }{ 591 { 592 name: "invalid json", 593 serialized: map[string]interface{}{ 594 "include": 3, 595 }, 596 expectedError: "json: cannot unmarshal number into Go struct field domainFilterSerde.include of type []string", 597 }, 598 { 599 name: "include and regex", 600 serialized: map[string]interface{}{ 601 "include": []string{"example.com"}, 602 "regexInclude": "example.com", 603 }, 604 expectedError: "cannot have both domain list and regex", 605 }, 606 { 607 name: "exclude and regex", 608 serialized: map[string]interface{}{ 609 "exclude": []string{"example.com"}, 610 "regexInclude": "example.com", 611 }, 612 expectedError: "cannot have both domain list and regex", 613 }, 614 { 615 name: "include and regexExclude", 616 serialized: map[string]interface{}{ 617 "include": []string{"example.com"}, 618 "regexExclude": "example.com", 619 }, 620 expectedError: "cannot have both domain list and regex", 621 }, 622 { 623 name: "exclude and regexExclude", 624 serialized: map[string]interface{}{ 625 "exclude": []string{"example.com"}, 626 "regexExclude": "example.com", 627 }, 628 expectedError: "cannot have both domain list and regex", 629 }, 630 { 631 name: "invalid regex", 632 serialized: map[string]interface{}{ 633 "regexInclude": "*", 634 }, 635 expectedError: "invalid regexInclude: error parsing regexp: missing argument to repetition operator: `*`", 636 }, 637 { 638 name: "invalid regexExclude", 639 serialized: map[string]interface{}{ 640 "regexExclude": "*", 641 }, 642 expectedError: "invalid regexExclude: error parsing regexp: missing argument to repetition operator: `*`", 643 }, 644 } { 645 t.Run(tt.name, func(t *testing.T) { 646 var deserialized DomainFilter 647 toJson, _ := json.Marshal(tt.serialized) 648 err := json.Unmarshal(toJson, &deserialized) 649 assert.EqualError(t, err, tt.expectedError) 650 }) 651 } 652 } 653 654 func assertSerializes[T any](t *testing.T, domainFilter DomainFilter, expectedSerialization map[string]T) { 655 serialized, err := json.Marshal(domainFilter) 656 assert.NoError(t, err, "serializing") 657 expected, err := json.Marshal(expectedSerialization) 658 require.NoError(t, err) 659 assert.JSONEq(t, string(expected), string(serialized), "json serialization") 660 } 661 662 func deserialize[T any](t *testing.T, serialized map[string]T) DomainFilter { 663 inJson, err := json.Marshal(serialized) 664 require.NoError(t, err) 665 var deserialized DomainFilter 666 err = json.Unmarshal(inJson, &deserialized) 667 assert.NoError(t, err, "deserializing") 668 669 return deserialized 670 } 671 672 func TestDomainFilterMatchParent(t *testing.T) { 673 parentMatchTests := []domainFilterTest{ 674 { 675 []string{"a.example.com."}, 676 []string{}, 677 []string{"example.com"}, 678 true, 679 map[string][]string{ 680 "include": {"a.example.com"}, 681 }, 682 }, 683 { 684 []string{" a.example.com "}, 685 []string{}, 686 []string{"example.com"}, 687 true, 688 map[string][]string{ 689 "include": {"a.example.com"}, 690 }, 691 }, 692 { 693 []string{""}, 694 []string{}, 695 []string{"example.com"}, 696 true, 697 map[string][]string{}, 698 }, 699 { 700 []string{".a.example.com."}, 701 []string{}, 702 []string{"example.com"}, 703 false, 704 map[string][]string{ 705 "include": {".a.example.com"}, 706 }, 707 }, 708 { 709 []string{"a.example.com.", "b.example.com"}, 710 []string{}, 711 []string{"example.com"}, 712 true, 713 map[string][]string{ 714 "include": {"a.example.com", "b.example.com"}, 715 }, 716 }, 717 { 718 []string{"a.example.com"}, 719 []string{}, 720 []string{"b.example.com"}, 721 false, 722 map[string][]string{ 723 "include": {"a.example.com"}, 724 }, 725 }, 726 { 727 []string{"example.com"}, 728 []string{}, 729 []string{"example.com"}, 730 false, 731 map[string][]string{ 732 "include": {"example.com"}, 733 }, 734 }, 735 { 736 []string{"example.com"}, 737 []string{}, 738 []string{"anexample.com"}, 739 false, 740 map[string][]string{ 741 "include": {"example.com"}, 742 }, 743 }, 744 { 745 []string{""}, 746 []string{}, 747 []string{""}, 748 true, 749 map[string][]string{}, 750 }, 751 } 752 for i, tt := range parentMatchTests { 753 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 754 domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions) 755 756 assertSerializes(t, domainFilter, tt.expectedSerialization) 757 deserialized := deserialize(t, map[string][]string{ 758 "include": tt.domainFilter, 759 "exclude": tt.exclusions, 760 }) 761 762 for _, domain := range tt.domains { 763 assert.Equal(t, tt.expected, domainFilter.MatchParent(domain), "%v", domain) 764 assert.Equal(t, tt.expected, domainFilter.MatchParent(domain+"."), "%v", domain+".") 765 766 assert.Equal(t, tt.expected, deserialized.MatchParent(domain), "deserialized %v", domain) 767 assert.Equal(t, tt.expected, deserialized.MatchParent(domain+"."), "deserialized %v", domain+".") 768 } 769 }) 770 } 771 }