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  }