Files
httprunner/runner.go
2021-10-31 18:00:25 +08:00

361 lines
8.8 KiB
Go

package hrp
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/pkg/errors"
)
// run API test with default configs
func Run(t *testing.T, testcases ...ITestCase) error {
return NewRunner().WithTestingT(t).SetDebug(true).Run(testcases...)
}
func NewRunner() *Runner {
return &Runner{
t: &testing.T{},
debug: false, // default to turn off debug
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: 30 * time.Second,
},
}
}
type Runner struct {
t *testing.T
debug bool
client *http.Client
}
func (r *Runner) WithTestingT(t *testing.T) *Runner {
log.Info().Msg("[init] WithTestingT")
r.t = t
return r
}
func (r *Runner) SetDebug(debug bool) *Runner {
log.Info().Bool("debug", debug).Msg("[init] SetDebug")
r.debug = debug
return r
}
func (r *Runner) SetProxyUrl(proxyUrl string) *Runner {
log.Info().Str("proxyUrl", proxyUrl).Msg("[init] SetProxyUrl")
p, err := url.Parse(proxyUrl)
if err != nil {
log.Error().Err(err).Str("proxyUrl", proxyUrl).Msg("[init] invalid proxyUrl")
return r
}
r.client.Transport = &http.Transport{
Proxy: http.ProxyURL(p),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return r
}
func (r *Runner) Run(testcases ...ITestCase) error {
for _, iTestCase := range testcases {
testcase, err := iTestCase.ToTestCase()
if err != nil {
log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed")
return err
}
if err := r.runCase(testcase); err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
return err
}
}
return nil
}
func (r *Runner) runCase(testcase *TestCase) error {
config := &testcase.Config
if err := r.parseConfig(config); err != nil {
return err
}
log.Info().Str("testcase", config.Name).Msg("run testcase start")
extractedVariables := make(map[string]interface{})
for _, step := range testcase.TestSteps {
// override variables
// step variables > extracted variables from previous steps
stepVariables := mergeVariables(step.ToStruct().Variables, extractedVariables)
// step variables > testcase config variables
stepVariables = mergeVariables(stepVariables, config.Variables)
// parse step variables
parsedVariables, err := parseVariables(stepVariables)
if err != nil {
log.Error().Interface("variables", config.Variables).Err(err).Msg("parse step variables failed")
return err
}
step.ToStruct().Variables = parsedVariables
stepData, err := r.runStep(step, config)
if err != nil {
return err
}
// update extracted variables
for k, v := range stepData.ExportVars {
extractedVariables[k] = v
}
}
log.Info().Str("testcase", config.Name).Msg("run testcase end")
return nil
}
func (r *Runner) runStep(step IStep, config *TConfig) (stepData *StepData, err error) {
log.Info().Str("step", step.Name()).Msg("run step start")
if tc, ok := step.(*testcaseWithOptionalArgs); ok {
// run referenced testcase
log.Info().Str("testcase", tc.step.Name).Msg("run referenced testcase")
// TODO: override testcase config
stepData, err = r.runStepTestCase(tc.step)
if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed")
return
}
} else {
// run request
tStep := parseStep(step, config)
stepData, err = r.runStepRequest(tStep)
if err != nil {
log.Error().Err(err).Msg("run request step failed")
return
}
}
log.Info().
Str("step", step.Name()).
Bool("success", stepData.Success).
Interface("exportVars", stepData.ExportVars).
Msg("run step end")
return
}
func (r *Runner) runStepRequest(step *TStep) (stepData *StepData, err error) {
stepData = &StepData{
Name: step.Name,
Success: false,
ResponseLength: 0,
}
rawUrl := step.Request.URL
method := step.Request.Method
req := &http.Request{
Method: string(method),
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
}
// prepare request headers
if len(step.Request.Headers) > 0 {
headers, err := parseHeaders(step.Request.Headers, step.Variables)
if err != nil {
return nil, errors.Wrap(err, "parse headers failed")
}
for key, value := range headers {
req.Header.Add(key, value)
}
}
if length := req.Header.Get("Content-Length"); length != "" {
if l, err := strconv.ParseInt(length, 10, 64); err == nil {
req.ContentLength = l
}
}
// prepare request params
var queryParams url.Values
if len(step.Request.Params) > 0 {
params, err := parseData(step.Request.Params, step.Variables)
if err != nil {
return nil, errors.Wrap(err, "parse data failed")
}
parsedParams := params.(map[string]interface{})
if len(parsedParams) > 0 {
queryParams = make(url.Values)
for k, v := range parsedParams {
queryParams.Add(k, fmt.Sprint(v))
}
}
}
if queryParams != nil {
// append params to url
paramStr := queryParams.Encode()
if strings.IndexByte(rawUrl, '?') == -1 {
rawUrl = rawUrl + "?" + paramStr
} else {
rawUrl = rawUrl + "&" + paramStr
}
}
// prepare request cookies
for cookieName, cookieValue := range step.Request.Cookies {
req.AddCookie(&http.Cookie{
Name: cookieName,
Value: cookieValue,
})
}
// prepare request body
if step.Request.Body != nil {
data, err := parseData(step.Request.Body, step.Variables)
if err != nil {
return nil, err
}
var dataBytes []byte
switch vv := data.(type) {
case map[string]interface{}:
contentType := req.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
// post form data
formData := make(url.Values)
for k, v := range vv {
formData.Add(k, fmt.Sprint(v))
}
dataBytes = []byte(formData.Encode())
} else {
// post json
dataBytes, err = json.Marshal(vv)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
}
case string:
dataBytes = []byte(vv)
case []byte:
dataBytes = vv
case bytes.Buffer:
dataBytes = vv.Bytes()
default: // unexpected body type
return nil, errors.New("unexpected request body type")
}
setBodyBytes(req, dataBytes)
}
// prepare url
u, err := url.Parse(rawUrl)
if err != nil {
return nil, errors.Wrap(err, "parse url failed")
}
req.URL = u
req.Host = u.Host
// log & print request
if r.debug {
reqDump, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, errors.Wrap(err, "dump request failed")
}
fmt.Println("-------------------- request --------------------")
fmt.Println(string(reqDump))
}
// do request action
resp, err := r.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "do request failed")
}
defer resp.Body.Close()
// log & print response
if r.debug {
fmt.Println("==================== response ===================")
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, errors.Wrap(err, "dump response failed")
}
fmt.Println(string(respDump))
fmt.Println("--------------------------------------------------")
}
// new response object
respObj, err := NewResponseObject(r.t, resp)
if err != nil {
err = errors.Wrap(err, "init ResponseObject error")
return
}
// extract variables from response
extractors := step.Extract
extractMapping := respObj.Extract(extractors)
stepData.ExportVars = extractMapping
// override step variables with extracted variables
stepVariables := mergeVariables(step.Variables, extractMapping)
// validate response
err = respObj.Validate(step.Validators, stepVariables)
if err != nil {
return
}
stepData.Success = true
stepData.ResponseLength = resp.ContentLength
return
}
func (r *Runner) runStepTestCase(step *TStep) (stepData *StepData, err error) {
stepData = &StepData{
Name: step.Name,
Success: false,
}
testcase := step.TestCase
err = r.runCase(testcase)
return
}
func (r *Runner) parseConfig(config *TConfig) error {
// parse config variables
parsedVariables, err := parseVariables(config.Variables)
if err != nil {
log.Error().Interface("variables", config.Variables).Err(err).Msg("parse config variables failed")
return err
}
config.Variables = parsedVariables
// parse config name
parsedName, err := parseString(config.Name, config.Variables)
if err != nil {
return err
}
config.Name = convertString(parsedName)
// parse config base url
parsedBaseURL, err := parseString(config.BaseURL, config.Variables)
if err != nil {
return err
}
config.BaseURL = convertString(parsedBaseURL)
return nil
}
func (r *Runner) GetSummary() *TestCaseSummary {
return &TestCaseSummary{}
}
func setBodyBytes(req *http.Request, data []byte) {
req.Body = ioutil.NopCloser(bytes.NewReader(data))
req.ContentLength = int64(len(data))
}