mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-11 10:00:23 +08:00
365 lines
8.4 KiB
Go
365 lines
8.4 KiB
Go
package postman2case
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"github.com/httprunner/httprunner/v4/hrp"
|
|
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
|
)
|
|
|
|
const (
|
|
enumBodyRaw = "raw"
|
|
enumBodyUrlEncoded = "urlencoded"
|
|
enumBodyFormData = "formdata"
|
|
enumBodyFile = "file"
|
|
enumBodyGraphQL = "graphql"
|
|
)
|
|
|
|
const (
|
|
enumFieldTypeText = "text"
|
|
enumFieldTypeFile = "file"
|
|
)
|
|
|
|
const (
|
|
suffixName = ".converted"
|
|
extensionJSON = ".json"
|
|
extensionYAML = ".yaml"
|
|
)
|
|
|
|
var contentTypeMap = map[string]string{
|
|
"text": "text/plain",
|
|
"javascript": "application/javascript",
|
|
"json": "application/json",
|
|
"html": "text/html",
|
|
"xml": "application/xml",
|
|
}
|
|
|
|
func NewCollection(path string) *collection {
|
|
return &collection{
|
|
path: path,
|
|
}
|
|
}
|
|
|
|
type collection struct {
|
|
path string
|
|
outputDir string
|
|
}
|
|
|
|
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) {
|
|
testCase, err := c.makeTestCase()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
jsonPath = c.genOutputPath(extensionJSON)
|
|
err = builtin.Dump2JSON(testCase, jsonPath)
|
|
return
|
|
}
|
|
|
|
func (c *collection) GenYAML() (yamlPath string, err error) {
|
|
testCase, err := c.makeTestCase()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
yamlPath = c.genOutputPath(extensionYAML)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func getFilenameWithoutExtension(path string) string {
|
|
base := filepath.Base(path)
|
|
ext := filepath.Ext(base)
|
|
return base[0:len(base)-len(ext)] + suffixName
|
|
}
|
|
|
|
func (c *collection) makeTestCase() (*hrp.TCase, error) {
|
|
tCollection, err := c.load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
teststeps, err := c.prepareTestSteps(tCollection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tCase := &hrp.TCase{
|
|
Config: c.prepareConfig(tCollection),
|
|
TestSteps: teststeps,
|
|
}
|
|
return tCase, nil
|
|
}
|
|
|
|
func (c *collection) load() (*TCollection, error) {
|
|
collection := &TCollection{}
|
|
err := builtin.LoadFile(c.path, collection)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "load postman collection failed")
|
|
}
|
|
return collection, nil
|
|
}
|
|
|
|
func (c *collection) prepareConfig(tCollection *TCollection) *hrp.TConfig {
|
|
return hrp.NewConfig(tCollection.Info.Name).
|
|
SetVerifySSL(false)
|
|
}
|
|
|
|
func (c *collection) prepareTestSteps(tCollection *TCollection) ([]*hrp.TStep, error) {
|
|
// recursively convert collection items into a list
|
|
var itemList []TItem
|
|
for _, item := range tCollection.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 *collection) prepareTestStep(item *TItem) (*hrp.TStep, error) {
|
|
log.Info().
|
|
Str("method", item.Request.Method).
|
|
Str("url", item.Request.URL.Raw).
|
|
Msg("convert teststep")
|
|
|
|
step := &tStep{
|
|
hrp.TStep{
|
|
Request: &hrp.Request{},
|
|
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.makeRequestHeadersAndCookies(item); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := step.makeRequestBody(item); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := step.makeValidate(item); err != nil {
|
|
return nil, err
|
|
}
|
|
return &step.TStep, nil
|
|
}
|
|
|
|
type tStep struct {
|
|
hrp.TStep
|
|
}
|
|
|
|
// makeRequestName indicates the step name the same as item name
|
|
func (s *tStep) makeRequestName(item *TItem) error {
|
|
s.Name = item.Name
|
|
return nil
|
|
}
|
|
|
|
func (s *tStep) makeRequestMethod(item *TItem) error {
|
|
s.Request.Method = hrp.HTTPMethod(item.Request.Method)
|
|
return nil
|
|
}
|
|
|
|
func (s *tStep) 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 *tStep) 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 *tStep) makeRequestHeadersAndCookies(item *TItem) error {
|
|
s.Request.Headers = make(map[string]string)
|
|
for _, field := range item.Request.Headers {
|
|
if field.Disabled {
|
|
continue
|
|
}
|
|
if strings.EqualFold(field.Key, "cookie") {
|
|
s.Request.Cookies[field.Key] = field.Value
|
|
continue
|
|
}
|
|
s.Request.Headers[field.Key] = field.Value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *tStep) 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.New("not supported body type")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *tStep) makeRequestBodyRaw(item *TItem) (err error) {
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
err = fmt.Errorf("make request body raw failed: %v", p)
|
|
}
|
|
}()
|
|
|
|
// extract language type
|
|
iOptions := item.Request.Body.Options
|
|
iLanguage := iOptions.(map[string]interface{})["raw"]
|
|
languageType := iLanguage.(map[string]interface{})["language"].(string)
|
|
|
|
// make request body and indicate Content-Type
|
|
rawBody := item.Request.Body.Raw
|
|
if languageType == "json" {
|
|
var iBody interface{}
|
|
err = json.Unmarshal([]byte(rawBody), &iBody)
|
|
if err != nil {
|
|
return errors.Wrap(err, "make request body raw failed")
|
|
}
|
|
s.Request.Body = iBody
|
|
} else {
|
|
s.Request.Body = rawBody
|
|
}
|
|
s.Request.Headers["Content-Type"] = contentTypeMap[languageType]
|
|
return
|
|
}
|
|
|
|
func (s *tStep) makeRequestBodyFormData(item *TItem) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = errors.Wrap(err, "make request body form-data failed")
|
|
}
|
|
}()
|
|
payload := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(payload)
|
|
for _, field := range item.Request.Body.FormData {
|
|
if field.Disabled {
|
|
continue
|
|
}
|
|
// form data could be text or file
|
|
if field.Type == enumFieldTypeText {
|
|
err = writer.WriteField(field.Key, field.Value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if field.Type == enumFieldTypeFile {
|
|
err = writeFormDataFile(writer, &field)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
err = writer.Close()
|
|
s.Request.Body = payload.String()
|
|
s.Request.Headers["Content-Type"] = writer.FormDataContentType()
|
|
return
|
|
}
|
|
|
|
func writeFormDataFile(writer *multipart.Writer, field *TField) error {
|
|
file, err := os.Open(field.Src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
formFile, err := writer.CreateFormFile(field.Key, filepath.Base(field.Src))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(formFile, file)
|
|
return err
|
|
}
|
|
|
|
func (s *tStep) 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 example response
|
|
func (s *tStep) makeValidate(item *TItem) error {
|
|
return nil
|
|
}
|