diff --git a/convert.go b/convert.go index 396416f8..8ef22ea7 100644 --- a/convert.go +++ b/convert.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" @@ -29,6 +30,10 @@ func loadFromJSON(path string) (*TCase, error) { decoder := json.NewDecoder(bytes.NewReader(file)) decoder.UseNumber() err = decoder.Decode(tc) + if err != nil { + return tc, err + } + err = convertCompatTestCase(tc) return tc, err } @@ -48,9 +53,83 @@ func loadFromYAML(path string) (*TCase, error) { tc := &TCase{} err = yaml.Unmarshal(file, tc) + if err != nil { + return tc, nil + } + err = convertCompatTestCase(tc) return tc, err } +func convertCompatTestCase(tc *TCase) (err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("convert compat testcase error: %v", p) + } + }() + for _, step := range tc.TestSteps { + // 1. deal with request body compatible with HttpRunner + if step.Request != nil && step.Request.Body == nil { + if step.Request.Json != nil { + step.Request.Headers["Content-Type"] = "application/json; charset=utf-8" + step.Request.Body = step.Request.Json + } else if step.Request.Data != nil { + step.Request.Body = step.Request.Data + } + } + + // 2. deal with validators compatible with HttpRunner + for i, iValidator := range step.Validators { + validatorMap := iValidator.(map[string]interface{}) + validator := Validator{} + _, checkExisted := validatorMap["check"] + _, assertExisted := validatorMap["assert"] + _, expectExisted := validatorMap["expect"] + // check priority: HRP > HttpRunner + if checkExisted && assertExisted && expectExisted { + // HRP validator format + validator.Check = validatorMap["check"].(string) + validator.Assert = validatorMap["assert"].(string) + validator.Expect = validatorMap["expect"] + if msg, existed := validatorMap["msg"]; existed { + validator.Message = msg.(string) + } + validator.Check = convertCheckExpr(validator.Check) + step.Validators[i] = validator + } else if len(validatorMap) == 1 { + // HttpRunner validator format + for assertMethod, iValidatorContent := range validatorMap { + checkAndExpect := iValidatorContent.([]interface{}) + if len(checkAndExpect) != 2 { + return fmt.Errorf("unexpected validator format: %v", validatorMap) + } + validator.Check = checkAndExpect[0].(string) + validator.Assert = assertMethod + validator.Expect = checkAndExpect[1] + } + validator.Check = convertCheckExpr(validator.Check) + step.Validators[i] = validator + } else { + return fmt.Errorf("unexpected validator format: %v", validatorMap) + } + } + } + return err +} + +// convertCheckExpr deals with check expression including hyphen +func convertCheckExpr(checkExpr string) string { + if strings.Contains(checkExpr, textExtractorSubRegexp) { + return checkExpr + } + checkItems := strings.Split(checkExpr, ".") + for i, checkItem := range checkItems { + if strings.Contains(checkItem, "-") && !strings.Contains(checkItem, "\"") { + checkItems[i] = fmt.Sprintf("\"%s\"", checkItem) + } + } + return strings.Join(checkItems, ".") +} + func (tc *TCase) ToTestCase() (*TestCase, error) { testCase := &TestCase{ Config: tc.Config, diff --git a/convert_test.go b/convert_test.go index 53a88e56..75aea861 100644 --- a/convert_test.go +++ b/convert_test.go @@ -34,3 +34,30 @@ func TestLoadCase(t *testing.T) { t.Fail() } } + +func Test_convertCheckExpr(t *testing.T) { + exprs := []struct { + before string + after string + }{ + // normal check expression + {"a.b.c", "a.b.c"}, + {"headers.\"Content-Type\"", "headers.\"Content-Type\""}, + // check expression using regex + {"covering (.*) testing,", "covering (.*) testing,"}, + {" (.*) a-b-c", " (.*) a-b-c"}, + // abnormal check expression + {"-", "\"-\""}, + {"b-c", "\"b-c\""}, + {"a.b-c.d", "a.\"b-c\".d"}, + {"a-b.c-d", "\"a-b\".\"c-d\""}, + {"\"a-b\".c-d", "\"a-b\".\"c-d\""}, + {"headers.Content-Type", "headers.\"Content-Type\""}, + {"body.I-am-a-Key.name", "body.\"I-am-a-Key\".name"}, + } + for _, expr := range exprs { + if !assert.Equal(t, convertCheckExpr(expr.before), expr.after) { + t.Fail() + } + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cc54b050..f00f32c2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,8 +1,9 @@ # Release History -## v0.6.3 (2022-02-22) +## v0.6.3 (2022-03-03) - feat: support customized setup/teardown hooks (variable assignment not supported) +- compat: support testcase generated by HttpRunner ## v0.6.2 (2022-02-22) diff --git a/examples/compat_test.go b/examples/compat_test.go new file mode 100644 index 00000000..85a61b01 --- /dev/null +++ b/examples/compat_test.go @@ -0,0 +1,25 @@ +package examples + +import ( + "testing" + + "github.com/httprunner/hrp" +) + +// generated by examples/har/demo.har using HttpRunner v3.1.6 +const demoHttpRunnerJSONPath = "demo_httprunner.json" +const demoHttpRunnerYAMLPath = "demo_httprunner.yaml" + +func TestCompatTestCase(t *testing.T) { + testcaseFromJSON := &hrp.TestCasePath{Path: demoHttpRunnerJSONPath} + err := hrp.NewRunner(t).Run(testcaseFromJSON) + if err != nil { + t.Fatalf("run testcase error: %v", err) + } + + testcaseFromYAML := &hrp.TestCasePath{Path: demoHttpRunnerYAMLPath} + err = hrp.NewRunner(t).Run(testcaseFromYAML) + if err != nil { + t.Fatalf("run testcase error: %v", err) + } +} diff --git a/examples/demo_httprunner.json b/examples/demo_httprunner.json new file mode 100644 index 00000000..de017c96 --- /dev/null +++ b/examples/demo_httprunner.json @@ -0,0 +1,135 @@ +{ + "config": { + "name": "testcase description", + "variables": {}, + "verify": false + }, + "teststeps": [ + { + "name": "/get", + "request": { + "url": "https://postman-echo.com/get", + "params": { + "foo1": "HDnY8", + "foo2": "34.5" + }, + "method": "GET", + "headers": { + "Host": "postman-echo.com", + "User-Agent": "HttpRunnerPlus", + "Accept-Encoding": "gzip" + } + }, + "validate": [ + { + "eq": [ + "status_code", + 200 + ] + }, + { + "eq": [ + "headers.Content-Type", + "application/json; charset=utf-8" + ] + }, + { + "eq": [ + "body.url", + "https://postman-echo.com/get?foo1=HDnY8&foo2=34.5" + ] + } + ] + }, + { + "name": "/post", + "request": { + "url": "https://postman-echo.com/post", + "method": "POST", + "cookies": { + "sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk" + }, + "headers": { + "Host": "postman-echo.com", + "User-Agent": "Go-http-client/1.1", + "Content-Length": "28", + "Content-Type": "application/json; charset=UTF-8", + "Cookie": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk", + "Accept-Encoding": "gzip" + }, + "json": { + "foo1": "HDnY8", + "foo2": 12.3 + } + }, + "validate": [ + { + "eq": [ + "status_code", + 200 + ] + }, + { + "eq": [ + "headers.Content-Type", + "application/json; charset=utf-8" + ] + }, + { + "eq": [ + "body.url", + "https://postman-echo.com/post" + ] + } + ] + }, + { + "name": "/post", + "request": { + "url": "https://postman-echo.com/post", + "method": "POST", + "cookies": { + "sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw" + }, + "headers": { + "Host": "postman-echo.com", + "User-Agent": "Go-http-client/1.1", + "Content-Length": "20", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Cookie": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw", + "Accept-Encoding": "gzip" + }, + "data": { + "foo1": "HDnY8", + "foo2": "12.3" + } + }, + "validate": [ + { + "eq": [ + "status_code", + 200 + ] + }, + { + "eq": [ + "headers.Content-Type", + "application/json; charset=utf-8" + ] + }, + { + "eq": [ + "body.data", + "" + ] + }, + { + "eq": [ + "body.url", + "https://postman-echo.com/post" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/demo_httprunner.yaml b/examples/demo_httprunner.yaml new file mode 100644 index 00000000..0f39723f --- /dev/null +++ b/examples/demo_httprunner.yaml @@ -0,0 +1,81 @@ +config: + name: testcase description + variables: {} + verify: false +teststeps: +- name: /get + request: + headers: + Accept-Encoding: gzip + Host: postman-echo.com + User-Agent: HttpRunnerPlus + method: GET + params: + foo1: HDnY8 + foo2: '34.5' + url: https://postman-echo.com/get + validate: + - eq: + - status_code + - 200 + - eq: + - headers.Content-Type + - application/json; charset=utf-8 + - eq: + - body.url + - https://postman-echo.com/get?foo1=HDnY8&foo2=34.5 +- name: /post + request: + cookies: + sails.sid: s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk + headers: + Accept-Encoding: gzip + Content-Length: '28' + Content-Type: application/json; charset=UTF-8 + Cookie: sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk + Host: postman-echo.com + User-Agent: Go-http-client/1.1 + json: + foo1: HDnY8 + foo2: 12.3 + method: POST + url: https://postman-echo.com/post + validate: + - eq: + - status_code + - 200 + - eq: + - headers.Content-Type + - application/json; charset=utf-8 + - eq: + - body.url + - https://postman-echo.com/post +- name: /post + request: + cookies: + sails.sid: s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw + data: + foo1: HDnY8 + foo2: '12.3' + headers: + Accept-Encoding: gzip + Content-Length: '20' + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + Cookie: sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw + Host: postman-echo.com + User-Agent: Go-http-client/1.1 + method: POST + url: https://postman-echo.com/post + validate: + - eq: + - status_code + - 200 + - eq: + - headers.Content-Type + - application/json; charset=utf-8 + - eq: + - body.data + - '' + - eq: + - body.url + - https://postman-echo.com/post diff --git a/examples/rendezvous_test.go b/examples/rendezvous_test.go index 1d004a2e..409167f0 100644 --- a/examples/rendezvous_test.go +++ b/examples/rendezvous_test.go @@ -7,6 +7,8 @@ import ( "github.com/httprunner/hrp/internal/builtin" ) +const rendezvousTestJSONPath = "rendezvous_test.json" + var rendezvousTestcase = &hrp.TestCase{ Config: hrp.NewConfig("run request with functions"). SetBaseURL("https://postman-echo.com"). @@ -59,7 +61,7 @@ func TestRendezvousDump2JSON(t *testing.T) { if err != nil { t.Fatalf("ToTCase error: %v", err) } - err = builtin.Dump2JSON(tCase, "rendezvous_test.json") + err = builtin.Dump2JSON(tCase, rendezvousTestJSONPath) if err != nil { t.Fatalf("dump to json error: %v", err) } diff --git a/internal/har2case/core.go b/internal/har2case/core.go index ed2ce221..b22ba0a0 100644 --- a/internal/har2case/core.go +++ b/internal/har2case/core.go @@ -145,7 +145,7 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { step := &tStep{ TStep: hrp.TStep{ Request: &hrp.Request{}, - Validators: make([]hrp.Validator, 0), + Validators: make([]interface{}, 0), }, } if err := step.makeRequestMethod(entry); err != nil { diff --git a/internal/har2case/core_test.go b/internal/har2case/core_test.go index 96b0c232..db71fbd2 100644 --- a/internal/har2case/core_test.go +++ b/internal/har2case/core_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/httprunner/hrp" ) var ( @@ -98,13 +100,16 @@ func TestMakeTestCase(t *testing.T) { } // make validators - if !assert.Equal(t, "status_code", tCase.TestSteps[0].Validators[0].Check) { + validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator) + if !ok || !assert.Equal(t, "status_code", validator.Check) { t.Fail() } - if !assert.Equal(t, "headers.\"Content-Type\"", tCase.TestSteps[0].Validators[1].Check) { + validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator) + if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) { t.Fail() } - if !assert.Equal(t, "body.url", tCase.TestSteps[0].Validators[2].Check) { + validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator) + if !ok || !assert.Equal(t, "body.url", validator.Check) { t.Fail() } } diff --git a/models.go b/models.go index f5e43cfa..9b524c1e 100644 --- a/models.go +++ b/models.go @@ -97,6 +97,8 @@ type Request struct { Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` Cookies map[string]string `json:"cookies,omitempty" yaml:"cookies,omitempty"` Body interface{} `json:"body,omitempty" yaml:"body,omitempty"` + Json interface{} `json:"json,omitempty" yaml:"json,omitempty"` + Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` Timeout float32 `json:"timeout,omitempty" yaml:"timeout,omitempty"` AllowRedirects bool `json:"allow_redirects,omitempty" yaml:"allow_redirects,omitempty"` Verify bool `json:"verify,omitempty" yaml:"verify,omitempty"` @@ -122,7 +124,7 @@ type TStep struct { SetupHooks []string `json:"setup_hooks,omitempty" yaml:"setup_hooks,omitempty"` TeardownHooks []string `json:"teardown_hooks,omitempty" yaml:"teardown_hooks,omitempty"` Extract map[string]string `json:"extract,omitempty" yaml:"extract,omitempty"` - Validators []Validator `json:"validate,omitempty" yaml:"validate,omitempty"` + Validators []interface{} `json:"validate,omitempty" yaml:"validate,omitempty"` Export []string `json:"export,omitempty" yaml:"export,omitempty"` } @@ -301,11 +303,11 @@ type testCaseInOut struct { type testCaseSummary struct { Name string `json:"name" yaml:"name"` Success bool `json:"success" yaml:"success"` - CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` //TODO + CaseId string `json:"case_id,omitempty" yaml:"case_id,omitempty"` // TODO Stat *testStepStat `json:"stat" yaml:"stat"` Time *testCaseTime `json:"time" yaml:"time"` InOut *testCaseInOut `json:"in_out" yaml:"in_out"` - Log string `json:"log,omitempty" yaml:"log,omitempty"` //TODO + Log string `json:"log,omitempty" yaml:"log,omitempty"` // TODO Records []*stepData `json:"records" yaml:"records"` } @@ -330,7 +332,7 @@ type address struct { type SessionData struct { Success bool `json:"success" yaml:"success"` ReqResps *reqResps `json:"req_resps" yaml:"req_resps"` - Address *address `json:"address,omitempty" yaml:"address,omitempty"` //TODO + Address *address `json:"address,omitempty" yaml:"address,omitempty"` // TODO Validators []*validationResult `json:"validators,omitempty" yaml:"validators,omitempty"` } diff --git a/response.go b/response.go index 2861b119..1085c382 100644 --- a/response.go +++ b/response.go @@ -114,8 +114,12 @@ func (v *responseObject) Extract(extractors map[string]string) map[string]interf return extractMapping } -func (v *responseObject) Validate(validators []Validator, variablesMapping map[string]interface{}) (err error) { - for _, validator := range validators { +func (v *responseObject) Validate(iValidators []interface{}, variablesMapping map[string]interface{}) (err error) { + for _, iValidator := range iValidators { + validator, ok := iValidator.(Validator) + if !ok { + return errors.New("validator type error") + } // parse check value checkItem := validator.Check var checkValue interface{} @@ -160,6 +164,7 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s } v.validationResults = append(v.validationResults, validResult) log.Info(). + Str("checkExpr", validator.Check). Str("assertMethod", assertMethod). Interface("expectValue", expectValue). Interface("checkValue", checkValue). @@ -168,7 +173,8 @@ func (v *responseObject) Validate(validators []Validator, variablesMapping map[s if !result { v.t.Fail() return errors.New(fmt.Sprintf( - "do assertion failed, assertMethod: %v, checkValue: %v, expectValue: %v", + "do assertion failed, checkExpr: %v, assertMethod: %v, checkValue: %v, expectValue: %v", + validator.Check, assertMethod, checkValue, expectValue, diff --git a/runner.go b/runner.go index 7dd0178b..75a15ee5 100644 --- a/runner.go +++ b/runner.go @@ -703,6 +703,15 @@ func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err erro if err != nil { return stepResult, err } + // check request body format if Content-Type specified as application/json + if strings.HasPrefix(req.Header.Get("Content-Type"), "application/json") { + switch data.(type) { + case bool, float64, string, map[string]interface{}, []interface{}, nil: + break + default: + return stepResult, errors.Errorf("request body type inconsistent with Content-Type: %v", req.Header.Get("Content-Type")) + } + } requestMap["body"] = data var dataBytes []byte switch vv := data.(type) { diff --git a/step_test.go b/step_test.go index f180983f..64d03785 100644 --- a/step_test.go +++ b/step_test.go @@ -42,7 +42,8 @@ func TestRunRequestGetToStruct(t *testing.T) { if tStep.Request.Cookies["user"] != "debugtalk" { t.Fatalf("tStep.Request.Cookies mismatch") } - if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 { + validator, ok := tStep.Validators[0].(Validator) + if !ok || validator.Check != "status_code" || validator.Expect != 200 { t.Fatalf("tStep.Validators mismatch") } } @@ -67,7 +68,8 @@ func TestRunRequestPostDataToStruct(t *testing.T) { if tStep.Request.Body != "a=1&b=2" { t.Fatalf("tStep.Request.Data mismatch") } - if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 { + validator, ok := tStep.Validators[0].(Validator) + if !ok || validator.Check != "status_code" || validator.Expect != 200 { t.Fatalf("tStep.Validators mismatch") } }