github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/prober/multihttp/script_test.go (about)

     1  package multihttp
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	kitlog "github.com/go-kit/kit/log" //nolint:staticcheck // TODO(mem): replace in BBE
    13  	"github.com/go-kit/log/level"
    14  	"github.com/grafana/synthetic-monitoring-agent/internal/k6runner"
    15  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    16  	"github.com/grafana/synthetic-monitoring-agent/internal/testhelper"
    17  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    18  	"github.com/mccutchen/go-httpbin/v2/httpbin"
    19  	"github.com/prometheus/client_golang/prometheus"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  func TestBuildQueryParams(t *testing.T) {
    24  	testcases := map[string]struct {
    25  		request  sm.MultiHttpEntryRequest
    26  		expected []string
    27  	}{
    28  		"trivial": {
    29  			request: sm.MultiHttpEntryRequest{
    30  				QueryFields: []*sm.QueryField{
    31  					{
    32  						Name:  "q",
    33  						Value: "hello",
    34  					},
    35  				},
    36  			},
    37  			expected: []string{`url.searchParams.append('q', 'hello')`},
    38  		},
    39  		"multiple": {
    40  			request: sm.MultiHttpEntryRequest{
    41  				QueryFields: []*sm.QueryField{
    42  					{
    43  						Name:  "q",
    44  						Value: "hello",
    45  					},
    46  					{
    47  						Name:  "w",
    48  						Value: "goodbye",
    49  					},
    50  				},
    51  			},
    52  			expected: []string{`url.searchParams.append('q', 'hello')`, `url.searchParams.append('w', 'goodbye')`},
    53  		},
    54  		"without value": {
    55  			request: sm.MultiHttpEntryRequest{
    56  				QueryFields: []*sm.QueryField{
    57  					{
    58  						Name:  "q",
    59  						Value: "",
    60  					},
    61  				},
    62  			},
    63  			expected: []string{`url.searchParams.append('q', '')`},
    64  		},
    65  		"variable in query value": {
    66  			request: sm.MultiHttpEntryRequest{
    67  				QueryFields: []*sm.QueryField{
    68  					{
    69  						Name:  "q",
    70  						Value: "${variable}",
    71  					},
    72  				},
    73  			},
    74  			expected: []string{`url.searchParams.append('q', vars['variable'])`},
    75  		},
    76  		"multiple variables in query value": {
    77  			request: sm.MultiHttpEntryRequest{
    78  				QueryFields: []*sm.QueryField{
    79  					{
    80  						Name:  "q",
    81  						Value: "${variable1}and${variable2}",
    82  					},
    83  				},
    84  			},
    85  			expected: []string{`url.searchParams.append('q', vars['variable1']+'and'+vars['variable2'])`},
    86  		},
    87  	}
    88  	for name, tc := range testcases {
    89  		t.Run(name, func(t *testing.T) {
    90  			actual := buildQueryParams("url", &tc.request)
    91  			require.Equal(t, tc.expected, actual)
    92  		})
    93  	}
    94  }
    95  
    96  func TestBuildUrl(t *testing.T) {
    97  	testcases := map[string]struct {
    98  		request  sm.MultiHttpEntryRequest
    99  		expected string
   100  	}{
   101  		"trivial": {
   102  			request: sm.MultiHttpEntryRequest{
   103  				Url: "https://www.example.org/",
   104  			},
   105  			expected: `'https://www.example.org/'`,
   106  		},
   107  		"variable in url": {
   108  			request: sm.MultiHttpEntryRequest{
   109  				Url: "${variable}",
   110  				QueryFields: []*sm.QueryField{
   111  					{
   112  						Name:  "q",
   113  						Value: "hello",
   114  					},
   115  				},
   116  			},
   117  			expected: `vars['variable']`,
   118  		},
   119  		"multiple variables in url": {
   120  			request: sm.MultiHttpEntryRequest{
   121  				Url:         "https://www.${variable1}.com/${variable2}",
   122  				QueryFields: []*sm.QueryField{},
   123  			},
   124  			expected: `'https://www.'+vars['variable1']+'.com/'+vars['variable2']`,
   125  		},
   126  	}
   127  
   128  	for name, tc := range testcases {
   129  		t.Run(name, func(t *testing.T) {
   130  			actual := performVariableExpansion(tc.request.Url)
   131  			require.Equal(t, tc.expected, actual)
   132  		})
   133  	}
   134  }
   135  
   136  func TestBuildHeaders(t *testing.T) {
   137  	type input struct {
   138  		headers []*sm.HttpHeader
   139  		body    *sm.HttpRequestBody
   140  	}
   141  
   142  	testcases := map[string]struct {
   143  		input    input
   144  		expected string
   145  	}{
   146  		"one header": {
   147  			input: input{
   148  				headers: []*sm.HttpHeader{
   149  					{
   150  						Name:  "Content-Type",
   151  						Value: "application/json",
   152  					},
   153  				},
   154  			},
   155  			expected: `{"Content-Type":'application/json'}`,
   156  		},
   157  		"two headers": {
   158  			input: input{
   159  				headers: []*sm.HttpHeader{
   160  					{
   161  						Name:  "Content-Type",
   162  						Value: "application/json",
   163  					},
   164  					{
   165  						Name:  "Accept",
   166  						Value: "text/html",
   167  					},
   168  				},
   169  			},
   170  			expected: `{"Content-Type":'application/json',"Accept":'text/html'}`,
   171  		},
   172  		"blank value": {
   173  			input: input{
   174  				headers: []*sm.HttpHeader{
   175  					{
   176  						Name:  "Content-Type",
   177  						Value: "",
   178  					},
   179  				},
   180  			},
   181  			expected: `{"Content-Type":''}`,
   182  		},
   183  		"body-content-type+content-encoding": {
   184  			input: input{
   185  				body: &sm.HttpRequestBody{
   186  					ContentType:     "text/plain",
   187  					ContentEncoding: "none",
   188  					Payload:         []byte("test"),
   189  				},
   190  			},
   191  			expected: `{'Content-Type':"text/plain",'Content-Encoding':"none"}`,
   192  		},
   193  		"body-content-type": {
   194  			input: input{
   195  				body: &sm.HttpRequestBody{
   196  					ContentType: "text/plain",
   197  					Payload:     []byte("test"),
   198  				},
   199  			},
   200  			expected: `{'Content-Type':"text/plain"}`,
   201  		},
   202  		"body-content-encoding": {
   203  			input: input{
   204  				body: &sm.HttpRequestBody{
   205  					ContentEncoding: "none",
   206  					Payload:         []byte("test"),
   207  				},
   208  			},
   209  			expected: `{'Content-Encoding':"none"}`,
   210  		},
   211  		"body-content-type+content-encoding+headers": {
   212  			input: input{
   213  				body: &sm.HttpRequestBody{
   214  					ContentType:     "text/plain",
   215  					ContentEncoding: "none",
   216  					Payload:         []byte("test"),
   217  				},
   218  				headers: []*sm.HttpHeader{
   219  					{
   220  						Name:  "X-Some-Header",
   221  						Value: "some value",
   222  					},
   223  				},
   224  			},
   225  			expected: `{'Content-Type':"text/plain",'Content-Encoding':"none","X-Some-Header":'some value'}`,
   226  		},
   227  		"body-content-type+headers": {
   228  			input: input{
   229  				body: &sm.HttpRequestBody{
   230  					ContentType: "text/plain",
   231  					Payload:     []byte("test"),
   232  				},
   233  				headers: []*sm.HttpHeader{
   234  					{
   235  						Name:  "X-Some-Header",
   236  						Value: "some value",
   237  					},
   238  				},
   239  			},
   240  			expected: `{'Content-Type':"text/plain","X-Some-Header":'some value'}`,
   241  		},
   242  		"body-content-encoding+headers": {
   243  			input: input{
   244  				body: &sm.HttpRequestBody{
   245  					ContentEncoding: "none",
   246  					Payload:         []byte("test"),
   247  				},
   248  				headers: []*sm.HttpHeader{
   249  					{
   250  						Name:  "X-Some-Header",
   251  						Value: "some value",
   252  					},
   253  				},
   254  			},
   255  			expected: `{'Content-Encoding':"none","X-Some-Header":'some value'}`,
   256  		},
   257  		"empty": {
   258  			input: input{
   259  				body:    nil,
   260  				headers: nil,
   261  			},
   262  			expected: ``,
   263  		},
   264  		"do what I say": {
   265  			input: input{
   266  				body: &sm.HttpRequestBody{
   267  					ContentType:     "text/plain",
   268  					ContentEncoding: "none",
   269  					Payload:         []byte("test"),
   270  				},
   271  				headers: []*sm.HttpHeader{
   272  					{
   273  						Name:  "Content-Type",
   274  						Value: "application/json",
   275  					},
   276  				},
   277  			},
   278  			expected: `{'Content-Type':"text/plain",'Content-Encoding':"none","Content-Type":'application/json'}`,
   279  		},
   280  		"variable in value": {
   281  			input: input{
   282  				body: nil,
   283  				headers: []*sm.HttpHeader{
   284  					{
   285  						Name:  "Authorization",
   286  						Value: "Bearer ${accessToken}",
   287  					},
   288  				},
   289  			},
   290  			expected: `{"Authorization":'Bearer '+vars['accessToken']}`,
   291  		},
   292  		"multiple variables in value": {
   293  			input: input{
   294  				body: nil,
   295  				headers: []*sm.HttpHeader{
   296  					{
   297  						Name:  "Authorization",
   298  						Value: "Bearer ${accessToken}${andsomeother}",
   299  					},
   300  				},
   301  			},
   302  			expected: `{"Authorization":'Bearer '+vars['accessToken']+vars['andsomeother']}`,
   303  		},
   304  	}
   305  
   306  	for name, testcase := range testcases {
   307  		t.Run(name, func(t *testing.T) {
   308  			actual := buildHeaders(testcase.input.headers, testcase.input.body)
   309  			require.Equal(t, testcase.expected, actual)
   310  		})
   311  	}
   312  }
   313  
   314  func TestBuildBody(t *testing.T) {
   315  	type input struct {
   316  		body *sm.HttpRequestBody
   317  	}
   318  
   319  	testcases := map[string]struct {
   320  		input    input
   321  		expected string
   322  	}{
   323  		"not empty": {
   324  			input:    input{body: &sm.HttpRequestBody{Payload: []byte("test")}},
   325  			expected: `encoding.b64decode("dGVzdA", 'rawstd', "s")`,
   326  		},
   327  		"nil": {
   328  			input:    input{body: nil},
   329  			expected: "null",
   330  		},
   331  		"empty": {
   332  			input:    input{body: &sm.HttpRequestBody{Payload: []byte("")}},
   333  			expected: `""`,
   334  		},
   335  	}
   336  
   337  	for name, testcase := range testcases {
   338  		t.Run(name, func(t *testing.T) {
   339  			actual := buildBody(testcase.input.body)
   340  			require.Equal(t, testcase.expected, actual)
   341  		})
   342  	}
   343  }
   344  
   345  func TestInterpolateBodyVariables(t *testing.T) {
   346  	t.Parallel()
   347  
   348  	type input struct {
   349  		body *sm.HttpRequestBody
   350  	}
   351  
   352  	testcases := map[string]struct {
   353  		input    input
   354  		expected []string
   355  	}{
   356  		"no variables": {
   357  			input:    input{body: &sm.HttpRequestBody{Payload: []byte("test")}},
   358  			expected: []string{},
   359  		},
   360  		"basic": {
   361  			input: input{body: &sm.HttpRequestBody{Payload: []byte("test ${variable1}")}},
   362  			expected: []string{
   363  				"body=body.replaceAll('${variable1}', vars['variable1'])",
   364  			},
   365  		},
   366  		"several variables with repeats": {
   367  			input: input{body: &sm.HttpRequestBody{Payload: []byte("${variable1} is ${variable1} fun ${variable2} ok ${variable3}")}},
   368  			expected: []string{
   369  				"body=body.replaceAll('${variable1}', vars['variable1'])",
   370  				"body=body.replaceAll('${variable2}', vars['variable2'])",
   371  				"body=body.replaceAll('${variable3}', vars['variable3'])",
   372  			},
   373  		},
   374  	}
   375  	for name, tc := range testcases {
   376  		t.Run(name, func(t *testing.T) {
   377  			t.Parallel()
   378  
   379  			actual := interpolateBodyVariables("body", tc.input.body)
   380  			require.Equal(t, tc.expected, actual)
   381  		})
   382  	}
   383  }
   384  
   385  func TestAssertionConditionName(t *testing.T) {
   386  	testcases := map[string]struct {
   387  		condition sm.MultiHttpEntryAssertionConditionVariant
   388  		subject   string
   389  		value     string
   390  		expected  string
   391  	}{
   392  		"TestAssertionConditionNameNotContains": {
   393  			condition: sm.MultiHttpEntryAssertionConditionVariant_NOT_CONTAINS,
   394  			subject:   "subject",
   395  			value:     "value",
   396  			expected:  `subject does not contain \"value\"`,
   397  		},
   398  		"TestAssertionConditionNameContains": {
   399  			condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   400  			subject:   "subject",
   401  			value:     "value",
   402  			expected:  `subject contains \"value\"`,
   403  		},
   404  		"TestAssertionConditionNameEquals": {
   405  			condition: sm.MultiHttpEntryAssertionConditionVariant_EQUALS,
   406  			subject:   "subject",
   407  			value:     "value",
   408  			expected:  `subject equals \"value\"`,
   409  		},
   410  		"TestAssertionConditionNameStartsWith": {
   411  			condition: sm.MultiHttpEntryAssertionConditionVariant_STARTS_WITH,
   412  			subject:   "subject",
   413  			value:     "value",
   414  			expected:  `subject starts with \"value\"`,
   415  		},
   416  		"TestAssertionConditionNameEndsWith": {
   417  			condition: sm.MultiHttpEntryAssertionConditionVariant_ENDS_WITH,
   418  			subject:   "subject",
   419  			value:     "value",
   420  			expected:  `subject ends with \"value\"`,
   421  		},
   422  	}
   423  
   424  	for name, testcase := range testcases {
   425  		t.Run(name, func(t *testing.T) {
   426  			var b strings.Builder
   427  
   428  			cond := assertionCondition(testcase.condition)
   429  			cond.Name(&b, testcase.subject, testcase.value)
   430  			require.Equal(t, testcase.expected, b.String())
   431  		})
   432  	}
   433  }
   434  
   435  func TestAssertionConditionRender(t *testing.T) {
   436  	testcases := map[string]struct {
   437  		condition sm.MultiHttpEntryAssertionConditionVariant
   438  		subject   string
   439  		value     string
   440  		expected  string
   441  	}{
   442  		"TestAssertionConditionRenderNotContains": {
   443  			condition: sm.MultiHttpEntryAssertionConditionVariant_NOT_CONTAINS,
   444  			subject:   "subject",
   445  			value:     "val'ue",
   446  			expected:  `!subject.includes("val\'ue")`,
   447  		},
   448  		"TestAssertionConditionRenderContains": {
   449  			condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   450  			subject:   "subject",
   451  			value:     "val'ue",
   452  			expected:  `subject.includes("val\'ue")`,
   453  		},
   454  		"TestAssertionConditionRenderEquals": {
   455  			condition: sm.MultiHttpEntryAssertionConditionVariant_EQUALS,
   456  			subject:   "subject",
   457  			value:     "val'ue",
   458  			expected:  `subject === "val\'ue"`,
   459  		},
   460  		"TestAssertionConditionRenderStartsWith": {
   461  			condition: sm.MultiHttpEntryAssertionConditionVariant_STARTS_WITH,
   462  			subject:   "subject",
   463  			value:     "val'ue",
   464  			expected:  `subject.startsWith("val\'ue")`,
   465  		},
   466  		"TestAssertionConditionRenderEndsWith": {
   467  			condition: sm.MultiHttpEntryAssertionConditionVariant_ENDS_WITH,
   468  			subject:   "subject",
   469  			value:     "val'ue",
   470  			expected:  `subject.endsWith("val\'ue")`,
   471  		},
   472  	}
   473  
   474  	for name, testcase := range testcases {
   475  		t.Run(name, func(t *testing.T) {
   476  			var b strings.Builder
   477  
   478  			cond := assertionCondition(testcase.condition)
   479  			cond.Render(&b, testcase.subject, testcase.value)
   480  			require.Equal(t, testcase.expected, b.String())
   481  		})
   482  	}
   483  }
   484  
   485  func TestBuildChecks(t *testing.T) {
   486  	testcases := map[string]struct {
   487  		urlVarName string
   488  		method     string
   489  		assertion  *sm.MultiHttpEntryAssertion
   490  		expected   string
   491  	}{
   492  		"TestBuildChecksTextAssertionWithBodySubject": {
   493  			urlVarName: "url",
   494  			method:     "GET",
   495  			assertion: &sm.MultiHttpEntryAssertion{
   496  				Type:      sm.MultiHttpEntryAssertionType_TEXT,
   497  				Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   498  				Subject:   sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
   499  				Value:     "value",
   500  			},
   501  			expected: `currentCheck = check(response, { "body contains \"value\"": response => response.body.includes("value") }, {"url": url.toString(), "method": "GET"});
   502  	if(!currentCheck) {
   503  		console.error("Assertion failed:", "response.body.includes(\"value\")");
   504  		fail()
   505  	};
   506  `,
   507  		},
   508  		"TestBuildChecksTextAssertionWithHeadersSubject": {
   509  			urlVarName: "url",
   510  			method:     "GET",
   511  			assertion: &sm.MultiHttpEntryAssertion{
   512  				Type:      sm.MultiHttpEntryAssertionType_TEXT,
   513  				Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   514  				Subject:   sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS,
   515  				Value:     "value",
   516  			},
   517  			expected: `currentCheck = check(response, { "header contains \"value\"": response => { const values = Object.entries(response.headers).map(header => header[0].toLowerCase() + ': ' + header[1]); return !!values.find(value => value.includes("value")); } }, {"url": url.toString(), "method": "GET"});
   518  	if(!currentCheck) {
   519  		console.error("Assertion failed:", "value.includes(\"value\")");
   520  		fail()
   521  	};
   522  `,
   523  		},
   524  		"TestBuildChecksTextAssertionWithHeadersSubjectAndExpression": {
   525  			urlVarName: "url",
   526  			method:     "GET",
   527  			assertion: &sm.MultiHttpEntryAssertion{
   528  				Type:       sm.MultiHttpEntryAssertionType_TEXT,
   529  				Condition:  sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   530  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS,
   531  				Expression: "Content-Type",
   532  				Value:      "value",
   533  			},
   534  			expected: `currentCheck = check(response, { "header contains \"value\"": response => { return assertHeader(response.headers, "Content-Type", v => value.includes("value")); } }, {"url": url.toString(), "method": "GET"});
   535  	if(!currentCheck) {
   536  		console.error("Assertion failed:", "value.includes(\"value\")");
   537  		fail()
   538  	};
   539  `,
   540  		},
   541  		"TestBuildChecksTextAssertionWithStatusCodeSubject": {
   542  			urlVarName: "url",
   543  			method:     "GET",
   544  			assertion: &sm.MultiHttpEntryAssertion{
   545  				Type:      sm.MultiHttpEntryAssertionType_TEXT,
   546  				Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   547  				Subject:   sm.MultiHttpEntryAssertionSubjectVariant_HTTP_STATUS_CODE,
   548  				Value:     "value",
   549  			},
   550  			expected: `currentCheck = check(response, { "status code contains \"value\"": response => response.status.toString().includes("value") }, {"url": url.toString(), "method": "GET"});
   551  	if(!currentCheck) {
   552  		console.error("Assertion failed:", "response.status.toString().includes(\"value\")");
   553  		fail()
   554  	};
   555  `,
   556  		},
   557  		"TestBuildChecksJsonPathValueAssertionWithBodySubject": {
   558  			urlVarName: "url",
   559  			method:     "GET",
   560  			assertion: &sm.MultiHttpEntryAssertion{
   561  				Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   562  				Condition:  sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   563  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
   564  				Expression: "/path/to/value",
   565  				Value:      "value",
   566  			},
   567  			expected: `currentCheck = check(response, { "/path/to/value contains \"value\"": response => jsonpath.query(response.json(), "/path/to/value").some(values => values.includes("value")) }, {"url": url.toString(), "method": "GET"});
   568  	if(!currentCheck) {
   569  		console.error("Assertion failed:", "values.includes(\"value\")");
   570  		fail()
   571  	};
   572  `,
   573  		},
   574  		"TestBuildChecksJsonPathAssertionWithBodySubject": {
   575  			urlVarName: "url",
   576  			method:     "GET",
   577  			assertion: &sm.MultiHttpEntryAssertion{
   578  				Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
   579  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
   580  				Expression: "/path/to/value",
   581  			},
   582  			expected: `currentCheck = check(response, { "/path/to/value exists": response => jsonpath.query(response.json(), "/path/to/value").length > 0 }, {"url": url.toString(), "method": "GET"});
   583  	if(!currentCheck) {
   584  		console.error("Assertion failed:", "JsonPath expression /path/to/value");
   585  		fail()
   586  	};
   587  `,
   588  		},
   589  		"TestBuildChecksRegexAssertionWithBodySubject": {
   590  			urlVarName: "url",
   591  			method:     "GET",
   592  			assertion: &sm.MultiHttpEntryAssertion{
   593  				Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   594  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
   595  				Expression: "regex",
   596  			},
   597  			expected: `currentCheck = check(response, { "body matches /regex/": response => { const expr = new RegExp("regex"); return expr.test(response.body); } }, {"url": url.toString(), "method": "GET"});
   598  	if(!currentCheck) {
   599  		console.error("Assertion failed:", "Body matchesregex");
   600  		fail()
   601  	};
   602  `,
   603  		},
   604  		"TestBuildChecksRegexAssertionWithHeadersSubject": {
   605  			urlVarName: "url",
   606  			method:     "GET",
   607  			assertion: &sm.MultiHttpEntryAssertion{
   608  				Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   609  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS,
   610  				Expression: "regex",
   611  			},
   612  			expected: `currentCheck = check(response, { "headers matches /regex/": response => { const expr = new RegExp("regex"); const values = Object.entries(response.headers).map(header => header[0].toLowerCase() + ': ' + header[1]); return !!values.find(value => expr.test(value)); } }, {"url": url.toString(), "method": "GET"});
   613  	if(!currentCheck) {
   614  		console.error("Assertion failed:", "Headers matchregex");
   615  		fail()
   616  	};
   617  `,
   618  		},
   619  		"TestBuildChecksRegexAssertionWithStatusCodeSubject": {
   620  			urlVarName: "url",
   621  			method:     "GET",
   622  			assertion: &sm.MultiHttpEntryAssertion{
   623  				Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   624  				Subject:    sm.MultiHttpEntryAssertionSubjectVariant_HTTP_STATUS_CODE,
   625  				Expression: "regex",
   626  			},
   627  			expected: `currentCheck = check(response, { "status matches /regex/": response => { const expr = new RegExp("regex"); return expr.test(response.status.toString()); } }, {"url": url.toString(), "method": "GET"});
   628  	if(!currentCheck) {
   629  		console.error("Assertion failed:", "Status matchesregex");
   630  		fail()
   631  	};
   632  `,
   633  		},
   634  	}
   635  
   636  	for name, testcase := range testcases {
   637  		t.Run(name, func(t *testing.T) {
   638  			actual := buildChecks(testcase.urlVarName, testcase.method, testcase.assertion)
   639  			require.Equal(t, testcase.expected, actual)
   640  		})
   641  	}
   642  }
   643  
   644  func TestBuildVars(t *testing.T) {
   645  	testcases := map[string]struct {
   646  		input    sm.MultiHttpEntryVariable
   647  		expected string
   648  	}{
   649  		"TestBuildVarsJsonPath": {
   650  			input: sm.MultiHttpEntryVariable{
   651  				Name:       "name",
   652  				Type:       sm.MultiHttpEntryVariableType_JSON_PATH,
   653  				Expression: "jsonPath",
   654  			},
   655  			expected: `vars['name'] = jsonpath.query(response.json(), 'jsonPath')[0];`,
   656  		},
   657  		"TestBuildVarsRegex": {
   658  			input: sm.MultiHttpEntryVariable{
   659  				Name:       "name",
   660  				Type:       sm.MultiHttpEntryVariableType_REGEX,
   661  				Expression: "regex",
   662  			},
   663  			expected: `match = new RegExp('regex').exec(response.body); vars['name'] = match ? match[1] || match[0] : null;`,
   664  		},
   665  		"TestBuildVarsCssSelector": {
   666  			input: sm.MultiHttpEntryVariable{
   667  				Name:       "name",
   668  				Type:       sm.MultiHttpEntryVariableType_CSS_SELECTOR,
   669  				Expression: "cssSelector",
   670  			},
   671  			expected: `vars['name'] = response.html('cssSelector').html();`,
   672  		},
   673  		"TestBuildVarsCssSelectorWithAttribute": {
   674  			input: sm.MultiHttpEntryVariable{
   675  				Name:       "name",
   676  				Type:       sm.MultiHttpEntryVariableType_CSS_SELECTOR,
   677  				Expression: "cssSelector",
   678  				Attribute:  "attribute",
   679  			},
   680  			expected: `vars['name'] = response.html('cssSelector').first().attr('attribute');`,
   681  		},
   682  	}
   683  
   684  	for name, testcase := range testcases {
   685  		t.Run(name, func(t *testing.T) {
   686  			actual := buildVars(&testcase.input)
   687  			require.Equal(t, testcase.expected, actual)
   688  		})
   689  	}
   690  }
   691  
   692  // TestSettingsToScript tests the conversion of a MultiHttpSettings to a
   693  // Javascript script.
   694  func TestSettingsToScript(t *testing.T) {
   695  	testServer := httptest.NewServer(httpbin.New())
   696  	t.Cleanup(testServer.Close)
   697  
   698  	settings := &sm.MultiHttpSettings{
   699  		LogResponses: true,
   700  		Entries: []*sm.MultiHttpEntry{
   701  			{
   702  				Request: &sm.MultiHttpEntryRequest{
   703  					Method: sm.HttpMethod_GET,
   704  					Url:    testServer.URL + "/response-headers?foo=bar",
   705  				},
   706  				Assertions: []*sm.MultiHttpEntryAssertion{
   707  					{
   708  						Type:      sm.MultiHttpEntryAssertionType_TEXT,
   709  						Subject:   sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS,
   710  						Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   711  						Value:     "foo: bar",
   712  					},
   713  				},
   714  			},
   715  			{
   716  				Request: &sm.MultiHttpEntryRequest{
   717  					Method: sm.HttpMethod_GET,
   718  					Url:    testServer.URL + "/status/266",
   719  				},
   720  				Assertions: []*sm.MultiHttpEntryAssertion{
   721  					{
   722  						Type:      sm.MultiHttpEntryAssertionType_TEXT,
   723  						Subject:   sm.MultiHttpEntryAssertionSubjectVariant_HTTP_STATUS_CODE,
   724  						Condition: sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   725  						Value:     "266",
   726  					},
   727  				},
   728  			},
   729  			{
   730  				Request: &sm.MultiHttpEntryRequest{
   731  					Method: sm.HttpMethod_GET,
   732  					Url:    testServer.URL + "/json",
   733  				},
   734  				Assertions: []*sm.MultiHttpEntryAssertion{
   735  					{
   736  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   737  						Expression: "$.slideshow.author",
   738  						Condition:  sm.MultiHttpEntryAssertionConditionVariant_CONTAINS,
   739  						Value:      "Yours",
   740  					},
   741  					{
   742  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   743  						Expression: "$.slideshow.date",
   744  						Condition:  sm.MultiHttpEntryAssertionConditionVariant_NOT_CONTAINS,
   745  						Value:      "2023",
   746  					},
   747  					{
   748  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   749  						Expression: "$.slideshow.author",
   750  						Condition:  sm.MultiHttpEntryAssertionConditionVariant_EQUALS,
   751  						Value:      "Yours Truly",
   752  					},
   753  					{
   754  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   755  						Expression: "$.slideshow.author",
   756  						Condition:  sm.MultiHttpEntryAssertionConditionVariant_STARTS_WITH,
   757  						Value:      "Yours",
   758  					},
   759  					{
   760  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_VALUE,
   761  						Expression: "$.slideshow.author",
   762  						Condition:  sm.MultiHttpEntryAssertionConditionVariant_ENDS_WITH,
   763  						Value:      "Truly",
   764  					},
   765  				},
   766  				Variables: []*sm.MultiHttpEntryVariable{
   767  					{
   768  						Type:       sm.MultiHttpEntryVariableType_JSON_PATH,
   769  						Name:       "author",
   770  						Expression: "$.slideshow.author",
   771  					},
   772  					{
   773  						Type:       sm.MultiHttpEntryVariableType_JSON_PATH,
   774  						Name:       "date",
   775  						Expression: "$.slideshow.date",
   776  					},
   777  				},
   778  			},
   779  			{
   780  				Request: &sm.MultiHttpEntryRequest{
   781  					Method: sm.HttpMethod_GET,
   782  					Url:    testServer.URL + "/json",
   783  				},
   784  				Assertions: []*sm.MultiHttpEntryAssertion{
   785  					{
   786  						Type:       sm.MultiHttpEntryAssertionType_JSON_PATH_ASSERTION,
   787  						Expression: `$.slideshow.title`,
   788  					},
   789  				},
   790  			},
   791  			{
   792  				Request: &sm.MultiHttpEntryRequest{
   793  					Method: sm.HttpMethod_GET,
   794  					Url:    testServer.URL + "/html",
   795  				},
   796  				Assertions: []*sm.MultiHttpEntryAssertion{
   797  					{
   798  						Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   799  						Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_BODY,
   800  						Expression: "had .+ excited the curiosity of the mariners",
   801  					},
   802  					{
   803  						Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   804  						Subject:    sm.MultiHttpEntryAssertionSubjectVariant_RESPONSE_HEADERS,
   805  						Expression: "content-type: .*; charset=utf-8",
   806  					},
   807  					{
   808  						Type:       sm.MultiHttpEntryAssertionType_REGEX_ASSERTION,
   809  						Subject:    sm.MultiHttpEntryAssertionSubjectVariant_HTTP_STATUS_CODE,
   810  						Expression: "2..",
   811  					},
   812  				},
   813  			},
   814  			{
   815  				Request: &sm.MultiHttpEntryRequest{
   816  					Method: sm.HttpMethod_GET,
   817  					Url:    testServer.URL + "/get",
   818  					QueryFields: []*sm.QueryField{
   819  						{Name: "foo", Value: "bar"},
   820  						{Name: "baz", Value: ""},
   821  					},
   822  				},
   823  			},
   824  			{
   825  				Request: &sm.MultiHttpEntryRequest{
   826  					Url: testServer.URL + "/gzip",
   827  					Headers: []*sm.HttpHeader{
   828  						{Name: "Accept", Value: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"},
   829  						{Name: "Accept-Encoding", Value: "gzip, deflate, br"},
   830  					},
   831  				},
   832  			},
   833  		},
   834  	}
   835  
   836  	require.NoError(t, settings.Validate())
   837  
   838  	actual, err := settingsToScript(settings)
   839  	require.NoError(t, err)
   840  	require.NotEmpty(t, actual)
   841  
   842  	check := model.Check{
   843  		Check: sm.Check{
   844  			Target:  settings.Entries[0].Request.Url,
   845  			Job:     "test",
   846  			Timeout: 10000,
   847  			Settings: sm.CheckSettings{
   848  				Multihttp: settings,
   849  			},
   850  		},
   851  	}
   852  
   853  	// Run tests for each supported k6 version.
   854  	for _, k6path := range testhelper.K6Paths(t) {
   855  		t.Run(filepath.Base(k6path), func(t *testing.T) {
   856  			ctx, cancel := testhelper.Context(context.Background(), t)
   857  			t.Cleanup(cancel)
   858  
   859  			store := testhelper.NoopSecretStore{}
   860  			runner, err := k6runner.New(k6runner.RunnerOpts{Uri: k6path})
   861  			require.NoError(t, err)
   862  
   863  			logger := testhelper.Logger(t)
   864  			prober, err := NewProber(ctx, check, logger, runner, http.Header{}, &store)
   865  			require.NoError(t, err)
   866  
   867  			reg := prometheus.NewPedanticRegistry()
   868  			require.NotNil(t, reg)
   869  
   870  			var buf bytes.Buffer
   871  
   872  			userLogger := level.NewFilter(kitlog.NewLogfmtLogger(&buf), level.AllowInfo(), level.SquelchNoLevel(false))
   873  			require.NotNil(t, userLogger)
   874  
   875  			success, duration := prober.Probe(ctx, check.Target, reg, userLogger, "test-execution-id")
   876  
   877  			t.Log("Log entries:\n" + buf.String())
   878  
   879  			require.True(t, success)
   880  			require.NotEqual(t, 0, duration)
   881  		})
   882  	}
   883  }
   884  
   885  func TestReplaceVariablesInString(t *testing.T) {
   886  	testcases := map[string]struct {
   887  		input    string
   888  		expected string
   889  	}{
   890  		"no replacements": {
   891  			input:    "plain string",
   892  			expected: `'plain string'`,
   893  		},
   894  		"one variable": {
   895  			input:    "this is a ${var} to replace",
   896  			expected: `'this is a '+vars['var']+' to replace'`,
   897  		},
   898  		"two variables": {
   899  			input:    "this is ${v1} and ${v2}",
   900  			expected: `'this is '+vars['v1']+' and '+vars['v2']`,
   901  		},
   902  		"multiple instances": {
   903  			input:    "this is ${v1}, ${v2} and ${v1} again",
   904  			expected: `'this is '+vars['v1']+', '+vars['v2']+' and '+vars['v1']+' again'`,
   905  		},
   906  	}
   907  
   908  	for name, testcase := range testcases {
   909  		actual := performVariableExpansion(testcase.input)
   910  		require.Equal(t, testcase.expected, actual, name)
   911  	}
   912  }