github.com/Jeffail/benthos/v3@v3.65.0/internal/docs/yaml_test.go (about) 1 package docs_test 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/Jeffail/benthos/v3/internal/docs" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 "gopkg.in/yaml.v3" 11 ) 12 13 func TestFieldsFromNode(t *testing.T) { 14 tests := []struct { 15 name string 16 yaml string 17 fields docs.FieldSpecs 18 }{ 19 { 20 name: "flat object", 21 yaml: `a: foo 22 b: bar 23 c: 21`, 24 fields: docs.FieldSpecs{ 25 docs.FieldString("a", "").HasDefault("foo"), 26 docs.FieldString("b", "").HasDefault("bar"), 27 docs.FieldInt("c", "").HasDefault(int64(21)), 28 }, 29 }, 30 { 31 name: "nested object", 32 yaml: `a: foo 33 b: 34 d: bar 35 e: 22 36 c: true`, 37 fields: docs.FieldSpecs{ 38 docs.FieldString("a", "").HasDefault("foo"), 39 docs.FieldCommon("b", "").WithChildren( 40 docs.FieldString("d", "").HasDefault("bar"), 41 docs.FieldInt("e", "").HasDefault(int64(22)), 42 ), 43 docs.FieldBool("c", "").HasDefault(true), 44 }, 45 }, 46 { 47 name: "array of strings", 48 yaml: `a: 49 - foo`, 50 fields: docs.FieldSpecs{ 51 docs.FieldString("a", "").Array().HasDefault([]string{"foo"}), 52 }, 53 }, 54 { 55 name: "array of ints", 56 yaml: `a: 57 - 5 58 - 8`, 59 fields: docs.FieldSpecs{ 60 docs.FieldInt("a", "").Array().HasDefault([]int64{5, 8}), 61 }, 62 }, 63 { 64 name: "nested array of strings", 65 yaml: `a: 66 b: 67 - foo 68 - bar`, 69 fields: docs.FieldSpecs{ 70 docs.FieldCommon("a", "").WithChildren( 71 docs.FieldString("b", "").Array().HasDefault([]string{"foo", "bar"}), 72 ), 73 }, 74 }, 75 } 76 77 for _, test := range tests { 78 t.Run(test.name, func(t *testing.T) { 79 confBytes := []byte(test.yaml) 80 81 var node yaml.Node 82 require.NoError(t, yaml.Unmarshal(confBytes, &node)) 83 84 assert.Equal(t, test.fields, docs.FieldsFromYAML(&node)) 85 }) 86 } 87 } 88 89 func TestFieldsNodeToMap(t *testing.T) { 90 spec := docs.FieldSpecs{ 91 docs.FieldString("a", ""), 92 docs.FieldInt("b", "").HasDefault(11), 93 docs.FieldCommon("c", "").WithChildren( 94 docs.FieldBool("d", "").HasDefault(true), 95 docs.FieldString("e", "").HasDefault("evalue"), 96 docs.FieldCommon("f", "").WithChildren( 97 docs.FieldInt("g", "").HasDefault(12), 98 docs.FieldString("h", ""), 99 docs.FieldFloat("i", "").HasDefault(13.0), 100 ), 101 ), 102 } 103 104 var node yaml.Node 105 err := yaml.Unmarshal([]byte(` 106 a: setavalue 107 c: 108 f: 109 g: 22 110 h: sethvalue 111 i: 23.1 112 `), &node) 113 require.NoError(t, err) 114 115 generic, err := spec.YAMLToMap(&node, docs.ToValueConfig{}) 116 require.NoError(t, err) 117 118 assert.Equal(t, map[string]interface{}{ 119 "a": "setavalue", 120 "b": 11, 121 "c": map[string]interface{}{ 122 "d": true, 123 "e": "evalue", 124 "f": map[string]interface{}{ 125 "g": 22, 126 "h": "sethvalue", 127 "i": 23.1, 128 }, 129 }, 130 }, generic) 131 } 132 133 func TestFieldsNodeToMapTypeCoercion(t *testing.T) { 134 tests := []struct { 135 name string 136 spec docs.FieldSpecs 137 yaml string 138 result interface{} 139 }{ 140 { 141 name: "string fields", 142 spec: docs.FieldSpecs{ 143 docs.FieldCommon("a", "").HasType("string"), 144 docs.FieldCommon("b", "").HasType("string"), 145 docs.FieldCommon("c", "").HasType("string"), 146 docs.FieldCommon("d", "").HasType("string"), 147 docs.FieldCommon("e", "").HasType("string").Array(), 148 docs.FieldCommon("f", "").HasType("string").Map(), 149 }, 150 yaml: ` 151 a: no 152 b: false 153 c: 10 154 d: 30.4 155 e: 156 - no 157 - false 158 - 10 159 f: 160 "1": no 161 "2": false 162 "3": 10 163 `, 164 result: map[string]interface{}{ 165 "a": "no", 166 "b": "false", 167 "c": "10", 168 "d": "30.4", 169 "e": []interface{}{ 170 "no", "false", "10", 171 }, 172 "f": map[string]interface{}{ 173 "1": "no", "2": "false", "3": "10", 174 }, 175 }, 176 }, 177 { 178 name: "bool fields", 179 spec: docs.FieldSpecs{ 180 docs.FieldCommon("a", "").HasType("bool"), 181 docs.FieldCommon("b", "").HasType("bool"), 182 docs.FieldCommon("c", "").HasType("bool"), 183 docs.FieldCommon("d", "").HasType("bool").Array(), 184 docs.FieldCommon("e", "").HasType("bool").Map(), 185 }, 186 yaml: ` 187 a: no 188 b: false 189 c: true 190 d: 191 - no 192 - false 193 - true 194 e: 195 "1": no 196 "2": false 197 "3": true 198 `, 199 result: map[string]interface{}{ 200 "a": false, 201 "b": false, 202 "c": true, 203 "d": []interface{}{ 204 false, false, true, 205 }, 206 "e": map[string]interface{}{ 207 "1": false, "2": false, "3": true, 208 }, 209 }, 210 }, 211 { 212 name: "int fields", 213 spec: docs.FieldSpecs{ 214 docs.FieldCommon("a", "").HasType("int"), 215 docs.FieldCommon("b", "").HasType("int"), 216 docs.FieldCommon("c", "").HasType("int"), 217 docs.FieldCommon("d", "").HasType("int").Array(), 218 docs.FieldCommon("e", "").HasType("int").Map(), 219 }, 220 yaml: ` 221 a: 11 222 b: -12 223 c: 13.4 224 d: 225 - 11 226 - -12 227 - 13.4 228 e: 229 "1": 11 230 "2": -12 231 "3": 13.4 232 `, 233 result: map[string]interface{}{ 234 "a": 11, 235 "b": -12, 236 "c": 13, 237 "d": []interface{}{ 238 11, -12, 13, 239 }, 240 "e": map[string]interface{}{ 241 "1": 11, "2": -12, "3": 13, 242 }, 243 }, 244 }, 245 { 246 name: "float fields", 247 spec: docs.FieldSpecs{ 248 docs.FieldCommon("a", "").HasType("float"), 249 docs.FieldCommon("b", "").HasType("float"), 250 docs.FieldCommon("c", "").HasType("float"), 251 docs.FieldCommon("d", "").HasType("float").Array(), 252 docs.FieldCommon("e", "").HasType("float").Map(), 253 }, 254 yaml: ` 255 a: 11 256 b: -12 257 c: 13.4 258 d: 259 - 11 260 - -12 261 - 13.4 262 e: 263 "1": 11 264 "2": -12 265 "3": 13.4 266 `, 267 result: map[string]interface{}{ 268 "a": 11.0, 269 "b": -12.0, 270 "c": 13.4, 271 "d": []interface{}{ 272 11.0, -12.0, 13.4, 273 }, 274 "e": map[string]interface{}{ 275 "1": 11.0, "2": -12.0, "3": 13.4, 276 }, 277 }, 278 }, 279 { 280 name: "recurse array of objects", 281 spec: docs.FieldSpecs{ 282 docs.FieldCommon("foo", "").WithChildren( 283 docs.FieldCommon("eles", "").Array().WithChildren( 284 docs.FieldCommon("bar", "").HasType(docs.FieldTypeString).HasDefault("default"), 285 ), 286 ), 287 }, 288 yaml: ` 289 foo: 290 eles: 291 - bar: bar1 292 - bar: bar2 293 `, 294 result: map[string]interface{}{ 295 "foo": map[string]interface{}{ 296 "eles": []interface{}{ 297 map[string]interface{}{ 298 "bar": "bar1", 299 }, 300 map[string]interface{}{ 301 "bar": "bar2", 302 }, 303 }, 304 }, 305 }, 306 }, 307 { 308 name: "recurse map of objects", 309 spec: docs.FieldSpecs{ 310 docs.FieldCommon("foo", "").WithChildren( 311 docs.FieldCommon("eles", "").Map().WithChildren( 312 docs.FieldCommon("bar", "").HasType(docs.FieldTypeString).HasDefault("default"), 313 ), 314 ), 315 }, 316 yaml: ` 317 foo: 318 eles: 319 first: 320 bar: bar1 321 second: 322 bar: bar2 323 `, 324 result: map[string]interface{}{ 325 "foo": map[string]interface{}{ 326 "eles": map[string]interface{}{ 327 "first": map[string]interface{}{ 328 "bar": "bar1", 329 }, 330 "second": map[string]interface{}{ 331 "bar": "bar2", 332 }, 333 }, 334 }, 335 }, 336 }, 337 { 338 name: "component field", 339 spec: docs.FieldSpecs{ 340 docs.FieldString("a", "").HasDefault("adefault"), 341 docs.FieldCommon("b", "").HasType(docs.FieldTypeProcessor), 342 docs.FieldBool("c", ""), 343 }, 344 yaml: ` 345 b: 346 bloblang: 'root = "hello world"' 347 c: true 348 `, 349 result: map[string]interface{}{ 350 "a": "adefault", 351 "b": &yaml.Node{ 352 Kind: yaml.MappingNode, 353 Tag: "!!map", 354 Line: 3, 355 Column: 3, 356 Content: []*yaml.Node{ 357 { 358 Kind: yaml.ScalarNode, 359 Tag: "!!str", 360 Value: "bloblang", 361 Line: 3, 362 Column: 3, 363 }, 364 { 365 Kind: yaml.ScalarNode, 366 Style: yaml.SingleQuotedStyle, 367 Tag: "!!str", 368 Value: `root = "hello world"`, 369 Line: 3, 370 Column: 13, 371 }, 372 }, 373 }, 374 "c": true, 375 }, 376 }, 377 { 378 name: "component field in array", 379 spec: docs.FieldSpecs{ 380 docs.FieldString("a", "").HasDefault("adefault"), 381 docs.FieldCommon("b", "").Array().HasType(docs.FieldTypeProcessor), 382 docs.FieldBool("c", ""), 383 }, 384 yaml: ` 385 b: 386 - bloblang: 'root = "hello world"' 387 c: true 388 `, 389 result: map[string]interface{}{ 390 "a": "adefault", 391 "b": []interface{}{ 392 &yaml.Node{ 393 Kind: yaml.MappingNode, 394 Tag: "!!map", 395 Line: 3, 396 Column: 5, 397 Content: []*yaml.Node{ 398 { 399 Kind: yaml.ScalarNode, 400 Tag: "!!str", 401 Value: "bloblang", 402 Line: 3, 403 Column: 5, 404 }, 405 { 406 Kind: yaml.ScalarNode, 407 Style: yaml.SingleQuotedStyle, 408 Tag: "!!str", 409 Value: `root = "hello world"`, 410 Line: 3, 411 Column: 15, 412 }, 413 }, 414 }, 415 }, 416 "c": true, 417 }, 418 }, 419 { 420 name: "component field in map", 421 spec: docs.FieldSpecs{ 422 docs.FieldString("a", "").HasDefault("adefault"), 423 docs.FieldCommon("b", "").Map().HasType(docs.FieldTypeProcessor), 424 docs.FieldBool("c", ""), 425 }, 426 yaml: ` 427 b: 428 foo: 429 bloblang: 'root = "hello world"' 430 c: true 431 `, 432 result: map[string]interface{}{ 433 "a": "adefault", 434 "b": map[string]interface{}{ 435 "foo": &yaml.Node{ 436 Kind: yaml.MappingNode, 437 Tag: "!!map", 438 Line: 4, 439 Column: 5, 440 Content: []*yaml.Node{ 441 { 442 Kind: yaml.ScalarNode, 443 Tag: "!!str", 444 Value: "bloblang", 445 Line: 4, 446 Column: 5, 447 }, 448 { 449 Kind: yaml.ScalarNode, 450 Style: yaml.SingleQuotedStyle, 451 Tag: "!!str", 452 Value: `root = "hello world"`, 453 Line: 4, 454 Column: 15, 455 }, 456 }, 457 }, 458 }, 459 "c": true, 460 }, 461 }, 462 { 463 name: "array of array of string", 464 spec: docs.FieldSpecs{ 465 docs.FieldString("foo", "").ArrayOfArrays(), 466 }, 467 yaml: ` 468 foo: 469 - 470 - bar1 471 - bar2 472 - 473 - bar3 474 `, 475 result: map[string]interface{}{ 476 "foo": []interface{}{ 477 []interface{}{"bar1", "bar2"}, 478 []interface{}{"bar3"}, 479 }, 480 }, 481 }, 482 { 483 name: "array of array of int, float and bool", 484 spec: docs.FieldSpecs{ 485 docs.FieldInt("foo", "").ArrayOfArrays(), 486 docs.FieldFloat("bar", "").ArrayOfArrays(), 487 docs.FieldBool("baz", "").ArrayOfArrays(), 488 }, 489 yaml: ` 490 foo: [[3,4],[5]] 491 bar: [[3.3,4.4],[5.5]] 492 baz: [[true,false],[true]] 493 `, 494 result: map[string]interface{}{ 495 "foo": []interface{}{ 496 []interface{}{3, 4}, []interface{}{5}, 497 }, 498 "bar": []interface{}{ 499 []interface{}{3.3, 4.4}, []interface{}{5.5}, 500 }, 501 "baz": []interface{}{ 502 []interface{}{true, false}, []interface{}{true}, 503 }, 504 }, 505 }, 506 } 507 508 for _, test := range tests { 509 t.Run(test.name, func(t *testing.T) { 510 var node yaml.Node 511 err := yaml.Unmarshal([]byte(test.yaml), &node) 512 require.NoError(t, err) 513 514 generic, err := test.spec.YAMLToMap(&node, docs.ToValueConfig{}) 515 require.NoError(t, err) 516 517 assert.Equal(t, test.result, generic) 518 }) 519 } 520 } 521 522 func TestFieldToNode(t *testing.T) { 523 tests := []struct { 524 name string 525 spec docs.FieldSpec 526 recurse bool 527 expected string 528 }{ 529 { 530 name: "no recurse single node null", 531 spec: docs.FieldCommon("foo", ""), 532 expected: `null 533 `, 534 }, 535 { 536 name: "no recurse with children", 537 spec: docs.FieldCommon("foo", "").WithChildren( 538 docs.FieldCommon("bar", ""), 539 docs.FieldCommon("baz", ""), 540 ), 541 expected: `{} 542 `, 543 }, 544 { 545 name: "no recurse map", 546 spec: docs.FieldCommon("foo", "").Map(), 547 expected: `{} 548 `, 549 }, 550 { 551 name: "recurse with children", 552 spec: docs.FieldCommon("foo", "").WithChildren( 553 docs.FieldCommon("bar", "").HasType(docs.FieldTypeString), 554 docs.FieldCommon("baz", "").HasType(docs.FieldTypeString).HasDefault("baz default"), 555 docs.FieldCommon("buz", "").HasType(docs.FieldTypeInt), 556 docs.FieldCommon("bev", "").HasType(docs.FieldTypeFloat), 557 docs.FieldCommon("bun", "").HasType(docs.FieldTypeBool), 558 docs.FieldCommon("bud", "").Array(), 559 ), 560 recurse: true, 561 expected: `bar: "" 562 baz: baz default 563 buz: 0 564 bev: 0 565 bun: false 566 bud: [] 567 `, 568 }, 569 } 570 571 for _, test := range tests { 572 test := test 573 t.Run(test.name, func(t *testing.T) { 574 n, err := test.spec.ToYAML(test.recurse) 575 require.NoError(t, err) 576 577 b, err := yaml.Marshal(n) 578 require.NoError(t, err) 579 580 assert.Equal(t, test.expected, string(b)) 581 }) 582 } 583 } 584 585 func TestYAMLComponentLinting(t *testing.T) { 586 for _, t := range docs.Types() { 587 docs.RegisterDocs(docs.ComponentSpec{ 588 Name: fmt.Sprintf("testlintfoo%v", string(t)), 589 Type: t, 590 Config: docs.FieldComponent().WithChildren( 591 docs.FieldString("foo1", "").Linter(func(ctx docs.LintContext, line, col int, v interface{}) []docs.Lint { 592 if v == "lint me please" { 593 return []docs.Lint{ 594 docs.NewLintError(line, "this is a custom lint"), 595 } 596 } 597 return nil 598 }).Optional(), 599 docs.FieldString("foo2", "").Advanced().OmitWhen(func(field, parent interface{}) (string, bool) { 600 if field == "drop me" { 601 return "because foo", true 602 } 603 return "", false 604 }).Optional(), 605 docs.FieldCommon("foo3", "").HasType(docs.FieldTypeProcessor).Optional(), 606 docs.FieldAdvanced("foo4", "").Array().HasType(docs.FieldTypeProcessor).Optional(), 607 docs.FieldCommon("foo5", "").Map().HasType(docs.FieldTypeProcessor).Optional(), 608 docs.FieldDeprecated("foo6").Optional(), 609 docs.FieldAdvanced("foo7", "").Array().WithChildren( 610 docs.FieldString("foochild1", "").Optional(), 611 ).Optional(), 612 docs.FieldAdvanced("foo8", "").Map().WithChildren( 613 docs.FieldInt("foochild1", "").Optional(), 614 ).Optional(), 615 ), 616 }) 617 docs.RegisterDocs(docs.ComponentSpec{ 618 Name: fmt.Sprintf("testlintbar%v", string(t)), 619 Type: t, 620 Status: docs.StatusDeprecated, 621 Config: docs.FieldComponent().WithChildren( 622 docs.FieldString("bar1", "").Optional(), 623 ), 624 }) 625 } 626 627 type testCase struct { 628 name string 629 inputType docs.Type 630 inputConf string 631 rejectDeprecated bool 632 633 res []docs.Lint 634 } 635 636 tests := []testCase{ 637 { 638 name: "ignores comments", 639 inputType: docs.TypeInput, 640 inputConf: ` 641 testlintfooinput: 642 # comment here 643 foo1: hello world # And what's this?`, 644 }, 645 { 646 name: "no problem with deprecated component", 647 inputType: docs.TypeInput, 648 inputConf: ` 649 testlintbarinput: 650 bar1: hello world`, 651 }, 652 { 653 name: "no problem with deprecated fields", 654 inputType: docs.TypeInput, 655 inputConf: ` 656 testlintfooinput: 657 foo1: hello world 658 foo6: hello world`, 659 }, 660 { 661 name: "reject deprecated component", 662 inputType: docs.TypeInput, 663 inputConf: ` 664 testlintbarinput: 665 bar1: hello world`, 666 rejectDeprecated: true, 667 res: []docs.Lint{ 668 docs.NewLintError(2, "component testlintbarinput is deprecated"), 669 }, 670 }, 671 { 672 name: "reject deprecated fields", 673 inputType: docs.TypeInput, 674 inputConf: ` 675 testlintfooinput: 676 foo1: hello world 677 foo6: hello world`, 678 rejectDeprecated: true, 679 res: []docs.Lint{ 680 docs.NewLintError(4, "field foo6 is deprecated"), 681 }, 682 }, 683 { 684 name: "allows anchors", 685 inputType: docs.TypeInput, 686 inputConf: ` 687 testlintfooinput: &test-anchor 688 foo1: hello world 689 processors: 690 - testlintfooprocessor: *test-anchor`, 691 }, 692 { 693 name: "lints through anchors", 694 inputType: docs.TypeInput, 695 inputConf: ` 696 testlintfooinput: &test-anchor 697 foo1: hello world 698 nope: bad field 699 processors: 700 - testlintfooprocessor: *test-anchor`, 701 res: []docs.Lint{ 702 docs.NewLintError(4, "field nope not recognised"), 703 }, 704 }, 705 { 706 name: "unknown fields", 707 inputType: docs.TypeInput, 708 inputConf: ` 709 type: testlintfooinput 710 testlintfooinput: 711 not_recognised: yuh 712 foo1: hello world 713 also_not_recognised: nah 714 definitely_not_recognised: huh`, 715 res: []docs.Lint{ 716 docs.NewLintError(4, "field not_recognised not recognised"), 717 docs.NewLintError(6, "field also_not_recognised not recognised"), 718 docs.NewLintError(7, "field definitely_not_recognised is invalid when the component type is testlintfooinput (input)"), 719 }, 720 }, 721 { 722 name: "reserved field unknown fields", 723 inputType: docs.TypeInput, 724 inputConf: ` 725 testlintfooinput: 726 not_recognised: yuh 727 foo1: hello world 728 processors: 729 - testlintfooprocessor: 730 also_not_recognised: nah`, 731 res: []docs.Lint{ 732 docs.NewLintError(3, "field not_recognised not recognised"), 733 docs.NewLintError(7, "field also_not_recognised not recognised"), 734 }, 735 }, 736 { 737 name: "collision of labels", 738 inputType: docs.TypeInput, 739 inputConf: ` 740 label: foo 741 testlintfooinput: 742 foo1: hello world 743 processors: 744 - label: bar 745 testlintfooprocessor: {} 746 - label: foo 747 testlintfooprocessor: {}`, 748 res: []docs.Lint{ 749 docs.NewLintError(8, "Label 'foo' collides with a previously defined label at line 2"), 750 }, 751 }, 752 { 753 name: "empty processors", 754 inputType: docs.TypeInput, 755 inputConf: ` 756 testlintfooinput: 757 foo1: hello world 758 processors: []`, 759 res: []docs.Lint{ 760 docs.NewLintError(4, "field processors is empty and can be removed"), 761 }, 762 }, 763 { 764 name: "custom omit func", 765 inputType: docs.TypeInput, 766 inputConf: ` 767 testlintfooinput: 768 foo1: hello world 769 foo2: drop me`, 770 res: []docs.Lint{ 771 docs.NewLintError(4, "because foo"), 772 }, 773 }, 774 { 775 name: "nested array not an array", 776 inputType: docs.TypeInput, 777 inputConf: ` 778 testlintfooinput: 779 foo4: 780 key1: 781 testlintfooprocessor: 782 foo1: somevalue 783 not_recognised: nah`, 784 res: []docs.Lint{ 785 docs.NewLintError(4, "expected array value"), 786 }, 787 }, 788 { 789 name: "nested fields", 790 inputType: docs.TypeInput, 791 inputConf: ` 792 testlintfooinput: 793 foo3: 794 testlintfooprocessor: 795 foo1: somevalue 796 not_recognised: nah`, 797 res: []docs.Lint{ 798 docs.NewLintError(6, "field not_recognised not recognised"), 799 }, 800 }, 801 { 802 name: "array for string", 803 inputType: docs.TypeInput, 804 inputConf: ` 805 testlintfooinput: 806 foo3: 807 testlintfooprocessor: 808 foo1: [ somevalue ] 809 `, 810 res: []docs.Lint{ 811 docs.NewLintError(5, "expected string value"), 812 }, 813 }, 814 { 815 name: "nested map fields", 816 inputType: docs.TypeInput, 817 inputConf: ` 818 testlintfooinput: 819 foo5: 820 key1: 821 testlintfooprocessor: 822 foo1: somevalue 823 not_recognised: nah`, 824 res: []docs.Lint{ 825 docs.NewLintError(7, "field not_recognised not recognised"), 826 }, 827 }, 828 { 829 name: "nested map not a map", 830 inputType: docs.TypeInput, 831 inputConf: ` 832 testlintfooinput: 833 foo5: 834 - testlintfooprocessor: 835 foo1: somevalue 836 not_recognised: nah`, 837 res: []docs.Lint{ 838 docs.NewLintError(4, "expected object value"), 839 }, 840 }, 841 { 842 name: "array field", 843 inputType: docs.TypeInput, 844 inputConf: ` 845 testlintfooinput: 846 foo7: 847 - foochild1: yep`, 848 }, 849 { 850 name: "array field bad", 851 inputType: docs.TypeInput, 852 inputConf: ` 853 testlintfooinput: 854 foo7: 855 - wat: no`, 856 res: []docs.Lint{ 857 docs.NewLintError(4, "field wat not recognised"), 858 }, 859 }, 860 { 861 name: "array field not array", 862 inputType: docs.TypeInput, 863 inputConf: ` 864 testlintfooinput: 865 foo7: 866 key1: 867 wat: no`, 868 res: []docs.Lint{ 869 docs.NewLintError(4, "expected array value"), 870 }, 871 }, 872 { 873 name: "map field", 874 inputType: docs.TypeInput, 875 inputConf: ` 876 testlintfooinput: 877 foo8: 878 key1: 879 foochild1: 10`, 880 }, 881 { 882 name: "map field bad", 883 inputType: docs.TypeInput, 884 inputConf: ` 885 testlintfooinput: 886 foo8: 887 key1: 888 wat: nope`, 889 res: []docs.Lint{ 890 docs.NewLintError(5, "field wat not recognised"), 891 }, 892 }, 893 { 894 name: "map field not map", 895 inputType: docs.TypeInput, 896 inputConf: ` 897 testlintfooinput: 898 foo8: 899 - wat: nope`, 900 res: []docs.Lint{ 901 docs.NewLintError(4, "expected object value"), 902 }, 903 }, 904 { 905 name: "custom lint", 906 inputType: docs.TypeInput, 907 inputConf: ` 908 testlintfooinput: 909 foo1: lint me please`, 910 res: []docs.Lint{ 911 docs.NewLintError(3, "this is a custom lint"), 912 }, 913 }, 914 } 915 916 for _, test := range tests { 917 test := test 918 t.Run(test.name, func(t *testing.T) { 919 lintCtx := docs.NewLintContext() 920 lintCtx.RejectDeprecated = test.rejectDeprecated 921 922 var node yaml.Node 923 require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) 924 lints := docs.LintYAML(lintCtx, test.inputType, &node) 925 assert.Equal(t, test.res, lints) 926 }) 927 } 928 } 929 930 func TestYAMLLinting(t *testing.T) { 931 type testCase struct { 932 name string 933 inputSpec docs.FieldSpec 934 inputConf string 935 936 res []docs.Lint 937 } 938 939 tests := []testCase{ 940 { 941 name: "expected string got array", 942 inputSpec: docs.FieldString("foo", ""), 943 inputConf: `["foo","bar"]`, 944 res: []docs.Lint{ 945 docs.NewLintError(1, "expected string value"), 946 }, 947 }, 948 { 949 name: "expected array got string", 950 inputSpec: docs.FieldString("foo", "").Array(), 951 inputConf: `"foo"`, 952 res: []docs.Lint{ 953 docs.NewLintError(1, "expected array value"), 954 }, 955 }, 956 { 957 name: "expected object got string", 958 inputSpec: docs.FieldCommon("foo", "").WithChildren( 959 docs.FieldString("bar", ""), 960 ), 961 inputConf: `"foo"`, 962 res: []docs.Lint{ 963 docs.NewLintError(1, "expected object value"), 964 }, 965 }, 966 { 967 name: "expected string got object", 968 inputSpec: docs.FieldCommon("foo", "").WithChildren( 969 docs.FieldString("bar", ""), 970 ), 971 inputConf: `bar: {}`, 972 res: []docs.Lint{ 973 docs.NewLintError(1, "expected string value"), 974 }, 975 }, 976 { 977 name: "expected string got object nested", 978 inputSpec: docs.FieldCommon("foo", "").WithChildren( 979 docs.FieldCommon("bar", "").WithChildren( 980 docs.FieldString("baz", ""), 981 ), 982 ), 983 inputConf: `bar: 984 baz: {}`, 985 res: []docs.Lint{ 986 docs.NewLintError(2, "expected string value"), 987 }, 988 }, 989 { 990 name: "missing non-optional field", 991 inputSpec: docs.FieldCommon("foo", "").WithChildren( 992 docs.FieldString("bar", "").HasDefault("barv"), 993 docs.FieldString("baz", ""), 994 docs.FieldString("buz", "").Optional(), 995 docs.FieldString("bev", ""), 996 ), 997 inputConf: `bev: hello world`, 998 res: []docs.Lint{ 999 docs.NewLintError(1, "field baz is required"), 1000 }, 1001 }, 1002 } 1003 1004 for _, test := range tests { 1005 test := test 1006 t.Run(test.name, func(t *testing.T) { 1007 var node yaml.Node 1008 require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) 1009 1010 lints := test.inputSpec.LintYAML(docs.NewLintContext(), &node) 1011 assert.Equal(t, test.res, lints) 1012 }) 1013 } 1014 } 1015 1016 func TestYAMLSanitation(t *testing.T) { 1017 for _, t := range docs.Types() { 1018 docs.RegisterDocs(docs.ComponentSpec{ 1019 Name: fmt.Sprintf("testyamlsanitfoo%v", string(t)), 1020 Type: t, 1021 Config: docs.FieldComponent().WithChildren( 1022 docs.FieldCommon("foo1", ""), 1023 docs.FieldAdvanced("foo2", ""), 1024 docs.FieldCommon("foo3", "").HasType(docs.FieldTypeProcessor), 1025 docs.FieldAdvanced("foo4", "").Array().HasType(docs.FieldTypeProcessor), 1026 docs.FieldCommon("foo5", "").Map().HasType(docs.FieldTypeProcessor), 1027 docs.FieldDeprecated("foo6"), 1028 ), 1029 }) 1030 docs.RegisterDocs(docs.ComponentSpec{ 1031 Name: fmt.Sprintf("testyamlsanitbar%v", string(t)), 1032 Type: t, 1033 Config: docs.FieldComponent().Array().WithChildren( 1034 docs.FieldCommon("bar1", ""), 1035 docs.FieldAdvanced("bar2", ""), 1036 docs.FieldCommon("bar3", "").HasType(docs.FieldTypeProcessor), 1037 ), 1038 }) 1039 docs.RegisterDocs(docs.ComponentSpec{ 1040 Name: fmt.Sprintf("testyamlsanitbaz%v", string(t)), 1041 Type: t, 1042 Config: docs.FieldComponent().Map().WithChildren( 1043 docs.FieldCommon("baz1", ""), 1044 docs.FieldAdvanced("baz2", ""), 1045 docs.FieldCommon("baz3", "").HasType(docs.FieldTypeProcessor), 1046 ), 1047 }) 1048 } 1049 1050 type testCase struct { 1051 name string 1052 inputType docs.Type 1053 inputConf string 1054 inputFilter func(f docs.FieldSpec) bool 1055 1056 res string 1057 err string 1058 } 1059 1060 tests := []testCase{ 1061 { 1062 name: "input with processors", 1063 inputType: docs.TypeInput, 1064 inputConf: `testyamlsanitfooinput: 1065 foo1: simple field 1066 foo2: advanced field 1067 foo6: deprecated field 1068 someotherinput: 1069 ignore: me please 1070 processors: 1071 - testyamlsanitbarprocessor: 1072 bar1: bar value 1073 bar5: undocumented field 1074 someotherprocessor: 1075 ignore: me please 1076 `, 1077 res: `testyamlsanitfooinput: 1078 foo1: simple field 1079 foo2: advanced field 1080 foo6: deprecated field 1081 processors: 1082 - testyamlsanitbarprocessor: 1083 bar1: bar value 1084 bar5: undocumented field 1085 `, 1086 }, 1087 { 1088 name: "output array with nested map processor", 1089 inputType: docs.TypeOutput, 1090 inputConf: `testyamlsanitbaroutput: 1091 - bar1: simple field 1092 bar3: 1093 testyamlsanitbazprocessor: 1094 customkey1: 1095 baz1: simple field 1096 someotherprocessor: 1097 ignore: me please 1098 - bar2: advanced field 1099 `, 1100 res: `testyamlsanitbaroutput: 1101 - bar1: simple field 1102 bar3: 1103 testyamlsanitbazprocessor: 1104 customkey1: 1105 baz1: simple field 1106 - bar2: advanced field 1107 `, 1108 }, 1109 { 1110 name: "output with empty processors", 1111 inputType: docs.TypeOutput, 1112 inputConf: `testyamlsanitbaroutput: 1113 - bar1: simple field 1114 processors: [] 1115 `, 1116 res: `testyamlsanitbaroutput: 1117 - bar1: simple field 1118 `, 1119 }, 1120 { 1121 name: "metrics map with nested map processor", 1122 inputType: docs.TypeMetrics, 1123 inputConf: `testyamlsanitbazmetrics: 1124 customkey1: 1125 baz1: simple field 1126 baz3: 1127 testyamlsanitbazprocessor: 1128 customkey1: 1129 baz1: simple field 1130 someotherprocessor: 1131 ignore: me please 1132 customkey2: 1133 baz2: advanced field 1134 `, 1135 res: `testyamlsanitbazmetrics: 1136 customkey1: 1137 baz1: simple field 1138 baz3: 1139 testyamlsanitbazprocessor: 1140 customkey1: 1141 baz1: simple field 1142 customkey2: 1143 baz2: advanced field 1144 `, 1145 }, 1146 { 1147 name: "ratelimit with array field processor", 1148 inputType: docs.TypeRateLimit, 1149 inputConf: `testyamlsanitfoorate_limit: 1150 foo1: simple field 1151 foo4: 1152 - testyamlsanitbazprocessor: 1153 customkey1: 1154 baz1: simple field 1155 someotherprocessor: 1156 ignore: me please 1157 `, 1158 res: `testyamlsanitfoorate_limit: 1159 foo1: simple field 1160 foo4: 1161 - testyamlsanitbazprocessor: 1162 customkey1: 1163 baz1: simple field 1164 `, 1165 }, 1166 { 1167 name: "ratelimit with map field processor", 1168 inputType: docs.TypeRateLimit, 1169 inputConf: `testyamlsanitfoorate_limit: 1170 foo1: simple field 1171 foo5: 1172 customkey1: 1173 testyamlsanitbazprocessor: 1174 customkey1: 1175 baz1: simple field 1176 someotherprocessor: 1177 ignore: me please 1178 `, 1179 res: `testyamlsanitfoorate_limit: 1180 foo1: simple field 1181 foo5: 1182 customkey1: 1183 testyamlsanitbazprocessor: 1184 customkey1: 1185 baz1: simple field 1186 `, 1187 }, 1188 { 1189 name: "input with processors no deprecated", 1190 inputType: docs.TypeInput, 1191 inputFilter: docs.ShouldDropDeprecated(true), 1192 inputConf: `testyamlsanitfooinput: 1193 foo1: simple field 1194 foo2: advanced field 1195 foo6: deprecated field 1196 someotherinput: 1197 ignore: me please 1198 processors: 1199 - testyamlsanitfooprocessor: 1200 foo1: simple field 1201 foo2: advanced field 1202 foo6: deprecated field 1203 someotherprocessor: 1204 ignore: me please 1205 `, 1206 res: `testyamlsanitfooinput: 1207 foo1: simple field 1208 foo2: advanced field 1209 processors: 1210 - testyamlsanitfooprocessor: 1211 foo1: simple field 1212 foo2: advanced field 1213 `, 1214 }, 1215 } 1216 1217 for _, test := range tests { 1218 test := test 1219 t.Run(test.name, func(t *testing.T) { 1220 var node yaml.Node 1221 require.NoError(t, yaml.Unmarshal([]byte(test.inputConf), &node)) 1222 err := docs.SanitiseYAML(test.inputType, &node, docs.SanitiseConfig{ 1223 RemoveTypeField: true, 1224 Filter: test.inputFilter, 1225 RemoveDeprecated: false, 1226 }) 1227 if len(test.err) > 0 { 1228 assert.EqualError(t, err, test.err) 1229 } else { 1230 assert.NoError(t, err) 1231 1232 resBytes, err := yaml.Marshal(node.Content[0]) 1233 require.NoError(t, err) 1234 assert.Equal(t, test.res, string(resBytes)) 1235 } 1236 }) 1237 } 1238 }