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: response:
header: header:
server: mock server: mock
content-type: application/json
body: | body: |
{ {
"count": 1, "count": 1,
"items": [{ "items": [{
"title": "fix: there is a bug on page {{ randEnum "one" }}", "title": "fix: there is a bug on page {{ randEnum "one" "two" "three" "four" }}",
"number": 123, "number": {{randInt 100 199}},
"float": {{randFloat 0.0 1.0}},
"status": "{{randWeightEnum (weightObject 4 "open") (weightObject 1 "closed")}}",
"message": "{{.Response.Header.server}}", "message": "{{.Response.Header.server}}",
"author": "someone", "author": "{{env "USER"}}",
"status": "success" "created": "{{ now.Format "2006-01-02T15:04:05Z07:00" }}"
}] }],
"uptime": "{{uptime}}"
} }
- name: base64 - name: base64
request: request:

View File

@ -10,4 +10,28 @@ title = "用例模板"
``` ```
182{{shuffle "09876543"}} 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,470 +16,529 @@ limitations under the License.
package mock package mock
import ( import (
"context" "bytes"
"encoding/base64" "context"
"encoding/json" "encoding/base64"
"fmt" "encoding/json"
"io" "fmt"
"net" "io"
"net/http" "net"
"strings" "net/http"
"sync" "strings"
"time" "sync"
"time"
"github.com/swaggest/openapi-go/openapi3" "github.com/swaggest/openapi-go/openapi3"
"github.com/swaggest/rest/gorillamux" "github.com/swaggest/rest/gorillamux"
"github.com/linuxsuren/api-testing/pkg/version" "github.com/linuxsuren/api-testing/pkg/version"
"github.com/linuxsuren/api-testing/pkg/logging" "github.com/linuxsuren/api-testing/pkg/logging"
"github.com/linuxsuren/api-testing/pkg/render" "github.com/linuxsuren/api-testing/pkg/render"
"github.com/linuxsuren/api-testing/pkg/util" "github.com/linuxsuren/api-testing/pkg/util"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
var ( var (
memLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("memory") memLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("memory")
) )
type inMemoryServer struct { type inMemoryServer struct {
data map[string][]map[string]interface{} data map[string][]map[string]interface{}
mux *mux.Router mux *mux.Router
listener net.Listener listener net.Listener
port int port int
prefix string prefix string
wg sync.WaitGroup wg sync.WaitGroup
ctx context.Context ctx context.Context
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
reader Reader reader Reader
metrics RequestMetrics metrics RequestMetrics
} }
func NewInMemoryServer(ctx context.Context, port int) DynamicServer { func NewInMemoryServer(ctx context.Context, port int) DynamicServer {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
return &inMemoryServer{ return &inMemoryServer{
port: port, port: port,
wg: sync.WaitGroup{}, wg: sync.WaitGroup{},
ctx: ctx, ctx: ctx,
cancelFunc: cancel, cancelFunc: cancel,
metrics: NewNoopMetrics(), metrics: NewNoopMetrics(),
} }
} }
func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler http.Handler, err error) { func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler http.Handler, err error) {
s.reader = reader s.reader = reader
// init the data // init the data
s.data = make(map[string][]map[string]interface{}) s.data = make(map[string][]map[string]interface{})
s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter() s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter()
s.prefix = prefix s.prefix = prefix
handler = s.mux handler = s.mux
s.metrics.AddMetricsHandler(s.mux) s.metrics.AddMetricsHandler(s.mux)
err = s.Load() err = s.Load()
return return
} }
func (s *inMemoryServer) Load() (err error) { func (s *inMemoryServer) Load() (err error) {
var server *Server var server *Server
if server, err = s.reader.Parse(); err != nil { if server, err = s.reader.Parse(); err != nil {
return return
} }
memLogger.Info("start to run all the APIs from objects", "count", len(server.Objects)) memLogger.Info("start to run all the APIs from objects", "count", len(server.Objects))
for _, obj := range server.Objects { for _, obj := range server.Objects {
memLogger.Info("start mock server from object", "name", obj.Name) memLogger.Info("start mock server from object", "name", obj.Name)
s.startObject(obj) s.startObject(obj)
s.initObjectData(obj) s.initObjectData(obj)
} }
memLogger.Info("start to run all the APIs from items", "count", len(server.Items)) memLogger.Info("start to run all the APIs from items", "count", len(server.Items))
for _, item := range server.Items { for _, item := range server.Items {
s.startItem(item) s.startItem(item)
} }
memLogger.Info("start webhook servers", "count", len(server.Webhooks)) memLogger.Info("start webhook servers", "count", len(server.Webhooks))
for _, item := range server.Webhooks { for _, item := range server.Webhooks {
if err = s.startWebhook(&item); err != nil { if err = s.startWebhook(&item); err != nil {
return return
} }
} }
s.handleOpenAPI() s.handleOpenAPI()
for _, proxy := range server.Proxies { for _, proxy := range server.Proxies {
memLogger.Info("start to proxy", "target", proxy.Target) memLogger.Info("start to proxy", "target", proxy.Target)
s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) { s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) {
api := fmt.Sprintf("%s/%s", proxy.Target, strings.TrimPrefix(req.URL.Path, s.prefix)) api := fmt.Sprintf("%s/%s", proxy.Target, strings.TrimPrefix(req.URL.Path, s.prefix))
api, err = render.Render("proxy api", api, s) api, err = render.Render("proxy api", api, s)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to render proxy api") memLogger.Error(err, "failed to render proxy api")
return return
} }
memLogger.Info("redirect to", "target", api) memLogger.Info("redirect to", "target", api)
targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body) targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to create proxy request") memLogger.Error(err, "failed to create proxy request")
return return
} }
resp, err := http.DefaultClient.Do(targetReq) resp, err := http.DefaultClient.Do(targetReq)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to do proxy request") memLogger.Error(err, "failed to do proxy request")
return return
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
memLogger.Error(err, "failed to read response body") memLogger.Error(err, "failed to read response body")
return return
} }
for k, v := range resp.Header { for k, v := range resp.Header {
w.Header().Add(k, v[0]) w.Header().Add(k, v[0])
} }
w.Write(data) w.Write(data)
}) })
} }
return return
} }
func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) { func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) {
var handler http.Handler var handler http.Handler
if handler, err = s.SetupHandler(reader, prefix); err == nil { if handler, err = s.SetupHandler(reader, prefix); err == nil {
if s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)); err == nil { if s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)); err == nil {
go func() { go func() {
err = http.Serve(s.listener, handler) err = http.Serve(s.listener, handler)
}() }()
} }
} }
return return
} }
func (s *inMemoryServer) EnableMetrics() { func (s *inMemoryServer) EnableMetrics() {
s.metrics = NewInMemoryMetrics() s.metrics = NewInMemoryMetrics()
} }
func (s *inMemoryServer) startObject(obj Object) { func (s *inMemoryServer) startObject(obj Object) {
// create a simple CRUD server // create a simple CRUD server
s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) { s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) {
fmt.Println("mock server received request", req.URL.Path) fmt.Println("mock server received request", req.URL.Path)
s.metrics.RecordRequest(req.URL.Path) s.metrics.RecordRequest(req.URL.Path)
method := req.Method method := req.Method
w.Header().Set(util.ContentType, util.JSON) w.Header().Set(util.ContentType, util.JSON)
switch method { switch method {
case http.MethodGet: case http.MethodGet:
// list all items // list all items
allItems := s.data[obj.Name] allItems := s.data[obj.Name]
filteredItems := make([]map[string]interface{}, 0) filteredItems := make([]map[string]interface{}, 0)
for i, item := range allItems { for i, item := range allItems {
exclude := false exclude := false
for k, v := range req.URL.Query() { for k, v := range req.URL.Query() {
if len(v) == 0 { if len(v) == 0 {
continue continue
} }
if val, ok := item[k]; ok && val != v[0] { if val, ok := item[k]; ok && val != v[0] {
exclude = true exclude = true
break break
} }
} }
if !exclude { if !exclude {
filteredItems = append(filteredItems, allItems[i]) filteredItems = append(filteredItems, allItems[i])
} }
} }
if len(filteredItems) != len(allItems) { if len(filteredItems) != len(allItems) {
allItems = filteredItems allItems = filteredItems
} }
data, err := json.Marshal(allItems) data, err := json.Marshal(allItems)
writeResponse(w, data, err) writeResponse(w, data, err)
case http.MethodPost: case http.MethodPost:
// create an item // create an item
if data, err := io.ReadAll(req.Body); err == nil { if data, err := io.ReadAll(req.Body); err == nil {
objData := map[string]interface{}{} objData := map[string]interface{}{}
jsonErr := json.Unmarshal(data, &objData) jsonErr := json.Unmarshal(data, &objData)
if jsonErr != nil { if jsonErr != nil {
memLogger.Info(jsonErr.Error()) memLogger.Info(jsonErr.Error())
return return
} }
s.data[obj.Name] = append(s.data[obj.Name], objData) s.data[obj.Name] = append(s.data[obj.Name], objData)
_, _ = w.Write(data) _, _ = w.Write(data)
} else { } else {
memLogger.Info("failed to read from body", "error", err) memLogger.Info("failed to read from body", "error", err)
} }
default: default:
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
} }
}) })
// handle a single object // handle a single object
s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) { s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) {
s.metrics.RecordRequest(req.URL.Path) s.metrics.RecordRequest(req.URL.Path)
w.Header().Set(util.ContentType, util.JSON) w.Header().Set(util.ContentType, util.JSON)
objects := s.data[obj.Name] objects := s.data[obj.Name]
if objects != nil { if objects != nil {
name := mux.Vars(req)["name"] name := mux.Vars(req)["name"]
var data []byte var data []byte
for _, obj := range objects { for _, obj := range objects {
if obj["name"] == name { if obj["name"] == name {
data, _ = json.Marshal(obj) data, _ = json.Marshal(obj)
break break
} }
} }
if len(data) == 0 { if len(data) == 0 {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
method := req.Method method := req.Method
switch method { switch method {
case http.MethodGet: case http.MethodGet:
writeResponse(w, data, nil) writeResponse(w, data, nil)
case http.MethodPut: case http.MethodPut:
objData := map[string]interface{}{} objData := map[string]interface{}{}
if data, err := io.ReadAll(req.Body); err == nil { if data, err := io.ReadAll(req.Body); err == nil {
jsonErr := json.Unmarshal(data, &objData) jsonErr := json.Unmarshal(data, &objData)
if jsonErr != nil { if jsonErr != nil {
memLogger.Info(jsonErr.Error()) memLogger.Info(jsonErr.Error())
return return
} }
for i, item := range s.data[obj.Name] { for i, item := range s.data[obj.Name] {
if item["name"] == name { if item["name"] == name {
s.data[obj.Name][i] = objData s.data[obj.Name][i] = objData
break break
} }
} }
_, _ = w.Write(data) _, _ = w.Write(data)
} }
case http.MethodDelete: case http.MethodDelete:
for i, item := range s.data[obj.Name] { for i, item := range s.data[obj.Name] {
if item["name"] == name { if item["name"] == name {
if len(s.data[obj.Name]) == i+1 { if len(s.data[obj.Name]) == i+1 {
s.data[obj.Name] = s.data[obj.Name][:i] s.data[obj.Name] = s.data[obj.Name][:i]
} else { } else {
s.data[obj.Name] = append(s.data[obj.Name][:i], s.data[obj.Name][i+1]) s.data[obj.Name] = append(s.data[obj.Name][:i], s.data[obj.Name][i+1])
} }
writeResponse(w, []byte(`{"msg": "deleted"}`), nil) writeResponse(w, []byte(`{"msg": "deleted"}`), nil)
} }
} }
default: default:
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
} }
} }
}) })
} }
func (s *inMemoryServer) startItem(item Item) { func (s *inMemoryServer) startItem(item Item) {
method := util.EmptyThenDefault(item.Request.Method, http.MethodGet) method := util.EmptyThenDefault(item.Request.Method, http.MethodGet)
memLogger.Info("register mock service", "method", method, "path", item.Request.Path, "encoder", item.Response.Encoder) memLogger.Info("register mock service", "method", method, "path", item.Request.Path, "encoder", item.Response.Encoder)
var headerSlices []string var headerSlices []string
for k, v := range item.Request.Header { for k, v := range item.Request.Header {
headerSlices = append(headerSlices, k, v) headerSlices = append(headerSlices, k, v)
} }
adHandler := &advanceHandler{item: &item, metrics: s.metrics} adHandler := &advanceHandler{item: &item, metrics: s.metrics}
s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...) s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...)
} }
type advanceHandler struct { type advanceHandler struct {
item *Item item *Item
metrics RequestMetrics metrics RequestMetrics
} }
func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) { func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) {
h.metrics.RecordRequest(req.URL.Path) h.metrics.RecordRequest(req.URL.Path)
memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path, memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path,
"encoder", h.item.Response.Encoder) "encoder", h.item.Response.Encoder)
var err error var err error
if h.item.Response.Encoder == "base64" { if h.item.Response.Encoder == "base64" {
h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(h.item.Response.Body) h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(h.item.Response.Body)
} else if h.item.Response.Encoder == "url" { } else if h.item.Response.Encoder == "url" {
var resp *http.Response var resp *http.Response
if resp, err = http.Get(h.item.Response.Body); err == nil { if resp, err = http.Get(h.item.Response.Body); err == nil {
h.item.Response.BodyData, err = io.ReadAll(resp.Body) h.item.Response.BodyData, err = io.ReadAll(resp.Body)
} }
} else { } else {
h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item) h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item)
} }
if err == nil { if err == nil {
h.item.Param = mux.Vars(req) h.item.Param = mux.Vars(req)
if h.item.Param == nil { if h.item.Param == nil {
h.item.Param = make(map[string]string) h.item.Param = make(map[string]string)
} }
h.item.Param["Host"] = req.Host h.item.Param["Host"] = req.Host
if h.item.Response.Header == nil { if h.item.Response.Header == nil {
h.item.Response.Header = make(map[string]string) h.item.Response.Header = make(map[string]string)
} }
h.item.Response.Header[headerMockServer] = fmt.Sprintf("api-testing: %s", version.GetVersion()) h.item.Response.Header[headerMockServer] = fmt.Sprintf("api-testing: %s", version.GetVersion())
h.item.Response.Header[util.ContentLength] = fmt.Sprintf("%d", len(h.item.Response.BodyData)) h.item.Response.Header[util.ContentLength] = fmt.Sprintf("%d", len(h.item.Response.BodyData))
for k, v := range h.item.Response.Header { for k, v := range h.item.Response.Header {
hv, hErr := render.Render("mock-server-header", v, &h.item) hv, hErr := render.Render("mock-server-header", v, &h.item)
if hErr != nil { if hErr != nil {
hv = v hv = v
memLogger.Error(hErr, "failed render mock-server-header", "value", v) memLogger.Error(hErr, "failed render mock-server-header", "value", v)
} }
w.Header().Set(k, hv) w.Header().Set(k, hv)
} }
w.WriteHeader(util.ZeroThenDefault(h.item.Response.StatusCode, http.StatusOK)) w.WriteHeader(util.ZeroThenDefault(h.item.Response.StatusCode, http.StatusOK))
} }
writeResponse(w, h.item.Response.BodyData, err) writeResponse(w, h.item.Response.BodyData, err)
} }
func writeResponse(w http.ResponseWriter, data []byte, err error) { func writeResponse(w http.ResponseWriter, data []byte, err error) {
if err == nil { if err == nil {
w.Write(data) w.Write(data)
} else { } else {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))
} }
} }
func (s *inMemoryServer) initObjectData(obj Object) { func (s *inMemoryServer) initObjectData(obj Object) {
if obj.Sample == "" { if obj.Sample == "" {
return return
} }
defaultCount := 1 defaultCount := 1
if obj.InitCount == nil { if obj.InitCount == nil {
obj.InitCount = &defaultCount obj.InitCount = &defaultCount
} }
for i := 0; i < *obj.InitCount; i++ { for i := 0; i < *obj.InitCount; i++ {
objData, jsonErr := jsonStrToInterface(obj.Sample) objData, jsonErr := jsonStrToInterface(obj.Sample)
if jsonErr == nil { if jsonErr == nil {
s.data[obj.Name] = append(s.data[obj.Name], objData) s.data[obj.Name] = append(s.data[obj.Name], objData)
} else { } else {
memLogger.Info(jsonErr.Error()) memLogger.Info(jsonErr.Error())
} }
} }
} }
func (s *inMemoryServer) startWebhook(webhook *Webhook) (err error) { func (s *inMemoryServer) startWebhook(webhook *Webhook) (err error) {
if webhook.Timer == "" || webhook.Name == "" { if webhook.Timer == "" || webhook.Name == "" {
return return
} }
var duration time.Duration var duration time.Duration
duration, err = time.ParseDuration(webhook.Timer) duration, err = time.ParseDuration(webhook.Timer)
if err != nil { if err != nil {
memLogger.Error(err, "Error parsing webhook timer") memLogger.Error(err, "Error parsing webhook timer")
return return
} }
s.wg.Add(1) s.wg.Add(1)
go func(wh *Webhook) { go func(wh *Webhook) {
defer s.wg.Done() defer s.wg.Done()
memLogger.Info("start webhook server", "name", wh.Name) memLogger.Info("start webhook server", "name", wh.Name)
timer := time.NewTimer(duration) timer := time.NewTimer(duration)
for { for {
timer.Reset(duration) timer.Reset(duration)
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
memLogger.Info("stop webhook server", "name", wh.Name) memLogger.Info("stop webhook server", "name", wh.Name)
return return
case <-timer.C: case <-timer.C:
client := http.DefaultClient if err = runWebhook(s.ctx, s, wh); err != nil {
memLogger.Error(err, "Error when run webhook")
}
}
}
}(webhook)
return
}
payload, err := render.RenderAsReader("mock webhook server payload", wh.Request.Body, wh) func runWebhook(ctx context.Context, objCtx interface{}, wh *Webhook) (err error) {
if err != nil { client := http.DefaultClient
memLogger.Error(err, "Error when render payload")
continue
}
method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost) var payload io.Reader
api, err := render.Render("webhook request api", wh.Request.Path, s) payload, err = render.RenderAsReader("mock webhook server payload", wh.Request.Body, wh)
if err != nil { if err != nil {
memLogger.Error(err, "Error when render api", "raw", wh.Request.Path) err = fmt.Errorf("error when render payload: %w", err)
continue return
} }
req, err := http.NewRequestWithContext(s.ctx, method, api, payload) method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost)
if err != nil { var api string
memLogger.Error(err, "Error when create request") api, err = render.Render("webhook request api", wh.Request.Path, objCtx)
continue if err != nil {
} err = fmt.Errorf("error when render api: %w", err)
return
}
resp, err := client.Do(req) var bearerToken string
if err != nil { bearerToken, err = getBearerToken(ctx, wh.Request)
memLogger.Error(err, "Error when sending webhook") if err != nil {
} else { memLogger.Error(err, "Error when render bearer token")
memLogger.Info("received from webhook", "code", resp.StatusCode) return
} }
}
} var req *http.Request
}(webhook) req, err = http.NewRequestWithContext(ctx, method, api, payload)
return 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() { func (s *inMemoryServer) handleOpenAPI() {
s.mux.HandleFunc("/api.json", func(w http.ResponseWriter, req *http.Request) { s.mux.HandleFunc("/api.json", func(w http.ResponseWriter, req *http.Request) {
// Setup OpenAPI schema // Setup OpenAPI schema
reflector := openapi3.NewReflector() reflector := openapi3.NewReflector()
reflector.SpecSchema().SetTitle("Mock Server API") reflector.SpecSchema().SetTitle("Mock Server API")
reflector.SpecSchema().SetVersion(version.GetVersion()) reflector.SpecSchema().SetVersion(version.GetVersion())
reflector.SpecSchema().SetDescription("Powered by https://github.com/linuxsuren/api-testing") reflector.SpecSchema().SetDescription("Powered by https://github.com/linuxsuren/api-testing")
// Walk the router with OpenAPI collector // Walk the router with OpenAPI collector
c := gorillamux.NewOpenAPICollector(reflector) c := gorillamux.NewOpenAPICollector(reflector)
_ = s.mux.Walk(c.Walker) _ = s.mux.Walk(c.Walker)
// Get the resulting schema // Get the resulting schema
if jsonData, err := reflector.Spec.MarshalJSON(); err == nil { if jsonData, err := reflector.Spec.MarshalJSON(); err == nil {
w.Header().Set(util.ContentType, util.JSON) w.Header().Set(util.ContentType, util.JSON)
_, _ = w.Write(jsonData) _, _ = w.Write(jsonData)
} else { } else {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error())) _, _ = w.Write([]byte(err.Error()))
} }
}) })
} }
func jsonStrToInterface(jsonStr string) (objData map[string]interface{}, err error) { func jsonStrToInterface(jsonStr string) (objData map[string]interface{}, err error) {
if jsonStr, err = render.Render("init object", jsonStr, nil); err == nil { if jsonStr, err = render.Render("init object", jsonStr, nil); err == nil {
objData = map[string]interface{}{} objData = map[string]interface{}{}
err = json.Unmarshal([]byte(jsonStr), &objData) err = json.Unmarshal([]byte(jsonStr), &objData)
} }
return return
} }
func (s *inMemoryServer) GetPort() string { func (s *inMemoryServer) GetPort() string {
return util.GetPort(s.listener) return util.GetPort(s.listener)
} }
func (s *inMemoryServer) Stop() (err error) { func (s *inMemoryServer) Stop() (err error) {
if s.listener != nil { if s.listener != nil {
err = s.listener.Close() err = s.listener.Close()
} else { } else {
memLogger.Info("listener is nil") memLogger.Info("listener is nil")
} }
if s.cancelFunc != nil { if s.cancelFunc != nil {
s.cancelFunc() s.cancelFunc()
} }
s.wg.Wait() s.wg.Wait()
return return
} }

View File

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

View File

@ -16,47 +16,54 @@ limitations under the License.
package mock package mock
type Object struct { type Object struct {
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
InitCount *int `yaml:"initCount" json:"initCount"` InitCount *int `yaml:"initCount" json:"initCount"`
Sample string `yaml:"sample" json:"sample"` Sample string `yaml:"sample" json:"sample"`
} }
type Item struct { type Item struct {
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
Request Request `yaml:"request" json:"request"` Request Request `yaml:"request" json:"request"`
Response Response `yaml:"response" json:"response"` Response Response `yaml:"response" json:"response"`
Param map[string]string Param map[string]string
} }
type Request struct { type Request struct {
Path string `yaml:"path" json:"path"` Path string `yaml:"path" json:"path"`
Method string `yaml:"method" json:"method"` Method string `yaml:"method" json:"method"`
Header map[string]string `yaml:"header" json:"header"` Header map[string]string `yaml:"header" json:"header"`
Body string `yaml:"body" json:"body"` 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 { type Response struct {
Encoder string `yaml:"encoder" json:"encoder"` Encoder string `yaml:"encoder" json:"encoder"`
Body string `yaml:"body" json:"body"` Body string `yaml:"body" json:"body"`
Header map[string]string `yaml:"header" json:"header"` Header map[string]string `yaml:"header" json:"header"`
StatusCode int `yaml:"statusCode" json:"statusCode"` StatusCode int `yaml:"statusCode" json:"statusCode"`
BodyData []byte BodyData []byte
} }
type Webhook struct { type Webhook struct {
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
Timer string `yaml:"timer" json:"timer"` Timer string `yaml:"timer" json:"timer"`
Request Request `yaml:"request" json:"request"` Request RequestWithAuth `yaml:"request" json:"request"`
} }
type Proxy struct { type Proxy struct {
Path string `yaml:"path" json:"path"` Path string `yaml:"path" json:"path"`
Target string `yaml:"target" json:"target"` Target string `yaml:"target" json:"target"`
} }
type Server struct { type Server struct {
Objects []Object `yaml:"objects" json:"objects"` Objects []Object `yaml:"objects" json:"objects"`
Items []Item `yaml:"items" json:"items"` Items []Item `yaml:"items" json:"items"`
Proxies []Proxy `yaml:"proxies" json:"proxies"` Proxies []Proxy `yaml:"proxies" json:"proxies"`
Webhooks []Webhook `yaml:"webhooks" json:"webhooks"` Webhooks []Webhook `yaml:"webhooks" json:"webhooks"`
} }

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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -30,6 +30,7 @@ import (
mathrand "math/rand" mathrand "math/rand"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/linuxsuren/api-testing/pkg/version" "github.com/linuxsuren/api-testing/pkg/version"
@ -179,18 +180,60 @@ var advancedFuncs = []AdvancedFunc{{
h.Write(data) h.Write(data)
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
}, },
}, {
FuncName: "randFloat",
Func: func(from float64, to float64) float64 {
return mathrand.Float64()*(to-from) + from
},
}, { }, {
FuncName: "randEnum", FuncName: "randEnum",
Func: func(items ...string) string { Func: func(items ...string) string {
return items[mathrand.Intn(len(items))] 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", FuncName: "randEmail",
Func: func() string { Func: func() string {
return fmt.Sprintf("%s@%s.com", util.String(3), util.String(3)) 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 // GetAdvancedFuncs returns all the advanced functions
func GetAdvancedFuncs() []AdvancedFunc { func GetAdvancedFuncs() []AdvancedFunc {
return advancedFuncs 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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) { verify: func(t *testing.T, s string) {
assert.Equal(t, 20, len(s), s) 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", name: "randEnum",
text: `{{randEnum "a" "b" "c"}}`, text: `{{randEnum "a" "b" "c"}}`,
verify: func(t *testing.T, s string) { verify: func(t *testing.T, s string) {
assert.Contains(t, []string{"a", "b", "c"}, s) 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", name: "randEmail",
text: `{{randEmail}}`, text: `{{randEmail}}`,
@ -103,6 +115,18 @@ func TestRender(t *testing.T) {
verify: func(t *testing.T, s string) { verify: func(t *testing.T, s string) {
assert.Equal(t, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", s) 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {