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 }