package convert import ( "fmt" "net/url" "reflect" "strings" "github.com/pkg/errors" "github.com/rs/zerolog/log" hrp "github.com/httprunner/httprunner/v5" "github.com/httprunner/httprunner/v5/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" enumBodyFormData = "formdata" enumBodyFile = "file" enumBodyGraphQL = "graphql" ) const ( enumFieldTypeText = "text" enumFieldTypeFile = "file" ) var contentTypeMap = map[string]string{ "text": "text/plain", "javascript": "application/javascript", "json": "application/json", "html": "text/html", "xml": "application/xml", } func LoadPostmanCase(path string) (*hrp.TestCaseDef, error) { log.Info().Str("path", path).Msg("load postman case file") casePostman, err := loadCasePostman(path) if err != nil { return nil, err } // convert to TestCase format return casePostman.ToTestCase() } func loadCasePostman(path string) (*CasePostman, error) { casePostman := new(CasePostman) err := hrp.LoadFileObject(path, casePostman) if err != nil { return nil, errors.Wrap(err, "load postman file failed") } if casePostman.Items == nil { return nil, errors.New("invalid postman case file, missing items") } return casePostman, nil } func (c *CasePostman) ToTestCase() (*hrp.TestCaseDef, error) { teststeps, err := c.prepareTestSteps() if err != nil { return nil, err } tCase := &hrp.TestCaseDef{ Config: c.prepareConfig(), Steps: teststeps, } err = hrp.ConvertCaseCompatibility(tCase) if err != nil { return nil, err } return tCase, nil } func (c *CasePostman) prepareConfig() *hrp.TConfig { return hrp.NewConfig(c.Info.Name). SetVerifySSL(false) } func (c *CasePostman) prepareTestSteps() ([]*hrp.TStep, error) { // recursively convert collection items into a list var itemList []TItem for _, item := range c.Items { extractItemList(item, &itemList) } var steps []*hrp.TStep for _, item := range itemList { step, err := c.prepareTestStep(&item) if err != nil { return nil, err } steps = append(steps, step) } return steps, nil } func extractItemList(item TItem, itemList *[]TItem) { // current item contains no other items and request is not empty if len(item.Items) == 0 { if !reflect.DeepEqual(item.Request, TRequest{}) { *itemList = append(*itemList, item) } return } // look up all items inside for _, i := range item.Items { // append item name i.Name = fmt.Sprintf("%s - %s", item.Name, i.Name) extractItemList(i, itemList) } } func (c *CasePostman) prepareTestStep(item *TItem) (*hrp.TStep, error) { log.Info(). Str("method", item.Request.Method). Str("url", item.Request.URL.Raw). Msg("convert teststep") step := &stepFromPostman{ TStep: hrp.TStep{ Request: &hrp.Request{}, StepConfig: hrp.StepConfig{ Validators: make([]interface{}, 0), }, }, } if err := step.makeRequestName(item); err != nil { return nil, err } if err := step.makeRequestMethod(item); err != nil { return nil, err } if err := step.makeRequestURL(item); err != nil { return nil, err } if err := step.makeRequestParams(item); err != nil { return nil, err } if err := step.makeRequestHeaders(item); err != nil { return nil, err } if err := step.makeRequestCookies(item); err != nil { return nil, err } if err := step.makeRequestBody(item); err != nil { return nil, err } return &step.TStep, nil } type stepFromPostman struct { hrp.TStep } // makeRequestName indicates the step name the same as item name func (s *stepFromPostman) makeRequestName(item *TItem) error { s.StepName = item.Name return nil } func (s *stepFromPostman) makeRequestMethod(item *TItem) error { s.Request.Method = hrp.HTTPMethod(item.Request.Method) return nil } 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 { pathVar := ":" + field.Key rawUrl = strings.Replace(rawUrl, pathVar, field.Value, -1) } u, err := url.Parse(rawUrl) if err != nil { return errors.Wrap(err, "parse URL error") } s.Request.URL = fmt.Sprintf("%s://%s", u.Scheme, u.Host+u.Path) return nil } func (s *stepFromPostman) makeRequestParams(item *TItem) error { s.Request.Params = make(map[string]interface{}) for _, field := range item.Request.URL.Query { if field.Disabled { continue } s.Request.Params[field.Key] = field.Value } 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 } s.Request.Headers[field.Key] = field.Value } 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 } s.parseRequestCookiesMap(field.Value) } return nil } func (s *stepFromPostman) parseRequestCookiesMap(cookies string) { for _, cookie := range strings.Split(cookies, ";") { cookie = strings.TrimSpace(cookie) index := strings.Index(cookie, "=") if index == -1 { log.Warn().Str("cookie", cookie).Msg("cookie format invalid") continue } s.Request.Cookies[cookie[:index]] = cookie[index+1:] } } func (s *stepFromPostman) makeRequestBody(item *TItem) error { mode := item.Request.Body.Mode if mode == "" { return nil } switch mode { case enumBodyRaw: return s.makeRequestBodyRaw(item) case enumBodyFormData: return s.makeRequestBodyFormData(item) case enumBodyUrlEncoded: return s.makeRequestBodyUrlEncoded(item) case enumBodyFile, enumBodyGraphQL: return errors.Errorf("unsupported body type: %v", mode) } return nil } 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) } }() languageType := "text" iOptions := item.Request.Body.Options if iOptions != nil { iLanguage := iOptions.(map[string]interface{})["raw"] if iLanguage != nil { languageType = iLanguage.(map[string]interface{})["language"].(string) } } s.Request.Body = item.Request.Body.Raw contentType := s.Request.Headers["Content-Type"] if strings.Contains(contentType, "application/json") || languageType == "json" { var iBody interface{} err = json.Unmarshal([]byte(item.Request.Body.Raw), &iBody) if err != nil { return errors.Wrap(err, "make request body (raw -> json) failed") } s.Request.Body = iBody } if contentType == "" { s.Request.Headers["Content-Type"] = contentTypeMap[languageType] } return } func (s *stepFromPostman) makeRequestBodyFormData(item *TItem) error { s.Request.Upload = make(map[string]interface{}) for _, field := range item.Request.Body.FormData { if field.Disabled { continue } // form data could be text or file if field.Type == enumFieldTypeText { s.Request.Upload[field.Key] = field.Value } else if field.Type == enumFieldTypeFile { s.Request.Upload[field.Key] = field.Src } else { return errors.Errorf("make request body form data failed: unexpect field type: %v", field.Type) } } return nil } func (s *stepFromPostman) makeRequestBodyUrlEncoded(item *TItem) error { payloadMap := make(map[string]string) for _, field := range item.Request.Body.URLEncoded { if field.Disabled { continue } payloadMap[field.Key] = field.Value } s.Request.Body = payloadMap s.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded" return nil } // TODO makeValidate from test scripts func (s *stepFromPostman) makeValidate(item *TItem) error { return nil }