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:
Rick 2025-02-15 14:54:17 +08:00 committed by GitHub
parent 430d9127c6
commit 4b4ee3a60c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 559 additions and 398 deletions

View File

@ -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:

View File

@ -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" }}
```

View File

@ -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

View File

@ -55,7 +55,7 @@ proxies:
- path: /v1/invalid-template
target: http://localhost:{{.GetPort}
webhooks:
- timer: 1ms
- timer: 1m
name: baidu
request:
method: GET

View File

@ -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 {

View File

@ -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

View File

@ -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) {