diff --git a/js/modules/k6/grpc/client.go b/js/modules/k6/grpc/client.go index 1a9538a7df4..17249fa9988 100644 --- a/js/modules/k6/grpc/client.go +++ b/js/modules/k6/grpc/client.go @@ -2,6 +2,9 @@ package grpc import ( "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" "errors" "fmt" "io" @@ -108,6 +111,101 @@ func (c *Client) LoadProtoset(protosetPath string) ([]MethodInfo, error) { return c.convertToMethodInfo(fdset) } +// Note: this function was lifted from `lib/options.go` +func decryptPrivateKey(key, password []byte) ([]byte, error) { + block, _ := pem.Decode(key) + if block == nil { + return nil, errors.New("failed to decode PEM key") + } + + blockType := block.Type + if blockType == "ENCRYPTED PRIVATE KEY" { + return nil, errors.New("encrypted pkcs8 formatted key is not supported") + } + /* + Even though `DecryptPEMBlock` has been deprecated since 1.16.x it is still + being used here because it is deprecated due to it not supporting *good* cryptography + ultimately though we want to support something so we will be using it for now. + */ + decryptedKey, err := x509.DecryptPEMBlock(block, password) //nolint:staticcheck + if err != nil { + return nil, err + } + key = pem.EncodeToMemory(&pem.Block{ + Type: blockType, + Bytes: decryptedKey, + }) + return key, nil +} + +func buildTLSConfig(parentConfig *tls.Config, certificate, key []byte, caCertificates [][]byte) (*tls.Config, error) { + var cp *x509.CertPool + if len(caCertificates) > 0 { + cp, _ = x509.SystemCertPool() + for i, caCert := range caCertificates { + if ok := cp.AppendCertsFromPEM(caCert); !ok { + return nil, fmt.Errorf("failed to append ca certificate [%d] from PEM", i) + } + } + } + + // Ignoring 'TLS MinVersion is too low' because this tls.Config will inherit MinValue and MaxValue + // from the vu state tls.Config + + //nolint:golint,gosec + tlsCfg := &tls.Config{ + CipherSuites: parentConfig.CipherSuites, + InsecureSkipVerify: parentConfig.InsecureSkipVerify, + MinVersion: parentConfig.MinVersion, + MaxVersion: parentConfig.MaxVersion, + Renegotiation: parentConfig.Renegotiation, + RootCAs: cp, + } + if len(certificate) > 0 && len(key) > 0 { + cert, err := tls.X509KeyPair(certificate, key) + if err != nil { + return nil, fmt.Errorf("failed to append certificate from PEM: %w", err) + } + tlsCfg.Certificates = []tls.Certificate{cert} + } + return tlsCfg, nil +} + +func buildTLSConfigFromMap(parentConfig *tls.Config, tlsConfigMap map[string]interface{}) (*tls.Config, error) { + var cert, key, pass []byte + var ca [][]byte + var err error + if certstr, ok := tlsConfigMap["cert"].(string); ok { + cert = []byte(certstr) + } + if keystr, ok := tlsConfigMap["key"].(string); ok { + key = []byte(keystr) + } + if passwordStr, ok := tlsConfigMap["password"].(string); ok { + pass = []byte(passwordStr) + if len(pass) > 0 { + if key, err = decryptPrivateKey(key, pass); err != nil { + return nil, err + } + } + } + if cas, ok := tlsConfigMap["cacerts"]; ok { + var caCertsArray []interface{} + if caCertsArray, ok = cas.([]interface{}); ok { + ca = make([][]byte, len(caCertsArray)) + for i, entry := range caCertsArray { + var entryStr string + if entryStr, ok = entry.(string); ok { + ca[i] = []byte(entryStr) + } + } + } else if caCertStr, caCertStrOk := cas.(string); caCertStrOk { + ca = [][]byte{[]byte(caCertStr)} + } + } + return buildTLSConfig(parentConfig, cert, key, ca) +} + // Connect is a block dial to the gRPC server at the given address (host:port) func (c *Client) Connect(addr string, params map[string]interface{}) (bool, error) { state := c.vu.State() @@ -125,9 +223,13 @@ func (c *Client) Connect(addr string, params map[string]interface{}) (bool, erro var tcred credentials.TransportCredentials if !p.IsPlaintext { tlsCfg := state.TLSConfig.Clone() + if len(p.TLS) > 0 { + if tlsCfg, err = buildTLSConfigFromMap(tlsCfg, p.TLS); err != nil { + return false, err + } + } tlsCfg.NextProtos = []string{"h2"} - // TODO(rogchap): Would be good to add support for custom RootCAs (self signed) tcred = credentials.NewTLS(tlsCfg) } else { tcred = insecure.NewCredentials() @@ -373,6 +475,7 @@ type connectParams struct { Timeout time.Duration MaxReceiveSize int64 MaxSendSize int64 + TLS map[string]interface{} } func (c *Client) parseConnectParams(raw map[string]interface{}) (connectParams, error) { @@ -421,7 +524,43 @@ func (c *Client) parseConnectParams(raw map[string]interface{}) (connectParams, if params.MaxSendSize < 0 { return params, fmt.Errorf("invalid maxSendSize value: '%#v, it needs to be a positive integer", v) } + case "tls": + var ok bool + params.TLS, ok = v.(map[string]interface{}) + if !ok { + return params, fmt.Errorf("invalid tls value: '%#v', expected (optional) keys: cert, key, password, and cacerts", v) + } + // optional map keys below + if cert, certok := params.TLS["cert"]; certok { + if _, ok = cert.(string); !ok { + return params, fmt.Errorf("invalid tls cert value: '%#v', it needs to be a PEM formatted string", v) + } + } + if key, keyok := params.TLS["key"]; keyok { + if _, ok = key.(string); !ok { + return params, fmt.Errorf("invalid tls key value: '%#v', it needs to be a PEM formatted string", v) + } + } + if pass, passok := params.TLS["password"]; passok { + if _, ok = pass.(string); !ok { + return params, fmt.Errorf("invalid tls password value: '%#v', it needs to be a string", v) + } + } + if cacerts, cacertsok := params.TLS["cacerts"]; cacertsok { + var cacertsArray []interface{} + if cacertsArray, ok = cacerts.([]interface{}); ok { + for _, cacertsArrayEntry := range cacertsArray { + if _, ok = cacertsArrayEntry.(string); !ok { + return params, fmt.Errorf("invalid tls cacerts value: '%#v',"+ + " it needs to be a string or an array of PEM formatted strings", v) + } + } + } else if _, ok = cacerts.(string); !ok { + return params, fmt.Errorf("invalid tls cacerts value: '%#v',"+ + " it needs to be a string or an array of PEM formatted strings", v) + } + } default: return params, fmt.Errorf("unknown connect param: %q", k) } diff --git a/js/modules/k6/grpc/client_test.go b/js/modules/k6/grpc/client_test.go index e19e65d6527..b0c37137bb3 100644 --- a/js/modules/k6/grpc/client_test.go +++ b/js/modules/k6/grpc/client_test.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "errors" + "fmt" "io" "net/url" "os" @@ -70,7 +72,7 @@ func TestClient(t *testing.T) { samples := make(chan metrics.SampleContainer, 1000) testRuntime := modulestest.NewRuntime(t) - cwd, err := os.Getwd() + cwd, err := os.Getwd() //nolint:golint,forbidigo require.NoError(t, err) fs := fsext.NewOsFs() if isWindows { @@ -870,6 +872,256 @@ func TestClient(t *testing.T) { } } +func TestClient_TlsParameters(t *testing.T) { + t.Parallel() + + testingKey := func(s string) string { + t.Helper() + return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") + } + + clientAuthCA := []byte("-----BEGIN CERTIFICATE-----\nMIIBWzCCAQGgAwIBAgIJAIQMBgLi+DV6MAoGCCqGSM49BAMCMBAxDjAMBgNVBAMM\nBU15IENBMCAXDTIyMDEyMTEyMjkzNloYDzMwMjEwNTI0MTIyOTM2WjAQMQ4wDAYD\nVQQDDAVNeSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHnrghULHa2hSa/C\nWimwCn42KWdlPqd6/zs3JgLIxTvBHJJlfbhWbBqtybqyovWd3QykHMIpx0NZmpYn\nG8FoWpmjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\nDgQWBBSkukBA8lgFvvBJAYKsoSUR+PX71jAKBggqhkjOPQQDAgNIADBFAiEAiFF7\nY54CMNRSBSVMgd4mQgrzJInRH88KpLsQ7VeOAaQCIEa0vaLln9zxIDZQKocml4Db\nAEJr8tDzMKIds6sRTBT4\n-----END CERTIFICATE-----") + localHostCert := "-----BEGIN CERTIFICATE-----\\nMIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS\\nMRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw\\nMDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\\nMIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r\\nbFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U\\naUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P\\nYfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk\\nPOGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu\\nh7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE\\nAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud\\nDgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv\\nbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI\\n5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv\\ncxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2\\n+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B\\ngrw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK\\n5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/\\nWkBKOclmOV2xlTVuPw==\\n-----END CERTIFICATE-----" + clientAuth := "-----BEGIN CERTIFICATE-----\\nMIIBVzCB/6ADAgECAgkAg/SeNG3XqB0wCgYIKoZIzj0EAwIwEDEOMAwGA1UEAwwF\\nTXkgQ0EwIBcNMjIwMTIxMTUxMjM0WhgPMzAyMTA1MjQxNTEyMzRaMBExDzANBgNV\\nBAMMBmNsaWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKM7OJQMYG4KLtDA\\ngZ8zOg2PimHMmQnjD2HtI4cSwIUJJnvHWLowbFe9fk6XeP9b3dK1ImUI++/EZdVr\\nABAcngejPzA9MA4GA1UdDwEB/wQEAwIBBjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW\\nBBSttJe1mcPEnBOZ6wvKPG4zL0m1CzAKBggqhkjOPQQDAgNHADBEAiBPSLgKA/r9\\nu/FW6W+oy6Odm1kdNMGCI472iTn545GwJgIgb3UQPOUTOj0IN4JLJYfmYyXviqsy\\nzk9eWNHFXDA9U6U=\\n-----END CERTIFICATE-----" + clientAuthKey := testingKey("-----BEGIN EC TESTING KEY-----\\nMHcCAQEEINDaMGkOT3thu1A0LfLJr3Jd011/aEG6OArmEQaujwgpoAoGCCqGSM49\\nAwEHoUQDQgAEozs4lAxgbgou0MCBnzM6DY+KYcyZCeMPYe0jhxLAhQkme8dYujBs\\nV71+Tpd4/1vd0rUiZQj778Rl1WsAEByeBw==\\n-----END EC TESTING KEY-----") + clientAuthKeyEncrypted := testingKey("-----BEGIN EC TESTING KEY-----\\nProc-Type: 4,ENCRYPTED\\nDEK-Info: AES-256-CBC,3E311E9B602231BFB5C752071EE7D652\\n\\nsAKeqbacug0v4ruE1A0CACwGVEGBQVOl1CiGVp5RsxgNZKXzMS6EsTTNLw378coF\\nKXbF+he05HIuzToOz2ANLXov1iCrVpotKVB4l2obTQvg+5VET902ky99Mc9Us7jd\\nUwW8LpXlSlhcNWuUfK6wyosL42TbcIxjqZWaESW+6ww=\\n-----END EC TESTING KEY-----") + clientAuthBad := "-----BEGIN CERTIFICATE-----\\nMIIB2TCCAX6gAwIBAgIUJIZKiR78AH2ioZ+Jae/sElgH85kwCgYIKoZIzj0EAwIw\\nEDEOMAwGA1UEAwwFTXkgQ0EwHhcNMjMwNzA3MTAyNjQ2WhcNMjQwNzA2MTAyNjQ2\\nWjARMQ8wDQYDVQQDDAZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASj\\nOziUDGBuCi7QwIGfMzoNj4phzJkJ4w9h7SOHEsCFCSZ7x1i6MGxXvX5Ol3j/W93S\\ntSJlCPvvxGXVawAQHJ4Ho4G0MIGxMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQD\\nAgWgMCwGCWCGSAGG+EIBDQQfFh1Mb2NhbCBUZXN0IENsaWVudCBDZXJ0aWZpY2F0\\nZTAdBgNVHQ4EFgQUrbSXtZnDxJwTmesLyjxuMy9JtQswHwYDVR0jBBgwFoAUpLpA\\nQPJYBb7wSQGCrKElEfj1+9YwDgYDVR0PAQH/BAQDAgXgMBMGA1UdJQQMMAoGCCsG\\nAQUFBwMEMAoGCCqGSM49BAMCA0kAMEYCIQDcHrzug3V3WvUU+tEKhG1C4cPG5rPJ\\n/y3oOoM0roOnsgIhAP23UmiC6Qdgj+MOhXWSaNt3exWvlxdKmLm2edkxaTs+\\n-----END CERTIFICATE-----" + + trivialKeyPassword := "abc123" + + type testState struct { + *modulestest.Runtime + httpBin *httpmultibin.HTTPMultiBin + samples chan metrics.SampleContainer + } + + setup := func(t *testing.T) testState { + t.Helper() + + tb := httpmultibin.NewHTTPMultiBin(t) + samples := make(chan metrics.SampleContainer, 1000) + testRuntime := modulestest.NewRuntime(t) + + cwd, err := os.Getwd() //nolint:golint,forbidigo + require.NoError(t, err) + fs := fsext.NewOsFs() + if isWindows { + fs = fsext.NewTrimFilePathSeparatorFs(fs) + } + testRuntime.VU.InitEnvField.CWD = &url.URL{Path: cwd} + testRuntime.VU.InitEnvField.FileSystems = map[string]fsext.Fs{"file": fs} + + return testState{ + Runtime: testRuntime, + httpBin: tb, + samples: samples, + } + } + + tests := []testcase{ + { + name: "ConnectTlsEmptyTlsSuccess", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: `client.connect("GRPCBIN_ADDR", { tls: { }});`, + }, + }, + { + name: "ConnectTlsInvalidTlsParamCertType", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: `client.connect("GRPCBIN_ADDR", { tls: { cert: 0 }});`, + err: `invalid grpc.connect() parameters: invalid tls cert value: 'map[string]interface {}{"cert":0}', it needs to be a PEM formatted string`, + }, + }, + { + name: "ConnectTlsInvalidTlsParamKeyType", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: `client.connect("GRPCBIN_ADDR", { tls: { cert: "", key: 0 }});`, + err: `invalid grpc.connect() parameters: invalid tls key value: 'map[string]interface {}{"cert":"", "key":0}', it needs to be a PEM formatted string`, + }, + }, + { + name: "ConnectTlsInvalidTlsParamPasswordType", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: `client.connect("GRPCBIN_ADDR", { tls: { cert: "", key: "", password: 0 }});`, + err: `invalid grpc.connect() parameters: invalid tls password value: 'map[string]interface {}{"cert":"", "key":"", "password":0}', it needs to be a string`, + }, + }, + { + name: "ConnectTlsInvalidTlsParamCACertsType", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: `client.connect("GRPCBIN_ADDR", { tls: { cert: "", key: "", cacerts: 0 }});`, + err: `invalid grpc.connect() parameters: invalid tls cacerts value: 'map[string]interface {}{"cacerts":0, "cert":"", "key":""}', it needs to be a string or an array of PEM formatted strings`, + }, + }, + { + name: "ConnectTls", + setup: func(tb *httpmultibin.HTTPMultiBin) { + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientAuthCA) + tb.ServerHTTP2.TLS.ClientAuth = tls.RequireAndVerifyClientCert + tb.ServerHTTP2.TLS.ClientCAs = clientCAPool + }, + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{code: fmt.Sprintf(`client.connect("GRPCBIN_ADDR", { tls: { cacerts: "%s", cert: "%s", key: "%s" }});`, localHostCert, clientAuth, clientAuthKey)}, + }, + { + name: "ConnectTlsEncryptedKey", + setup: func(tb *httpmultibin.HTTPMultiBin) { + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientAuthCA) + tb.ServerHTTP2.TLS.ClientAuth = tls.RequireAndVerifyClientCert + tb.ServerHTTP2.TLS.ClientCAs = clientCAPool + }, + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{code: fmt.Sprintf(`client.connect("GRPCBIN_ADDR", { tls: { cacerts: ["%s"], cert: "%s", key: "%s", password: "%s" }});`, localHostCert, clientAuth, clientAuthKeyEncrypted, trivialKeyPassword)}, + }, + { + name: "ConnectTlsEncryptedKeyDecryptionFailed", + initString: codeBlock{code: "var client = new grpc.Client();"}, + vuString: codeBlock{ + code: fmt.Sprintf(`client.connect("GRPCBIN_ADDR", { timeout: '5s', tls: { cert: "%s", key: "%s", password: "abc321" }});`, + clientAuth, + clientAuthKeyEncrypted, + ), + err: "x509: decryption password incorrect", + }, + }, + { + name: "ConnectTlsClientCertNoClientAuth", + setup: func(tb *httpmultibin.HTTPMultiBin) { + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientAuthCA) + tb.ServerHTTP2.TLS.ClientAuth = tls.RequireAndVerifyClientCert + tb.ServerHTTP2.TLS.ClientCAs = clientCAPool + }, + initString: codeBlock{code: `var client = new grpc.Client();`}, + vuString: codeBlock{ + code: fmt.Sprintf(`client.connect("GRPCBIN_ADDR", { tls: { cacerts: ["%s"], cert: "%s", key: "%s" }});`, + localHostCert, + clientAuthBad, + clientAuthKey), + err: "remote error: tls: bad certificate", + }, + }, + { + name: "ConnectTlsClientCertWithPasswordNoClientAuth", + setup: func(tb *httpmultibin.HTTPMultiBin) { + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientAuthCA) + tb.ServerHTTP2.TLS.ClientAuth = tls.RequireAndVerifyClientCert + tb.ServerHTTP2.TLS.ClientCAs = clientCAPool + }, + initString: codeBlock{code: `var client = new grpc.Client();`}, + vuString: codeBlock{ + code: fmt.Sprintf(` + client.connect("GRPCBIN_ADDR", { tls: { cacerts: ["%s"], cert: "%s", key: "%s", password: "%s" }}); + `, + localHostCert, + clientAuthBad, + clientAuthKeyEncrypted, + trivialKeyPassword), + err: "remote error: tls: bad certificate", + }, + }, + { + name: "ConnectTlsInvokeSuccess", + setup: func(tb *httpmultibin.HTTPMultiBin) { + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(clientAuthCA) + tb.ServerHTTP2.TLS.ClientAuth = tls.RequireAndVerifyClientCert + tb.ServerHTTP2.TLS.ClientCAs = clientCAPool + tb.GRPCStub.EmptyCallFunc = func(context.Context, *grpc_testing.Empty) (*grpc_testing.Empty, error) { + return &grpc_testing.Empty{}, nil + } + }, + initString: codeBlock{code: ` + var client = new grpc.Client(); + client.load([], "../../../../lib/testutils/httpmultibin/grpc_testing/test.proto");`}, + vuString: codeBlock{ + code: fmt.Sprintf(` + client.connect("GRPCBIN_ADDR", { timeout: '5s', tls: { cacerts: ["%s"], cert: "%s", key: "%s" }}); + var resp = client.invoke("grpc.testing.TestService/EmptyCall", {}) + if (resp.status !== grpc.StatusOK) { + throw new Error("unexpected error: " + JSON.stringify(resp.error) + "or status: " + resp.status) + }`, + localHostCert, + clientAuth, + clientAuthKey), + }, + }, + } + assertResponse := func(t *testing.T, cb codeBlock, err error, val goja.Value, ts testState) { + if isWindows && cb.windowsErr != "" && err != nil { + err = errors.New(strings.ReplaceAll(err.Error(), cb.windowsErr, cb.err)) + } + if cb.err == "" { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), cb.err) + } + if cb.val != nil { + require.NotNil(t, val) + assert.Equal(t, cb.val, val.Export()) + } + if cb.asserts != nil { + cb.asserts(t, ts.httpBin, ts.samples, err) + } + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ts := setup(t) + + m, ok := New().NewModuleInstance(ts.VU).(*ModuleInstance) + require.True(t, ok) + require.NoError(t, ts.VU.Runtime().Set("grpc", m.Exports().Named)) + + // setup necessary environment if needed by a test + if tt.setup != nil { + tt.setup(ts.httpBin) + } + + replace := func(code string) (goja.Value, error) { + return ts.VU.Runtime().RunString(ts.httpBin.Replacer.Replace(code)) + } + + val, err := replace(tt.initString.code) + assertResponse(t, tt.initString, err, val, ts) + + registry := metrics.NewRegistry() + root, err := lib.NewGroup("", nil) + require.NoError(t, err) + + state := &lib.State{ + Group: root, + Dialer: ts.httpBin.Dialer, + TLSConfig: ts.httpBin.TLSClientConfig, + Samples: ts.samples, + Options: lib.Options{ + SystemTags: metrics.NewSystemTagSet( + metrics.TagName, + metrics.TagURL, + ), + UserAgent: null.StringFrom("k6-test"), + }, + BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), + Tags: lib.NewVUStateTags(registry.RootTagSet()), + } + ts.MoveToVUContext(state) + val, err = replace(tt.vuString.code) + assertResponse(t, tt.vuString, err, val, ts) + }) + } +} + func TestDebugStat(t *testing.T) { t.Parallel()