diff --git a/docs/cmd/hrp_postman2case.md b/docs/cmd/hrp_postman2case.md new file mode 100644 index 00000000..23c196e7 --- /dev/null +++ b/docs/cmd/hrp_postman2case.md @@ -0,0 +1,26 @@ +## hrp postman2case + +convert postman collection to json/yaml testcase files + +### Synopsis + +convert postman collection to json/yaml testcase files + +``` +hrp postman2case $postman_path... [flags] +``` + +### Options + +``` + -h, --help help for postman2case + -d, --output-dir string specify output directory, default to the same dir with postman collection file + -j, --to-json convert to JSON format (default true) + -y, --to-yaml convert to YAML format +``` + +### SEE ALSO + +* [hrp](hrp.md) - Next-Generation API Testing Solution. + +###### Auto generated by spf13/cobra on 12-May-2022 diff --git a/examples/data/har/demo.json b/examples/data/har/demo.json deleted file mode 100644 index 292ad513..00000000 --- a/examples/data/har/demo.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "config": { - "name": "testcase description" - }, - "teststeps": [ - { - "name": "", - "request": { - "method": "GET", - "url": "https://postman-echo.com/get", - "params": { - "foo1": "HDnY8", - "foo2": "34.5" - }, - "headers": { - "Accept-Encoding": "gzip", - "Host": "postman-echo.com", - "User-Agent": "HttpRunnerPlus" - } - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "assert response status code" - }, - { - "check": "headers.\"Content-Type\"", - "assert": "equals", - "expect": "application/json; charset=utf-8", - "msg": "assert response header Content-Type" - }, - { - "check": "body.url", - "assert": "equals", - "expect": "https://postman-echo.com/get?foo1=HDnY8\u0026foo2=34.5", - "msg": "assert response body url" - } - ] - }, - { - "name": "", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "headers": { - "Accept-Encoding": "gzip", - "Content-Length": "28", - "Content-Type": "application/json; charset=UTF-8", - "Host": "postman-echo.com", - "User-Agent": "Go-http-client/1.1" - }, - "cookies": { - "sails.sid": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk" - }, - "body": { - "foo1": "HDnY8", - "foo2": 12.3 - } - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "assert response status code" - }, - { - "check": "headers.\"Content-Type\"", - "assert": "equals", - "expect": "application/json; charset=utf-8", - "msg": "assert response header Content-Type" - }, - { - "check": "body.url", - "assert": "equals", - "expect": "https://postman-echo.com/post", - "msg": "assert response body url" - } - ] - }, - { - "name": "", - "request": { - "method": "POST", - "url": "https://postman-echo.com/post", - "headers": { - "Accept-Encoding": "gzip", - "Content-Length": "20", - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - "Host": "postman-echo.com", - "User-Agent": "Go-http-client/1.1" - }, - "cookies": { - "sails.sid": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw" - }, - "body": "foo1=HDnY8\u0026foo2=12.3" - }, - "validate": [ - { - "check": "status_code", - "assert": "equals", - "expect": 200, - "msg": "assert response status code" - }, - { - "check": "headers.\"Content-Type\"", - "assert": "equals", - "expect": "application/json; charset=utf-8", - "msg": "assert response header Content-Type" - }, - { - "check": "body.data", - "assert": "equals", - "expect": "", - "msg": "assert response body data" - }, - { - "check": "body.url", - "assert": "equals", - "expect": "https://postman-echo.com/post", - "msg": "assert response body url" - } - ] - } - ] -} \ No newline at end of file diff --git a/examples/data/postman2case/postman_collection.json b/examples/data/postman2case/demo.json similarity index 58% rename from examples/data/postman2case/postman_collection.json rename to examples/data/postman2case/demo.json index 5cedbcf8..3b7a9e30 100644 --- a/examples/data/postman2case/postman_collection.json +++ b/examples/data/postman2case/demo.json @@ -51,7 +51,7 @@ }, "response": [ { - "name": "Get with params", + "name": "Get with params case1", "originalRequest": { "method": "GET", "header": [], @@ -88,10 +88,115 @@ ] } }, + "status": "OK", + "code": 200, "_postman_previewlanguage": "json", - "header": null, + "header": [ + { + "key": "Date", + "value": "Mon, 16 May 2022 12:12:28 GMT" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "508" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "ETag", + "value": "W/\"1fc-x4EIPFQzoLX0HenCFPx6HNfG0lc\"" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "set-cookie", + "value": "sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils; Path=/; HttpOnly" + } + ], "cookie": [], - "body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}" + "body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k2\": \"v2\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k2=v2\"\n}" + }, + { + "name": "Get with params case2", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "https://postman-echo.com/:path?k1=v1&k3=v3", + "protocol": "https", + "host": [ + "postman-echo", + "com" + ], + "path": [ + ":path" + ], + "query": [ + { + "key": "k1", + "value": "v1" + }, + { + "key": "k2", + "value": "v2", + "disabled": true + }, + { + "key": "k3", + "value": "v3" + } + ], + "variable": [ + { + "key": "path", + "value": "get" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "Date", + "value": "Mon, 16 May 2022 12:14:04 GMT" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "504" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "ETag", + "value": "W/\"1f8-tMaKs4xmwr+3su3I8mcgR0p+ucw\"" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + }, + { + "key": "set-cookie", + "value": "sails.sid=s%3AMNuX_i0KgaP_KuuMpYB8RtCNipCGJWVw.4ETfPHxE81Omqb6Yli%2FezUU8CXyYBcN3%2Bxkx5htwh8Y; Path=/; HttpOnly" + } + ], + "cookie": [], + "body": "{\n \"args\": {\n \"k1\": \"v1\",\n \"k3\": \"v3\"\n },\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"PostmanRuntime/7.29.0\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AX2aa_Z7gbcUqIWAjlBkytBRmQ4WCvc3D.pX9Qxh8aO9Ict0BL4CrRhdDJmz81UVmwFsV5Nx30Ils\"\n },\n \"url\": \"https://postman-echo.com/get?k1=v1&k3=v3\"\n}" } ] } @@ -279,6 +384,11 @@ "value": "bbx", "type": "text", "disabled": true + }, + { + "key": "Connection", + "value": "close", + "type": "text" } ], "url": { @@ -301,7 +411,7 @@ }, "response": [ { - "name": "Get request headers", + "name": "Get request headers case1", "originalRequest": { "method": "GET", "header": [ @@ -315,6 +425,11 @@ "value": "bbx", "type": "text", "disabled": true + }, + { + "key": "Cookie", + "value": "Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY", + "type": "text" } ], "url": { @@ -335,10 +450,37 @@ ] } }, + "status": "OK", + "code": 200, "_postman_previewlanguage": "json", - "header": null, + "header": [ + { + "key": "Date", + "value": "Mon, 16 May 2022 12:14:25 GMT" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "541" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "ETag", + "value": "W/\"21d-ld5UvFTaRM6lihVnvCj6mZm5Of0\"" + }, + { + "key": "Vary", + "value": "Accept-Encoding" + } + ], "cookie": [], - "body": "{\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"HttpRunner\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n }\n}" + "body": "{\n \"headers\": {\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-port\": \"443\",\n \"host\": \"postman-echo.com\",\n \"user-agent\": \"HttpRunner\",\n \"cookie\": \"Cookie_1=c1; Cookie_2=c2; sails.sid=s%3AGX6aS9b_phvUSUk66w7ZBgWuOPI7IIKT.ayEGTaW4U35eAWyPz%2Fh6Q74DonNcbqw3H5Q5Zv%2BfKMY\",\n \"accept\": \"*/*\",\n \"accept-encoding\": \"gzip, deflate, br\"\n }\n}" } ] } diff --git a/examples/data/postman2case/patch.yml b/examples/data/postman2case/patch.yml new file mode 100644 index 00000000..c657b5ef --- /dev/null +++ b/examples/data/postman2case/patch.yml @@ -0,0 +1,4 @@ +headers: + User-Agent: "this header will be created or updated" +cookies: + Cookie1: "this cookie will be created or updated" diff --git a/examples/data/postman2case/profile.yml b/examples/data/postman2case/profile.yml new file mode 100644 index 00000000..42e2e9f4 --- /dev/null +++ b/examples/data/postman2case/profile.yml @@ -0,0 +1,4 @@ +headers: + Header1: "all original headers will be overridden" +cookies: + Cookie1: "all original cookies will be overridden" \ No newline at end of file diff --git a/hrp/cmd/convert.go b/hrp/cmd/convert.go index 0247d147..48a9f4bc 100644 --- a/hrp/cmd/convert.go +++ b/hrp/cmd/convert.go @@ -7,7 +7,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/convert" + "github.com/httprunner/httprunner/v4/hrp/internal/convert/case2script" ) var convertCmd = &cobra.Command{ @@ -18,15 +18,16 @@ var convertCmd = &cobra.Command{ setLogLevel(logLevel) }, RunE: func(cmd *cobra.Command, args []string) error { + // TODO: integrate har2case, postman2case, etc. in convert command (forward compatibility) if !pytestFlag && !gotestFlag { return errors.New("please specify convertion type") } var err error if gotestFlag { - err = convert.Convert2TestScripts("gotest", args...) + err = case2script.Convert2TestScripts("gotest", args...) } else { - err = convert.Convert2TestScripts("pytest", args...) + err = case2script.Convert2TestScripts("pytest", args...) } if err != nil { log.Error().Err(err).Msg("convert test scripts failed") diff --git a/hrp/cmd/har2case.go b/hrp/cmd/har2case.go index eecd40cc..42fab1bd 100644 --- a/hrp/cmd/har2case.go +++ b/hrp/cmd/har2case.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/har2case" + "github.com/httprunner/httprunner/v4/hrp/internal/convert/har2case" ) // har2caseCmd represents the har2case command @@ -22,7 +22,7 @@ var har2caseCmd = &cobra.Command{ var outputFiles []string for _, arg := range args { // must choose one - if !genYAMLFlag && !genJSONFlag { + if !har2caseGenYAMLFlag && !har2caseGenJSONFlag { return errors.New("please select convert format type") } var outputPath string @@ -31,17 +31,22 @@ var har2caseCmd = &cobra.Command{ har := har2case.NewHAR(arg) // specify output dir - if outputDir != "" { - har.SetOutputDir(outputDir) + if har2caseOutputDir != "" { + har.SetOutputDir(har2caseOutputDir) } // specify profile - if profilePath != "" { - har.SetProfile(profilePath) + if har2caseProfilePath != "" { + har.SetProfile(har2caseProfilePath) + } + + // specify profile + if har2casePatchPath != "" { + har.SetPatch(har2casePatchPath) } // generate json/yaml files - if genYAMLFlag { + if har2caseGenYAMLFlag { outputPath, err = har.GenYAML() } else { outputPath, err = har.GenJSON() // default @@ -57,16 +62,18 @@ var har2caseCmd = &cobra.Command{ } var ( - genJSONFlag bool - genYAMLFlag bool - outputDir string - profilePath string + har2caseGenJSONFlag bool + har2caseGenYAMLFlag bool + har2caseOutputDir string + har2caseProfilePath string + har2casePatchPath string ) func init() { rootCmd.AddCommand(har2caseCmd) - har2caseCmd.Flags().BoolVarP(&genJSONFlag, "to-json", "j", true, "convert to JSON format") - har2caseCmd.Flags().BoolVarP(&genYAMLFlag, "to-yaml", "y", false, "convert to YAML format") - har2caseCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") - har2caseCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers and cookies") + har2caseCmd.Flags().BoolVarP(&har2caseGenJSONFlag, "to-json", "j", true, "convert to JSON format") + har2caseCmd.Flags().BoolVarP(&har2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format") + har2caseCmd.Flags().StringVarP(&har2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file") + har2caseCmd.Flags().StringVarP(&har2caseProfilePath, "profile", "p", "", "specify profile path to override headers and cookies") + har2caseCmd.Flags().StringVarP(&har2casePatchPath, "patch", "r", "", "specify the path of the file used to replace headers and cookies") } diff --git a/hrp/cmd/postman2case.go b/hrp/cmd/postman2case.go index 5ccedacb..2e0c1369 100644 --- a/hrp/cmd/postman2case.go +++ b/hrp/cmd/postman2case.go @@ -6,7 +6,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/httprunner/httprunner/v4/hrp/internal/postman2case" + "github.com/httprunner/httprunner/v4/hrp/internal/convert/postman2case" ) // postman2caseCmd represents the postman2case command @@ -22,24 +22,34 @@ var postman2caseCmd = &cobra.Command{ var outputFiles []string for _, arg := range args { // must choose one - if !postman2JSONFlag && !postman2YAMLFlag { + if !postman2caseGenJSONFlag && !postman2caseGenYAMLFlag { return errors.New("please select convert format type") } var outputPath string var err error - postman := postman2case.NewCollection(arg) + collection := postman2case.NewCollection(arg) // specify output dir - if postman2Dir != "" { - postman.SetOutputDir(postman2Dir) + if postman2caseOutputDir != "" { + collection.SetOutputDir(postman2caseOutputDir) + } + + // specify profile path + if postman2caseProfilePath != "" { + collection.SetProfile(postman2caseProfilePath) + } + + // specify patch path + if postman2casePatchPath != "" { + collection.SetPatch(postman2casePatchPath) } // generate json/yaml files - if genYAMLFlag { - outputPath, err = postman.GenYAML() + if postman2caseGenYAMLFlag { + outputPath, err = collection.GenYAML() } else { - outputPath, err = postman.GenJSON() // default + outputPath, err = collection.GenJSON() // default } if err != nil { return err @@ -52,14 +62,18 @@ var postman2caseCmd = &cobra.Command{ } var ( - postman2JSONFlag bool - postman2YAMLFlag bool - postman2Dir string + postman2caseGenJSONFlag bool + postman2caseGenYAMLFlag bool + postman2caseOutputDir string + postman2caseProfilePath string + postman2casePatchPath string ) func init() { rootCmd.AddCommand(postman2caseCmd) - postman2caseCmd.Flags().BoolVarP(&postman2JSONFlag, "to-json", "j", true, "convert to JSON format") - postman2caseCmd.Flags().BoolVarP(&postman2YAMLFlag, "to-yaml", "y", false, "convert to YAML format") - postman2caseCmd.Flags().StringVarP(&postman2Dir, "output-dir", "d", "", "specify output directory, default to the same dir with postman collection file") + postman2caseCmd.Flags().BoolVarP(&postman2caseGenJSONFlag, "to-json", "j", true, "convert to JSON format") + postman2caseCmd.Flags().BoolVarP(&postman2caseGenYAMLFlag, "to-yaml", "y", false, "convert to YAML format") + postman2caseCmd.Flags().StringVarP(&postman2caseOutputDir, "output-dir", "d", "", "specify output directory, default to the same dir with postman collection file") + postman2caseCmd.Flags().StringVarP(&postman2caseProfilePath, "profile", "p", "", "specify profile path to override original headers (except for Content-Type) and cookies") + postman2caseCmd.Flags().StringVarP(&postman2casePatchPath, "patch", "r", "", "specify patch path to create or update headers and cookies") } diff --git a/hrp/internal/convert/main.go b/hrp/internal/convert/case2script/main.go similarity index 99% rename from hrp/internal/convert/main.go rename to hrp/internal/convert/case2script/main.go index ea58dd6e..bfc75b27 100644 --- a/hrp/internal/convert/main.go +++ b/hrp/internal/convert/case2script/main.go @@ -1,4 +1,4 @@ -package convert +package case2script import ( _ "embed" diff --git a/hrp/internal/convert/testcase.tmpl b/hrp/internal/convert/case2script/testcase.tmpl similarity index 100% rename from hrp/internal/convert/testcase.tmpl rename to hrp/internal/convert/case2script/testcase.tmpl diff --git a/hrp/internal/har2case/README.md b/hrp/internal/convert/har2case/README.md similarity index 100% rename from hrp/internal/har2case/README.md rename to hrp/internal/convert/har2case/README.md diff --git a/hrp/internal/har2case/core.go b/hrp/internal/convert/har2case/core.go similarity index 84% rename from hrp/internal/har2case/core.go rename to hrp/internal/convert/har2case/core.go index 25824855..0e96a96d 100644 --- a/hrp/internal/har2case/core.go +++ b/hrp/internal/convert/har2case/core.go @@ -22,6 +22,13 @@ const ( suffixYAML = ".yaml" ) +const ( + configProfile = "profile" + configPatch = "patch" + keyHeaders = "headers" + keyCookies = "cookies" +) + func NewHAR(path string) *har { return &har{ path: path, @@ -33,6 +40,7 @@ type har struct { filterStr string excludeStr string profile map[string]interface{} + patch map[string]interface{} outputDir string } @@ -46,6 +54,16 @@ func (h *har) SetProfile(path string) { } } +func (h *har) SetPatch(path string) { + log.Info().Str("path", path).Msg("set patch") + h.patch = make(map[string]interface{}) + err := builtin.LoadFile(path, h.patch) + if err != nil { + log.Warn().Str("path", path). + Msg("invalid patch format, ignore!") + } +} + func (h *har) SetOutputDir(dir string) { log.Info().Str("dir", dir).Msg("set output directory") h.outputDir = dir @@ -146,6 +164,7 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { Validators: make([]interface{}, 0), }, profile: h.profile, + patch: h.patch, } if err := step.makeRequestMethod(entry); err != nil { return nil, err @@ -174,6 +193,7 @@ func (h *har) prepareTestStep(entry *Entry) (*hrp.TStep, error) { type tStep struct { hrp.TStep profile map[string]interface{} + patch map[string]interface{} } func (s *tStep) makeRequestMethod(entry *Entry) error { @@ -199,43 +219,59 @@ func (s *tStep) makeRequestParams(entry *Entry) error { return nil } +func (s *tStep) updateRequestInfo(config string, key string) bool { + var m map[string]interface{} + switch config { + case configProfile: + m = s.profile + case configPatch: + m = s.patch + default: + return false + } + iRequestMap, existed := m[key] + if existed { + requestMap, ok := iRequestMap.(map[string]interface{}) + if ok { + for k, v := range requestMap { + switch key { + case keyHeaders: + s.Request.Headers[k] = fmt.Sprintf("%v", v) + case keyCookies: + s.Request.Cookies[k] = fmt.Sprintf("%v", v) + } + } + return true + } + log.Warn().Interface(key, iRequestMap).Msgf("%v from %v is not a map, ignore!", key, config) + } + return false +} + func (s *tStep) makeRequestCookies(entry *Entry) error { s.Request.Cookies = make(map[string]string) - cookies, ok := s.profile["cookies"] - if ok { - // use cookies from profile - cookies, ok := cookies.(map[string]interface{}) - if ok { - for k, v := range cookies { - s.Request.Cookies[k] = fmt.Sprintf("%v", v) - } - return nil - } - log.Warn().Interface("cookies", cookies). - Msg("cookies from profile is not a map, ignore!") + + // override all cookies according to the profile + if s.updateRequestInfo(configProfile, keyCookies) { + return nil } // use cookies from har for _, cookie := range entry.Request.Cookies { s.Request.Cookies[cookie.Name] = cookie.Value } + + // create or update the cookies indicated in the patch + s.updateRequestInfo(configPatch, keyCookies) return nil } func (s *tStep) makeRequestHeaders(entry *Entry) error { s.Request.Headers = make(map[string]string) - headers, ok := s.profile["headers"] - if ok { - // use headers from profile - cookies, ok := headers.(map[string]interface{}) - if ok { - for k, v := range cookies { - s.Request.Headers[k] = fmt.Sprintf("%v", v) - } - return nil - } - log.Warn().Interface("headers", headers). - Msg("headers from profile is not a map, ignore!") + + // override all headers according to the profile + if s.updateRequestInfo(configProfile, keyHeaders) { + return nil } // use headers from har @@ -245,6 +281,9 @@ func (s *tStep) makeRequestHeaders(entry *Entry) error { } s.Request.Headers[header.Name] = header.Value } + + // create or update the headers indicated in the patch + s.updateRequestInfo(configPatch, keyHeaders) return nil } diff --git a/hrp/internal/har2case/core_test.go b/hrp/internal/convert/har2case/core_test.go similarity index 90% rename from hrp/internal/har2case/core_test.go rename to hrp/internal/convert/har2case/core_test.go index de2ee910..ce6466fe 100644 --- a/hrp/internal/har2case/core_test.go +++ b/hrp/internal/convert/har2case/core_test.go @@ -1,6 +1,7 @@ package har2case import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -9,9 +10,9 @@ import ( ) var ( - harPath = "../../../examples/data/har/demo.har" - harPath2 = "../../../examples/data/har/postman-echo.har" - profilePath = "../../../examples/data/har/profile.yml" + harPath = "../../../../examples/data/har/demo.har" + harPath2 = "../../../../examples/data/har/postman-echo.har" + profilePath = "../../../../examples/data/har/profile.yml" ) func TestGenJSON(t *testing.T) { @@ -381,3 +382,32 @@ func TestMakeValidate(t *testing.T) { t.Fatal() } } + +func Test_tStep_makeRequestCookies(t *testing.T) { + type fields struct { + TStep hrp.TStep + profile map[string]interface{} + patch map[string]interface{} + } + type args struct { + entry *Entry + } + tests := []struct { + name string + fields fields + args args + wantErr assert.ErrorAssertionFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &tStep{ + TStep: tt.fields.TStep, + profile: tt.fields.profile, + patch: tt.fields.patch, + } + tt.wantErr(t, s.makeRequestCookies(tt.args.entry), fmt.Sprintf("makeRequestCookies(%v)", tt.args.entry)) + }) + } +} diff --git a/hrp/internal/har2case/har.go b/hrp/internal/convert/har2case/har.go similarity index 100% rename from hrp/internal/har2case/har.go rename to hrp/internal/convert/har2case/har.go diff --git a/hrp/internal/postman2case/collection.go b/hrp/internal/convert/postman2case/collection.go similarity index 100% rename from hrp/internal/postman2case/collection.go rename to hrp/internal/convert/postman2case/collection.go diff --git a/hrp/internal/postman2case/core.go b/hrp/internal/convert/postman2case/core.go similarity index 72% rename from hrp/internal/postman2case/core.go rename to hrp/internal/convert/postman2case/core.go index f4b8b4e3..1f15cbf5 100644 --- a/hrp/internal/postman2case/core.go +++ b/hrp/internal/convert/postman2case/core.go @@ -3,7 +3,6 @@ package postman2case import ( "bytes" "fmt" - "github.com/httprunner/httprunner/v4/hrp/internal/json" "io" "mime/multipart" "net/url" @@ -17,6 +16,7 @@ import ( "github.com/httprunner/httprunner/v4/hrp" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/httprunner/httprunner/v4/hrp/internal/json" ) const ( @@ -33,11 +33,18 @@ const ( ) const ( - suffixName = ".converted" + suffixName = ".converted" // distinguish the converted json(testcase) from the origin json(collection) extensionJSON = ".json" extensionYAML = ".yaml" ) +const ( + configProfile = "profile" + configPatch = "patch" + keyHeaders = "headers" + keyCookies = "cookies" +) + var contentTypeMap = map[string]string{ "text": "text/plain", "javascript": "application/javascript", @@ -54,9 +61,31 @@ func NewCollection(path string) *collection { type collection struct { path string + profile map[string]interface{} + patch map[string]interface{} outputDir string } +func (c *collection) SetProfile(path string) { + log.Info().Str("path", path).Msg("set profile") + c.profile = make(map[string]interface{}) + err := builtin.LoadFile(path, c.profile) + if err != nil { + log.Warn().Str("path", path). + Msg("invalid profile format, ignore!") + } +} + +func (c *collection) SetPatch(path string) { + log.Info().Str("path", path).Msg("set patch") + c.patch = make(map[string]interface{}) + err := builtin.LoadFile(path, c.patch) + if err != nil { + log.Warn().Str("path", path). + Msg("invalid patch format, ignore!") + } +} + func (c *collection) SetOutputDir(dir string) { log.Info().Str("dir", dir).Msg("set output directory") c.outputDir = dir @@ -169,10 +198,12 @@ func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) { Msg("convert teststep") step := &tStep{ - hrp.TStep{ + TStep: hrp.TStep{ Request: &hrp.Request{}, Validators: make([]interface{}, 0), }, + profile: c.profile, + patch: c.patch, } if err := step.makeRequestName(item); err != nil { return nil, err @@ -186,20 +217,22 @@ func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) { if err := step.makeRequestParams(item); err != nil { return nil, err } - if err := step.makeRequestHeadersAndCookies(item); err != nil { + if err := step.makeRequestHeaders(item); err != nil { + return nil, err + } + if err := step.makeRequestCookies(item); err != nil { return nil, err } if err := step.makeRequestBody(item); err != nil { return nil, err } - if err := step.makeValidate(item); err != nil { - return nil, err - } return &step.TStep, nil } type tStep struct { hrp.TStep + profile map[string]interface{} + patch map[string]interface{} } // makeRequestName indicates the step name the same as item name @@ -239,21 +272,89 @@ func (s *tStep) makeRequestParams(item *TItem) error { return nil } -func (s *tStep) makeRequestHeadersAndCookies(item *TItem) error { - s.Request.Headers = make(map[string]string) - for _, field := range item.Request.Headers { - if field.Disabled { - continue +func (s *tStep) updateRequestInfo(config string, key string) bool { + var m map[string]interface{} + switch config { + case configProfile: + m = s.profile + case configPatch: + m = s.patch + default: + return false + } + iRequestMap, existed := m[key] + if existed { + requestMap, ok := iRequestMap.(map[string]interface{}) + if ok { + for k, v := range requestMap { + switch key { + case keyHeaders: + s.Request.Headers[k] = fmt.Sprintf("%v", v) + case keyCookies: + s.Request.Cookies[k] = fmt.Sprintf("%v", v) + } + } + return true } - if strings.EqualFold(field.Key, "cookie") { - s.Request.Cookies[field.Key] = field.Value + log.Warn().Interface(key, iRequestMap).Msgf("%v from %v is not a map, ignore!", key, config) + } + return false +} + +func (s *tStep) makeRequestHeaders(item *TItem) error { + s.Request.Headers = make(map[string]string) + + // override all headers according to the profile + if s.updateRequestInfo(configProfile, keyHeaders) { + return nil + } + + // headers defined in postman collection + for _, field := range item.Request.Headers { + if field.Disabled || strings.EqualFold(field.Key, "cookie") { continue } s.Request.Headers[field.Key] = field.Value } + + // create or update the headers indicated in the patch + s.updateRequestInfo(configPatch, keyHeaders) return nil } +func (s *tStep) makeRequestCookies(item *TItem) error { + s.Request.Cookies = make(map[string]string) + + // override all cookies according to the profile + if s.updateRequestInfo(configProfile, keyCookies) { + return nil + } + + // cookies defined in postman collection + for _, field := range item.Request.Headers { + if field.Disabled || !strings.EqualFold(field.Key, "cookie") { + continue + } + s.parseRequestCookiesMap(field.Value) + } + + // create or update the cookies indicated in the patch + s.updateRequestInfo(configPatch, keyCookies) + return nil +} + +func (s *tStep) parseRequestCookiesMap(cookies string) { + for _, cookie := range strings.Split(cookies, ";") { + cookie = strings.TrimSpace(cookie) + index := strings.Index(cookie, "=") + if index == -1 { + log.Warn().Str("cookie", cookie).Msg("cookie format invalid") + continue + } + s.Request.Cookies[cookie[0:index]] = cookie[index+1:] + } +} + func (s *tStep) makeRequestBody(item *TItem) error { mode := item.Request.Body.Mode if mode == "" { @@ -267,7 +368,7 @@ func (s *tStep) makeRequestBody(item *TItem) error { case enumBodyUrlEncoded: return s.makeRequestBodyUrlEncoded(item) case enumBodyFile, enumBodyGraphQL: - return errors.New("not supported body type") + return errors.Errorf("unsupported body type: %v", mode) } return nil } diff --git a/hrp/internal/convert/postman2case/core_test.go b/hrp/internal/convert/postman2case/core_test.go new file mode 100644 index 00000000..a102e136 --- /dev/null +++ b/hrp/internal/convert/postman2case/core_test.go @@ -0,0 +1,155 @@ +package postman2case + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + collectionPath = "../../../../examples/data/postman2case/demo.json" + profilePath = "../../../../examples/data/postman2case/profile.yml" + patchPath = "../../../../examples/data/postman2case/patch.yml" +) + +func TestGenJSON(t *testing.T) { + jsonPath, err := NewCollection(collectionPath).GenJSON() + if !assert.NoError(t, err) { + t.Fatal() + } + if !assert.NotEmpty(t, jsonPath) { + t.Fatal() + } +} + +func TestGenYAML(t *testing.T) { + yamlPath, err := NewCollection(collectionPath).GenYAML() + if !assert.NoError(t, err) { + t.Fatal() + } + if !assert.NotEmpty(t, yamlPath) { + t.Fatal() + } +} + +func TestLoadCollection(t *testing.T) { + tCollection, err := NewCollection(collectionPath).load() + if !assert.NoError(t, err) { + t.Fatal(err) + } + if !assert.Equal(t, "postman collection demo", tCollection.Info.Name) { + t.Fatal() + } +} + +func TestMakeTestCase(t *testing.T) { + tCase, err := NewCollection(collectionPath).makeTestCase() + if !assert.NoError(t, err) { + t.Fatal() + } + // check name + if !assert.Equal(t, "postman collection demo", tCase.Config.Name) { + t.Fatal() + } + // check method + if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) { + t.Fatal() + } + if !assert.EqualValues(t, "POST", tCase.TestSteps[1].Request.Method) { + t.Fatal() + } + // check url + if !assert.Equal(t, "https://postman-echo.com/get", tCase.TestSteps[0].Request.URL) { + t.Fatal() + } + if !assert.Equal(t, "https://postman-echo.com/post", tCase.TestSteps[1].Request.URL) { + t.Fatal() + } + // check params + if !assert.Equal(t, "v1", tCase.TestSteps[0].Request.Params["k1"]) { + t.Fatal() + } + // check cookies (pass, postman collection doesn't contains cookies) + // check headers + if !assert.Contains(t, tCase.TestSteps[1].Request.Headers["Content-Type"], "multipart/form-data") { + t.Fatal() + } + if !assert.Equal(t, "application/x-www-form-urlencoded", tCase.TestSteps[2].Request.Headers["Content-Type"]) { + t.Fatal() + } + if !assert.Equal(t, "application/json", tCase.TestSteps[3].Request.Headers["Content-Type"]) { + t.Fatal() + } + if !assert.Equal(t, "text/plain", tCase.TestSteps[4].Request.Headers["Content-Type"]) { + t.Fatal() + } + if !assert.Equal(t, "HttpRunner", tCase.TestSteps[5].Request.Headers["User-Agent"]) { + t.Fatal() + } + // check body + if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) { + t.Fatal() + } + if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, tCase.TestSteps[2].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": "v2"}, tCase.TestSteps[3].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, "have a nice day", tCase.TestSteps[4].Request.Body) { + t.Fatal() + } + if !assert.Equal(t, nil, tCase.TestSteps[5].Request.Body) { + t.Fatal() + } +} + +func TestMakeTestCaseWithProfile(t *testing.T) { + c := NewCollection(collectionPath) + c.SetProfile(profilePath) + tCase, err := c.makeTestCase() + if !assert.NoError(t, err) { + t.Fatal() + } + for _, step := range tCase.TestSteps { + if step.Request.Method == "GET" && !assert.Len(t, step.Request.Headers, 1) { + t.Fatal() + } + if step.Request.Method == "POST" && !assert.Len(t, step.Request.Headers, 2) { + t.Fatal() + } + if !assert.Equal(t, "all original headers will be overridden", step.Request.Headers["Header1"]) { + t.Fatal() + } + if !assert.Len(t, step.Request.Cookies, 1) { + t.Fatal() + } + if !assert.Equal(t, "all original cookies will be overridden", step.Request.Cookies["Cookie1"]) { + t.Fatal() + } + } +} + +func TestMakeTestCaseWithPatch(t *testing.T) { + c := NewCollection(collectionPath) + c.SetPatch(patchPath) + tCase, err := c.makeTestCase() + if !assert.NoError(t, err) { + t.Fatal() + } + // create cookies Cookie1 indicated in patch + if !assert.Equal(t, "this cookie will be created or updated", tCase.TestSteps[0].Request.Cookies["Cookie1"]) { + t.Fatal() + } + // update header User-Agent indicated in patch + if !assert.Equal(t, "this header will be created or updated", tCase.TestSteps[5].Request.Headers["User-Agent"]) { + t.Fatal() + } + // pass header Connection which is not indicated in patch + if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) { + t.Fatal() + } +} diff --git a/hrp/internal/postman2case/core_test.go b/hrp/internal/postman2case/core_test.go deleted file mode 100644 index 47b9eabc..00000000 --- a/hrp/internal/postman2case/core_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package postman2case - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -var collectionPath = "../../../examples/data/postman2case/postman_collection.json" - -func TestLoadPostmanCollection(t *testing.T) { - c, err := NewCollection(collectionPath).load() - if !assert.NoError(t, err) { - t.Fatal(err) - } - if !assert.Equal(t, "postman collection demo", c.Info.Name) { - t.Fatal() - } -} - -func TestGenJSON(t *testing.T) { - jsonPath, err := NewCollection(collectionPath).GenJSON() - if !assert.NoError(t, err) { - t.Fatal() - } - if !assert.NotEmpty(t, jsonPath) { - t.Fatal() - } -} - -func TestGenYAML(t *testing.T) { - yamlPath, err := NewCollection(collectionPath).GenYAML() - if !assert.NoError(t, err) { - t.Fatal() - } - if !assert.NotEmpty(t, yamlPath) { - t.Fatal() - } -}