diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..77c6ed97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "debugtalk" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9e..186ade81 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2021 debugtalk Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/boomer.go b/boomer.go new file mode 100644 index 00000000..5e7cd8ba --- /dev/null +++ b/boomer.go @@ -0,0 +1,44 @@ +package httpboomer + +import ( + "time" + + "github.com/myzhan/boomer" +) + +func HttpBoomer() *Boomer { + return &Boomer{} +} + +type Boomer struct { +} + +func (b *Boomer) Run(testcases ...*TestCase) { + var taskSlice []*boomer.Task + for _, testcase := range testcases { + task := convertBoomerTask(testcase) + taskSlice = append(taskSlice, task) + } + boomer.Run(taskSlice...) +} + +func convertBoomerTask(testcase *TestCase) *boomer.Task { + return &boomer.Task{ + Name: testcase.Config.Name, + Weight: testcase.Config.Weight, + Fn: func() { + for _, step := range testcase.TestSteps { + start := time.Now() + err := step.Run() + elapsed := time.Since(start).Nanoseconds() / int64(time.Millisecond) + + tStep := step.ToStruct() + if err == nil { + boomer.RecordSuccess(string(tStep.Request.Method), tStep.Name, elapsed, int64(0)) + } else { + boomer.RecordFailure(string(tStep.Request.Method), tStep.Name, elapsed, err.Error()) + } + } + }, + } +} diff --git a/boomer_test.go b/boomer_test.go new file mode 100644 index 00000000..6f0a3066 --- /dev/null +++ b/boomer_test.go @@ -0,0 +1,22 @@ +package httpboomer + +import ( + "testing" +) + +func TestHttpBoomer(t *testing.T) { + testcase1 := &TestCase{ + Config: TConfig{ + Name: "TestCase1", + Weight: 2, + }, + } + testcase2 := &TestCase{ + Config: TConfig{ + Name: "TestCase2", + Weight: 3, + }, + } + + HttpBoomer().Run(testcase1, testcase2) +} diff --git a/extract.go b/extract.go new file mode 100644 index 00000000..a35cc765 --- /dev/null +++ b/extract.go @@ -0,0 +1,25 @@ +package httpboomer + +// implements IStep interface +type StepRequestExtraction struct { + *TStep +} + +func (req *StepRequestExtraction) WithJmesPath(jmesPath string, varName string) *StepRequestExtraction { + req.TStep.Extract[varName] = jmesPath + return req +} + +func (req *StepRequestExtraction) Validate() *StepRequestValidation { + return &StepRequestValidation{ + TStep: req.TStep, + } +} + +func (req *StepRequestExtraction) ToStruct() *TStep { + return req.TStep +} + +func (req *StepRequestExtraction) Run() error { + return req.TStep.Run() +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..e39aff3f --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/httprunner/httpboomer + +go 1.16 + +require ( + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/myzhan/boomer v1.6.0 + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/shirou/gopsutil v3.21.8+incompatible // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.9 // indirect + github.com/ugorji/go v1.2.6 // indirect + github.com/zeromq/goczmq v4.1.0+incompatible // indirect + github.com/zeromq/gomq v0.0.0-20201031135124-cef4e507bb8e // indirect + github.com/zeromq/gomq/zmtp v0.0.0-20201031135124-cef4e507bb8e // indirect + golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..4943d537 --- /dev/null +++ b/go.sum @@ -0,0 +1,44 @@ +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= +github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/myzhan/boomer v1.6.0 h1:xjgvmhDjgU9IEKnB7nU1HyoVEfj8SuuU3u6oY3Nugj0= +github.com/myzhan/boomer v1.6.0/go.mod h1:Ma68Td5C5EAc1M9XA7yjC/tXg9u5qviNujytnX099ZQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shirou/gopsutil v3.21.8+incompatible h1:sh0foI8tMRlCidUJR+KzqWYWxrkuuPIGiO6Vp+KXdCU= +github.com/shirou/gopsutil v3.21.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= +github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= +github.com/zeromq/goczmq v4.1.0+incompatible h1:cGVQaU6kIwwrGso0Pgbl84tzAz/h7FJ3wYQjSonjFFc= +github.com/zeromq/goczmq v4.1.0+incompatible/go.mod h1:1uZybAJoSRCvZMH2rZxEwWBSmC4T7CB/xQOfChwPEzg= +github.com/zeromq/gomq v0.0.0-20201031135124-cef4e507bb8e h1:vGjfCnWv/zWeO1ivv4+OUPgTzG/WV1iGfZwVdtUpLkM= +github.com/zeromq/gomq v0.0.0-20201031135124-cef4e507bb8e/go.mod h1:SkCxcSQ7BQEA9FvDzbj+3hV6EMhSywyxWnHwUXVIyLY= +github.com/zeromq/gomq/zmtp v0.0.0-20201031135124-cef4e507bb8e h1:pjp04/sSr2TYuaPdt+u6Cc1M38Aocp+3er0akr3auFg= +github.com/zeromq/gomq/zmtp v0.0.0-20201031135124-cef4e507bb8e/go.mod h1:LBjWEodY/ESvKRwLw3bc7mhn49oiI8qlXUqeqLn0pcU= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/models.go b/models.go new file mode 100644 index 00000000..e714e49e --- /dev/null +++ b/models.go @@ -0,0 +1,72 @@ +package httpboomer + +type Variables map[string]interface{} +type Params map[string]interface{} +type Headers map[string]string +type Cookies map[string]string + +type enumHTTPMethod string + +const ( + GET enumHTTPMethod = "GET" + HEAD enumHTTPMethod = "HEAD" + POST enumHTTPMethod = "POST" + PUT enumHTTPMethod = "PUT" + DELETE enumHTTPMethod = "DELETE" + OPTIONS enumHTTPMethod = "OPTIONS" + PATCH enumHTTPMethod = "PATCH" +) + +type TConfig struct { + Name string `json:"name"` + Verify bool `json:"verify"` + BaseURL string `json:"base_url"` + Variables Variables `json:"variables"` + Parameters Variables `json:"parameters"` + Export []string `json:"export"` + Weight int `json:"weight"` +} + +type TRequest struct { + Method enumHTTPMethod `json:"method"` + URL string `json:"url"` + Params Params `json:"params"` + Headers Headers `json:"headers"` + Cookies Cookies `json:"cookies"` + Data interface{} `json:"data"` + JSON interface{} `json:"json"` + Timeout float32 `json:"timeout"` + AllowRedirects bool `json:"allow_redirects"` + Verify bool `json:"verify"` +} + +type TValidator struct { + Check string // get value with jmespath + Comparator string + Expect interface{} + Message string +} + +type TStep struct { + Name string `json:"name"` + Request TRequest `json:"request"` + Variables Variables `json:"variables"` + SetupHooks []string `json:"setup_hooks"` + TeardownHooks []string `json:"teardown_hooks"` + Extract map[string]string `json:"extract"` + Validators []TValidator `json:"validators"` + Export []string `json:"export"` +} + +// interface for all types of steps +type IStep interface { + ToStruct() *TStep + Run() error +} + +type TestCase struct { + Config TConfig `json:"config"` + TestSteps []IStep `json:"teststeps"` +} + +type TestCaseSummary struct{} diff --git a/request.go b/request.go new file mode 100644 index 00000000..b7366396 --- /dev/null +++ b/request.go @@ -0,0 +1,148 @@ +package httpboomer + +func RunRequest(name string) *Request { + return &Request{ + TStep: &TStep{ + Name: name, + Variables: make(Variables), + }, + } +} + +type Request struct { + *TStep +} + +func (req *Request) WithVariables(variables Variables) *Request { + req.TStep.Variables = variables + return req +} + +func (req *Request) GET(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = GET + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) HEAD(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = HEAD + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) POST(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = POST + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) PUT(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = PUT + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) DELETE(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = DELETE + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) OPTIONS(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = OPTIONS + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) PATCH(url string) *RequestWithOptionalArgs { + req.TStep.Request.Method = PATCH + req.TStep.Request.URL = url + return &RequestWithOptionalArgs{ + TStep: req.TStep, + } +} + +func (req *Request) Run() error { + return req.TStep.Run() +} + +// implements IStep interface +type RequestWithOptionalArgs struct { + *TStep +} + +func (req *RequestWithOptionalArgs) SetVerify(verify bool) *RequestWithOptionalArgs { + req.TStep.Request.Verify = verify + return req +} + +func (req *RequestWithOptionalArgs) SetTimeout(timeout float32) *RequestWithOptionalArgs { + req.TStep.Request.Timeout = timeout + return req +} + +func (req *RequestWithOptionalArgs) SetProxies(proxies map[string]string) *RequestWithOptionalArgs { + // TODO + return req +} + +func (req *RequestWithOptionalArgs) SetAllowRedirects(allowRedirects bool) *RequestWithOptionalArgs { + req.TStep.Request.AllowRedirects = allowRedirects + return req +} + +func (req *RequestWithOptionalArgs) SetAuth(auth map[string]string) *RequestWithOptionalArgs { + // TODO + return req +} + +func (req *RequestWithOptionalArgs) WithParams(params Params) *RequestWithOptionalArgs { + req.TStep.Request.Params = params + return req +} + +func (req *RequestWithOptionalArgs) WithHeaders(headers Headers) *RequestWithOptionalArgs { + req.TStep.Request.Headers = headers + return req +} + +func (req *RequestWithOptionalArgs) WithCookies(cookies Cookies) *RequestWithOptionalArgs { + req.TStep.Request.Cookies = cookies + return req +} + +func (req *RequestWithOptionalArgs) WithData(data interface{}) *RequestWithOptionalArgs { + req.TStep.Request.Data = data + return req +} + +func (req *RequestWithOptionalArgs) WithJSON(json interface{}) *RequestWithOptionalArgs { + req.TStep.Request.JSON = json + return req +} + +func (req *RequestWithOptionalArgs) Validate() *StepRequestValidation { + return &StepRequestValidation{ + TStep: req.TStep, + } +} + +func (req *RequestWithOptionalArgs) ToStruct() *TStep { + return req.TStep +} + +func (req *RequestWithOptionalArgs) Run() error { + return req.TStep.Run() +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 00000000..ff2eb5cb --- /dev/null +++ b/request_test.go @@ -0,0 +1,79 @@ +package httpboomer + +import ( + "testing" +) + +var ( + tStepGET = RunRequest("get with params"). + GET("/get"). + WithParams(Params{"foo1": "bar1", "foo2": "bar2"}). + WithHeaders(Headers{"User-Agent": "HttpBoomer"}). + WithCookies(Cookies{"user": "debugtalk"}). + Validate(). + AssertEqual("status_code", 200, "check status code") + tStepPOSTData = RunRequest("post form data"). + POST("/post"). + WithParams(Params{"foo1": "bar1", "foo2": "bar2"}). + WithHeaders(Headers{"User-Agent": "HttpBoomer", "Content-Type": "application/x-www-form-urlencoded"}). + WithData("a=1&b=2"). + WithCookies(Cookies{"user": "debugtalk"}). + Validate(). + AssertEqual("status_code", 200, "check status code") +) + +func TestRunRequestGetToStruct(t *testing.T) { + tStep := tStepGET.ToStruct() + if tStep.Request.Method != GET { + t.Fatalf("tStep.Request.Method != GET") + } + if tStep.Request.URL != "/get" { + t.Fatalf("tStep.Request.URL != '/get'") + } + if tStep.Request.Params["foo1"] != "bar1" || tStep.Request.Params["foo2"] != "bar2" { + t.Fatalf("tStep.Request.Params mismatch") + } + if tStep.Request.Headers["User-Agent"] != "HttpBoomer" { + t.Fatalf("tStep.Request.Headers mismatch") + } + if tStep.Request.Cookies["user"] != "debugtalk" { + t.Fatalf("tStep.Request.Cookies mismatch") + } + if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 { + t.Fatalf("tStep.Validators mismatch") + } +} + +func TestRunRequestPostDataToStruct(t *testing.T) { + tStep := tStepPOSTData.ToStruct() + if tStep.Request.Method != POST { + t.Fatalf("tStep.Request.Method != POST") + } + if tStep.Request.URL != "/post" { + t.Fatalf("tStep.Request.URL != '/post'") + } + if tStep.Request.Params["foo1"] != "bar1" || tStep.Request.Params["foo2"] != "bar2" { + t.Fatalf("tStep.Request.Params mismatch") + } + if tStep.Request.Headers["User-Agent"] != "HttpBoomer" { + t.Fatalf("tStep.Request.Headers mismatch") + } + if tStep.Request.Cookies["user"] != "debugtalk" { + t.Fatalf("tStep.Request.Cookies mismatch") + } + if tStep.Request.Data != "a=1&b=2" { + t.Fatalf("tStep.Request.Data mismatch") + } + if tStep.Validators[0].Check != "status_code" || tStep.Validators[0].Expect != 200 { + t.Fatalf("tStep.Validators mismatch") + } +} + +func TestRunRequestRun(t *testing.T) { + if err := tStepGET.Run(); err != nil { + t.Fatalf("tStep.Run() error: %s", err) + } + if err := tStepPOSTData.Run(); err != nil { + t.Fatalf("tStepPOSTData.Run() error: %s", err) + } +} diff --git a/response.go b/response.go new file mode 100644 index 00000000..eb187cb3 --- /dev/null +++ b/response.go @@ -0,0 +1 @@ +package httpboomer diff --git a/runner.go b/runner.go new file mode 100644 index 00000000..eba8ccf6 --- /dev/null +++ b/runner.go @@ -0,0 +1,34 @@ +package httpboomer + +func HttpRunner() *Runner { + return &Runner{} +} + +type Runner struct { +} + +func (r *Runner) Run(testcases ...*TestCase) error { + for _, testcase := range testcases { + if err := r.runCase(testcase); err != nil { + return err + } + } + return nil +} + +func (r *Runner) runCase(testcase *TestCase) error { + for _, step := range testcase.TestSteps { + if err := r.runStep(step); err != nil { + return err + } + } + return nil +} + +func (r *Runner) runStep(req IStep) error { + return req.Run() +} + +func (r *Runner) GetSummary() *TestCaseSummary { + return &TestCaseSummary{} +} diff --git a/runner_test.go b/runner_test.go new file mode 100644 index 00000000..57220114 --- /dev/null +++ b/runner_test.go @@ -0,0 +1,37 @@ +package httpboomer + +import ( + "testing" +) + +func TestHttpRunner(t *testing.T) { + testcase1 := &TestCase{ + Config: TConfig{ + Name: "TestCase1", + BaseURL: "http://httpbin.org", + }, + TestSteps: []IStep{ + RunRequest("headers"). + GET("/headers"). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertEqual("headers.Host", "httpbin.org", "check http response host"), + RunRequest("user-agent"). + GET("/user-agent"). + Validate(). + AssertEqual("status_code", 200, "check status code"). + AssertEqual("body.\"user-agent\"", "python-requests", "check User-Agent"), + }, + } + testcase2 := &TestCase{ + Config: TConfig{ + Name: "TestCase2", + Weight: 3, + }, + } + + err := HttpRunner().Run(testcase1, testcase2) + if err != nil { + t.Fatalf("run testcase error: %v", err) + } +} diff --git a/step.go b/step.go new file mode 100644 index 00000000..7d9aa793 --- /dev/null +++ b/step.go @@ -0,0 +1,5 @@ +package httpboomer + +func (step *TStep) Run() error { + return nil +} diff --git a/validate.go b/validate.go new file mode 100644 index 00000000..a415f231 --- /dev/null +++ b/validate.go @@ -0,0 +1,25 @@ +package httpboomer + +// implements IStep interface +type StepRequestValidation struct { + *TStep +} + +func (req *StepRequestValidation) AssertEqual(jmesPath string, expected interface{}, msg string) *StepRequestValidation { + validator := TValidator{ + Check: jmesPath, + Comparator: "equals", + Expect: expected, + Message: msg, + } + req.TStep.Validators = append(req.TStep.Validators, validator) + return req +} + +func (req *StepRequestValidation) ToStruct() *TStep { + return req.TStep +} + +func (req *StepRequestValidation) Run() error { + return req.TStep.Run() +}