github.com/grafana/synthetic-monitoring-agent@v0.64.2/pkg/pb/synthetic_monitoring/checks_extra_test.go (about)

     1  package synthetic_monitoring
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"flag"
     8  	"reflect"
     9  	"slices"
    10  	"strconv"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/rs/zerolog"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  var testDebugOutput = flag.Bool("test.debug-output", false, "include test debug output")
    19  
    20  func TestCheckValidate(t *testing.T) {
    21  	type TestCase struct {
    22  		input       Check
    23  		expectError bool
    24  	}
    25  
    26  	testcases := map[string]TestCase{
    27  		"invalid tenant": {
    28  			input: Check{
    29  				Id:        1,
    30  				TenantId:  BadID,
    31  				Target:    "127.0.0.1",
    32  				Job:       "job",
    33  				Frequency: 1000,
    34  				Timeout:   1000,
    35  				Probes:    []int64{1},
    36  				Settings: CheckSettings{
    37  					Ping: &PingSettings{},
    38  				},
    39  			},
    40  			expectError: true,
    41  		},
    42  		"invalid label": {
    43  			input: Check{
    44  				Id:        1,
    45  				TenantId:  1,
    46  				Target:    "127.0.0.1",
    47  				Job:       "job",
    48  				Frequency: 1000,
    49  				Timeout:   1000,
    50  				Probes:    []int64{1},
    51  				Settings: CheckSettings{
    52  					Ping: &PingSettings{},
    53  				},
    54  				Labels: []Label{{Name: "name ", Value: "value"}},
    55  			},
    56  			expectError: true,
    57  		},
    58  		"duplicate label names": {
    59  			input: Check{
    60  				Id:        1,
    61  				TenantId:  1,
    62  				Target:    "127.0.0.1",
    63  				Job:       "job",
    64  				Frequency: 1000,
    65  				Timeout:   1000,
    66  				Probes:    []int64{1},
    67  				Settings: CheckSettings{
    68  					Ping: &PingSettings{},
    69  				},
    70  				Labels: []Label{
    71  					{Name: "name", Value: "1"},
    72  					{Name: "name", Value: "2"},
    73  				},
    74  			},
    75  			expectError: true,
    76  		},
    77  		"duplicate label values": {
    78  			input: Check{
    79  				Id:        1,
    80  				TenantId:  1,
    81  				Target:    "127.0.0.1",
    82  				Job:       "job",
    83  				Frequency: 1000,
    84  				Timeout:   1000,
    85  				Probes:    []int64{1},
    86  				Settings: CheckSettings{
    87  					Ping: &PingSettings{},
    88  				},
    89  				Labels: []Label{
    90  					{Name: "name_1", Value: "1"},
    91  					{Name: "name_2", Value: "1"},
    92  				},
    93  			},
    94  			expectError: false,
    95  		},
    96  		"multiple settings": {
    97  			input: Check{
    98  				Id:        1,
    99  				TenantId:  1,
   100  				Target:    "127.0.0.1",
   101  				Job:       "job",
   102  				Frequency: 1000,
   103  				Timeout:   1000,
   104  				Probes:    []int64{1},
   105  				Settings: CheckSettings{
   106  					Ping: &PingSettings{},
   107  					Http: &HttpSettings{},
   108  				},
   109  				Labels: []Label{{Name: "name ", Value: "value"}},
   110  			},
   111  			expectError: true,
   112  		},
   113  		"valid timeout & frequency": { // test for case when frequency > max timeout
   114  			input: Check{
   115  				Id:        1,
   116  				TenantId:  1,
   117  				Target:    "127.0.0.1",
   118  				Job:       "job",
   119  				Frequency: 60000, // 60 seconds
   120  				Timeout:   5000,  // 5 seconds
   121  				Probes:    []int64{1},
   122  				Settings: CheckSettings{
   123  					Ping: &PingSettings{},
   124  				},
   125  			},
   126  			expectError: false,
   127  		},
   128  		"invalid timeout": { // issue #101
   129  			input: Check{
   130  				Id:        1,
   131  				TenantId:  1,
   132  				Target:    "127.0.0.1",
   133  				Job:       "job",
   134  				Frequency: 1000,
   135  				Timeout:   1001, // timeout should be equal or less than frequency
   136  				Probes:    []int64{1},
   137  				Settings: CheckSettings{
   138  					Ping: &PingSettings{},
   139  				},
   140  			},
   141  			expectError: true,
   142  		},
   143  		"invalid HTTP target": {
   144  			input: Check{
   145  				Id:        1,
   146  				TenantId:  1,
   147  				Target:    "ftp://example.org/",
   148  				Job:       "job",
   149  				Frequency: 1000,
   150  				Timeout:   1000,
   151  				Probes:    []int64{1},
   152  				Settings: CheckSettings{
   153  					Http: &HttpSettings{},
   154  				},
   155  				Labels: []Label{{Name: "name ", Value: "value"}},
   156  			},
   157  			expectError: true,
   158  		},
   159  		"valid proxy URL": {
   160  			input: Check{
   161  				Id:        1,
   162  				TenantId:  1,
   163  				Target:    "http://example.org/",
   164  				Job:       "job",
   165  				Frequency: 1000,
   166  				Timeout:   1000,
   167  				Probes:    []int64{1},
   168  				Settings: CheckSettings{
   169  					Http: &HttpSettings{
   170  						ProxyURL: "http://proxy.example.org/",
   171  					},
   172  				},
   173  			},
   174  			expectError: false,
   175  		},
   176  		"valid proxy URL and headers": {
   177  			input: Check{
   178  				Id:        1,
   179  				TenantId:  1,
   180  				Target:    "http://example.org/",
   181  				Job:       "job",
   182  				Frequency: 1000,
   183  				Timeout:   1000,
   184  				Probes:    []int64{1},
   185  				Settings: CheckSettings{
   186  					Http: &HttpSettings{
   187  						ProxyURL:            "http://proxy.example.org/",
   188  						ProxyConnectHeaders: []string{"h1: v1", "h2:v2"},
   189  					},
   190  				},
   191  			},
   192  			expectError: false,
   193  		},
   194  		"proxy headers without url": {
   195  			input: Check{
   196  				Id:        1,
   197  				TenantId:  1,
   198  				Target:    "http://example.org/",
   199  				Job:       "job",
   200  				Frequency: 1000,
   201  				Timeout:   1000,
   202  				Probes:    []int64{1},
   203  				Settings: CheckSettings{
   204  					Http: &HttpSettings{
   205  						ProxyConnectHeaders: []string{"h1: v1", "h2:v2"},
   206  					},
   207  				},
   208  			},
   209  			expectError: true,
   210  		},
   211  		"valid HTTP check with long URL": {
   212  			input: Check{
   213  				Id:        1,
   214  				TenantId:  1,
   215  				Target:    "http://example.org/" + strings.Repeat("x", maxValidLabelValueLength-len("http://example.org/")),
   216  				Job:       "job",
   217  				Frequency: 1000,
   218  				Timeout:   1000,
   219  				Probes:    []int64{1},
   220  				Settings: CheckSettings{
   221  					Http: &HttpSettings{},
   222  				},
   223  			},
   224  			expectError: false,
   225  		},
   226  		"valid multihttp check": {
   227  			input: Check{
   228  				Id:        1,
   229  				TenantId:  1,
   230  				Target:    "https://example.org/",
   231  				Job:       "job",
   232  				Frequency: 60000,
   233  				Timeout:   10000,
   234  				Probes:    []int64{1},
   235  				Settings: CheckSettings{
   236  					Multihttp: &MultiHttpSettings{
   237  						Entries: []*MultiHttpEntry{
   238  							{
   239  								Request: &MultiHttpEntryRequest{
   240  									Url: "https://example.org/",
   241  								},
   242  							},
   243  						},
   244  					},
   245  				},
   246  			},
   247  			expectError: false,
   248  		},
   249  		"valid multihttp variable": {
   250  			input: Check{
   251  				Id:        1,
   252  				TenantId:  1,
   253  				Target:    "${variable}",
   254  				Job:       "job",
   255  				Frequency: 60000,
   256  				Timeout:   10000,
   257  				Probes:    []int64{1},
   258  				Settings: CheckSettings{
   259  					Multihttp: &MultiHttpSettings{
   260  						Entries: []*MultiHttpEntry{
   261  							{
   262  								Request: &MultiHttpEntryRequest{
   263  									Url: "${variable}",
   264  								},
   265  							},
   266  						},
   267  					},
   268  				},
   269  			},
   270  			expectError: false,
   271  		},
   272  		"valid multihttp with variable in second url": {
   273  			input: Check{
   274  				Id:        1,
   275  				TenantId:  1,
   276  				Target:    "https://www.example.com",
   277  				Job:       "job",
   278  				Frequency: 60000,
   279  				Timeout:   10000,
   280  				Probes:    []int64{1},
   281  				Settings: CheckSettings{
   282  					Multihttp: &MultiHttpSettings{
   283  						Entries: []*MultiHttpEntry{
   284  							{
   285  								Request: &MultiHttpEntryRequest{
   286  									Url: "https://www.example.com",
   287  								},
   288  							},
   289  							{
   290  								Request: &MultiHttpEntryRequest{
   291  									Url: "${variable}",
   292  								},
   293  							},
   294  						},
   295  					},
   296  				},
   297  			},
   298  			expectError: false,
   299  		},
   300  		"empty multihttp check": {
   301  			input: Check{
   302  				Id:        1,
   303  				TenantId:  1,
   304  				Target:    "",
   305  				Job:       "job",
   306  				Frequency: 60000,
   307  				Timeout:   10000,
   308  				Probes:    []int64{1},
   309  				Settings: CheckSettings{
   310  					Multihttp: &MultiHttpSettings{
   311  						Entries: []*MultiHttpEntry{
   312  							{
   313  								Request: &MultiHttpEntryRequest{
   314  									Url: "https://example.org/",
   315  								},
   316  							},
   317  						},
   318  					},
   319  				},
   320  			},
   321  			expectError: true,
   322  		},
   323  		"invalid multihttp URL": {
   324  			input: Check{
   325  				Id:        1,
   326  				TenantId:  1,
   327  				Target:    "example.com", // this is fine
   328  				Job:       "job",
   329  				Frequency: 60000,
   330  				Timeout:   10000,
   331  				Probes:    []int64{1},
   332  				Settings: CheckSettings{
   333  					Multihttp: &MultiHttpSettings{
   334  						Entries: []*MultiHttpEntry{
   335  							{
   336  								Request: &MultiHttpEntryRequest{
   337  									Url: "example.com", // this is the problem
   338  								},
   339  							},
   340  						},
   341  					},
   342  				},
   343  			},
   344  			expectError: true,
   345  		},
   346  		"multihttp target must not be an URL": {
   347  			input: Check{
   348  				Id:        1,
   349  				TenantId:  1,
   350  				Target:    "example.com", // this is fine
   351  				Job:       "job",
   352  				Frequency: 60000,
   353  				Timeout:   10000,
   354  				Probes:    []int64{1},
   355  				Settings: CheckSettings{
   356  					Multihttp: &MultiHttpSettings{
   357  						Entries: []*MultiHttpEntry{
   358  							{
   359  								Request: &MultiHttpEntryRequest{
   360  									Url: "http://example.com",
   361  								},
   362  							},
   363  						},
   364  					},
   365  				},
   366  			},
   367  			expectError: false,
   368  		},
   369  		"invalid multihttp second target": {
   370  			input: Check{
   371  				Id:        1,
   372  				TenantId:  1,
   373  				Target:    "https://www.example.com",
   374  				Job:       "job",
   375  				Frequency: 60000,
   376  				Timeout:   10000,
   377  				Probes:    []int64{1},
   378  				Settings: CheckSettings{
   379  					Multihttp: &MultiHttpSettings{
   380  						Entries: []*MultiHttpEntry{
   381  							{
   382  								Request: &MultiHttpEntryRequest{
   383  									Url: "https://www.example.com",
   384  								},
   385  							},
   386  							{
   387  								Request: &MultiHttpEntryRequest{
   388  									Url: "notavalidurlatall",
   389  								},
   390  							},
   391  						},
   392  					},
   393  				},
   394  			},
   395  			expectError: true,
   396  		},
   397  		"valid multihttp variables everywhere": {
   398  			input: Check{
   399  				Id:        1,
   400  				TenantId:  1,
   401  				Target:    "${variable}",
   402  				Job:       "job",
   403  				Frequency: 60000,
   404  				Timeout:   10000,
   405  				Probes:    []int64{1},
   406  				Settings: CheckSettings{
   407  					Multihttp: &MultiHttpSettings{
   408  						Entries: []*MultiHttpEntry{
   409  							{
   410  								Request: &MultiHttpEntryRequest{
   411  									Url: "${variable}",
   412  									Headers: []*HttpHeader{
   413  										{
   414  											Name:  "Authorization",
   415  											Value: "Bearer ${variable}",
   416  										},
   417  									},
   418  									Body: &HttpRequestBody{
   419  										ContentType:     "application/json",
   420  										ContentEncoding: "gzip",
   421  										Payload:         []byte("${variable}"),
   422  									},
   423  								},
   424  							},
   425  						},
   426  					},
   427  				},
   428  			},
   429  			expectError: false,
   430  		},
   431  		"invalid internal job": {
   432  			input: Check{
   433  				Id:        1,
   434  				TenantId:  1,
   435  				Target:    "http://example.org/",
   436  				Job:       internalMarker,
   437  				Frequency: 1000,
   438  				Timeout:   1000,
   439  				Probes:    []int64{1},
   440  				Settings: CheckSettings{
   441  					Http: &HttpSettings{},
   442  				},
   443  			},
   444  			expectError: true,
   445  		},
   446  		"invalid internal target": {
   447  			input: Check{
   448  				Id:        1,
   449  				TenantId:  1,
   450  				Target:    internalMarker,
   451  				Job:       "valid-job",
   452  				Frequency: 1000,
   453  				Timeout:   1000,
   454  				Probes:    []int64{1},
   455  				Settings: CheckSettings{
   456  					Scripted: &ScriptedSettings{},
   457  				},
   458  			},
   459  			expectError: true,
   460  		},
   461  	}
   462  
   463  	// add trivial cases for all check types
   464  	for _, checkType := range CheckTypeValues() {
   465  		testcases["valid "+checkType.String()] = TestCase{
   466  			input:       GetCheckInstance(checkType),
   467  			expectError: false,
   468  		}
   469  	}
   470  
   471  	for name, testcase := range testcases {
   472  		t.Run(name, func(t *testing.T) {
   473  			err := testcase.input.Validate()
   474  			checkError(t, testcase.expectError, err, testcase.input)
   475  		})
   476  	}
   477  }
   478  
   479  func TestCheckType(t *testing.T) {
   480  	type testcase struct {
   481  		input    Check
   482  		expected CheckType
   483  	}
   484  
   485  	testcases := make(map[string]testcase)
   486  
   487  	for _, checkType := range CheckTypeValues() {
   488  		testcases[checkType.String()] = testcase{
   489  			input:    GetCheckInstance(checkType),
   490  			expected: checkType,
   491  		}
   492  	}
   493  
   494  	for name, testcase := range testcases {
   495  		t.Run(name, func(t *testing.T) {
   496  			actual := testcase.input.Type()
   497  			require.Equal(t, testcase.expected, actual)
   498  		})
   499  	}
   500  }
   501  
   502  func TestCheckClass(t *testing.T) {
   503  	testcases := map[string]struct {
   504  		input    Check
   505  		expected CheckClass
   506  	}{
   507  		CheckTypeDns.String(): {
   508  			input:    GetCheckInstance(CheckTypeDns),
   509  			expected: CheckClass_PROTOCOL,
   510  		},
   511  		CheckTypeHttp.String(): {
   512  			input:    GetCheckInstance(CheckTypeHttp),
   513  			expected: CheckClass_PROTOCOL,
   514  		},
   515  		CheckTypePing.String(): {
   516  			input:    GetCheckInstance(CheckTypePing),
   517  			expected: CheckClass_PROTOCOL,
   518  		},
   519  		CheckTypeTcp.String(): {
   520  			input:    GetCheckInstance(CheckTypeTcp),
   521  			expected: CheckClass_PROTOCOL,
   522  		},
   523  		CheckTypeTraceroute.String(): {
   524  			input:    GetCheckInstance(CheckTypeTraceroute),
   525  			expected: CheckClass_PROTOCOL,
   526  		},
   527  		CheckTypeScripted.String(): {
   528  			input:    GetCheckInstance(CheckTypeScripted),
   529  			expected: CheckClass_SCRIPTED,
   530  		},
   531  		CheckTypeMultiHttp.String(): {
   532  			input:    GetCheckInstance(CheckTypeMultiHttp),
   533  			expected: CheckClass_SCRIPTED,
   534  		},
   535  		CheckTypeGrpc.String(): {
   536  			input:    GetCheckInstance(CheckTypeGrpc),
   537  			expected: CheckClass_PROTOCOL,
   538  		},
   539  		CheckTypeBrowser.String(): {
   540  			input:    GetCheckInstance(CheckTypeBrowser),
   541  			expected: CheckClass_BROWSER,
   542  		},
   543  	}
   544  
   545  	for name, testcase := range testcases {
   546  		t.Run(name, func(t *testing.T) {
   547  			actual := testcase.input.Class()
   548  			require.Equal(t, testcase.expected, actual)
   549  		})
   550  	}
   551  
   552  	hasTest := make(map[CheckType]bool)
   553  	for _, checkType := range CheckTypeValues() {
   554  		hasTest[checkType] = false
   555  	}
   556  
   557  	for _, testcase := range testcases {
   558  		hasTest[testcase.input.Type()] = true
   559  	}
   560  
   561  	for checkType, found := range hasTest {
   562  		require.True(t, found, "missing test for check type %s", checkType)
   563  	}
   564  }
   565  
   566  func TestCheckTypeString(t *testing.T) {
   567  	testcases := map[string]struct {
   568  		input    CheckType
   569  		expected string
   570  	}{
   571  		"dns": {
   572  			input:    CheckTypeDns,
   573  			expected: "dns",
   574  		},
   575  		"http": {
   576  			input:    CheckTypeHttp,
   577  			expected: "http",
   578  		},
   579  		"ping": {
   580  			input:    CheckTypePing,
   581  			expected: "ping",
   582  		},
   583  		"tcp": {
   584  			input:    CheckTypeTcp,
   585  			expected: "tcp",
   586  		},
   587  		"traceroute": {
   588  			input:    CheckTypeTraceroute,
   589  			expected: "traceroute",
   590  		},
   591  		"scripted": {
   592  			input:    CheckTypeScripted,
   593  			expected: "scripted",
   594  		},
   595  		"multihttp": {
   596  			input:    CheckTypeMultiHttp,
   597  			expected: "multihttp",
   598  		},
   599  		"browser": {
   600  			input:    CheckTypeBrowser,
   601  			expected: "browser",
   602  		},
   603  	}
   604  
   605  	for name, testcase := range testcases {
   606  		actual := testcase.input.String()
   607  		require.Equal(t, testcase.expected, actual, "testcase %s", name)
   608  	}
   609  }
   610  
   611  func TestCheckTypeClass(t *testing.T) {
   612  	testcases := map[string]struct {
   613  		input    CheckType
   614  		expected CheckClass
   615  	}{
   616  		CheckTypeDns.String(): {
   617  			input:    CheckTypeDns,
   618  			expected: CheckClass_PROTOCOL,
   619  		},
   620  		CheckTypeHttp.String(): {
   621  			input:    CheckTypeHttp,
   622  			expected: CheckClass_PROTOCOL,
   623  		},
   624  		CheckTypePing.String(): {
   625  			input:    CheckTypePing,
   626  			expected: CheckClass_PROTOCOL,
   627  		},
   628  		CheckTypeTcp.String(): {
   629  			input:    CheckTypeTcp,
   630  			expected: CheckClass_PROTOCOL,
   631  		},
   632  		CheckTypeTraceroute.String(): {
   633  			input:    CheckTypeTraceroute,
   634  			expected: CheckClass_PROTOCOL,
   635  		},
   636  		CheckTypeScripted.String(): {
   637  			input:    CheckTypeScripted,
   638  			expected: CheckClass_SCRIPTED,
   639  		},
   640  		CheckTypeMultiHttp.String(): {
   641  			input:    CheckTypeMultiHttp,
   642  			expected: CheckClass_SCRIPTED,
   643  		},
   644  		CheckTypeGrpc.String(): {
   645  			input:    CheckTypeGrpc,
   646  			expected: CheckClass_PROTOCOL,
   647  		},
   648  		CheckTypeBrowser.String(): {
   649  			input:    CheckTypeBrowser,
   650  			expected: CheckClass_BROWSER,
   651  		},
   652  	}
   653  
   654  	for name, testcase := range testcases {
   655  		t.Run(name, func(t *testing.T) {
   656  			actual := testcase.input.Class()
   657  			require.Equal(t, testcase.expected, actual)
   658  		})
   659  	}
   660  
   661  	hasTest := make(map[CheckType]bool)
   662  	for _, checkType := range CheckTypeValues() {
   663  		hasTest[checkType] = false
   664  	}
   665  
   666  	for _, testcase := range testcases {
   667  		hasTest[testcase.input] = true
   668  	}
   669  
   670  	for checkType, found := range hasTest {
   671  		require.True(t, found, "missing test for check type %s", checkType)
   672  	}
   673  }
   674  
   675  func TestValidateHost(t *testing.T) {
   676  	testcases := map[string]struct {
   677  		input       string
   678  		expectError bool
   679  	}{
   680  		// valid hostnames
   681  		"hostname": {
   682  			input:       "grafana.com",
   683  			expectError: false,
   684  		},
   685  
   686  		// invalid hostnames
   687  		"invalid hostname": {
   688  			input:       "grafana-com",
   689  			expectError: true,
   690  		},
   691  
   692  		// valid IP addresses
   693  		"IPv4": {
   694  			input:       "1.2.3.4",
   695  			expectError: false,
   696  		},
   697  		"IPv4 loopback": {
   698  			input:       "127.0.0.1",
   699  			expectError: false,
   700  		},
   701  		"IPv4 local multicast": {
   702  			input:       "224.0.0.1",
   703  			expectError: false,
   704  		},
   705  		"IPv4 control multicast": {
   706  			input:       "224.0.1.1",
   707  			expectError: false,
   708  		},
   709  		"IPv6": {
   710  			input:       "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
   711  			expectError: false,
   712  		},
   713  		"IPv6 unicast": {
   714  			input:       "2001:DB8:0:0:8:800:200C:417A",
   715  			expectError: false,
   716  		},
   717  		"IPv6 link-local unicast": {
   718  			input:       "FE80::1",
   719  			expectError: false,
   720  		},
   721  		"IPv6 multicast": {
   722  			input:       "FF01:0:0:0:0:0:0:101",
   723  			expectError: false,
   724  		},
   725  		"IPv6 multicast all local nodes": {
   726  			input:       "FF02::1",
   727  			expectError: false,
   728  		},
   729  		"IPv6 loopback": {
   730  			input:       "0:0:0:0:0:0:0:1",
   731  			expectError: false,
   732  		},
   733  		"IPv6 loopback short": {
   734  			input:       "::1",
   735  			expectError: false,
   736  		},
   737  		"IPv6 unespecified short": {
   738  			input:       "::",
   739  			expectError: false,
   740  		},
   741  		"IPv4 as IPv6": {
   742  			input:       "0:0:0:0:0:0:13.1.68.3",
   743  			expectError: false,
   744  		},
   745  		"IPv4 in IPv6": {
   746  			input:       "0:0:0:0:FF:FF:13.1.68.3",
   747  			expectError: false,
   748  		},
   749  		"IPv4 as IPv6 short": {
   750  			input:       "::13.1.68.3",
   751  			expectError: false,
   752  		},
   753  		"IPv4 in IPv6 short": {
   754  			input:       "::FFFF:13.1.68.3",
   755  			expectError: false,
   756  		},
   757  
   758  		// invalid IP addresses
   759  		"invalid IPv4": {
   760  			input:       "0.0.0.256",
   761  			expectError: true,
   762  		},
   763  		"invalid IPv6": {
   764  			input:       "::10000",
   765  			expectError: true,
   766  		},
   767  	}
   768  
   769  	for name, testcase := range testcases {
   770  		t.Run(name, func(t *testing.T) {
   771  			err := validateHost(testcase.input)
   772  			checkError(t, testcase.expectError, err, testcase.input)
   773  		})
   774  	}
   775  }
   776  
   777  func TestValidateDnsTarget(t *testing.T) {
   778  	testcases := map[string]struct {
   779  		input       string
   780  		expectError bool
   781  	}{
   782  		// valid hostnames
   783  		"hostname": {
   784  			input:       "grafana.com",
   785  			expectError: false,
   786  		},
   787  
   788  		// localhost is valid
   789  		"localhost": {
   790  			input:       "localhost",
   791  			expectError: false,
   792  		},
   793  
   794  		// localhost. is valid
   795  		"localhost.": {
   796  			input:       "localhost.",
   797  			expectError: false,
   798  		},
   799  
   800  		// single label fully qualified dns name is valid
   801  		"org.": {
   802  			input:       "org.",
   803  			expectError: false,
   804  		},
   805  
   806  		// multi-label dns name is valid
   807  		"grafana.com.": {
   808  			input:       "grafana.com.",
   809  			expectError: false,
   810  		},
   811  
   812  		// single label is invalid
   813  		"org": {
   814  			input:       "org",
   815  			expectError: true,
   816  		},
   817  
   818  		// For DNS entries, IP address is valid
   819  		"127.0.0.1": {
   820  			input:       "127.0.0.1",
   821  			expectError: false,
   822  		},
   823  
   824  		// IP address disguised as multi-label fully qualified
   825  		// dns name are also valid
   826  		"127.0.0.1.": {
   827  			input:       "127.0.0.1.",
   828  			expectError: false,
   829  		},
   830  
   831  		// empty label
   832  		"foo..bar": {
   833  			input:       "foo..bar",
   834  			expectError: true,
   835  		},
   836  
   837  		// label too long
   838  		"foo.a62a.bar": {
   839  			input:       "foo." + strings.Repeat("a", 64) + ".bar",
   840  			expectError: true,
   841  		},
   842  
   843  		// zeroconf
   844  		"_srv._tcp.example.org": {
   845  			input:       "_srv._tcp.example.org",
   846  			expectError: false,
   847  		},
   848  	}
   849  
   850  	for name, testcase := range testcases {
   851  		t.Run(name, func(t *testing.T) {
   852  			err := validateDnsTarget(testcase.input)
   853  			checkError(t, testcase.expectError, err, testcase.input)
   854  		})
   855  	}
   856  }
   857  
   858  func TestCheckFQHN(t *testing.T) {
   859  	genstr := func(n int) string {
   860  		var sb strings.Builder
   861  		sb.Grow(n)
   862  
   863  		for i := 1; i <= n; i++ {
   864  			_ = sb.WriteByte(byte('a' + (i % ('z' - 'a' + 1))))
   865  		}
   866  
   867  		return sb.String()
   868  	}
   869  
   870  	testcases := map[string]struct {
   871  		input       string
   872  		expectError bool
   873  	}{
   874  		"empty": {
   875  			input:       "",
   876  			expectError: true,
   877  		},
   878  		"too long": {
   879  			input:       genstr(256),
   880  			expectError: true,
   881  		},
   882  		"start with .": {
   883  			input:       ".x",
   884  			expectError: true,
   885  		},
   886  		"end with . 1": {
   887  			input:       "x.",
   888  			expectError: true,
   889  		},
   890  		"end with . 2": {
   891  			input:       "x.y.",
   892  			expectError: true,
   893  		},
   894  		"must have at least two labels": {
   895  			input:       "x",
   896  			expectError: true,
   897  		},
   898  		"label must start with letter or digit 1": {
   899  			input:       "0.x",
   900  			expectError: false,
   901  		},
   902  		"label must start with letter or digit 2": {
   903  			input:       "-.x",
   904  			expectError: true,
   905  		},
   906  		"label must start with letter or digit 3": {
   907  			input:       "x.y",
   908  			expectError: false,
   909  		},
   910  		"label must start with letter or digit 4": {
   911  			input:       "1x.y",
   912  			expectError: false,
   913  		},
   914  		"label must end with a letter or digit 1": {
   915  			input:       "-.x",
   916  			expectError: true,
   917  		},
   918  		"label must end with a letter or digit 2": {
   919  			input:       "x.y",
   920  			expectError: false,
   921  		},
   922  		"label must end with a letter or digit 3": {
   923  			input:       "xy.z",
   924  			expectError: false,
   925  		},
   926  		"label must end with a letter or digit 4": {
   927  			input:       "x1.y",
   928  			expectError: false,
   929  		},
   930  		"label must contain only letters, digits or dash 1": {
   931  			input:       "x=y.z",
   932  			expectError: true,
   933  		},
   934  		"label must contain only letters, digits or dash 2": {
   935  			input:       "x-0.y-z",
   936  			expectError: false,
   937  		},
   938  		"labels must be 63 characters or less 1": {
   939  			input:       genstr(64) + ".x",
   940  			expectError: true,
   941  		},
   942  		"labels must be 63 characters or less 2": {
   943  			input:       genstr(63) + "." + genstr(63),
   944  			expectError: false,
   945  		},
   946  		"valid, all lowercase": {
   947  			input:       "grafana.com",
   948  			expectError: false,
   949  		},
   950  		"valid, all uppercase": {
   951  			input:       "GRAFANA.COM",
   952  			expectError: false,
   953  		},
   954  		"valid, mixed case": {
   955  			input:       "gRaFaNa.CoM",
   956  			expectError: false,
   957  		},
   958  	}
   959  
   960  	for name, testcase := range testcases {
   961  		t.Run(name, func(t *testing.T) {
   962  			err := checkFQHN(testcase.input)
   963  			checkError(t, testcase.expectError, err, testcase.input)
   964  		})
   965  	}
   966  }
   967  
   968  func TestValidateHostPort(t *testing.T) {
   969  	testcases := map[string]struct {
   970  		input       string
   971  		expectError bool
   972  	}{
   973  		"trivial": {
   974  			input:       "grafana.com:25",
   975  			expectError: false,
   976  		},
   977  		"port 1": {
   978  			input:       "grafana.com:1",
   979  			expectError: false,
   980  		},
   981  		"port 65535": {
   982  			input:       "grafana.com:65535",
   983  			expectError: false,
   984  		},
   985  
   986  		"blank": {
   987  			input:       "",
   988  			expectError: true,
   989  		},
   990  
   991  		// invalid hosts
   992  		"no host": {
   993  			input:       ":25",
   994  			expectError: true,
   995  		},
   996  		"invalid domain": {
   997  			input:       "x:25",
   998  			expectError: true,
   999  		},
  1000  		"invalid host": {
  1001  			input:       "-.x:25",
  1002  			expectError: true,
  1003  		},
  1004  
  1005  		// invalid ports
  1006  		"no port": {
  1007  			input:       "grafana.com",
  1008  			expectError: true,
  1009  		},
  1010  		"empty port": {
  1011  			input:       "grafana.com:",
  1012  			expectError: true,
  1013  		},
  1014  		"port zero": {
  1015  			input:       "grafana.com:0",
  1016  			expectError: true,
  1017  		},
  1018  		"negative port": {
  1019  			input:       "grafana.com:-1",
  1020  			expectError: true,
  1021  		},
  1022  		"port too large": {
  1023  			input:       "grafana.com:65536",
  1024  			expectError: true,
  1025  		},
  1026  	}
  1027  
  1028  	for name, testcase := range testcases {
  1029  		t.Run(name, func(t *testing.T) {
  1030  			err := validateHostPort(testcase.input)
  1031  			checkError(t, testcase.expectError, err, testcase.input)
  1032  		})
  1033  	}
  1034  }
  1035  
  1036  func TestValidateHttpUrl(t *testing.T) {
  1037  	testcases := map[string]struct {
  1038  		input       string
  1039  		expectError bool
  1040  	}{
  1041  		"http": {
  1042  			input:       "http://example.org/",
  1043  			expectError: false,
  1044  		},
  1045  		"https": {
  1046  			input:       "https://example.org/",
  1047  			expectError: false,
  1048  		},
  1049  		"http port": {
  1050  			input:       "http://example.org:8000/",
  1051  			expectError: false,
  1052  		},
  1053  		"https port": {
  1054  			input:       "https://example.org:8443/",
  1055  			expectError: false,
  1056  		},
  1057  		"ipv4": {
  1058  			input:       "http://127.0.0.1/",
  1059  			expectError: false,
  1060  		},
  1061  		"ipv6": {
  1062  			input:       "http://[::1]/",
  1063  			expectError: false,
  1064  		},
  1065  		"ipv4 port": {
  1066  			input:       "http://127.0.0.1:80/",
  1067  			expectError: false,
  1068  		},
  1069  		"ipv6 port": {
  1070  			input:       "http://[::1]:80/",
  1071  			expectError: false,
  1072  		},
  1073  		"invalid scheme": {
  1074  			input:       "ftp://example.org/",
  1075  			expectError: true,
  1076  		},
  1077  		"with username": {
  1078  			input:       "http://user@example.org/",
  1079  			expectError: true,
  1080  		},
  1081  		"with username and password": {
  1082  			input:       "http://user:password@example.org/",
  1083  			expectError: true,
  1084  		},
  1085  
  1086  		"blank": {
  1087  			input:       "",
  1088  			expectError: true,
  1089  		},
  1090  		"no host": {
  1091  			input:       "http://",
  1092  			expectError: true,
  1093  		},
  1094  
  1095  		// these are covered by TestValidateHostPort
  1096  		"bad host": {
  1097  			input:       "http://test/",
  1098  			expectError: true,
  1099  		},
  1100  		"port too large": {
  1101  			input:       "http://example.org:65536/",
  1102  			expectError: true,
  1103  		},
  1104  		"port zero": {
  1105  			input:       "http://example.org:0/",
  1106  			expectError: true,
  1107  		},
  1108  		"negative port": {
  1109  			input:       "http://example.org:-1/",
  1110  			expectError: true,
  1111  		},
  1112  	}
  1113  
  1114  	for name, testcase := range testcases {
  1115  		t.Run(name, func(t *testing.T) {
  1116  			err := validateHttpUrl(testcase.input)
  1117  			checkError(t, testcase.expectError, err, testcase.input)
  1118  		})
  1119  	}
  1120  }
  1121  
  1122  func TestValidateLabel(t *testing.T) {
  1123  	genString := func(n int) string {
  1124  		var s strings.Builder
  1125  		s.Grow(n)
  1126  
  1127  		for range n {
  1128  			_ = s.WriteByte('x')
  1129  		}
  1130  
  1131  		return s.String()
  1132  	}
  1133  
  1134  	testcases := map[string]struct {
  1135  		input       Label
  1136  		expectError bool
  1137  	}{
  1138  		"trivial": {
  1139  			input:       Label{Name: "label", Value: "value"},
  1140  			expectError: false,
  1141  		},
  1142  		"name with underscore": {
  1143  			input:       Label{Name: "some_name", Value: "value"},
  1144  			expectError: false,
  1145  		},
  1146  		"name with leading underscore": {
  1147  			input:       Label{Name: "_name", Value: "value"},
  1148  			expectError: false,
  1149  		},
  1150  		"empty name": {
  1151  			input:       Label{Name: "", Value: "value"},
  1152  			expectError: true,
  1153  		},
  1154  		"invalid name": {
  1155  			input:       Label{Name: "foo@bar", Value: "value"},
  1156  			expectError: true,
  1157  		},
  1158  		"name with trailing blank": { // issue #99
  1159  			input:       Label{Name: "name ", Value: "value"},
  1160  			expectError: true,
  1161  		},
  1162  		"empty value": {
  1163  			input:       Label{Name: "name", Value: ""},
  1164  			expectError: true,
  1165  		},
  1166  		"long value": {
  1167  			input:       Label{Name: "name", Value: genString(MaxLabelValueLength)},
  1168  			expectError: false,
  1169  		},
  1170  		"value too long": {
  1171  			input:       Label{Name: "name", Value: genString(MaxLabelValueLength + 1)},
  1172  			expectError: true,
  1173  		},
  1174  	}
  1175  
  1176  	for name, testcase := range testcases {
  1177  		t.Run(name, func(t *testing.T) {
  1178  			err := testcase.input.Validate()
  1179  			checkError(t, testcase.expectError, err, testcase.input)
  1180  		})
  1181  	}
  1182  }
  1183  
  1184  func TestHttpSettingsValidate(t *testing.T) {
  1185  	testcases := map[string]struct {
  1186  		input       HttpSettings
  1187  		expectError bool
  1188  	}{
  1189  		"trivial": {
  1190  			input:       HttpSettings{},
  1191  			expectError: false,
  1192  		},
  1193  		"valid headers": {
  1194  			input: HttpSettings{
  1195  				Headers: []string{"header: value"},
  1196  			},
  1197  			expectError: false,
  1198  		},
  1199  		"no value is OK": {
  1200  			input: HttpSettings{
  1201  				Headers: []string{"header:"},
  1202  			},
  1203  			expectError: false,
  1204  		},
  1205  		"empty header is not OK": {
  1206  			input: HttpSettings{
  1207  				Headers: []string{": value"},
  1208  			},
  1209  			expectError: true,
  1210  		},
  1211  		"empty": {
  1212  			input: HttpSettings{
  1213  				Headers: []string{""},
  1214  			},
  1215  			expectError: true,
  1216  		},
  1217  		"no colon": {
  1218  			input: HttpSettings{
  1219  				Headers: []string{"header"},
  1220  			},
  1221  			expectError: true,
  1222  		},
  1223  		"multiple colons": {
  1224  			input: HttpSettings{
  1225  				Headers: []string{"origin:https://www.grafana.com"},
  1226  			},
  1227  			expectError: false,
  1228  		},
  1229  		"invalid name": {
  1230  			input: HttpSettings{
  1231  				Headers: []string{"hea;der: value"},
  1232  			},
  1233  			expectError: true,
  1234  		},
  1235  		"empty header/value": {
  1236  			input: HttpSettings{
  1237  				Headers: []string{":"},
  1238  			},
  1239  			expectError: true,
  1240  		},
  1241  		"blank header/value": {
  1242  			input: HttpSettings{
  1243  				Headers: []string{" : "},
  1244  			},
  1245  			expectError: true,
  1246  		},
  1247  		"non-ascii value": {
  1248  			input: HttpSettings{
  1249  				Headers: []string{"header: यूनिकोड टेक्स्ट"},
  1250  			},
  1251  			expectError: false,
  1252  		},
  1253  	}
  1254  
  1255  	for name, testcase := range testcases {
  1256  		t.Run(name, func(t *testing.T) {
  1257  			err := testcase.input.Validate()
  1258  			checkError(t, testcase.expectError, err, testcase.input)
  1259  		})
  1260  	}
  1261  }
  1262  
  1263  func TestCompressionAlgorithmMarshal(t *testing.T) {
  1264  	type testStruct struct {
  1265  		Compression CompressionAlgorithm `json:"compression,omitempty"`
  1266  	}
  1267  
  1268  	testcases := map[string]struct {
  1269  		unserialized testStruct
  1270  		serialized   []byte
  1271  	}{
  1272  		"none": {
  1273  			unserialized: testStruct{
  1274  				Compression: CompressionAlgorithm_none,
  1275  			},
  1276  			serialized: []byte(`{}`),
  1277  		},
  1278  		"gzip": {
  1279  			unserialized: testStruct{
  1280  				Compression: CompressionAlgorithm_gzip,
  1281  			},
  1282  			serialized: []byte(`{"compression":"gzip"}`),
  1283  		},
  1284  		"br": {
  1285  			unserialized: testStruct{
  1286  				Compression: CompressionAlgorithm_br,
  1287  			},
  1288  			serialized: []byte(`{"compression":"br"}`),
  1289  		},
  1290  		"deflate": {
  1291  			unserialized: testStruct{
  1292  				Compression: CompressionAlgorithm_deflate,
  1293  			},
  1294  			serialized: []byte(`{"compression":"deflate"}`),
  1295  		},
  1296  		"identity": {
  1297  			unserialized: testStruct{
  1298  				Compression: CompressionAlgorithm_identity,
  1299  			},
  1300  			serialized: []byte(`{"compression":"identity"}`),
  1301  		},
  1302  	}
  1303  
  1304  	for name, tc := range testcases {
  1305  		t.Run(name, func(t *testing.T) {
  1306  			actual, err := json.Marshal(tc.unserialized)
  1307  			require.NoError(t, err)
  1308  			require.Equal(t, tc.serialized, actual)
  1309  		})
  1310  	}
  1311  }
  1312  
  1313  func TestCompressionAlgorithmUnmarshal(t *testing.T) {
  1314  	type testStruct struct {
  1315  		Compression CompressionAlgorithm `json:"compression,omitempty"`
  1316  	}
  1317  
  1318  	testcases := map[string]struct {
  1319  		unserialized testStruct
  1320  		serialized   []byte
  1321  	}{
  1322  		"none": {
  1323  			unserialized: testStruct{
  1324  				Compression: CompressionAlgorithm_none,
  1325  			},
  1326  			serialized: []byte(`{}`),
  1327  		},
  1328  		"empty": {
  1329  			unserialized: testStruct{
  1330  				Compression: CompressionAlgorithm_none,
  1331  			},
  1332  			serialized: []byte(`{"compression":""}`),
  1333  		},
  1334  		"null": {
  1335  			unserialized: testStruct{
  1336  				Compression: CompressionAlgorithm_none,
  1337  			},
  1338  			serialized: []byte(`{"compression":null}`),
  1339  		},
  1340  		"gzip": {
  1341  			unserialized: testStruct{
  1342  				Compression: CompressionAlgorithm_gzip,
  1343  			},
  1344  			serialized: []byte(`{"compression":"gzip"}`),
  1345  		},
  1346  		"br": {
  1347  			unserialized: testStruct{
  1348  				Compression: CompressionAlgorithm_br,
  1349  			},
  1350  			serialized: []byte(`{"compression":"br"}`),
  1351  		},
  1352  		"deflate": {
  1353  			unserialized: testStruct{
  1354  				Compression: CompressionAlgorithm_deflate,
  1355  			},
  1356  			serialized: []byte(`{"compression":"deflate"}`),
  1357  		},
  1358  		"identity": {
  1359  			unserialized: testStruct{
  1360  				Compression: CompressionAlgorithm_identity,
  1361  			},
  1362  			serialized: []byte(`{"compression":"identity"}`),
  1363  		},
  1364  	}
  1365  
  1366  	for name, tc := range testcases {
  1367  		t.Run(name, func(t *testing.T) {
  1368  			var actual testStruct
  1369  
  1370  			err := json.Unmarshal(tc.serialized, &actual)
  1371  			require.NoError(t, err)
  1372  			require.Equal(t, tc.unserialized, actual)
  1373  		})
  1374  	}
  1375  }
  1376  
  1377  func checkError(t *testing.T, expectError bool, err error, input any) {
  1378  	t.Helper()
  1379  
  1380  	switch {
  1381  	case expectError && err == nil:
  1382  		// unexpected success
  1383  		t.Errorf("expecting failure for input %q, but got success", input)
  1384  
  1385  	case !expectError && err != nil:
  1386  		// unexpected failure
  1387  		t.Errorf("expecting success for input %q, but got failure: %s", input, err.Error())
  1388  
  1389  	case expectError && err != nil:
  1390  		// expected failure
  1391  		if *testDebugOutput {
  1392  			t.Logf("expecting failure for input %q, got failure: %s", input, err.Error())
  1393  		}
  1394  
  1395  	case !expectError && err == nil:
  1396  		// expected success
  1397  		if *testDebugOutput {
  1398  			t.Logf("expecting success for input %q, got success", input)
  1399  		}
  1400  	}
  1401  }
  1402  
  1403  type TestCases[T any] map[string]struct {
  1404  	input       T
  1405  	expectError bool
  1406  }
  1407  
  1408  func testValidate[T validatable](t *testing.T, testcases TestCases[T]) {
  1409  	t.Helper()
  1410  
  1411  	for name, testcase := range testcases {
  1412  		t.Run(name, func(t *testing.T) {
  1413  			err := testcase.input.Validate()
  1414  			if testcase.expectError {
  1415  				require.Error(t, err)
  1416  			} else {
  1417  				require.NoError(t, err)
  1418  			}
  1419  		})
  1420  	}
  1421  }
  1422  
  1423  type testValidatable struct {
  1424  	err error
  1425  }
  1426  
  1427  func (v testValidatable) Validate() error {
  1428  	return v.err
  1429  }
  1430  
  1431  // TestValidateCollection vefifies that validateCollection calls `Validate` on
  1432  // each member of a collection of `Validatable` objects.
  1433  func TestValidateCollection(t *testing.T) {
  1434  	invalid := errors.New("invalid")
  1435  
  1436  	testcases := map[string]struct {
  1437  		input       []testValidatable
  1438  		expectError bool
  1439  	}{
  1440  		"empty": {
  1441  			input:       nil,
  1442  			expectError: false,
  1443  		},
  1444  		"one valid": {
  1445  			input:       []testValidatable{{err: nil}},
  1446  			expectError: false,
  1447  		},
  1448  		"one invalid": {
  1449  			input:       []testValidatable{{err: errors.New("invalid")}},
  1450  			expectError: true,
  1451  		},
  1452  		"two valid": {
  1453  			input:       []testValidatable{{err: nil}, {err: nil}},
  1454  			expectError: false,
  1455  		},
  1456  		"two invalid": {
  1457  			input:       []testValidatable{{err: invalid}, {err: invalid}},
  1458  			expectError: true,
  1459  		},
  1460  		"mixed": {
  1461  			input:       []testValidatable{{err: nil}, {err: invalid}},
  1462  			expectError: true,
  1463  		},
  1464  	}
  1465  
  1466  	for name, testcase := range testcases {
  1467  		t.Run(name, func(t *testing.T) {
  1468  			err := validateCollection(testcase.input)
  1469  			if testcase.expectError {
  1470  				require.Error(t, err)
  1471  			} else {
  1472  				require.NoError(t, err)
  1473  			}
  1474  		})
  1475  	}
  1476  }
  1477  
  1478  func TestHttpHeaderValidate(t *testing.T) {
  1479  	testValidate(t, TestCases[HttpHeader]{
  1480  		"valid": {
  1481  			input:       HttpHeader{Name: "name", Value: "value"},
  1482  			expectError: false,
  1483  		},
  1484  		"an empty value is valid": {
  1485  			input:       HttpHeader{Name: "xyz", Value: ""},
  1486  			expectError: false,
  1487  		},
  1488  		"an empty name is invalid": {
  1489  			input:       HttpHeader{Name: "", Value: "xxx"},
  1490  			expectError: true,
  1491  		},
  1492  		"invalid header name": {
  1493  			input:       HttpHeader{Name: "x y z", Value: ""},
  1494  			expectError: true,
  1495  		},
  1496  	})
  1497  }
  1498  
  1499  func TestQueryFieldValidate(t *testing.T) {
  1500  	testValidate(t, TestCases[QueryField]{
  1501  		"valid": {
  1502  			input:       QueryField{Name: "name", Value: "value"},
  1503  			expectError: false,
  1504  		},
  1505  		"an empty value is valid": {
  1506  			input:       QueryField{Name: "xyz", Value: ""},
  1507  			expectError: false,
  1508  		},
  1509  		"an empty name is invalid": {
  1510  			input:       QueryField{Name: "", Value: "xxx"},
  1511  			expectError: true,
  1512  		},
  1513  		"a name with spaces is valid": {
  1514  			input:       QueryField{Name: "x y z", Value: ""},
  1515  			expectError: false,
  1516  		},
  1517  	})
  1518  }
  1519  
  1520  func TestHttpRequestBodyValidate(t *testing.T) {
  1521  	testValidate(t, TestCases[*HttpRequestBody]{
  1522  		"nil is valid": {
  1523  			input:       nil,
  1524  			expectError: false,
  1525  		},
  1526  		"no content type is invalid": {
  1527  			input: &HttpRequestBody{
  1528  				ContentType:     "",
  1529  				ContentEncoding: "identity",
  1530  				Payload:         []byte{42},
  1531  			},
  1532  			expectError: true,
  1533  		},
  1534  		"empty content encoding is valid": {
  1535  			input: &HttpRequestBody{
  1536  				ContentType:     "text/plain",
  1537  				ContentEncoding: "",
  1538  				Payload:         []byte{42},
  1539  			},
  1540  			expectError: false,
  1541  		},
  1542  		"content type must be a valid media type": {
  1543  			input: &HttpRequestBody{
  1544  				// media types should not have space before the /
  1545  				ContentType:     "json file",
  1546  				ContentEncoding: "",
  1547  				Payload:         []byte{42},
  1548  			},
  1549  			expectError: true,
  1550  		},
  1551  		"empty payload is valid": {
  1552  			input: &HttpRequestBody{
  1553  				ContentType:     "text/plain",
  1554  				ContentEncoding: "identity",
  1555  				Payload:         []byte{},
  1556  			},
  1557  			expectError: false,
  1558  		},
  1559  	})
  1560  }
  1561  
  1562  func TestMultiHttpEntryAssertionValidate(t *testing.T) {
  1563  	testValidate(t, TestCases[*MultiHttpEntryAssertion]{
  1564  		"nil is valid": {
  1565  			input:       nil,
  1566  			expectError: false,
  1567  		},
  1568  		"zero value is invalid": {
  1569  			// The zero value is a text assertion with "body" as the
  1570  			// subject and "contains" as the condition. The
  1571  			// "value" is empty, and that is not allowed.
  1572  			input:       &MultiHttpEntryAssertion{},
  1573  			expectError: true,
  1574  		},
  1575  		"text+body+contains with valid value": {
  1576  			input: &MultiHttpEntryAssertion{
  1577  				Type:      MultiHttpEntryAssertionType_TEXT,
  1578  				Subject:   MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
  1579  				Condition: MultiHttpEntryAssertionConditionVariant_CONTAINS,
  1580  				Value:     "foobar",
  1581  			},
  1582  			expectError: false,
  1583  		},
  1584  		"text does not allow expressions": {
  1585  			input: &MultiHttpEntryAssertion{
  1586  				Type:       MultiHttpEntryAssertionType_TEXT,
  1587  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
  1588  				Condition:  MultiHttpEntryAssertionConditionVariant_CONTAINS,
  1589  				Expression: "foo",
  1590  				Value:      "bar",
  1591  			},
  1592  			expectError: true,
  1593  		},
  1594  		"json path value with valid value": {
  1595  			input: &MultiHttpEntryAssertion{
  1596  				Type:       MultiHttpEntryAssertionType_JSON_PATH_VALUE,
  1597  				Condition:  MultiHttpEntryAssertionConditionVariant_CONTAINS,
  1598  				Expression: "$.data",
  1599  				Value:      "foo",
  1600  			},
  1601  			expectError: false,
  1602  		},
  1603  		"json path value does not allow for setting the subject": {
  1604  			input: &MultiHttpEntryAssertion{
  1605  				Type:       MultiHttpEntryAssertionType_JSON_PATH_VALUE,
  1606  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS, // invalid
  1607  				Condition:  MultiHttpEntryAssertionConditionVariant_CONTAINS,
  1608  				Expression: "$.data",
  1609  				Value:      "foo",
  1610  			},
  1611  			expectError: true,
  1612  		},
  1613  		"valid json path assertion": {
  1614  			input: &MultiHttpEntryAssertion{
  1615  				Type:       MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
  1616  				Expression: "$.foo",
  1617  			},
  1618  			expectError: false,
  1619  		},
  1620  		"json path assertion does not allow subject": {
  1621  			input: &MultiHttpEntryAssertion{
  1622  				Type:       MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
  1623  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS, // invalid
  1624  				Expression: "$.foo",
  1625  			},
  1626  			expectError: true,
  1627  		},
  1628  		"json path assertion does not allow condition": {
  1629  			input: &MultiHttpEntryAssertion{
  1630  				Type:       MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
  1631  				Condition:  1, // invalid
  1632  				Expression: "$.foo",
  1633  			},
  1634  			expectError: true,
  1635  		},
  1636  		"json path assertion does not allow value": {
  1637  			input: &MultiHttpEntryAssertion{
  1638  				Type:       MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
  1639  				Value:      "bar", // invalid
  1640  				Expression: "$.foo",
  1641  			},
  1642  			expectError: true,
  1643  		},
  1644  		"valid regexp assertion": {
  1645  			input: &MultiHttpEntryAssertion{
  1646  				Type:       MultiHttpEntryAssertionType_REGEX_ASSERTION,
  1647  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
  1648  				Expression: "foo",
  1649  			},
  1650  			expectError: false,
  1651  		},
  1652  		"regexp assertion does not allow value": {
  1653  			input: &MultiHttpEntryAssertion{
  1654  				Type:       MultiHttpEntryAssertionType_REGEX_ASSERTION,
  1655  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
  1656  				Expression: "foo",
  1657  				Value:      "bar", // invalid
  1658  			},
  1659  			expectError: true,
  1660  		},
  1661  		"regexp assertion does not allow condition": {
  1662  			input: &MultiHttpEntryAssertion{
  1663  				Type:       MultiHttpEntryAssertionType_REGEX_ASSERTION,
  1664  				Subject:    MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
  1665  				Expression: "foo",
  1666  				Condition:  1, // invalid
  1667  			},
  1668  			expectError: true,
  1669  		},
  1670  	})
  1671  }
  1672  
  1673  func TestHttpRegexFields(t *testing.T) {
  1674  	testValidate(t, TestCases[*HttpSettings]{
  1675  		"body matches regexp parses": {
  1676  			input: &HttpSettings{
  1677  				FailIfBodyMatchesRegexp: []string{".*good stuff.*"},
  1678  			},
  1679  			expectError: false,
  1680  		},
  1681  		"body matches invalid regexp errors": {
  1682  			input: &HttpSettings{
  1683  				FailIfBodyMatchesRegexp: []string{"*good stuff*"},
  1684  			},
  1685  			expectError: true,
  1686  		},
  1687  		"body not matches regexp parses": {
  1688  			input: &HttpSettings{
  1689  				FailIfBodyNotMatchesRegexp: []string{".*good stuff.*"},
  1690  			},
  1691  			expectError: false,
  1692  		},
  1693  		"body not matches invalid regexp errors": {
  1694  			input: &HttpSettings{
  1695  				FailIfBodyNotMatchesRegexp: []string{"*good stuff*"},
  1696  			},
  1697  			expectError: true,
  1698  		},
  1699  		"header matches regexp parses": {
  1700  			input: &HttpSettings{
  1701  				FailIfHeaderMatchesRegexp: []HeaderMatch{{Regexp: ".*good stuff.*"}},
  1702  			},
  1703  			expectError: false,
  1704  		},
  1705  		"header matches invalid regexp errors": {
  1706  			input: &HttpSettings{
  1707  				FailIfHeaderMatchesRegexp: []HeaderMatch{{Regexp: "*good stuff*"}},
  1708  			},
  1709  			expectError: true,
  1710  		},
  1711  		"header not matches regexp parses": {
  1712  			input: &HttpSettings{
  1713  				FailIfHeaderNotMatchesRegexp: []HeaderMatch{{Regexp: ".*good stuff.*"}},
  1714  			},
  1715  			expectError: false,
  1716  		},
  1717  		"header not matches invalid regexp errors": {
  1718  			input: &HttpSettings{
  1719  				FailIfHeaderNotMatchesRegexp: []HeaderMatch{{Regexp: "*good stuff*"}},
  1720  			},
  1721  			expectError: true,
  1722  		},
  1723  	})
  1724  }
  1725  
  1726  func TestMultiHttpEntryVariableValidate(t *testing.T) {
  1727  	testValidate(t, TestCases[*MultiHttpEntryVariable]{
  1728  		"zero value": {
  1729  			// The zero value is invalid because a name and an
  1730  			// expression are required.
  1731  			input:       &MultiHttpEntryVariable{},
  1732  			expectError: true,
  1733  		},
  1734  		"json path": {
  1735  			input: &MultiHttpEntryVariable{
  1736  				Type:       MultiHttpEntryVariableType_JSON_PATH,
  1737  				Name:       "foo",
  1738  				Expression: "$.bar",
  1739  			},
  1740  			expectError: false,
  1741  		},
  1742  		"json path without a name is invalid": {
  1743  			input: &MultiHttpEntryVariable{
  1744  				Type:       MultiHttpEntryVariableType_JSON_PATH,
  1745  				Name:       "",
  1746  				Expression: "$.bar",
  1747  			},
  1748  			expectError: true,
  1749  		},
  1750  		"json path without an expression is invalid": {
  1751  			input: &MultiHttpEntryVariable{
  1752  				Type:       MultiHttpEntryVariableType_JSON_PATH,
  1753  				Name:       "foo",
  1754  				Expression: "",
  1755  			},
  1756  			expectError: true,
  1757  		},
  1758  		"json path with an attribute is invalid": {
  1759  			input: &MultiHttpEntryVariable{
  1760  				Type:       MultiHttpEntryVariableType_JSON_PATH,
  1761  				Name:       "foo",
  1762  				Expression: "bar",
  1763  				Attribute:  "baz",
  1764  			},
  1765  			expectError: true,
  1766  		},
  1767  		"regexp": {
  1768  			input: &MultiHttpEntryVariable{
  1769  				Type:       MultiHttpEntryVariableType_REGEX,
  1770  				Name:       "foo",
  1771  				Expression: "bar",
  1772  			},
  1773  			expectError: false,
  1774  		},
  1775  		"regexp without a name is invalid": {
  1776  			input: &MultiHttpEntryVariable{
  1777  				Type:       MultiHttpEntryVariableType_REGEX,
  1778  				Name:       "",
  1779  				Expression: "bar",
  1780  			},
  1781  			expectError: true,
  1782  		},
  1783  		"regexp without an expression is invalid": {
  1784  			input: &MultiHttpEntryVariable{
  1785  				Type:       MultiHttpEntryVariableType_REGEX,
  1786  				Name:       "foo",
  1787  				Expression: "",
  1788  			},
  1789  			expectError: true,
  1790  		},
  1791  		"regexp with an attribute is invalid": {
  1792  			input: &MultiHttpEntryVariable{
  1793  				Type:       MultiHttpEntryVariableType_REGEX,
  1794  				Name:       "foo",
  1795  				Expression: "bar",
  1796  				Attribute:  "baz",
  1797  			},
  1798  			expectError: true,
  1799  		},
  1800  		"css selector": {
  1801  			input: &MultiHttpEntryVariable{
  1802  				Type:       MultiHttpEntryVariableType_CSS_SELECTOR,
  1803  				Name:       "foo",
  1804  				Expression: "bar",
  1805  			},
  1806  			expectError: false,
  1807  		},
  1808  		"css selector without a name is invalid": {
  1809  			input: &MultiHttpEntryVariable{
  1810  				Type:       MultiHttpEntryVariableType_CSS_SELECTOR,
  1811  				Name:       "",
  1812  				Expression: "bar",
  1813  			},
  1814  			expectError: true,
  1815  		},
  1816  		"css selector without an expression is invalid": {
  1817  			input: &MultiHttpEntryVariable{
  1818  				Type:       MultiHttpEntryVariableType_CSS_SELECTOR,
  1819  				Name:       "foo",
  1820  				Expression: "",
  1821  			},
  1822  			expectError: true,
  1823  		},
  1824  		"css selector with attribute": {
  1825  			input: &MultiHttpEntryVariable{
  1826  				Type:       MultiHttpEntryVariableType_CSS_SELECTOR,
  1827  				Name:       "foo",
  1828  				Expression: "bar",
  1829  				Attribute:  "baz",
  1830  			},
  1831  			expectError: false,
  1832  		},
  1833  	})
  1834  }
  1835  
  1836  func TestMultiHttpEntryRequestValidate(t *testing.T) {
  1837  	testValidate(t, TestCases[*MultiHttpEntryRequest]{
  1838  		"nil": {
  1839  			input:       nil,
  1840  			expectError: false,
  1841  		},
  1842  		"zero value": {
  1843  			// The zero value is invalid because it doesn't have a URL
  1844  			input:       &MultiHttpEntryRequest{},
  1845  			expectError: true,
  1846  		},
  1847  		"valid": {
  1848  			input: &MultiHttpEntryRequest{
  1849  				Method: HttpMethod_GET,
  1850  				Url:    "http://example.com",
  1851  			},
  1852  			expectError: false,
  1853  		},
  1854  		"invalid headers": {
  1855  			input: &MultiHttpEntryRequest{
  1856  				Method: HttpMethod_GET,
  1857  				Url:    "http://example.com",
  1858  				Headers: []*HttpHeader{
  1859  					{Name: "", Value: ""},
  1860  				},
  1861  			},
  1862  			expectError: true,
  1863  		},
  1864  		"invalid query": {
  1865  			input: &MultiHttpEntryRequest{
  1866  				Method: HttpMethod_GET,
  1867  				Url:    "http://example.com",
  1868  				QueryFields: []*QueryField{
  1869  					{Name: "", Value: ""},
  1870  				},
  1871  			},
  1872  			expectError: true,
  1873  		},
  1874  		"invalid body": {
  1875  			input: &MultiHttpEntryRequest{
  1876  				Method: HttpMethod_GET,
  1877  				Url:    "http://example.com",
  1878  				Body: &HttpRequestBody{
  1879  					ContentType:     "", // ContentType must not be empty
  1880  					ContentEncoding: "",
  1881  					Payload:         []byte(""),
  1882  				},
  1883  			},
  1884  			expectError: true,
  1885  		},
  1886  	})
  1887  }
  1888  
  1889  // TestMultiHttpSettingsValidate verifies that MultiHttpSettings.Validate
  1890  // performs the expected validations.
  1891  //
  1892  // Other tests validate various invalid values. This test verifies that a
  1893  // single invalid value is reported in order to verify that the inner
  1894  // validations are happening.
  1895  func TestMultiHttpSettingsValidate(t *testing.T) {
  1896  	createEntries := func(n int, method HttpMethod, url string) []*MultiHttpEntry {
  1897  		entries := make([]*MultiHttpEntry, n)
  1898  
  1899  		for i := range n {
  1900  			entries[i] = &MultiHttpEntry{
  1901  				Request: &MultiHttpEntryRequest{
  1902  					Method: method,
  1903  					Url:    url,
  1904  				},
  1905  			}
  1906  		}
  1907  
  1908  		return entries
  1909  	}
  1910  
  1911  	testValidate(t, TestCases[*MultiHttpSettings]{
  1912  		"zero value": {
  1913  			// the zero value for MultiHttpSettings is not usable
  1914  			// because at least one entry is required.
  1915  			input:       &MultiHttpSettings{},
  1916  			expectError: true,
  1917  		},
  1918  		"valid": {
  1919  			input: &MultiHttpSettings{
  1920  				Entries: createEntries(1, HttpMethod_GET, "http://example.com"),
  1921  			},
  1922  			expectError: false,
  1923  		},
  1924  		"many entries": {
  1925  			input: &MultiHttpSettings{
  1926  				Entries: createEntries(MaxMultiHttpTargets, HttpMethod_GET, "http://example.com"),
  1927  			},
  1928  			expectError: false,
  1929  		},
  1930  		"too many entries": {
  1931  			input: &MultiHttpSettings{
  1932  				Entries: createEntries(MaxMultiHttpTargets+1, HttpMethod_GET, "http://example.com"),
  1933  			},
  1934  			expectError: true,
  1935  		},
  1936  		"invalid request": {
  1937  			input: &MultiHttpSettings{
  1938  				Entries: []*MultiHttpEntry{
  1939  					{
  1940  						Request: &MultiHttpEntryRequest{
  1941  							Method: HttpMethod_GET,
  1942  							Url:    "",
  1943  						},
  1944  					},
  1945  				},
  1946  			},
  1947  			expectError: true,
  1948  		},
  1949  		"invalid assertion": {
  1950  			input: &MultiHttpSettings{
  1951  				Entries: []*MultiHttpEntry{
  1952  					{
  1953  						Request: &MultiHttpEntryRequest{
  1954  							Method: HttpMethod_GET,
  1955  							Url:    "http://example.com",
  1956  						},
  1957  						Assertions: []*MultiHttpEntryAssertion{
  1958  							{
  1959  								Type: -1,
  1960  							},
  1961  						},
  1962  					},
  1963  				},
  1964  			},
  1965  			expectError: true,
  1966  		},
  1967  		"invalid variable": {
  1968  			input: &MultiHttpSettings{
  1969  				Entries: []*MultiHttpEntry{
  1970  					{
  1971  						Request: &MultiHttpEntryRequest{
  1972  							Method: HttpMethod_GET,
  1973  							Url:    "http://example.com",
  1974  						},
  1975  						Variables: []*MultiHttpEntryVariable{
  1976  							{
  1977  								Type: -1,
  1978  							},
  1979  						},
  1980  					},
  1981  				},
  1982  			},
  1983  			expectError: true,
  1984  		},
  1985  	})
  1986  }
  1987  
  1988  func TestInClosedRange(t *testing.T) {
  1989  	testcases := map[string]struct {
  1990  		value    int64
  1991  		lower    int64
  1992  		upper    int64
  1993  		expected bool
  1994  	}{
  1995  		"too low":     {value: 0, lower: 1, upper: 5, expected: false},
  1996  		"lower bound": {value: 1, lower: 1, upper: 5, expected: true},
  1997  		"in range":    {value: 3, lower: 1, upper: 5, expected: true},
  1998  		"upper bound": {value: 5, lower: 1, upper: 5, expected: true},
  1999  		"too high":    {value: 6, lower: 1, upper: 5, expected: false},
  2000  	}
  2001  
  2002  	for name, tc := range testcases {
  2003  		actual := inClosedRange(tc.value, tc.lower, tc.upper)
  2004  		require.Equalf(t, tc.expected, actual, `%s`, name)
  2005  	}
  2006  }
  2007  
  2008  func TestGetCheckInstance(t *testing.T) {
  2009  	for _, checkType := range CheckTypeValues() {
  2010  		check := GetCheckInstance(checkType)
  2011  		require.NotNil(t, check)
  2012  		require.Equal(t, checkType, check.Type())
  2013  		require.NoError(t, check.Validate())
  2014  	}
  2015  }
  2016  
  2017  func requireRemoteInfoFields(t *testing.T) {
  2018  	t.Helper()
  2019  
  2020  	// The expected fields in the RemoteInfo struct. It's necessary to
  2021  	// assert this list so that if the implementation ever changes, we can
  2022  	// go update the MarshalZerologObject method accordingly.
  2023  	expectedFields := []string{
  2024  		"Name",
  2025  		"Url",
  2026  		"Username",
  2027  		"Password",
  2028  	}
  2029  
  2030  	remoteInfoType := reflect.TypeOf(RemoteInfo{})
  2031  	actualFields := make([]string, 0, remoteInfoType.NumField())
  2032  
  2033  	for i := range remoteInfoType.NumField() {
  2034  		field := remoteInfoType.Field(i)
  2035  		if field.IsExported() {
  2036  			actualFields = append(actualFields, field.Name)
  2037  		}
  2038  	}
  2039  
  2040  	require.ElementsMatch(t, expectedFields, actualFields,
  2041  		"RemoteInfo struct fields have changed. Update the expected fields list and review any code that depends on the field order.")
  2042  }
  2043  
  2044  func TestRemoteInfoMarshalZerologObject(t *testing.T) {
  2045  	requireRemoteInfoFields(t)
  2046  
  2047  	remoteInfo := RemoteInfo{
  2048  		Name:     "the name",
  2049  		Url:      "https://example.com",
  2050  		Username: "the username",
  2051  		Password: "the password",
  2052  	}
  2053  
  2054  	var buf bytes.Buffer
  2055  
  2056  	logger := zerolog.New(&buf)
  2057  
  2058  	logger.Info().Interface("remote_info", remoteInfo).Send()
  2059  
  2060  	// Note that the order of the expected fields is fixed. If the
  2061  	// implementation changes, this test will break.
  2062  	expected := `{"level":"info","remote_info":{"name":"the name","url":"https://example.com","username":"the username","password":"<encrypted>"}}` + "\n"
  2063  	actual := buf.String()
  2064  
  2065  	require.Equal(t, expected, actual)
  2066  }
  2067  
  2068  func TestRemoteInfoMarshalJSON(t *testing.T) {
  2069  	requireRemoteInfoFields(t)
  2070  
  2071  	remoteInfo := RemoteInfo{
  2072  		Name:     "the name",
  2073  		Url:      "https://example.com",
  2074  		Username: "the username",
  2075  		Password: "the password",
  2076  	}
  2077  
  2078  	var buf bytes.Buffer
  2079  
  2080  	err := json.NewEncoder(&buf).Encode(&remoteInfo)
  2081  	require.NoError(t, err)
  2082  
  2083  	expected := `{"name":"the name","url":"https://example.com","username":"the username","password":"\u003cencrypted\u003e"}`
  2084  	actual := strings.TrimSpace(buf.String())
  2085  	require.Equal(t, expected, actual, "JSON encoding of RemoteInfo did not match expected output")
  2086  }
  2087  
  2088  func TestSystemLabelsSliceIsSorted(t *testing.T) {
  2089  	systemLabelsSorted := slices.Clone(systemLabels)
  2090  	slices.Sort(systemLabelsSorted)
  2091  
  2092  	require.Equal(t, systemLabelsSorted, systemLabels, "systemLabels must be sorted")
  2093  }
  2094  
  2095  // BenchmarkIsSystemLabel pins down the performance of IsSystemLabel.
  2096  //
  2097  // The lookup inputs are weighted roughly 80% misses to 20% hits, which mirrors
  2098  // the common case of querying user-defined labels that are not reserved.
  2099  func BenchmarkIsSystemLabel(b *testing.B) {
  2100  	// hits: every reserved system label (~20% of the workload).
  2101  	hits := slices.Clone(systemLabels)
  2102  
  2103  	// misses: generated label names that are not reserved (~80% of the workload).
  2104  	missCount := 4 * len(hits)
  2105  	misses := make([]string, 0, missCount)
  2106  
  2107  	prefixes := []string{"alpha", "delta", "kilo", "november", "sierra", "zulu"}
  2108  	for i := range missCount {
  2109  		misses = append(misses, prefixes[i%len(prefixes)]+"_label_"+strconv.Itoa(i))
  2110  	}
  2111  
  2112  	// Interleave one hit for every four misses so neither path runs in a long
  2113  	// uninterrupted block.
  2114  	names := make([]string, 0, len(hits)+len(misses))
  2115  	for i, j := 0, 0; i < len(hits) || j < len(misses); {
  2116  		if i < len(hits) {
  2117  			names = append(names, hits[i])
  2118  			i++
  2119  		}
  2120  
  2121  		for k := 0; k < 4 && j < len(misses); k++ {
  2122  			names = append(names, misses[j])
  2123  			j++
  2124  		}
  2125  	}
  2126  
  2127  	b.ResetTimer()
  2128  
  2129  	var found bool
  2130  
  2131  	for i := 0; i < b.N; i++ {
  2132  		found = IsSystemLabel(names[i%len(names)])
  2133  	}
  2134  
  2135  	_ = found
  2136  }