refactor: hrp convert

This commit is contained in:
buyuxiang
2022-05-24 13:36:34 +08:00
parent 85e5b26ee3
commit ff9df1a251
37 changed files with 2245 additions and 626 deletions

View File

@@ -9,6 +9,7 @@
- fix: step request elapsed timing should contain ContentTransfer part
- fix #1288: unable to go get httprunner v4
- feat: support converting Postman collection to HttpRunner testcase
- refactor: improve the extensibility of `hrp convert` using interface `ICaseConverter`
**python version**

View File

@@ -30,10 +30,10 @@ Copyright 2017 debugtalk
### SEE ALSO
* [hrp boom](hrp_boom.md) - run load test with boomer
* [hrp convert](hrp_convert.md) - convert JSON/YAML testcases to pytest/gotest scripts
* [hrp convert](hrp_convert.md) - convert external cases to JSON/YAML/gotest/pytest testcases
* [hrp har2case](hrp_har2case.md) - convert HAR to json/yaml testcase files
* [hrp pytest](hrp_pytest.md) - run API test with pytest
* [hrp run](hrp_run.md) - run API test with go engine
* [hrp startproject](hrp_startproject.md) - create a scaffold project
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -41,4 +41,4 @@ hrp boom [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -1,6 +1,6 @@
## hrp convert
convert JSON/YAML testcases to pytest/gotest scripts
convert external cases to JSON/YAML/gotest/pytest testcases
```
hrp convert $path... [flags]
@@ -9,13 +9,17 @@ hrp convert $path... [flags]
### Options
```
--gotest convert to gotest scripts (TODO)
-h, --help help for convert
--pytest convert to pytest scripts (default true)
-h, --help help for convert
-d, --output-dir string specify output directory, default to the same dir with har file
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
--to-gotest convert to gotest scripts (TODO)
--to-json convert to JSON scripts (default)
--to-pytest convert to pytest scripts
--to-yaml convert to YAML scripts
```
### SEE ALSO
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -24,4 +24,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -1,26 +0,0 @@
## 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

View File

@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -35,4 +35,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -20,4 +20,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 9-May-2022
###### Auto generated by spf13/cobra on 23-May-2022

View File

@@ -1,3 +1,4 @@
override: true
headers:
Content-Type: "application/x-www-form-urlencoded"
cookies:

View File

@@ -1,4 +0,0 @@
headers:
User-Agent: "this header will be created or updated"
cookies:
Cookie1: "this cookie will be created or updated"

View File

@@ -1,4 +1,4 @@
headers:
Header1: "all original headers will be overridden"
User-Agent: "this header will be created or updated"
cookies:
Cookie1: "all original cookies will be overridden"
Cookie1: "this cookie will be created or updated"

View File

@@ -0,0 +1,5 @@
override: true
headers:
Header1: "all original headers will be overridden"
cookies:
Cookie1: "all original cookies will be overridden"

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.13.0
github.com/go-openapi/spec v0.20.6
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1
github.com/httprunner/funplugin v0.4.5

27
go.sum
View File

@@ -88,6 +88,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -131,6 +132,16 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -262,6 +273,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -288,16 +301,20 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/maja42/goval v1.2.1 h1:fyEgzddqPgCZsKcFLk4C6SdCHyEaAHYvtZG4mGzQOHU=
github.com/maja42/goval v1.2.1/go.mod h1:42LU+BQXL/veE9jnTTUOSj38GRmOTSThYSXRVodI5J4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -348,6 +365,8 @@ github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5Vgl
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -837,8 +856,9 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -854,6 +874,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -2,48 +2,61 @@ package cmd
import (
"errors"
"os"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/convert/case2script"
"github.com/httprunner/httprunner/v4/hrp/internal/convert"
)
var convertCmd = &cobra.Command{
Use: "convert $path...",
Short: "convert JSON/YAML testcases to pytest/gotest scripts",
Args: cobra.ExactValidArgs(1),
Short: "convert external cases to JSON/YAML/gotest/pytest testcases",
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
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 flagCount int
var outputType convert.OutputType
if toJSONFlag {
flagCount++
}
var err error
if gotestFlag {
err = case2script.Convert2TestScripts("gotest", args...)
} else {
err = case2script.Convert2TestScripts("pytest", args...)
if toYAMLFlag {
flagCount++
outputType = convert.OutputTypeYAML
}
if err != nil {
log.Error().Err(err).Msg("convert test scripts failed")
os.Exit(1)
if toGoTestFlag {
flagCount++
outputType = convert.OutputTypeGoTest
}
if toPyTestFlag {
flagCount++
outputType = convert.OutputTypePyTest
}
if flagCount > 1 {
return errors.New("please specify at most one conversion flag")
}
iCaseConverters := convert.LoadConverters(outputType, outputDir, profilePath, args)
convert.Run(iCaseConverters)
return nil
},
}
var (
pytestFlag bool
gotestFlag bool
toJSONFlag bool
toYAMLFlag bool
toGoTestFlag bool
toPyTestFlag bool
outputDir string
profilePath string
)
func init() {
rootCmd.AddCommand(convertCmd)
convertCmd.Flags().BoolVar(&pytestFlag, "pytest", true, "convert to pytest scripts")
convertCmd.Flags().BoolVar(&gotestFlag, "gotest", false, "convert to gotest scripts (TODO)")
convertCmd.Flags().BoolVar(&toPyTestFlag, "to-pytest", false, "convert to pytest scripts")
convertCmd.Flags().BoolVar(&toGoTestFlag, "to-gotest", false, "convert to gotest scripts (TODO)")
convertCmd.Flags().BoolVar(&toJSONFlag, "to-json", false, "convert to JSON scripts (default)")
convertCmd.Flags().BoolVar(&toYAMLFlag, "to-yaml", false, "convert to YAML scripts")
convertCmd.Flags().StringVarP(&outputDir, "output-dir", "d", "", "specify output directory, default to the same dir with har file")
convertCmd.Flags().StringVarP(&profilePath, "profile", "p", "", "specify profile path to override headers (except for auto-generated headers) and cookies")
}

View File

@@ -40,11 +40,6 @@ var har2caseCmd = &cobra.Command{
har.SetProfile(har2caseProfilePath)
}
// specify profile
if har2casePatchPath != "" {
har.SetPatch(har2casePatchPath)
}
// generate json/yaml files
if har2caseGenYAMLFlag {
outputPath, err = har.GenYAML()
@@ -66,7 +61,6 @@ var (
har2caseGenYAMLFlag bool
har2caseOutputDir string
har2caseProfilePath string
har2casePatchPath string
)
func init() {
@@ -75,5 +69,4 @@ func init() {
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")
}

View File

@@ -1,79 +0,0 @@
package cmd
import (
"errors"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/convert/postman2case"
)
// postman2caseCmd represents the postman2case command
var postman2caseCmd = &cobra.Command{
Use: "postman2case $postman_path...",
Short: "convert postman collection to json/yaml testcase files",
Long: `convert postman collection to json/yaml testcase files`,
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
RunE: func(cmd *cobra.Command, args []string) error {
var outputFiles []string
for _, arg := range args {
// must choose one
if !postman2caseGenJSONFlag && !postman2caseGenYAMLFlag {
return errors.New("please select convert format type")
}
var outputPath string
var err error
collection := postman2case.NewCollection(arg)
// specify output dir
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 postman2caseGenYAMLFlag {
outputPath, err = collection.GenYAML()
} else {
outputPath, err = collection.GenJSON() // default
}
if err != nil {
return err
}
outputFiles = append(outputFiles, outputPath)
}
log.Info().Strs("output", outputFiles).Msg("convert testcase success")
return nil
},
}
var (
postman2caseGenJSONFlag bool
postman2caseGenYAMLFlag bool
postman2caseOutputDir string
postman2caseProfilePath string
postman2casePatchPath string
)
func init() {
rootCmd.AddCommand(postman2caseCmd)
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")
}

View File

@@ -285,7 +285,8 @@ func LoadFile(path string, structObj interface{}) (err error) {
if err != nil {
return errors.Wrap(err, "read file failed")
}
// remove BOM at the beginning of file
file = bytes.Trim(file, "\xef\xbb\xbf")
ext := filepath.Ext(path)
switch ext {
case ".json", ".har":
@@ -351,3 +352,9 @@ func readFile(path string) ([]byte, error) {
}
return file, nil
}
func GetOutputNameWithoutExtension(path string) string {
base := filepath.Base(path)
ext := filepath.Ext(base)
return base[0:len(base)-len(ext)] + "_test"
}

View File

@@ -0,0 +1,68 @@
# hrp convert
## 快速上手
```shell
$ hrp convert -h
convert external cases to JSON/YAML/gotest/pytest testcases
Usage:
hrp convert $path... [flags]
Flags:
-h, --help help for convert
-d, --output-dir string specify output directory, default to the same dir with har file
-p, --profile string specify profile path to override headers (except for auto-generated headers) and cookies
--to-gotest convert to gotest scripts (TODO)
--to-json convert to JSON scripts (default true)
--to-pytest convert to pytest scripts
--to-yaml convert to YAML scripts
Global Flags:
--log-json set log to json format
-l, --log-level string set log level (default "INFO")
```
`hrp convert` 指令用于将 HAR/Postman/JMeter/Swagger 等格式的外部脚本转化为 JSON/YAML/gotest/pytest 形态的测试用例,同时也支持测试用例各个形态之间的相互转化,输出的测试用例文件名格式为 `不带扩展名的原文件名称` + `_test` + `json/yaml/go/py` 后缀。
该指令的所有参数的详细介绍如下:
1. `--to-json / --to-yaml / --to-gotest / --to-pytest` 用于将输入的外部脚本转化为对应形态的测试用例,四个参数中最多只能指定一个,如果不指定则默认会将输入转化为 JSON 形态的测试用例
2. `--output-dir` 后接测试用例的期望输出目录的路径,用于将转换生成的测试用例输出到对应的文件夹
3. `--profile` 后接 `profile` 配置文件的路径,目前支持替换(不存在则会创建)或者覆盖输入的外部脚本/测试用例中的 `Headers``Cookies` 信息,`profile` 文件的后缀可以为 `json/yaml/yml`,下面给出两类 `profile` 配置文件的示例:
- 根据 `profile` 替换指定的 `Headers``Cookies` 信息
```yaml
headers:
Header1: "this header will be created or updated"
cookies:
Cookie1: "this cookie will be created or updated"
```
- 根据 `profile` 覆盖原有的 `Headers``Cookies` 信息
```yaml
override: true
headers:
Header1: "all original headers will be overridden"
cookies:
Cookie1: "all original cookies will be overridden"
```
## 注意事项
1. 指定 `override``false/true` 可以选择 `profile` 的修改模式为替换/覆盖。需要注意的是,如果不指定该字段则 `profile` 的默认修改模式为**替换**模式,
2. 输入为 JSON/YAML 测试用例时,良好兼容 Golang/Python 双引擎之间的差异(请求体、断言部分的格式略有不同),输出的 JSON/YAML 则统一采用 Golang 引擎的风格
## 转换流程图
![flow chart](asset/flowgram.svg)
## 开发进度
| from \ to | JSON | YAML | GoTest | PyTest |
|:---------:|:----:|:----:|:------:|:------:|
| HAR | ✅ | ✅ | ❌ | ✅ |
| Postman | ✅ | ✅ | ❌ | ✅ |
| JMeter | ❌ | ❌ | ❌ | ❌ |
| Swagger | ❌ | ❌ | ❌ | ❌ |
| JSON | ✅ | ✅ | ❌ | ✅ |
| YAML | ✅ | ✅ | ❌ | ✅ |
| GoTest | ❌ | ❌ | ❌ | ❌ |
| PyTest | ❌ | ❌ | ❌ | ❌ |

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,120 +0,0 @@
package case2script
import (
_ "embed"
"fmt"
"os"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func Convert2TestScripts(destType string, paths ...string) error {
// report event
sdk.SendEvent(sdk.EventTracking{
Category: "ConvertTests",
Action: fmt.Sprintf("hrp convert --%s", destType),
})
if destType == "gotest" {
return convert2GoTestScripts(paths...)
} else {
// default to pytest
return convert2PyTestScripts(paths...)
}
}
func convert2PyTestScripts(paths ...string) error {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return err
}
args := append([]string{"-m", "httprunner", "make"}, paths...)
return builtin.ExecCommand(python3, args...)
}
func convert2GoTestScripts(paths ...string) error {
log.Warn().Msg("convert to gotest scripts is not supported yet")
os.Exit(1)
// TODO
var testCasePaths []hrp.ITestCase
for _, path := range paths {
testCasePath := hrp.TestCasePath(path)
testCasePaths = append(testCasePaths, &testCasePath)
}
testCases, err := hrp.LoadTestCases(testCasePaths...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
var pytestPaths []string
for _, testCase := range testCases {
tc := testCase.ToTCase()
converter := CaseConverter{
TCase: tc,
}
pytestPath, err := converter.ToPyTest()
if err != nil {
log.Error().Err(err).
Str("originPath", tc.Config.Path).
Msg("convert to pytest failed")
continue
}
log.Info().
Str("pytestPath", pytestPath).
Str("originPath", tc.Config.Path).
Msg("convert to pytest success")
pytestPaths = append(pytestPaths, pytestPath)
}
// format pytest scripts with black
python3, err := builtin.EnsurePython3Venv("black")
if err != nil {
return err
}
args := append([]string{"-m", "black"}, pytestPaths...)
return builtin.ExecCommand(python3, args...)
}
//go:embed testcase.tmpl
var testcaseTemplate string
type CaseConverter struct {
*hrp.TCase
}
func (c *CaseConverter) ToPyTest() (string, error) {
script := convertConfig(c.TCase.Config)
println(script)
return script, nil
}
func (c *CaseConverter) ToGoTest() (string, error) {
return "", nil
}
func convertConfig(config *hrp.TConfig) string {
script := fmt.Sprintf("Config('%s')", config.Name)
if config.Variables != nil {
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
}
if config.BaseURL != "" {
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
}
if config.Export != nil {
script += fmt.Sprintf(".export(*%v)", config.Export)
}
script += fmt.Sprintf(".verify(%v)", config.Verify)
return script
}

View File

@@ -0,0 +1,374 @@
package convert
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"reflect"
"github.com/go-openapi/spec"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk"
)
const (
suffixJSON = ".json"
suffixYAML = ".yaml"
suffixGoTest = ".go"
suffixPyTest = ".py"
)
type InputType int
const (
InputTypeUnknown InputType = iota // default input type: unknown
InputTypeHAR
InputTypePostman
InputTypeSwagger
InputTypeJMeter
InputTypeJSON
InputTypeYAML
InputTypeGoTest
InputTypePyTest
)
func (inputType InputType) String() string {
switch inputType {
case InputTypeHAR:
return "har"
case InputTypePostman:
return "postman"
case InputTypeSwagger:
return "swagger"
case InputTypeJMeter:
return "jmeter"
case InputTypeJSON:
return "json testcase"
case InputTypeYAML:
return "yaml testcase"
case InputTypeGoTest:
return "gotest script"
case InputTypePyTest:
return "pytest script"
default:
return "unknown"
}
}
type OutputType int
const (
OutputTypeJSON OutputType = iota // default output type: JSON
OutputTypeYAML
OutputTypeGoTest
OutputTypePyTest
)
func (outputType OutputType) String() string {
switch outputType {
case OutputTypeYAML:
return "yaml"
case OutputTypeGoTest:
return "gotest"
case OutputTypePyTest:
return "pytest"
default:
return "json"
}
}
// TCaseConverter holds the common properties of case converter
type TCaseConverter struct {
InputPath string
OutputDir string
Profile *Profile
InputType InputType
OutputType OutputType
CaseHAR *CaseHar
CasePostman *CasePostman
CaseSwagger *spec.Swagger
TCase *hrp.TCase
}
// Profile is used to override or update(create if not existed) original headers and cookies
type Profile struct {
Override bool `json:"override" yaml:"override"`
Headers map[string]string `json:"headers" yaml:"headers"`
Cookies map[string]string `json:"cookies" yaml:"cookies"`
}
func NewTCaseConverter(path string) (tCaseConverter *TCaseConverter) {
tCaseConverter = &TCaseConverter{
InputPath: path,
InputType: InputTypeUnknown,
}
extName := filepath.Ext(path)
if extName == "" {
log.Warn().Msg("extension name should be specified")
return
}
var err error
switch extName {
case ".har":
caseHAR := new(CaseHar)
err = builtin.LoadFile(path, caseHAR)
if err == nil && !reflect.DeepEqual(*caseHAR, CaseHar{}) {
tCaseConverter.InputType = InputTypeHAR
tCaseConverter.CaseHAR = caseHAR
}
case ".json":
tCase := new(hrp.TCase)
err = builtin.LoadFile(path, tCase)
if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) {
tCaseConverter.InputType = InputTypeJSON
tCaseConverter.TCase = tCase
break
}
casePostman := new(CasePostman)
err = builtin.LoadFile(path, casePostman)
if err == nil && !reflect.DeepEqual(*casePostman, CasePostman{}) {
tCaseConverter.InputType = InputTypePostman
tCaseConverter.CasePostman = casePostman
break
}
caseSwagger := new(spec.Swagger)
err = builtin.LoadFile(path, caseSwagger)
if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) {
tCaseConverter.InputType = InputTypeSwagger
tCaseConverter.CaseSwagger = caseSwagger
}
case ".yaml", ".yml":
tCase := new(hrp.TCase)
err = builtin.LoadFile(path, tCase)
if err == nil && !reflect.DeepEqual(*tCase, hrp.TCase{}) {
tCaseConverter.InputType = InputTypeYAML
tCaseConverter.TCase = tCase
break
}
caseSwagger := new(spec.Swagger)
err = builtin.LoadFile(path, caseSwagger)
if err == nil && !reflect.DeepEqual(*caseSwagger, spec.Swagger{}) {
tCaseConverter.InputType = InputTypeSwagger
tCaseConverter.CaseSwagger = caseSwagger
}
case ".go": // TODO
tCaseConverter.InputType = InputTypeGoTest
case ".py": // TODO
tCaseConverter.InputType = InputTypePyTest
case ".jmx": // TODO
tCaseConverter.InputType = InputTypeJMeter
default:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msgf("unsupported file type: %v", extName)
}
if tCaseConverter.InputType != InputTypeUnknown {
log.Info().
Str("input path", tCaseConverter.InputPath).
Msgf("load case as: %s", tCaseConverter.InputType.String())
} else {
log.Error().Err(err).
Str("input path", tCaseConverter.InputPath).
Msgf("failed to load case")
}
return
}
func (c *TCaseConverter) SetProfile(path string) {
log.Info().Str("input path", c.InputPath).Str("profile", path).Msg("set profile")
profile := new(Profile)
err := builtin.LoadFile(path, profile)
if err != nil {
log.Warn().Str("path", path).
Msg("failed to load profile, ignore!")
return
}
c.Profile = profile
}
func (c *TCaseConverter) SetOutputDir(dir string) {
log.Info().Str("input path", c.InputPath).Str("output directory", dir).Msg("set output directory")
c.OutputDir = dir
}
func (c *TCaseConverter) genOutputPath(suffix string) string {
outFileFullName := builtin.GetOutputNameWithoutExtension(c.InputPath) + suffix
if c.OutputDir != "" {
return filepath.Join(c.OutputDir, outFileFullName)
} else {
return filepath.Join(filepath.Dir(c.InputPath), outFileFullName)
}
// TODO avoid outFileFullName conflict?
}
func (c *TCaseConverter) ToPyTest() (string, error) {
script := convertConfig(c.TCase.Config)
println(script)
return script, nil
}
func convertConfig(config *hrp.TConfig) string {
script := fmt.Sprintf("Config('%s')", config.Name)
if config.Variables != nil {
script += fmt.Sprintf(".variables(**{%v})", config.Variables)
}
if config.BaseURL != "" {
script += fmt.Sprintf(".base_url('%s')", config.BaseURL)
}
if config.Export != nil {
script += fmt.Sprintf(".export(*%v)", config.Export)
}
script += fmt.Sprintf(".verify(%v)", config.Verify)
return script
}
func (c *TCaseConverter) ToGoTest() (string, error) {
return "", nil
}
// ICaseConverter represents all kinds of case converters which could convert case into JSON/YAML/gotest/pytest format
type ICaseConverter interface {
Struct() *TCaseConverter
ToJSON() (string, error)
ToJSONTemp() (string, error)
ToYAML() (string, error)
ToGoTest() (string, error)
ToPyTest() (string, error)
}
func LoadConverters(outputType OutputType, outputDir, profilePath string, args []string) []ICaseConverter {
// report event
sdk.SendEvent(sdk.EventTracking{
Category: "ConvertTests",
Action: fmt.Sprintf("hrp convert --to-%s", outputType.String()),
})
var iCaseConverters []ICaseConverter
for _, arg := range args {
tCaseConverter := NewTCaseConverter(arg)
tCaseConverter.OutputType = outputType
if outputDir != "" {
tCaseConverter.SetOutputDir(outputDir)
}
if profilePath != "" {
tCaseConverter.SetProfile(profilePath)
}
switch tCaseConverter.InputType {
case InputTypeHAR:
iCaseConverters = append(iCaseConverters, NewConverterHAR(tCaseConverter))
case InputTypePostman:
iCaseConverters = append(iCaseConverters, NewConverterPostman(tCaseConverter))
case InputTypeJSON:
iCaseConverters = append(iCaseConverters, NewConverterJSON(tCaseConverter))
case InputTypeYAML:
iCaseConverters = append(iCaseConverters, NewConverterYAML(tCaseConverter))
case InputTypeSwagger, InputTypeJMeter, InputTypeGoTest, InputTypePyTest:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msg("case type not supported yet, ignore!")
default:
log.Warn().
Str("input path", tCaseConverter.InputPath).
Msg("unknown case type, ignore!")
}
}
return iCaseConverters
}
func Run(iCaseConverters []ICaseConverter) {
var outputFiles []string
var err error
for _, iCaseConverter := range iCaseConverters {
log.Info().Str("input path", iCaseConverter.Struct().InputPath).Msg("start converting")
var outputFile string
switch iCaseConverter.Struct().OutputType {
case OutputTypeYAML:
outputFile, err = iCaseConverter.ToYAML()
case OutputTypeGoTest:
outputFile, err = iCaseConverter.ToGoTest()
case OutputTypePyTest:
outputFile, err = iCaseConverter.ToPyTest()
default:
outputFile, err = iCaseConverter.ToJSON()
}
if err != nil {
log.Error().Err(err).
Str("input path", iCaseConverter.Struct().InputPath).
Msg("error occurs during converting")
continue
}
outputFiles = append(outputFiles, outputFile)
}
log.Info().Strs("output files", outputFiles).Msg("conversion completed")
}
func makeTestCaseFromJSONYAML(iCaseConverter ICaseConverter) (*hrp.TCase, error) {
tCase := iCaseConverter.Struct().TCase
if tCase == nil {
return nil, errors.Errorf("empty json/yaml testcase occurs")
}
profile := iCaseConverter.Struct().Profile
if profile == nil {
return tCase, nil
}
for _, step := range tCase.TestSteps {
// override original headers and cookies
if profile.Override {
step.Request.Headers = make(map[string]string)
step.Request.Cookies = make(map[string]string)
}
// update (create if not existed) original headers and cookies
if step.Request.Headers == nil {
step.Request.Headers = make(map[string]string)
}
if step.Request.Cookies == nil {
step.Request.Cookies = make(map[string]string)
}
for k, v := range profile.Headers {
step.Request.Headers[k] = v
}
for k, v := range profile.Cookies {
step.Request.Cookies[k] = v
}
}
return tCase, nil
}
func convertToPyTest(iCaseConverter ICaseConverter) (string, error) {
// convert to temporary json testcase compatible with python engine style
jsonPath, err := iCaseConverter.ToJSONTemp()
inputType := iCaseConverter.Struct().InputType
if err != nil {
return "", errors.Wrapf(err, "(%s -> pytest step 1) failed to convert to temporary json testcase", inputType.String())
}
defer func() {
if jsonPath != "" {
if err = os.Remove(jsonPath); err != nil {
log.Error().Err(err).Msgf("(%s -> pytest step defer) failed to clean temporary json testcase", inputType.String())
}
}
}()
// convert from temporary json testcase to pytest
converterJSON := NewConverterJSON(NewTCaseConverter(jsonPath))
pyTestPath, err := converterJSON.MakePyTestScript()
if err != nil {
return "", errors.Wrap(err, "(json -> pytest step 2) failed to convert from temporary json testcase to pytest ")
}
// rename resultant pytest
renamedPyTestPath := iCaseConverter.Struct().genOutputPath(suffixPyTest)
err = os.Rename(pyTestPath, renamedPyTestPath)
if err != nil {
log.Error().Err(err).Msg("(json -> pytest step 3) failed to rename the resultant pytest file")
return pyTestPath, nil
}
return renamedPyTestPath, nil
}

View File

@@ -0,0 +1,60 @@
package convert
import (
_ "embed"
"os"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
func convert2GoTestScripts(paths ...string) error {
log.Warn().Msg("convert to gotest scripts is not supported yet")
os.Exit(1)
// TODO
var testCasePaths []hrp.ITestCase
for _, path := range paths {
testCasePath := hrp.TestCasePath(path)
testCasePaths = append(testCasePaths, &testCasePath)
}
testCases, err := hrp.LoadTestCases(testCasePaths...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
var pytestPaths []string
for _, testCase := range testCases {
tc := testCase.ToTCase()
converter := TCaseConverter{
TCase: tc,
}
pytestPath, err := converter.ToPyTest()
if err != nil {
log.Error().Err(err).
Str("originPath", tc.Config.Path).
Msg("convert to pytest failed")
continue
}
log.Info().
Str("pytestPath", pytestPath).
Str("originPath", tc.Config.Path).
Msg("convert to pytest success")
pytestPaths = append(pytestPaths, pytestPath)
}
// format pytest scripts with black
python3, err := builtin.EnsurePython3Venv("black")
if err != nil {
return err
}
args := append([]string{"-m", "black"}, pytestPaths...)
return builtin.ExecCommand(python3, args...)
}
//go:embed testcase.tmpl
var testcaseTemplate string

View File

@@ -0,0 +1,716 @@
package convert
import (
"encoding/base64"
"fmt"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"net/url"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// ==================== model definition starts here ====================
/*
HTTP Archive (HAR) format
https://w3c.github.io/web-performance/specs/HAR/Overview.html
this file is copied from https://github.com/mrichman/hargo/blob/master/types.go
*/
// CaseHar is a container type for deserialization
type CaseHar struct {
Log Log `json:"log"`
}
// Log represents the root of the exported data. This object MUST be present and its name MUST be "log".
type Log struct {
// The object contains the following name/value pairs:
// Required. Version number of the format.
Version string `json:"version"`
// Required. An object of type creator that contains the name and version
// information of the log creator application.
Creator Creator `json:"creator"`
// Optional. An object of type browser that contains the name and version
// information of the user agent.
Browser Browser `json:"browser"`
// Optional. An array of objects of type page, each representing one exported
// (tracked) page. Leave out this field if the application does not support
// grouping by pages.
Pages []Page `json:"pages,omitempty"`
// Required. An array of objects of type entry, each representing one
// exported (tracked) HTTP request.
Entries []Entry `json:"entries"`
// Optional. A comment provided by the user or the application. Sorting
// entries by startedDateTime (starting from the oldest) is preferred way how
// to export data since it can make importing faster. However the reader
// application should always make sure the array is sorted (if required for
// the import).
Comment string `json:"comment"`
}
// Creator contains information about the log creator application
type Creator struct {
// Required. The name of the application that created the log.
Name string `json:"name"`
// Required. The version number of the application that created the log.
Version string `json:"version"`
// Optional. A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// Browser that created the log
type Browser struct {
// Required. The name of the browser that created the log.
Name string `json:"name"`
// Required. The version number of the browser that created the log.
Version string `json:"version"`
// Optional. A comment provided by the user or the browser.
Comment string `json:"comment"`
}
// Page object for every exported web page and one <entry> object for every HTTP request.
// In case when an HTTP trace tool isn't able to group requests by a page,
// the <pages> object is empty and individual requests doesn't have a parent page.
type Page struct {
/* There is one <page> object for every exported web page and one <entry>
object for every HTTP request. In case when an HTTP trace tool isn't able to
group requests by a page, the <pages> object is empty and individual
requests doesn't have a parent page.
*/
// Date and time stamp for the beginning of the page load
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00).
StartedDateTime string `json:"startedDateTime"`
// Unique identifier of a page within the . Entries use it to refer the parent page.
ID string `json:"id"`
// Page title.
Title string `json:"title"`
// Detailed timing info about page load.
PageTiming PageTiming `json:"pageTiming"`
// (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// PageTiming describes timings for various events (states) fired during the page load.
// All times are specified in milliseconds. If a time info is not available appropriate field is set to -1.
type PageTiming struct {
// Content of the page loaded. Number of milliseconds since page load started
// (page.startedDateTime). Use -1 if the timing does not apply to the current
// request.
// Depeding on the browser, onContentLoad property represents DOMContentLoad
// event or document.readyState == interactive.
OnContentLoad int `json:"onContentLoad"`
// Page is loaded (onLoad event fired). Number of milliseconds since page
// load started (page.startedDateTime). Use -1 if the timing does not apply
// to the current request.
OnLoad int `json:"onLoad"`
// (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment"`
}
// Entry is a unique, optional Reference to the parent page.
// Leave out this field if the application does not support grouping by pages.
type Entry struct {
Pageref string `json:"pageref,omitempty"`
// Date and time stamp of the request start
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD).
StartedDateTime string `json:"startedDateTime"`
// Total elapsed time of the request in milliseconds. This is the sum of all
// timings available in the timings object (i.e. not including -1 values) .
Time float32 `json:"time"`
// Detailed info about the request.
Request Request `json:"request"`
// Detailed info about the response.
Response Response `json:"response"`
// Info about cache usage.
Cache Cache `json:"cache"`
// Detailed timing info about request/response round trip.
PageTimings PageTimings `json:"pageTimings"`
// optional (new in 1.2) IP address of the server that was connected
// (result of DNS resolution).
ServerIPAddress string `json:"serverIPAddress,omitempty"`
// optional (new in 1.2) Unique ID of the parent TCP/IP connection, can be
// the client port number. Note that a port number doesn't have to be unique
// identifier in cases where the port is shared for more connections. If the
// port isn't available for the application, any other unique connection ID
// can be used instead (e.g. connection index). Leave out this field if the
// application doesn't support this info.
Connection string `json:"connection,omitempty"`
// (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// Request contains detailed info about performed request.
type Request struct {
// Request method (GET, POST, ...).
Method string `json:"method"`
// Absolute URL of the request (fragments are not included).
URL string `json:"url"`
// Request HTTP Version.
HTTPVersion string `json:"httpVersion"`
// List of cookie objects.
Cookies []Cookie `json:"cookies"`
// List of header objects.
Headers []NVP `json:"headers"`
// List of query parameter objects.
QueryString []NVP `json:"queryString"`
// Posted data.
PostData PostData `json:"postData"`
// Total number of bytes from the start of the HTTP request message until
// (and including) the double CRLF before the body. Set to -1 if the info
// is not available.
HeaderSize int `json:"headerSize"`
// Size of the request body (POST data payload) in bytes. Set to -1 if the
// info is not available.
BodySize int `json:"bodySize"`
// (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment"`
}
// Response contains detailed info about the response.
type Response struct {
// Response status.
Status int `json:"status"`
// Response status description.
StatusText string `json:"statusText"`
// Response HTTP Version.
HTTPVersion string `json:"httpVersion"`
// List of cookie objects.
Cookies []Cookie `json:"cookies"`
// List of header objects.
Headers []NVP `json:"headers"`
// Details about the response body.
Content Content `json:"content"`
// Redirection target URL from the Location response header.
RedirectURL string `json:"redirectURL"`
// Total number of bytes from the start of the HTTP response message until
// (and including) the double CRLF before the body. Set to -1 if the info is
// not available.
// The size of received response-headers is computed only from headers that
// are really received from the server. Additional headers appended by the
// browser are not included in this number, but they appear in the list of
// header objects.
HeadersSize int `json:"headersSize"`
// Size of the received response body in bytes. Set to zero in case of
// responses coming from the cache (304). Set to -1 if the info is not
// available.
BodySize int `json:"bodySize"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// Cookie contains list of all cookies (used in <request> and <response> objects).
type Cookie struct {
// The name of the cookie.
Name string `json:"name"`
// The cookie value.
Value string `json:"value"`
// optional The path pertaining to the cookie.
Path string `json:"path,omitempty"`
// optional The host of the cookie.
Domain string `json:"domain,omitempty"`
// optional Cookie expiration time.
// (ISO 8601 YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.123+02:00).
Expires string `json:"expires,omitempty"`
// optional Set to true if the cookie is HTTP only, false otherwise.
HTTPOnly bool `json:"httpOnly,omitempty"`
// optional (new in 1.2) True if the cookie was transmitted over ssl, false
// otherwise.
Secure bool `json:"secure,omitempty"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment bool `json:"comment,omitempty"`
}
// NVP is simply a name/value pair with a comment
type NVP struct {
Name string `json:"name"`
Value string `json:"value"`
Comment string `json:"comment,omitempty"`
}
// PostData describes posted data, if any (embedded in <request> object).
type PostData struct {
// Mime type of posted data.
MimeType string `json:"mimeType"`
// List of posted parameters (in case of URL encoded parameters).
Params []PostParam `json:"params"`
// Plain text posted data
Text string `json:"text"`
// optional (new in 1.2) A comment provided by the user or the
// application.
Comment string `json:"comment,omitempty"`
}
// PostParam is a list of posted parameters, if any (embedded in <postData> object).
type PostParam struct {
// name of a posted parameter.
Name string `json:"name"`
// optional value of a posted parameter or content of a posted file.
Value string `json:"value,omitempty"`
// optional name of a posted file.
FileName string `json:"fileName,omitempty"`
// optional content type of a posted file.
ContentType string `json:"contentType,omitempty"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// Content describes details about response content (embedded in <response> object).
type Content struct {
// Length of the returned content in bytes. Should be equal to
// response.bodySize if there is no compression and bigger when the content
// has been compressed.
Size int `json:"size"`
// optional Number of bytes saved. Leave out this field if the information
// is not available.
Compression int `json:"compression,omitempty"`
// MIME type of the response text (value of the Content-Type response
// header). The charset attribute of the MIME type is included (if
// available).
MimeType string `json:"mimeType"`
// optional Response body sent from the server or loaded from the browser
// cache. This field is populated with textual content only. The text field
// is either HTTP decoded text or a encoded (e.g. "base64") representation of
// the response body. Leave out this field if the information is not
// available.
Text string `json:"text,omitempty"`
// optional (new in 1.2) Encoding used for response text field e.g
// "base64". Leave out this field if the text field is HTTP decoded
// (decompressed & unchunked), than trans-coded from its original character
// set into UTF-8.
Encoding string `json:"encoding,omitempty"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// Cache contains info about a request coming from browser cache.
type Cache struct {
// optional State of a cache entry before the request. Leave out this field
// if the information is not available.
BeforeRequest CacheObject `json:"beforeRequest,omitempty"`
// optional State of a cache entry after the request. Leave out this field if
// the information is not available.
AfterRequest CacheObject `json:"afterRequest,omitempty"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// CacheObject is used by both beforeRequest and afterRequest
type CacheObject struct {
// optional - Expiration time of the cache entry.
Expires string `json:"expires,omitempty"`
// The last time the cache entry was opened.
LastAccess string `json:"lastAccess"`
// Etag
ETag string `json:"eTag"`
// The number of times the cache entry has been opened.
HitCount int `json:"hitCount"`
// optional (new in 1.2) A comment provided by the user or the application.
Comment string `json:"comment,omitempty"`
}
// PageTimings describes various phases within request-response round trip.
// All times are specified in milliseconds.
type PageTimings struct {
Blocked int `json:"blocked,omitempty"`
// optional - Time spent in a queue waiting for a network connection. Use -1
// if the timing does not apply to the current request.
DNS int `json:"dns,omitempty"`
// optional - DNS resolution time. The time required to resolve a host name.
// Use -1 if the timing does not apply to the current request.
Connect int `json:"connect,omitempty"`
// optional - Time required to create TCP connection. Use -1 if the timing
// does not apply to the current request.
Send int `json:"send"`
// Time required to send HTTP request to the server.
Wait int `json:"wait"`
// Waiting for a response from the server.
Receive int `json:"receive"`
// Time required to read entire response from the server (or cache).
Ssl int `json:"ssl,omitempty"`
// optional (new in 1.2) - Time required for SSL/TLS negotiation. If this
// field is defined then the time is also included in the connect field (to
// ensure backward compatibility with HAR 1.1). Use -1 if the timing does not
// apply to the current request.
Comment string `json:"comment,omitempty"`
// optional (new in 1.2) - A comment provided by the user or the application.
}
// TestResult contains results for an individual HTTP request
type TestResult struct {
URL string `json:"url"`
Status int `json:"status"` // 200, 500, etc.
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Latency int `json:"latency"` // milliseconds
Method string `json:"method"`
HarFile string `json:"harfile"`
}
// ==================== model definition ends here ====================
func NewConverterHAR(converter *TCaseConverter) *ConverterHAR {
return &ConverterHAR{
converter: converter,
}
}
type ConverterHAR struct {
converter *TCaseConverter
}
func (c *ConverterHAR) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterHAR) ToJSON() (string, error) {
tCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(tCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterHAR) ToJSONTemp() (string, error) {
tCase, err := c.makeTestCaseTemp()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(tCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterHAR) ToYAML() (string, error) {
tCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(tCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterHAR) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from har to gotest scripts is not supported yet")
}
func (c *ConverterHAR) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterHAR) makeTestCase() (*hrp.TCase, error) {
teststeps, err := c.prepareTestSteps()
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(),
TestSteps: teststeps,
}
err = tCase.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterHAR) makeTestCaseTemp() (*hrp.TCase, error) {
teststeps, err := c.prepareTestSteps()
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(),
TestSteps: teststeps,
}
err = tCase.MakeCompat2PyEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterHAR) load() (*CaseHar, error) {
har := c.converter.CaseHAR
if har == nil {
return nil, errors.New("empty har case occurs")
}
return har, nil
}
func (c *ConverterHAR) prepareConfig() *hrp.TConfig {
return hrp.NewConfig("testcase description").
SetVerifySSL(false)
}
func (c *ConverterHAR) prepareTestSteps() ([]*hrp.TStep, error) {
har, err := c.load()
if err != nil {
return nil, err
}
var steps []*hrp.TStep
for _, entry := range har.Log.Entries {
step, err := c.prepareTestStep(&entry)
if err != nil {
return nil, err
}
steps = append(steps, step)
}
return steps, nil
}
func (c *ConverterHAR) prepareTestStep(entry *Entry) (*hrp.TStep, error) {
log.Info().
Str("method", entry.Request.Method).
Str("url", entry.Request.URL).
Msg("convert teststep")
step := &stepFromHAR{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]interface{}, 0),
},
profile: c.converter.Profile,
}
if err := step.makeRequestMethod(entry); err != nil {
return nil, err
}
if err := step.makeRequestURL(entry); err != nil {
return nil, err
}
if err := step.makeRequestParams(entry); err != nil {
return nil, err
}
if err := step.makeRequestCookies(entry); err != nil {
return nil, err
}
if err := step.makeRequestHeaders(entry); err != nil {
return nil, err
}
if err := step.makeRequestBody(entry); err != nil {
return nil, err
}
if err := step.makeValidate(entry); err != nil {
return nil, err
}
return &step.TStep, nil
}
type stepFromHAR struct {
hrp.TStep
profile *Profile
}
func (s *stepFromHAR) makeRequestMethod(entry *Entry) error {
s.Request.Method = hrp.HTTPMethod(entry.Request.Method)
return nil
}
func (s *stepFromHAR) makeRequestURL(entry *Entry) error {
u, err := url.Parse(entry.Request.URL)
if err != nil {
log.Error().Err(err).Msg("make request url failed")
return err
}
s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path)
return nil
}
func (s *stepFromHAR) makeRequestParams(entry *Entry) error {
s.Request.Params = make(map[string]interface{})
for _, param := range entry.Request.QueryString {
s.Request.Params[param.Name] = param.Value
}
return nil
}
func (s *stepFromHAR) makeRequestCookies(entry *Entry) error {
// use cookies from har
s.Request.Cookies = make(map[string]string)
for _, cookie := range entry.Request.Cookies {
s.Request.Cookies[cookie.Name] = cookie.Value
}
if s.profile == nil {
return nil
}
// override all cookies according to the profile
if s.profile.Override {
s.Request.Cookies = make(map[string]string)
}
// create or update the cookies according to the profile
for k, v := range s.profile.Cookies {
s.Request.Cookies[k] = v
}
return nil
}
func (s *stepFromHAR) makeRequestHeaders(entry *Entry) error {
// use headers from har
s.Request.Headers = make(map[string]string)
for _, header := range entry.Request.Headers {
if strings.EqualFold(header.Name, "cookie") {
continue
}
s.Request.Headers[header.Name] = header.Value
}
if s.profile == nil {
return nil
}
// override all headers according to the profile
if s.profile.Override {
s.Request.Headers = make(map[string]string)
}
// create or update the headers according to the profile
for k, v := range s.profile.Headers {
s.Request.Headers[k] = v
}
return nil
}
func (s *stepFromHAR) makeRequestBody(entry *Entry) error {
mimeType := entry.Request.PostData.MimeType
if mimeType == "" {
// GET/HEAD/DELETE without body
return nil
}
// POST/PUT with body
if strings.HasPrefix(mimeType, "application/json") {
// post json
var body interface{}
if entry.Request.PostData.Text == "" {
body = nil
} else {
err := json.Unmarshal([]byte(entry.Request.PostData.Text), &body)
if err != nil {
log.Error().Err(err).Msg("make request body failed")
return err
}
}
s.Request.Body = body
} else if strings.HasPrefix(mimeType, "application/x-www-form-urlencoded") {
// post form
var paramsList []string
for _, param := range entry.Request.PostData.Params {
paramsList = append(paramsList, fmt.Sprintf("%s=%s", param.Name, param.Value))
}
s.Request.Body = strings.Join(paramsList, "&")
} else if strings.HasPrefix(mimeType, "text/plain") {
// post raw data
s.Request.Body = entry.Request.PostData.Text
} else {
// TODO
log.Error().Msgf("makeRequestBody: Not implemented for mimeType %s", mimeType)
}
return nil
}
func (s *stepFromHAR) makeValidate(entry *Entry) error {
// make validator for response status code
s.Validators = append(s.Validators, hrp.Validator{
Check: "status_code",
Assert: "equals",
Expect: entry.Response.Status,
Message: "assert response status code",
})
// make validators for response headers
for _, header := range entry.Response.Headers {
// assert Content-Type
if strings.EqualFold(header.Name, "Content-Type") {
s.Validators = append(s.Validators, hrp.Validator{
Check: "headers.\"Content-Type\"",
Assert: "equals",
Expect: header.Value,
Message: "assert response header Content-Type",
})
}
}
// make validators for response body
respBody := entry.Response.Content
if respBody.Text == "" {
// response body is empty
return nil
}
if strings.HasPrefix(respBody.MimeType, "application/json") {
var data []byte
var err error
// response body is json
if respBody.Encoding == "base64" {
// decode base64 text
data, err = base64.StdEncoding.DecodeString(respBody.Text)
if err != nil {
return errors.Wrap(err, "decode base64 error")
}
} else if respBody.Encoding == "" {
// no encoding
data = []byte(respBody.Text)
} else {
// other encoding type
return nil
}
// convert to json
var body interface{}
if err = json.Unmarshal(data, &body); err != nil {
return errors.Wrap(err, "json.Unmarshal body error")
}
jsonBody, ok := body.(map[string]interface{})
if !ok {
return fmt.Errorf("response body is not json, not matched with MimeType")
}
// response body is json
keys := make([]string, 0, len(jsonBody))
for k := range jsonBody {
keys = append(keys, k)
}
// sort map keys to keep validators in stable order
sort.Strings(keys)
for _, key := range keys {
value := jsonBody[key]
switch v := value.(type) {
case map[string]interface{}:
continue
case []interface{}:
continue
default:
s.Validators = append(s.Validators, hrp.Validator{
Check: fmt.Sprintf("body.%s", key),
Assert: "equals",
Expect: v,
Message: fmt.Sprintf("assert response body %s", key),
})
}
}
}
return nil
}

View File

@@ -0,0 +1,373 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/httprunner/httprunner/v4/hrp"
)
var (
harPath = "../../../examples/data/har/demo.har"
harPath2 = "../../../examples/data/har/postman-echo.har"
harProfileOverridePath = "../../../examples/data/har/profile_override.yml"
)
var converterHAR = NewConverterHAR(NewTCaseConverter(harPath))
var converterHAR2 = NewConverterHAR(NewTCaseConverter(harPath2))
func TestHAR2JSON(t *testing.T) {
jsonPath, err := converterHAR.ToJSON()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.NotEmpty(t, jsonPath) {
t.Fatal()
}
}
func TestHAR2YAML(t *testing.T) {
yamlPath, err := converterHAR2.ToYAML()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.NotEmpty(t, yamlPath) {
t.Fatal()
}
}
func TestLoadHAR(t *testing.T) {
h, err := converterHAR.load()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, "GET", h.Log.Entries[0].Request.Method) {
t.Fatal()
}
if !assert.Equal(t, "POST", h.Log.Entries[1].Request.Method) {
t.Fatal()
}
}
func TestLoadHARWithProfile(t *testing.T) {
tCaseConverter := NewTCaseConverter(harPath)
tCaseConverter.SetProfile(harProfileOverridePath)
h := NewConverterHAR(tCaseConverter)
_, err := h.load()
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t,
map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
h.converter.Profile.Headers) {
t.Fatal()
}
if !assert.Equal(t,
map[string]string{"UserName": "debugtalk"},
h.converter.Profile.Cookies) {
t.Fatal()
}
}
func TestMakeTestCaseFromHAR(t *testing.T) {
tCase, err := converterHAR.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
// make request 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()
}
// make request 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()
}
// make request params
if !assert.Equal(t, "HDnY8", tCase.TestSteps[0].Request.Params["foo1"]) {
t.Fatal()
}
// make request cookies
if !assert.NotEmpty(t, tCase.TestSteps[1].Request.Cookies["sails.sid"]) {
t.Fatal()
}
// make request headers
if !assert.Equal(t, "HttpRunnerPlus", tCase.TestSteps[0].Request.Headers["User-Agent"]) {
t.Fatal()
}
if !assert.Equal(t, "postman-echo.com", tCase.TestSteps[0].Request.Headers["Host"]) {
t.Fatal()
}
// make request data
if !assert.Equal(t, nil, tCase.TestSteps[0].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, map[string]interface{}{"foo1": "HDnY8", "foo2": 12.3}, tCase.TestSteps[1].Request.Body) {
t.Fatal()
}
if !assert.Equal(t, "foo1=HDnY8&foo2=12.3", tCase.TestSteps[2].Request.Body) {
t.Fatal()
}
// make validators
validator, ok := tCase.TestSteps[0].Validators[0].(hrp.Validator)
if !ok || !assert.Equal(t, "status_code", validator.Check) {
t.Fatal()
}
validator, ok = tCase.TestSteps[0].Validators[1].(hrp.Validator)
if !ok || !assert.Equal(t, "headers.\"Content-Type\"", validator.Check) {
t.Fatal()
}
validator, ok = tCase.TestSteps[0].Validators[2].(hrp.Validator)
if !ok || !assert.Equal(t, "body.url", validator.Check) {
t.Fatal()
}
}
func TestMakeRequestURL(t *testing.T) {
entry := &Entry{
Request: Request{
URL: "http://127.0.0.1:8080/api/login",
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, "http://127.0.0.1:8080/api/login", step.Request.URL) {
t.Fatal()
}
}
func TestMakeRequestHeaders(t *testing.T) {
entry := &Entry{
Request: Request{
Method: "POST",
Headers: []NVP{
{Name: "Content-Type", Value: "application/json; charset=utf-8"},
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, map[string]string{
"Content-Type": "application/json; charset=utf-8",
}, step.Request.Headers) {
t.Fatal()
}
}
func TestMakeRequestHeadersWithProfileOverride(t *testing.T) {
tCaseConverter := NewTCaseConverter(harPath)
tCaseConverter.SetProfile(harProfileOverridePath)
h := NewConverterHAR(tCaseConverter)
entry := &Entry{
Request: Request{
Method: "POST",
Headers: []NVP{
{Name: "Content-Type", Value: "application/json; charset=utf-8"},
},
},
}
step, err := h.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
}, step.Request.Headers) {
t.Fatal()
}
}
func TestMakeRequestCookies(t *testing.T) {
entry := &Entry{
Request: Request{
Method: "POST",
Cookies: []Cookie{
{Name: "abc", Value: "123"},
{Name: "UserName", Value: "leolee"},
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, map[string]string{
"abc": "123",
"UserName": "leolee",
}, step.Request.Cookies) {
t.Fatal()
}
}
func TestMakeRequestCookiesWithProfileOverride(t *testing.T) {
tCaseConverter := NewTCaseConverter(harPath)
tCaseConverter.SetProfile(harProfileOverridePath)
h := NewConverterHAR(tCaseConverter)
entry := &Entry{
Request: Request{
Method: "POST",
Cookies: []Cookie{
{Name: "abc", Value: "123"},
{Name: "UserName", Value: "leolee"},
},
},
}
step, err := h.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, map[string]string{
"UserName": "debugtalk",
}, step.Request.Cookies) {
t.Fatal()
}
}
func TestMakeRequestDataParams(t *testing.T) {
entry := &Entry{
Request: Request{
Method: "POST",
PostData: PostData{
MimeType: "application/x-www-form-urlencoded; charset=utf-8",
Params: []PostParam{
{Name: "a", Value: "1"},
{Name: "b", Value: "2"},
},
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, "a=1&b=2", step.Request.Body) {
t.Fatal()
}
}
func TestMakeRequestDataJSON(t *testing.T) {
entry := &Entry{
Request: Request{
Method: "POST",
PostData: PostData{
MimeType: "application/json; charset=utf-8",
Text: "{\"a\":\"1\",\"b\":\"2\"}",
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, map[string]interface{}{"a": "1", "b": "2"}, step.Request.Body) {
t.Fatal()
}
}
func TestMakeRequestDataTextEmpty(t *testing.T) {
entry := &Entry{
Request: Request{
Method: "POST",
PostData: PostData{
MimeType: "application/json; charset=utf-8",
Text: "",
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
if !assert.Equal(t, nil, step.Request.Body) { // TODO
t.Fatal()
}
}
func TestMakeValidate(t *testing.T) {
entry := &Entry{
Response: Response{
Status: 200,
Headers: []NVP{
{Name: "Content-Type", Value: "application/json; charset=utf-8"},
},
Content: Content{
Size: 71,
MimeType: "application/json; charset=utf-8",
// map[Code:200 IsSuccess:true Message:<nil> Value:map[BlnResult:true]]
Text: "eyJJc1N1Y2Nlc3MiOnRydWUsIkNvZGUiOjIwMCwiTWVzc2FnZSI6bnVsbCwiVmFsdWUiOnsiQmxuUmVzdWx0Ijp0cnVlfX0=",
Encoding: "base64",
},
},
}
step, err := converterHAR.prepareTestStep(entry)
if !assert.NoError(t, err) {
t.Fatal()
}
validator, ok := step.Validators[0].(hrp.Validator)
if !ok {
t.Fatal()
}
if !assert.Equal(t, validator,
hrp.Validator{
Check: "status_code",
Expect: 200,
Assert: "equals",
Message: "assert response status code",
}) {
t.Fatal()
}
validator, ok = step.Validators[1].(hrp.Validator)
if !ok {
t.Fatal()
}
if !assert.Equal(t, validator,
hrp.Validator{
Check: "headers.\"Content-Type\"",
Expect: "application/json; charset=utf-8",
Assert: "equals",
Message: "assert response header Content-Type",
}) {
t.Fatal()
}
validator, ok = step.Validators[2].(hrp.Validator)
if !ok {
t.Fatal()
}
if !assert.Equal(t, validator,
hrp.Validator{
Check: "body.Code",
Expect: float64(200), // TODO
Assert: "equals",
Message: "assert response body Code",
}) {
t.Fatal()
}
}

View File

@@ -0,0 +1,111 @@
package convert
import (
"fmt"
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func NewConverterJSON(converter *TCaseConverter) *ConverterJSON {
return &ConverterJSON{
converter: converter,
}
}
type ConverterJSON struct {
converter *TCaseConverter
}
func (c *ConverterJSON) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterJSON) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterJSON) ToJSONTemp() (string, error) {
testCase, err := c.makeTestCaseTemp()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterJSON) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterJSON) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from json testcase to gotest scripts is not supported yet")
}
func (c *ConverterJSON) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterJSON) MakePyTestScript() (string, error) {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return "", err
}
args := append([]string{"-m", "httprunner", "make"}, c.converter.InputPath)
err = builtin.ExecCommand(python3, args...)
if err != nil {
return "", err
}
return c.converter.genOutputPath(suffixPyTest), nil
}
func (c *ConverterJSON) makeTestCase() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterJSON) makeTestCaseTemp() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat2PyEngine()
if err != nil {
return nil, err
}
return tCase, nil
}

View File

@@ -1,4 +1,4 @@
package postman2case
package convert
import (
"bytes"
@@ -19,6 +19,83 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// ==================== model definition starts here ====================
/*
Postman Collection format reference:
https://schema.postman.com/json/collection/v2.0.0/collection.json
https://schema.postman.com/json/collection/v2.1.0/collection.json
*/
// CasePostman represents the postman exported file
type CasePostman struct {
Info TInfo `json:"info"`
Items []TItem `json:"item"`
}
// TInfo gives information about the collection
type TInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Schema string `json:"schema"`
}
// TItem contains the detail information of request and expected responses
// item could be defined recursively
type TItem struct {
Items []TItem `json:"item"`
Name string `json:"name"`
Request TRequest `json:"request"`
Responses []TResponse `json:"response"`
}
type TRequest struct {
Method string `json:"method"`
Headers []TField `json:"header"`
Body TBody `json:"body"`
URL TUrl `json:"url"`
Description string `json:"description"`
}
type TResponse struct {
Name string `json:"name"`
OriginalRequest TRequest `json:"originalRequest"`
Status string `json:"status"`
Code int `json:"code"`
Headers []TField `json:"headers"`
Body string `json:"body"`
}
type TUrl struct {
Raw string `json:"raw"`
Protocol string `json:"protocol"`
Path []string `json:"path"`
Description string `json:"description"`
Query []TField `json:"query"`
Variable []TField `json:"variable"`
}
type TField struct {
Key string `json:"key"`
Value string `json:"value"`
Src string `json:"src"`
Description string `json:"description"`
Type string `json:"type"`
Disabled bool `json:"disabled"`
Enable bool `json:"enable"`
}
type TBody struct {
Mode string `json:"mode"`
FormData []TField `json:"formdata"`
URLEncoded []TField `json:"urlencoded"`
Raw string `json:"raw"`
Disabled bool `json:"disabled"`
Options interface{} `json:"options"`
}
// ==================== model definition ends here ====================
const (
enumBodyRaw = "raw"
enumBodyUrlEncoded = "urlencoded"
@@ -32,19 +109,6 @@ const (
enumFieldTypeFile = "file"
)
const (
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",
@@ -53,119 +117,131 @@ var contentTypeMap = map[string]string{
"xml": "application/xml",
}
func NewCollection(path string) *collection {
return &collection{
path: path,
func NewConverterPostman(converter *TCaseConverter) *ConverterPostman {
return &ConverterPostman{
converter: converter,
}
}
type collection struct {
path string
profile map[string]interface{}
patch map[string]interface{}
outputDir string
type ConverterPostman struct {
converter *TCaseConverter
}
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 *ConverterPostman) Struct() *TCaseConverter {
return c.converter
}
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
}
func (c *collection) GenJSON() (jsonPath string, err error) {
func (c *ConverterPostman) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath = c.genOutputPath(extensionJSON)
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
return
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *collection) GenYAML() (yamlPath string, err error) {
func (c *ConverterPostman) ToJSONTemp() (string, error) {
testCase, err := c.makeTestCaseTemp()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterPostman) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath = c.genOutputPath(extensionYAML)
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
return
}
func (c *collection) genOutputPath(suffix string) string {
file := getFilenameWithoutExtension(c.path) + suffix
if c.outputDir != "" {
return filepath.Join(c.outputDir, file)
} else {
return filepath.Join(filepath.Dir(c.path), file)
if err != nil {
return "", err
}
return yamlPath, nil
}
func getFilenameWithoutExtension(path string) string {
base := filepath.Base(path)
ext := filepath.Ext(base)
return base[0:len(base)-len(ext)] + suffixName
func (c *ConverterPostman) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from postman to gotest scripts is not supported yet")
}
func (c *collection) makeTestCase() (*hrp.TCase, error) {
tCollection, err := c.load()
func (c *ConverterPostman) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterPostman) makeTestCase() (*hrp.TCase, error) {
casePostman, err := c.load()
if err != nil {
return nil, err
}
teststeps, err := c.prepareTestSteps(tCollection)
teststeps, err := c.prepareTestSteps(casePostman)
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(tCollection),
Config: c.prepareConfig(casePostman),
TestSteps: teststeps,
}
err = tCase.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *collection) load() (*TCollection, error) {
collection := &TCollection{}
err := builtin.LoadFile(c.path, collection)
func (c *ConverterPostman) makeTestCaseTemp() (*hrp.TCase, error) {
casePostman, err := c.load()
if err != nil {
return nil, errors.Wrap(err, "load postman collection failed")
return nil, err
}
return collection, nil
teststeps, err := c.prepareTestSteps(casePostman)
if err != nil {
return nil, err
}
tCase := &hrp.TCase{
Config: c.prepareConfig(casePostman),
TestSteps: teststeps,
}
err = tCase.MakeCompat2PyEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *collection) prepareConfig(tCollection *TCollection) *hrp.TConfig {
return hrp.NewConfig(tCollection.Info.Name).
func (c *ConverterPostman) load() (*CasePostman, error) {
casePostman := c.converter.CasePostman
if casePostman == nil {
return nil, errors.New("empty postman case occurs")
}
return casePostman, nil
}
func (c *ConverterPostman) prepareConfig(casePostman *CasePostman) *hrp.TConfig {
return hrp.NewConfig(casePostman.Info.Name).
SetVerifySSL(false)
}
func (c *collection) prepareTestSteps(tCollection *TCollection) ([]*hrp.TStep, error) {
func (c *ConverterPostman) prepareTestSteps(casePostman *CasePostman) ([]*hrp.TStep, error) {
// recursively convert collection items into a list
var itemList []TItem
for _, item := range tCollection.Items {
for _, item := range casePostman.Items {
extractItemList(item, &itemList)
}
var steps []*hrp.TStep
for _, item := range itemList {
step, err := c.prepareTestStep(&item)
step, err := c.prepareTestStep(&item, steps)
if err != nil {
return nil, err
}
@@ -191,19 +267,18 @@ func extractItemList(item TItem, itemList *[]TItem) {
}
}
func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) {
func (c *ConverterPostman) prepareTestStep(item *TItem, steps []*hrp.TStep) (*hrp.TStep, error) {
log.Info().
Str("method", item.Request.Method).
Str("url", item.Request.URL.Raw).
Msg("convert teststep")
step := &tStep{
step := &stepFromPostman{
TStep: hrp.TStep{
Request: &hrp.Request{},
Validators: make([]interface{}, 0),
},
profile: c.profile,
patch: c.patch,
profile: c.converter.Profile,
}
if err := step.makeRequestName(item); err != nil {
return nil, err
@@ -223,30 +298,29 @@ func (c *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) {
if err := step.makeRequestCookies(item); err != nil {
return nil, err
}
if err := step.makeRequestBody(item); err != nil {
if err := step.makeRequestBody(item, steps); err != nil {
return nil, err
}
return &step.TStep, nil
}
type tStep struct {
type stepFromPostman struct {
hrp.TStep
profile map[string]interface{}
patch map[string]interface{}
profile *Profile
}
// makeRequestName indicates the step name the same as item name
func (s *tStep) makeRequestName(item *TItem) error {
func (s *stepFromPostman) makeRequestName(item *TItem) error {
s.Name = item.Name
return nil
}
func (s *tStep) makeRequestMethod(item *TItem) error {
func (s *stepFromPostman) makeRequestMethod(item *TItem) error {
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
return nil
}
func (s *tStep) makeRequestURL(item *TItem) error {
func (s *stepFromPostman) makeRequestURL(item *TItem) error {
rawUrl := item.Request.URL.Raw
// parse path variables like ":path" in https://postman-echo.com/:path?k1=v1&k2=v2
for _, field := range item.Request.URL.Variable {
@@ -261,7 +335,7 @@ func (s *tStep) makeRequestURL(item *TItem) error {
return nil
}
func (s *tStep) makeRequestParams(item *TItem) error {
func (s *stepFromPostman) makeRequestParams(item *TItem) error {
s.Request.Params = make(map[string]interface{})
for _, field := range item.Request.URL.Query {
if field.Disabled {
@@ -272,44 +346,9 @@ func (s *tStep) makeRequestParams(item *TItem) 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) 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
}
func (s *stepFromPostman) makeRequestHeaders(item *TItem) error {
// headers defined in postman collection
s.Request.Headers = make(map[string]string)
for _, field := range item.Request.Headers {
if field.Disabled || strings.EqualFold(field.Key, "cookie") {
continue
@@ -317,20 +356,23 @@ func (s *tStep) makeRequestHeaders(item *TItem) error {
s.Request.Headers[field.Key] = field.Value
}
// create or update the headers indicated in the patch
s.updateRequestInfo(configPatch, keyHeaders)
if s.profile == nil {
return nil
}
// override all headers according to the profile
if s.profile.Override {
s.Request.Headers = make(map[string]string)
}
// create or update the headers according to the profile
for k, v := range s.profile.Headers {
s.Request.Headers[k] = v
}
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
}
func (s *stepFromPostman) makeRequestCookies(item *TItem) error {
// cookies defined in postman collection
s.Request.Cookies = make(map[string]string)
for _, field := range item.Request.Headers {
if field.Disabled || !strings.EqualFold(field.Key, "cookie") {
continue
@@ -338,12 +380,21 @@ func (s *tStep) makeRequestCookies(item *TItem) error {
s.parseRequestCookiesMap(field.Value)
}
// create or update the cookies indicated in the patch
s.updateRequestInfo(configPatch, keyCookies)
if s.profile == nil {
return nil
}
// override all cookies according to the profile
if s.profile.Override {
s.Request.Cookies = make(map[string]string)
}
// create or update the cookies according to the profile
for k, v := range s.profile.Cookies {
s.Request.Cookies[k] = v
}
return nil
}
func (s *tStep) parseRequestCookiesMap(cookies string) {
func (s *stepFromPostman) parseRequestCookiesMap(cookies string) {
for _, cookie := range strings.Split(cookies, ";") {
cookie = strings.TrimSpace(cookie)
index := strings.Index(cookie, "=")
@@ -351,11 +402,11 @@ func (s *tStep) parseRequestCookiesMap(cookies string) {
log.Warn().Str("cookie", cookie).Msg("cookie format invalid")
continue
}
s.Request.Cookies[cookie[0:index]] = cookie[index+1:]
s.Request.Cookies[cookie[:index]] = cookie[index+1:]
}
}
func (s *tStep) makeRequestBody(item *TItem) error {
func (s *stepFromPostman) makeRequestBody(item *TItem, steps []*hrp.TStep) error {
mode := item.Request.Body.Mode
if mode == "" {
return nil
@@ -364,7 +415,7 @@ func (s *tStep) makeRequestBody(item *TItem) error {
case enumBodyRaw:
return s.makeRequestBodyRaw(item)
case enumBodyFormData:
return s.makeRequestBodyFormData(item)
return s.makeRequestBodyFormData(item, steps)
case enumBodyUrlEncoded:
return s.makeRequestBodyUrlEncoded(item)
case enumBodyFile, enumBodyGraphQL:
@@ -373,7 +424,7 @@ func (s *tStep) makeRequestBody(item *TItem) error {
return nil
}
func (s *tStep) makeRequestBodyRaw(item *TItem) (err error) {
func (s *stepFromPostman) makeRequestBodyRaw(item *TItem) (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("make request body raw failed: %v", p)
@@ -401,7 +452,7 @@ func (s *tStep) makeRequestBodyRaw(item *TItem) (err error) {
return
}
func (s *tStep) makeRequestBodyFormData(item *TItem) (err error) {
func (s *stepFromPostman) makeRequestBodyFormData(item *TItem, steps []*hrp.TStep) (err error) {
defer func() {
if err != nil {
err = errors.Wrap(err, "make request body form-data failed")
@@ -446,7 +497,7 @@ func writeFormDataFile(writer *multipart.Writer, field *TField) error {
return err
}
func (s *tStep) makeRequestBodyUrlEncoded(item *TItem) error {
func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error {
payloadMap := make(map[string]string)
for _, field := range item.Request.Body.URLEncoded {
if field.Disabled {
@@ -460,6 +511,6 @@ func (s *tStep) makeRequestBodyUrlEncoded(item *TItem) error {
}
// TODO makeValidate from example response
func (s *tStep) makeValidate(item *TItem) error {
func (s *stepFromPostman) makeValidate(item *TItem) error {
return nil
}

View File

@@ -1,4 +1,4 @@
package postman2case
package convert
import (
"testing"
@@ -7,13 +7,15 @@ import (
)
var (
collectionPath = "../../../../examples/data/postman2case/demo.json"
profilePath = "../../../../examples/data/postman2case/profile.yml"
patchPath = "../../../../examples/data/postman2case/patch.yml"
collectionPath = "../../../examples/data/postman2case/demo.json"
collectionProfileOverridePath = "../../../examples/data/postman2case/profile_override.yml"
collectionProfilePath = "../../../examples/data/postman2case/profile.yml"
)
func TestGenJSON(t *testing.T) {
jsonPath, err := NewCollection(collectionPath).GenJSON()
var converterPostman = NewConverterPostman(NewTCaseConverter(collectionPath))
func TestPostman2JSON(t *testing.T) {
jsonPath, err := converterPostman.ToJSON()
if !assert.NoError(t, err) {
t.Fatal()
}
@@ -22,8 +24,8 @@ func TestGenJSON(t *testing.T) {
}
}
func TestGenYAML(t *testing.T) {
yamlPath, err := NewCollection(collectionPath).GenYAML()
func TestPostman2YAML(t *testing.T) {
yamlPath, err := converterPostman.ToYAML()
if !assert.NoError(t, err) {
t.Fatal()
}
@@ -33,17 +35,17 @@ func TestGenYAML(t *testing.T) {
}
func TestLoadCollection(t *testing.T) {
tCollection, err := NewCollection(collectionPath).load()
casePostman, err := converterPostman.load()
if !assert.NoError(t, err) {
t.Fatal(err)
}
if !assert.Equal(t, "postman collection demo", tCollection.Info.Name) {
if !assert.Equal(t, "postman collection demo", casePostman.Info.Name) {
t.Fatal()
}
}
func TestMakeTestCase(t *testing.T) {
tCase, err := NewCollection(collectionPath).makeTestCase()
func TestMakeTestCaseFromCollection(t *testing.T) {
tCase, err := converterPostman.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
@@ -107,9 +109,10 @@ func TestMakeTestCase(t *testing.T) {
}
}
func TestMakeTestCaseWithProfile(t *testing.T) {
c := NewCollection(collectionPath)
c.SetProfile(profilePath)
func TestMakeTestCaseWithProfileOverride(t *testing.T) {
tCaseConverter := NewTCaseConverter(collectionPath)
tCaseConverter.SetProfile(collectionProfileOverridePath)
c := NewConverterPostman(tCaseConverter)
tCase, err := c.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
@@ -133,22 +136,23 @@ func TestMakeTestCaseWithProfile(t *testing.T) {
}
}
func TestMakeTestCaseWithPatch(t *testing.T) {
c := NewCollection(collectionPath)
c.SetPatch(patchPath)
func TestMakeTestCaseWithProfile(t *testing.T) {
tCaseConverter := NewTCaseConverter(collectionPath)
tCaseConverter.SetProfile(collectionProfilePath)
c := NewConverterPostman(tCaseConverter)
tCase, err := c.makeTestCase()
if !assert.NoError(t, err) {
t.Fatal()
}
// create cookies Cookie1 indicated in patch
// create cookies Cookie1 indicated in profile
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
// update header User-Agent indicated in profile
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
// pass header Connection which is not indicated in profile
if !assert.Equal(t, "close", tCase.TestSteps[5].Request.Headers["Connection"]) {
t.Fatal()
}

View File

@@ -0,0 +1,19 @@
package convert
import (
"fmt"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/version"
)
func convert2PyTestScripts(paths ...string) error {
httprunner := fmt.Sprintf("httprunner>=%s", version.HttpRunnerMinVersion)
python3, err := builtin.EnsurePython3Venv(httprunner)
if err != nil {
return err
}
args := append([]string{"-m", "httprunner", "make"}, paths...)
return builtin.ExecCommand(python3, args...)
}

View File

@@ -0,0 +1,94 @@
package convert
import (
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
func NewConverterYAML(converter *TCaseConverter) *ConverterYAML {
return &ConverterYAML{
converter: converter,
}
}
type ConverterYAML struct {
converter *TCaseConverter
}
func (c *ConverterYAML) Struct() *TCaseConverter {
return c.converter
}
func (c *ConverterYAML) ToJSON() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterYAML) ToJSONTemp() (string, error) {
testCase, err := c.makeTestCaseTemp()
if err != nil {
return "", err
}
jsonPath := c.converter.genOutputPath(suffixJSON)
err = builtin.Dump2JSON(testCase, jsonPath)
if err != nil {
return "", err
}
return jsonPath, nil
}
func (c *ConverterYAML) ToYAML() (string, error) {
testCase, err := c.makeTestCase()
if err != nil {
return "", err
}
yamlPath := c.converter.genOutputPath(suffixYAML)
err = builtin.Dump2YAML(testCase, yamlPath)
if err != nil {
return "", err
}
return yamlPath, nil
}
func (c *ConverterYAML) ToGoTest() (string, error) {
//TODO implement me
return "", errors.New("convert from yaml testcase to gotest scripts is not supported yet")
}
func (c *ConverterYAML) ToPyTest() (string, error) {
return convertToPyTest(c)
}
func (c *ConverterYAML) makeTestCase() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
return tCase, nil
}
func (c *ConverterYAML) makeTestCaseTemp() (*hrp.TCase, error) {
tCase, err := makeTestCaseFromJSONYAML(c)
if err != nil {
return nil, err
}
err = tCase.MakeCompat2PyEngine()
if err != nil {
return nil, err
}
return tCase, nil
}

View File

@@ -22,13 +22,6 @@ const (
suffixYAML = ".yaml"
)
const (
configProfile = "profile"
configPatch = "patch"
keyHeaders = "headers"
keyCookies = "cookies"
)
func NewHAR(path string) *har {
return &har{
path: path,
@@ -40,7 +33,6 @@ type har struct {
filterStr string
excludeStr string
profile map[string]interface{}
patch map[string]interface{}
outputDir string
}
@@ -54,16 +46,6 @@ 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
@@ -164,7 +146,6 @@ 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
@@ -193,7 +174,6 @@ 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 {
@@ -219,59 +199,43 @@ 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)
// override all cookies according to the profile
if s.updateRequestInfo(configProfile, keyCookies) {
return nil
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!")
}
// 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)
// override all headers according to the profile
if s.updateRequestInfo(configProfile, keyHeaders) {
return nil
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!")
}
// use headers from har
@@ -281,9 +245,6 @@ 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
}

View File

@@ -1,7 +1,6 @@
package har2case
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -12,7 +11,7 @@ import (
var (
harPath = "../../../../examples/data/har/demo.har"
harPath2 = "../../../../examples/data/har/postman-echo.har"
profilePath = "../../../../examples/data/har/profile.yml"
profilePath = "../../../../examples/data/har/profile_override.yml"
)
func TestGenJSON(t *testing.T) {
@@ -382,32 +381,3 @@ 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))
})
}
}

View File

@@ -1,74 +0,0 @@
package postman2case
/*
Postman Collection format reference:
https://schema.postman.com/json/collection/v2.0.0/collection.json
https://schema.postman.com/json/collection/v2.1.0/collection.json
*/
// TCollection represents the postman exported file
type TCollection struct {
Info TInfo `json:"info"`
Items []TItem `json:"item"`
}
// TInfo gives information about the collection
type TInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Schema string `json:"schema"`
}
// TItem contains the detail information of request and expected responses
// item could be defined recursively
type TItem struct {
Items []TItem `json:"item"`
Name string `json:"name"`
Request TRequest `json:"request"`
Responses []TResponse `json:"response"`
}
type TRequest struct {
Method string `json:"method"`
Headers []TField `json:"header"`
Body TBody `json:"body"`
URL TUrl `json:"url"`
Description string `json:"description"`
}
type TResponse struct {
Name string `json:"name"`
OriginalRequest TRequest `json:"originalRequest"`
Status string `json:"status"`
Code int `json:"code"`
Headers []TField `json:"headers"`
Body string `json:"body"`
}
type TUrl struct {
Raw string `json:"raw"`
Protocol string `json:"protocol"`
Path []string `json:"path"`
Description string `json:"description"`
Query []TField `json:"query"`
Variable []TField `json:"variable"`
}
type TField struct {
Key string `json:"key"`
Value string `json:"value"`
Src string `json:"src"`
Description string `json:"description"`
Type string `json:"type"`
Disabled bool `json:"disabled"`
Enable bool `json:"enable"`
}
type TBody struct {
Mode string `json:"mode"`
FormData []TField `json:"formdata"`
URLEncoded []TField `json:"urlencoded"`
Raw string `json:"raw"`
Disabled bool `json:"disabled"`
Options interface{} `json:"options"`
}

View File

@@ -47,7 +47,7 @@ func (path *APIPath) ToAPI() (*API, error) {
if err != nil {
return nil, err
}
err = convertCompatValidator(api.Validators)
err = convertValidatorCompat2GoEngine(api.Validators)
convertExtract(api.Extract)
return api, err
}

View File

@@ -64,7 +64,7 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
return nil, errors.New("incorrect testcase file format, expected config in file")
}
err = tc.makeCompat()
err = tc.MakeCompat2GoEngine()
if err != nil {
return nil, err
}
@@ -154,27 +154,28 @@ type TCase struct {
TestSteps []*TStep `json:"teststeps" yaml:"teststeps"`
}
// makeCompat converts TCase to compatible testcase
func (tc *TCase) makeCompat() error {
var err error
// MakeCompat2GoEngine converts TCase compatible with Golang engine style
func (tc *TCase) MakeCompat2GoEngine() (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("convert compat testcase error: %v", p)
err = fmt.Errorf("[MakeCompat2GoEngine] convert compat testcase error: %v", p)
}
}()
for _, step := range tc.TestSteps {
// 1. deal with request body compatible with HttpRunner
// 1. deal with request body compatibility
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
step.Request.Json = nil
} else if step.Request.Data != nil {
step.Request.Body = step.Request.Data
step.Request.Data = nil
}
}
// 2. deal with validators compatible with HttpRunner
err = convertCompatValidator(step.Validators)
// 2. deal with validators compatibility
err = convertValidatorCompat2GoEngine(step.Validators)
if err != nil {
return err
}
@@ -185,16 +186,19 @@ func (tc *TCase) makeCompat() error {
return nil
}
func convertCompatValidator(Validators []interface{}) (err error) {
func convertValidatorCompat2GoEngine(Validators []interface{}) (err error) {
for i, iValidator := range Validators {
if _, ok := iValidator.(Validator); ok {
continue
}
validatorMap := iValidator.(map[string]interface{})
validator := Validator{}
_, checkExisted := validatorMap["check"]
_, assertExisted := validatorMap["assert"]
_, expectExisted := validatorMap["expect"]
// check priority: HRP > HttpRunner
// validator check priority: Golang > Python engine style
if checkExisted && assertExisted && expectExisted {
// HRP validator format
// Golang engine style
validator.Check = validatorMap["check"].(string)
validator.Assert = validatorMap["assert"].(string)
validator.Expect = validatorMap["expect"]
@@ -203,8 +207,10 @@ func convertCompatValidator(Validators []interface{}) (err error) {
}
validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator
} else if len(validatorMap) == 1 {
// HttpRunner validator format
continue
}
if len(validatorMap) == 1 {
// Python engine style
for assertMethod, iValidatorContent := range validatorMap {
checkAndExpect := iValidatorContent.([]interface{})
if len(checkAndExpect) != 2 {
@@ -216,9 +222,9 @@ func convertCompatValidator(Validators []interface{}) (err error) {
}
validator.Check = convertCheckExpr(validator.Check)
Validators[i] = validator
} else {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
continue
}
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
return nil
}
@@ -244,6 +250,74 @@ func convertCheckExpr(checkExpr string) string {
return strings.Join(checkItems, ".")
}
// MakeCompat2PyEngine converts TCase compatible with Python engine style
func (tc *TCase) MakeCompat2PyEngine() (err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("[MakeCompat2PyEngine] convert compat testcase error: %v", p)
}
}()
for _, step := range tc.TestSteps {
// 1. deal with request body compatibility
if step.Request != nil && step.Request.Body != nil {
if strings.HasPrefix(step.Request.Headers["Content-Type"], "application/json") {
step.Request.Json = step.Request.Body
step.Request.Body = nil
continue
}
step.Request.Data = step.Request.Body
step.Request.Body = nil
}
// 2. deal with validators compatibility
err = convertValidatorCompat2PyEngine(step.Validators)
if err != nil {
return err
}
}
return
}
func convertValidatorCompat2PyEngine(Validators []interface{}) (err error) {
for i, iValidator := range Validators {
if v, ok := iValidator.(Validator); ok {
var iValidatorContent []interface{}
iValidatorContent = append(iValidatorContent, v.Check)
iValidatorContent = append(iValidatorContent, v.Expect)
newValidatorMap := make(map[string]interface{})
newValidatorMap[v.Assert] = iValidatorContent
Validators[i] = newValidatorMap
continue
}
validatorMap := iValidator.(map[string]interface{})
// validator check priority: Python > Golang engine style
if len(validatorMap) == 1 {
// Python engine style
for _, iValidatorContent := range validatorMap {
checkAndExpect := iValidatorContent.([]interface{})
if len(checkAndExpect) != 2 {
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
}
continue
}
_, checkExisted := validatorMap["check"]
_, assertExisted := validatorMap["assert"]
_, expectExisted := validatorMap["expect"]
if checkExisted && assertExisted && expectExisted {
// Golang engine style
var iValidatorContent []interface{}
iValidatorContent = append(iValidatorContent, validatorMap["check"])
iValidatorContent = append(iValidatorContent, validatorMap["expect"])
newValidatorMap := make(map[string]interface{})
newValidatorMap[validatorMap["assert"].(string)] = iValidatorContent
Validators[i] = newValidatorMap
continue
}
return fmt.Errorf("unexpected validator format: %v", validatorMap)
}
return
}
func LoadTestCases(iTestCases ...ITestCase) ([]*TestCase, error) {
testCases := make([]*TestCase, 0)