diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2ae3127c..cf16caf4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,10 +4,12 @@ **go version** -- fix: using '@FILEPATH' to indicate the path of the file +- fix: using `@FILEPATH` to indicate the path of the file - feat: support indicating type and filename when uploading file - feat: support to infer MIME type of the file automatically - +- feat: support omitting websocket url if not necessary +- feat: support multiple websocket connections each session +- fix: optimize websocket step initialization ## v4.1.6 (2022-07-04) diff --git a/hrp/parser_test.go b/hrp/parser_test.go index ced53b69..5f27010b 100644 --- a/hrp/parser_test.go +++ b/hrp/parser_test.go @@ -49,6 +49,22 @@ func TestBuildURL(t *testing.T) { if !assert.Equal(t, url, "https://httpbin.org/get") { t.Fatal() } + + // websocket url + url = buildURL("wss://ws.postman-echo.com/raw", "") + if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") { + t.Fatal() + } + + url = buildURL("wss://ws.postman-echo.com", "/raw") + if !assert.Equal(t, url, "wss://ws.postman-echo.com/raw") { + t.Fatal() + } + + url = buildURL("wss://ws.postman-echo.com/raw", "ws://echo.websocket.events") + if !assert.Equal(t, url, "ws://echo.websocket.events") { + t.Fatal() + } } func TestRegexCompileVariable(t *testing.T) { diff --git a/hrp/runner.go b/hrp/runner.go index 3442b978..d32c02f0 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -267,6 +267,9 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) { return nil, errors.Wrap(err, "parse testcase config failed") } + // init websocket params + initWebSocket(testcase) + // set testcase timeout in seconds if runner.testCase.Config.Timeout != 0 { timeout := time.Duration(runner.testCase.Config.Timeout*1000) * time.Millisecond diff --git a/hrp/session.go b/hrp/session.go index 6a57de06..35192bf4 100644 --- a/hrp/session.go +++ b/hrp/session.go @@ -17,11 +17,11 @@ type SessionRunner struct { // transactions stores transaction timing info. // key is transaction name, value is map of transaction type and time, e.g. start time and end time. transactions map[string]map[transactionType]time.Time - startTime time.Time // record start time of the testcase - summary *TestCaseSummary // record test case summary - wsConn *websocket.Conn // one websocket connection each session - pongResponseChan chan string // channel used to receive pong response message - closeResponseChan chan *wsCloseRespObject // channel used to receive close response message + startTime time.Time // record start time of the testcase + summary *TestCaseSummary // record test case summary + wsConnMap map[string]*websocket.Conn // save all websocket connections + pongResponseChan chan string // channel used to receive pong response message + closeResponseChan chan *wsCloseRespObject // channel used to receive close response message } func (r *SessionRunner) resetSession() { @@ -30,6 +30,7 @@ func (r *SessionRunner) resetSession() { r.transactions = make(map[string]map[transactionType]time.Time) r.startTime = time.Now() r.summary = newSummary() + r.wsConnMap = make(map[string]*websocket.Conn) r.pongResponseChan = make(chan string, 1) r.closeResponseChan = make(chan *wsCloseRespObject, 1) } @@ -102,11 +103,13 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error { // close websocket connection after all steps done defer func() { - if r.wsConn != nil { - log.Info().Str("testcase", config.Name).Msg("websocket disconnected") - err := r.wsConn.Close() - if err != nil { - log.Error().Err(err).Msg("websocket disconnection failed") + for _, wsConn := range r.wsConnMap { + if wsConn != nil { + log.Info().Str("testcase", config.Name).Msg("websocket disconnected") + err := wsConn.Close() + if err != nil { + log.Error().Err(err).Msg("websocket disconnection failed") + } } } }() diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go index 0967be1f..0e3131b9 100644 --- a/hrp/step_websocket.go +++ b/hrp/step_websocket.go @@ -128,40 +128,45 @@ func (s *StepWebSocket) Run(r *SessionRunner) (*StepResult, error) { return runStepWebSocket(r, s.step) } -func (s *StepWebSocket) OpenConnection(url string) *StepWebSocket { +func (s *StepWebSocket) withUrl(url ...string) *StepWebSocket { + if len(url) == 0 { + return s + } + if len(url) > 1 { + log.Warn().Msg("too many WebSocket step URL specified, using first URL") + } + s.step.WebSocket.URL = url[0] + return s +} + +func (s *StepWebSocket) OpenConnection(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsOpen - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } -func (s *StepWebSocket) PingPong(url string) *StepWebSocket { +func (s *StepWebSocket) PingPong(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsPing - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } -func (s *StepWebSocket) WriteAndRead(url string) *StepWebSocket { +func (s *StepWebSocket) WriteAndRead(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsWriteAndRead - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } -func (s *StepWebSocket) Read(url string) *StepWebSocket { +func (s *StepWebSocket) Read(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsRead - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } -func (s *StepWebSocket) Write(url string) *StepWebSocket { +func (s *StepWebSocket) Write(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsWrite - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } -func (s *StepWebSocket) CloseConnection(url string) *StepWebSocket { +func (s *StepWebSocket) CloseConnection(url ...string) *StepWebSocket { s.step.WebSocket.Type = wsClose - s.step.WebSocket.URL = url - return s + return s.withUrl(url...) } func (s *StepWebSocket) WithParams(params map[string]interface{}) *StepWebSocket { @@ -226,6 +231,23 @@ type WebSocketAction struct { Timeout int64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` } +func initWebSocket(testcase *TestCase) { + tCase := testcase.ToTCase() + for _, step := range tCase.TestSteps { + if step.WebSocket == nil { + continue + } + // init websocket action parameters + if step.WebSocket.Timeout <= 0 { + step.WebSocket.Timeout = defaultTimeout + } + // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 + if step.WebSocket.CloseStatusCode < 1000 || step.WebSocket.CloseStatusCode > 4999 { + step.WebSocket.CloseStatusCode = defaultCloseStatus + } + } +} + func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { stepResult = &StepResult{ Name: step.Name, @@ -259,9 +281,30 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er sessionData := newSessionData() parser := r.GetParser() + config := r.GetConfig() + + dummyReq := &Request{ + URL: step.WebSocket.URL, + Params: step.WebSocket.Params, + Headers: step.WebSocket.Headers, + } + rb := newRequestBuilder(parser, config, dummyReq) + + err = rb.prepareUrlParams(stepVariables) + if err != nil { + return + } + + err = rb.prepareHeaders(stepVariables) + if err != nil { + return + } + parsedURL := rb.req.URL.String() + parsedHeader := rb.req.Header // add request object to step variables, could be used in setup hooks stepVariables["hrp_step_name"] = step.Name + stepVariables["hrp_step_request"] = rb.requestMap // deal with setup hooks for _, setupHook := range step.SetupHooks { @@ -271,8 +314,6 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } } - // init websocket frame parameters - initStepWebsocket(step.WebSocket) var resp interface{} start := time.Now() @@ -282,21 +323,18 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } switch step.WebSocket.Type { case wsOpen: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Msg("open websocket connection") + log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("open websocket connection") // use the current websocket connection if existed - if r.wsConn != nil { + if r.wsConnMap[parsedURL] != nil { break } - resp, err = openWithTimeout(sessionData, r, step, stepVariables) + resp, err = openWithTimeout(parsedURL, parsedHeader, r, step) if err != nil { return stepResult, errors.Wrap(err, "open connection failed") } case wsPing: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Msg("send ping and expect pong") - if r.wsConn == nil { - return stepResult, errors.Errorf("try to use existing connection, but there is no connection") - } - err = writeWebSocket(r, step, stepVariables) + log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("send ping and expect pong") + err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "send ping message failed") } @@ -313,30 +351,30 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } }() case wsWriteAndRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Msg("write a message and read response") - err = writeWebSocket(r, step, stepVariables) + log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("write a message and read response") + err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "write message failed") } - resp, err = readMessageWithTimeout(r, step) + resp, err = readMessageWithTimeout(parsedURL, r, step) if err != nil { return stepResult, errors.Wrap(err, "read message failed") } case wsRead: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Msg("read only") - resp, err = readMessageWithTimeout(r, step) + log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("read only") + resp, err = readMessageWithTimeout(parsedURL, r, step) if err != nil { return stepResult, errors.Wrap(err, "read message failed") } case wsWrite: - log.Info().Msg("write only") - err = writeWebSocket(r, step, stepVariables) + log.Info().Str("url", parsedURL).Msg("write only") + err = writeWebSocket(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "write message failed") } case wsClose: - log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Msg("close webSocket connection") - resp, err = closeWithTimeout(r, step, stepVariables) + log.Info().Int64("timeout(ms)", step.WebSocket.Timeout).Str("url", parsedURL).Msg("close webSocket connection") + resp, err = closeWithTimeout(parsedURL, r, step, stepVariables) if err != nil { return stepResult, errors.Wrap(err, "close connection failed") } @@ -371,6 +409,7 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er } if respObj != nil { + sessionData.ReqResps.Request = rb.requestMap sessionData.ReqResps.Response = builtin.FormatResponse(respObj.respObjMeta) // extract variables from response @@ -428,56 +467,11 @@ func printWebSocketResponse(resp interface{}) error { return nil } -func initStepWebsocket(stepWebSocket *WebSocketAction) { - if stepWebSocket == nil { - return - } - if stepWebSocket.Timeout <= 0 { - stepWebSocket.Timeout = defaultTimeout - } - // close status code range: [1000, 4999]. ref: https://datatracker.ietf.org/doc/html/rfc6455#section-11.7 - if stepWebSocket.CloseStatusCode < 1000 || stepWebSocket.CloseStatusCode > 4999 { - stepWebSocket.CloseStatusCode = defaultCloseStatus - } -} - -// prepareDialInfo prepares url and headers before opening connection -func prepareDialInfo(r *SessionRunner, step *TStep, stepVariables map[string]interface{}) (*requestBuilder, error) { - parser := r.GetParser() - config := r.GetConfig() - - dummyReq := &Request{ - URL: step.WebSocket.URL, - Params: step.WebSocket.Params, - Headers: step.WebSocket.Headers, - } - dummyBuilder := newRequestBuilder(parser, config, dummyReq) - - err := dummyBuilder.prepareUrlParams(stepVariables) - if err != nil { - return nil, err - } - - err = dummyBuilder.prepareHeaders(stepVariables) - if err != nil { - return nil, err - } - - return dummyBuilder, nil -} - -func openWithTimeout(d *SessionData, r *SessionRunner, step *TStep, stepVariables map[string]interface{}) (*http.Response, error) { +func openWithTimeout(urlStr string, requestHeader http.Header, r *SessionRunner, step *TStep) (*http.Response, error) { openResponseChan := make(chan *http.Response) errorChan := make(chan error) go func() { - // prepare request and dial - rb, err := prepareDialInfo(r, step, stepVariables) - if err != nil { - errorChan <- errors.Wrap(err, "prepare dail info failed") - return - } - d.ReqResps.Request = rb.requestMap - conn, resp, err := r.hrpRunner.wsDialer.Dial(rb.req.URL.String(), rb.req.Header) + conn, resp, err := r.hrpRunner.wsDialer.Dial(urlStr, requestHeader) if err != nil { errorChan <- errors.Wrap(err, "dial tcp failed") return @@ -499,7 +493,7 @@ func openWithTimeout(d *SessionData, r *SessionRunner, step *TStep, stepVariable } return nil }) - r.wsConn = conn + r.wsConnMap[urlStr] = conn openResponseChan <- resp }() @@ -515,14 +509,15 @@ func openWithTimeout(d *SessionData, r *SessionRunner, step *TStep, stepVariable } } -func readMessageWithTimeout(r *SessionRunner, step *TStep) (*wsReadRespObject, error) { - if r.wsConn == nil { +func readMessageWithTimeout(urlString string, r *SessionRunner, step *TStep) (*wsReadRespObject, error) { + wsConn := r.wsConnMap[urlString] + if wsConn == nil { return nil, errors.New("try to use existing connection, but there is no connection") } readResponseChan := make(chan *wsReadRespObject) errorChan := make(chan error) go func() { - messageType, message, err := r.wsConn.ReadMessage() + messageType, message, err := wsConn.ReadMessage() if err != nil { errorChan <- err } else { @@ -544,18 +539,18 @@ func readMessageWithTimeout(r *SessionRunner, step *TStep) (*wsReadRespObject, e } } -func writeWebSocket(r *SessionRunner, step *TStep, stepVariables map[string]interface{}) error { - if r.wsConn == nil { +func writeWebSocket(urlString string, r *SessionRunner, step *TStep, stepVariables map[string]interface{}) error { + wsConn := r.wsConnMap[urlString] + if wsConn == nil { return errors.New("try to use existing connection, but there is no connection") } - // TODO: only support writing one kind of message each step here? // check priority: text message > binary message if step.WebSocket.TextMessage != nil { parsedMessage, parseErr := r.parser.Parse(step.WebSocket.TextMessage, stepVariables) if parseErr != nil { return parseErr } - writeErr := writeWithType(r.wsConn, step, websocket.TextMessage, parsedMessage) + writeErr := writeWithType(wsConn, step, websocket.TextMessage, parsedMessage) if writeErr != nil { return writeErr } @@ -564,13 +559,13 @@ func writeWebSocket(r *SessionRunner, step *TStep, stepVariables map[string]inte if parseErr != nil { return parseErr } - writeErr := writeWithType(r.wsConn, step, websocket.BinaryMessage, parsedMessage) + writeErr := writeWithType(wsConn, step, websocket.BinaryMessage, parsedMessage) if writeErr != nil { return writeErr } } else { log.Info().Msg("step with empty message") - err := writeWithAction(r.wsConn, step, websocket.BinaryMessage, []byte{}) + err := writeWithAction(wsConn, step, websocket.BinaryMessage, []byte{}) if err != nil { return err } @@ -610,13 +605,14 @@ func writeWithAction(c *websocket.Conn, step *TStep, messageType int, message [] } } -func closeWithTimeout(r *SessionRunner, step *TStep, stepVariables map[string]interface{}) (*wsCloseRespObject, error) { - if r.wsConn == nil { +func closeWithTimeout(urlString string, r *SessionRunner, step *TStep, stepVariables map[string]interface{}) (*wsCloseRespObject, error) { + wsConn := r.wsConnMap[urlString] + if wsConn == nil { return nil, errors.New("no connection needs to be closed") } errorChan := make(chan error) go func() { - err := writeWebSocket(r, step, stepVariables) + err := writeWebSocket(urlString, r, step, stepVariables) if err != nil { errorChan <- errors.Wrap(err, "send close message failed") return @@ -626,7 +622,7 @@ func closeWithTimeout(r *SessionRunner, step *TStep, stepVariables map[string]in var message []byte var readErr error for readErr == nil { - mt, message, readErr = r.wsConn.ReadMessage() + mt, message, readErr = wsConn.ReadMessage() if readErr == nil { log.Info(). Str("type", MessageType(mt).toString()). diff --git a/hrp/tests/protocol_test.go b/hrp/tests/protocol_test.go index 42e98207..405e8e79 100644 --- a/hrp/tests/protocol_test.go +++ b/hrp/tests/protocol_test.go @@ -56,38 +56,39 @@ func TestHTTPProtocol(t *testing.T) { func TestWebSocketProtocol(t *testing.T) { testcase := &hrp.TestCase{ Config: hrp.NewConfig("run request with WebSocket protocol"). - SetBaseURL("ws://echo.websocket.events"). WithVariables(map[string]interface{}{ - "n": 5, - "a": 12.3, - "b": 3.45, - "file": "./demo_file_load_ws_message.txt", + "n": 5, + "a": 12.3, + "b": 3.45, + "file": "./demo_file_load_ws_message.txt", + "wsEchoURL": "ws://echo.websocket.events", + "wsPostmanURL": "wss://ws.postman-echo.com/raw", }), TestSteps: []hrp.IStep{ hrp.NewStep("open connection"). WebSocket(). - OpenConnection("/"). + OpenConnection("$wsEchoURL"). // absolute url specified, disable base url anyway WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). Validate(). AssertEqual("status_code", 101, "check open status code"). AssertEqual("headers.Connection", "Upgrade", "check headers"), hrp.NewStep("ping pong test"). WebSocket(). - PingPong("/"). + PingPong("$wsEchoURL"). WithTimeout(5000), hrp.NewStep("read sponsor info"). WebSocket(). - Read("/"). + Read("$wsEchoURL"). WithTimeout(5000). Validate(). AssertContains("body", "Lob.com", "check sponsor message"), hrp.NewStep("write json"). WebSocket(). - Write("/"). + Write("$wsEchoURL"). WithTextMessage(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}), hrp.NewStep("read json"). WebSocket(). - Read("/"). + Read("$wsEchoURL"). Extract(). WithJmesPath("body.foo1", "varFoo1"). Validate(). @@ -95,25 +96,163 @@ func TestWebSocketProtocol(t *testing.T) { AssertEqual("body.foo2", 12.3, "check json foo2"), hrp.NewStep("write and read text"). WebSocket(). - WriteAndRead("/"). + WriteAndRead("$wsEchoURL"). WithTextMessage("$varFoo1"). Validate(). AssertLengthEqual("body", 5, "check length equal"), hrp.NewStep("write and read binary file"). WebSocket(). - WriteAndRead("/"). + WriteAndRead("$wsEchoURL"). WithBinaryMessage("${load_ws_message($file)}"), hrp.NewStep("write something redundant"). WebSocket(). - Write("/"). + Write("$wsEchoURL"). WithTextMessage("have a nice day!"), hrp.NewStep("write something redundant"). WebSocket(). - Write("/"). + Write("$wsEchoURL"). WithTextMessage("balabala ..."), hrp.NewStep("close connection"). WebSocket(). - CloseConnection("/"). + CloseConnection("$wsEchoURL"). + WithTimeout(30000). + WithCloseStatus(1000). + Validate(). + AssertEqual("status_code", 1000, "check close status code"), + hrp.NewStep("[postman-echo] open connection"). + WebSocket(). + OpenConnection("$wsPostmanURL"). + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). + Validate(). + AssertEqual("status_code", 101, "check open status code"). + AssertEqualFold("headers.Connection", "Upgrade", "check headers"). + AssertEqualFold("headers.Server", "nginx", "check server"). + AssertEqualFold("headers.Upgrade", "websocket", "checkout upgrade"), + hrp.NewStep("[postman-echo] write json"). + WebSocket(). + Write("$wsPostmanURL"). + WithTextMessage(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}), + hrp.NewStep("[postman-echo] read json"). + WebSocket(). + Read("$wsPostmanURL"). + Validate(). + AssertLengthEqual("body.foo1", 5, "check json foo1"). + AssertEqual("body.foo2", 12.3, "check json foo2"), + hrp.NewStep("[postman-echo] write and read text"). + WebSocket(). + WriteAndRead("$wsPostmanURL"). + WithTextMessage("$varFoo1"). + Validate(). + AssertLengthEqual("body", 5, "check length equal"), + hrp.NewStep("[postman-echo] close connection"). + WebSocket(). + CloseConnection("$wsPostmanURL"). + WithTimeout(30000). + WithCloseStatus(1000). + Validate(). + AssertEqual("status_code", 1000, "check close status code"), + }, + } + err := hrp.NewRunner(t).Run(testcase) + if err != nil { + t.Fatalf("run testcase error: %v", err) + } +} + +func TestWebSocketProtocolUsingRelativeURL(t *testing.T) { + testcase := &hrp.TestCase{ + Config: hrp.NewConfig("run request with WebSocket protocol"). + SetBaseURL("wss://ws.postman-echo.com"). + WithVariables(map[string]interface{}{ + "n": 5, + "a": 12.3, + "b": 3.45, + "file": "./demo_file_load_ws_message.txt", + }), + TestSteps: []hrp.IStep{ + hrp.NewStep("open connection"). + WebSocket(). + OpenConnection("/raw"). // relative url specified ==> wss://ws.postman-echo.com/raw + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). + Validate(). + AssertEqual("status_code", 101, "check open status code"). + AssertEqualFold("headers.Connection", "Upgrade", "check headers"). + AssertEqualFold("headers.Server", "nginx", "check server"). + AssertEqualFold("headers.Upgrade", "websocket", "checkout upgrade"), + hrp.NewStep("write json"). + WebSocket(). + Write("/raw"). + WithTextMessage(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}), + hrp.NewStep("read json"). + WebSocket(). + Read("/raw"). + Extract(). + WithJmesPath("body.foo1", "varFoo1"). + Validate(). + AssertLengthEqual("body.foo1", 5, "check json foo1"). + AssertEqual("body.foo2", 12.3, "check json foo2"), + hrp.NewStep("write and read text"). + WebSocket(). + WriteAndRead("/raw"). + WithTextMessage("$varFoo1"). + Validate(). + AssertLengthEqual("body", 5, "check length equal"), + hrp.NewStep("close connection"). + WebSocket(). + CloseConnection("/raw"). + WithTimeout(30000). + WithCloseStatus(1000). + Validate(). + AssertEqual("status_code", 1000, "check close status code"), + }, + } + err := hrp.NewRunner(t).Run(testcase) + if err != nil { + t.Fatalf("run testcase error: %v", err) + } +} + +func TestWebSocketProtocolUsingBaseURL(t *testing.T) { + testcase := &hrp.TestCase{ + Config: hrp.NewConfig("run request with WebSocket protocol"). + SetBaseURL("wss://ws.postman-echo.com/raw"). + WithVariables(map[string]interface{}{ + "n": 5, + "a": 12.3, + "b": 3.45, + "file": "./demo_file_load_ws_message.txt", + }), + TestSteps: []hrp.IStep{ + hrp.NewStep("open connection"). + WebSocket(). + OpenConnection(). // no url specified, using base url instead + WithHeaders(map[string]string{"User-Agent": "HttpRunnerPlus"}). + Validate(). + AssertEqual("status_code", 101, "check open status code"). + AssertEqualFold("headers.Connection", "Upgrade", "check headers"). + AssertEqualFold("headers.Server", "nginx", "check server"). + AssertEqualFold("headers.Upgrade", "websocket", "checkout upgrade"), + hrp.NewStep("write json"). + WebSocket(). + Write(). + WithTextMessage(map[string]interface{}{"foo1": "${gen_random_string($n)}", "foo2": "${max($a, $b)}"}), + hrp.NewStep("read json"). + WebSocket(). + Read(). + Extract(). + WithJmesPath("body.foo1", "varFoo1"). + Validate(). + AssertLengthEqual("body.foo1", 5, "check json foo1"). + AssertEqual("body.foo2", 12.3, "check json foo2"), + hrp.NewStep("write and read text"). + WebSocket(). + WriteAndRead(). + WithTextMessage("$varFoo1"). + Validate(). + AssertLengthEqual("body", 5, "check length equal"), + hrp.NewStep("close connection"). + WebSocket(). + CloseConnection(). WithTimeout(30000). WithCloseStatus(1000). Validate().