feat: add webhook bearer auth support (#619)
* feat: add webhook bearer auth support * add tpl func randFloat * refactor run webhook * add template func uptimeSeconds * add random enum with weight feature --------- Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
This commit is contained in:
parent
430d9127c6
commit
4b4ee3a60c
|
@ -7,16 +7,20 @@ items:
|
|||
response:
|
||||
header:
|
||||
server: mock
|
||||
content-type: application/json
|
||||
body: |
|
||||
{
|
||||
"count": 1,
|
||||
"items": [{
|
||||
"title": "fix: there is a bug on page {{ randEnum "one" }}",
|
||||
"number": 123,
|
||||
"title": "fix: there is a bug on page {{ randEnum "one" "two" "three" "four" }}",
|
||||
"number": {{randInt 100 199}},
|
||||
"float": {{randFloat 0.0 1.0}},
|
||||
"status": "{{randWeightEnum (weightObject 4 "open") (weightObject 1 "closed")}}",
|
||||
"message": "{{.Response.Header.server}}",
|
||||
"author": "someone",
|
||||
"status": "success"
|
||||
}]
|
||||
"author": "{{env "USER"}}",
|
||||
"created": "{{ now.Format "2006-01-02T15:04:05Z07:00" }}"
|
||||
}],
|
||||
"uptime": "{{uptime}}"
|
||||
}
|
||||
- name: base64
|
||||
request:
|
||||
|
|
|
@ -11,3 +11,27 @@ title = "用例模板"
|
|||
```
|
||||
182{{shuffle "09876543"}}
|
||||
```
|
||||
|
||||
## 带权重的随机枚举
|
||||
|
||||
下面的代码以 80% 的概率返回 `open`,以 20% 的概率返回 `closed`:
|
||||
|
||||
```
|
||||
{{randWeightEnum (weightObject 4 "open") (weightObject 1 "closed")}}
|
||||
```
|
||||
|
||||
## 时间
|
||||
|
||||
下面的代码可以生成当前时间,并制定时间格式:
|
||||
|
||||
```
|
||||
{{ now.Format "2006-01-02T15:04:05Z07:00" }}
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
下面的代码可以获取环境变量 `SHELL` 的值,在需要使用一个全局变量的时候,可以使用这个模板函数:
|
||||
|
||||
```
|
||||
{{ env "SHELL" }}
|
||||
```
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
package mock
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
@ -402,32 +403,8 @@ func (s *inMemoryServer) startWebhook(webhook *Webhook) (err error) {
|
|||
memLogger.Info("stop webhook server", "name", wh.Name)
|
||||
return
|
||||
case <-timer.C:
|
||||
client := http.DefaultClient
|
||||
|
||||
payload, err := render.RenderAsReader("mock webhook server payload", wh.Request.Body, wh)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when render payload")
|
||||
continue
|
||||
}
|
||||
|
||||
method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost)
|
||||
api, err := render.Render("webhook request api", wh.Request.Path, s)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when render api", "raw", wh.Request.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(s.ctx, method, api, payload)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when create request")
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when sending webhook")
|
||||
} else {
|
||||
memLogger.Info("received from webhook", "code", resp.StatusCode)
|
||||
if err = runWebhook(s.ctx, s, wh); err != nil {
|
||||
memLogger.Error(err, "Error when run webhook")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -435,6 +412,88 @@ func (s *inMemoryServer) startWebhook(webhook *Webhook) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func runWebhook(ctx context.Context, objCtx interface{}, wh *Webhook) (err error) {
|
||||
client := http.DefaultClient
|
||||
|
||||
var payload io.Reader
|
||||
payload, err = render.RenderAsReader("mock webhook server payload", wh.Request.Body, wh)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error when render payload: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost)
|
||||
var api string
|
||||
api, err = render.Render("webhook request api", wh.Request.Path, objCtx)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error when render api: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
var bearerToken string
|
||||
bearerToken, err = getBearerToken(ctx, wh.Request)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when render bearer token")
|
||||
return
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequestWithContext(ctx, method, api, payload)
|
||||
if err != nil {
|
||||
memLogger.Error(err, "Error when create request")
|
||||
return
|
||||
}
|
||||
|
||||
if bearerToken != "" {
|
||||
memLogger.V(7).Info("set bearer token", "token", bearerToken)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken))
|
||||
}
|
||||
|
||||
for k, v := range wh.Request.Header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error when sending webhook")
|
||||
} else {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
memLogger.V(7).Info("received from webhook", "code", resp.StatusCode, "response", string(data))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type bearerToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func getBearerToken(ctx context.Context, request RequestWithAuth) (token string, err error) {
|
||||
if request.BearerAPI == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if data, err = json.Marshal(&request); err == nil {
|
||||
client := http.DefaultClient
|
||||
var req *http.Request
|
||||
if req, err = http.NewRequestWithContext(ctx, http.MethodPost, request.BearerAPI, bytes.NewBuffer(data)); err == nil {
|
||||
req.Header.Set(util.ContentType, util.JSON)
|
||||
|
||||
var resp *http.Response
|
||||
if resp, err = client.Do(req); err == nil && resp.StatusCode == http.StatusOK {
|
||||
if data, err = io.ReadAll(resp.Body); err == nil {
|
||||
var tokenObj bearerToken
|
||||
if err = json.Unmarshal(data, &tokenObj); err == nil {
|
||||
token = tokenObj.Token
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *inMemoryServer) handleOpenAPI() {
|
||||
s.mux.HandleFunc("/api.json", func(w http.ResponseWriter, req *http.Request) {
|
||||
// Setup OpenAPI schema
|
||||
|
|
|
@ -55,7 +55,7 @@ proxies:
|
|||
- path: /v1/invalid-template
|
||||
target: http://localhost:{{.GetPort}
|
||||
webhooks:
|
||||
- timer: 1ms
|
||||
- timer: 1m
|
||||
name: baidu
|
||||
request:
|
||||
method: GET
|
||||
|
|
|
@ -35,6 +35,13 @@ type Request struct {
|
|||
Body string `yaml:"body" json:"body"`
|
||||
}
|
||||
|
||||
type RequestWithAuth struct {
|
||||
Request `yaml:",inline"`
|
||||
BearerAPI string `yaml:"bearerAPI" json:"bearerAPI"`
|
||||
Username string `yaml:"username" json:"username"`
|
||||
Password string `yaml:"password" json:"password"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Encoder string `yaml:"encoder" json:"encoder"`
|
||||
Body string `yaml:"body" json:"body"`
|
||||
|
@ -46,7 +53,7 @@ type Response struct {
|
|||
type Webhook struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Timer string `yaml:"timer" json:"timer"`
|
||||
Request Request `yaml:"request" json:"request"`
|
||||
Request RequestWithAuth `yaml:"request" json:"request"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
Copyright 2023-2025 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -30,6 +30,7 @@ import (
|
|||
mathrand "math/rand"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/linuxsuren/api-testing/pkg/version"
|
||||
|
||||
|
@ -179,18 +180,60 @@ var advancedFuncs = []AdvancedFunc{{
|
|||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
},
|
||||
}, {
|
||||
FuncName: "randFloat",
|
||||
Func: func(from float64, to float64) float64 {
|
||||
return mathrand.Float64()*(to-from) + from
|
||||
},
|
||||
}, {
|
||||
FuncName: "randEnum",
|
||||
Func: func(items ...string) string {
|
||||
return items[mathrand.Intn(len(items))]
|
||||
},
|
||||
}, {
|
||||
FuncName: "weightObject",
|
||||
Func: func(weight int, object interface{}) WeightEnum {
|
||||
return WeightEnum{
|
||||
Weight: weight,
|
||||
Object: object,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
FuncName: "randWeightEnum",
|
||||
Func: func(items ...WeightEnum) interface{} {
|
||||
var newItems []interface{}
|
||||
for _, item := range items {
|
||||
for j := 0; j < item.Weight; j++ {
|
||||
newItems = append(newItems, item.Object)
|
||||
}
|
||||
}
|
||||
return newItems[mathrand.Intn(len(newItems))]
|
||||
},
|
||||
}, {
|
||||
FuncName: "randEmail",
|
||||
Func: func() string {
|
||||
return fmt.Sprintf("%s@%s.com", util.String(3), util.String(3))
|
||||
},
|
||||
}, {
|
||||
FuncName: "uptime",
|
||||
Func: func() string {
|
||||
return time.Since(uptime).String()
|
||||
},
|
||||
}, {
|
||||
FuncName: "uptimeSeconds",
|
||||
Func: func() float64 {
|
||||
return time.Since(uptime).Seconds()
|
||||
},
|
||||
}}
|
||||
|
||||
// WeightEnum is a weight enum
|
||||
type WeightEnum struct {
|
||||
Weight int
|
||||
Object interface{}
|
||||
}
|
||||
|
||||
var uptime = time.Now()
|
||||
|
||||
// GetAdvancedFuncs returns all the advanced functions
|
||||
func GetAdvancedFuncs() []AdvancedFunc {
|
||||
return advancedFuncs
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2023-2024 API Testing Authors.
|
||||
Copyright 2023-2025 API Testing Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -81,12 +81,24 @@ func TestRender(t *testing.T) {
|
|||
verify: func(t *testing.T, s string) {
|
||||
assert.Equal(t, 20, len(s), s)
|
||||
},
|
||||
}, {
|
||||
name: "randFloat",
|
||||
text: `{{randFloat 1 2}}`,
|
||||
verify: func(t *testing.T, s string) {
|
||||
assert.NotEmpty(t, s)
|
||||
},
|
||||
}, {
|
||||
name: "randEnum",
|
||||
text: `{{randEnum "a" "b" "c"}}`,
|
||||
verify: func(t *testing.T, s string) {
|
||||
assert.Contains(t, []string{"a", "b", "c"}, s)
|
||||
},
|
||||
}, {
|
||||
name: "randWeightEnum",
|
||||
text: `{{randWeightEnum (weightObject 1 "a") (weightObject 2 "b") (weightObject 3 "c")}}`,
|
||||
verify: func(t *testing.T, s string) {
|
||||
assert.Contains(t, []string{"a", "b", "c"}, s)
|
||||
},
|
||||
}, {
|
||||
name: "randEmail",
|
||||
text: `{{randEmail}}`,
|
||||
|
@ -103,6 +115,18 @@ func TestRender(t *testing.T) {
|
|||
verify: func(t *testing.T, s string) {
|
||||
assert.Equal(t, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", s)
|
||||
},
|
||||
}, {
|
||||
name: "uptime",
|
||||
text: `{{uptime}}`,
|
||||
verify: func(t *testing.T, s string) {
|
||||
assert.NotEmpty(t, s)
|
||||
},
|
||||
}, {
|
||||
name: "uptimeSeconds",
|
||||
text: `{{uptimeSeconds}}`,
|
||||
verify: func(t *testing.T, s string) {
|
||||
assert.NotEmpty(t, s)
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
Loading…
Reference in New Issue