From 474751531a95d383a53cc616a5fd0c8afb8d1aa7 Mon Sep 17 00:00:00 2001 From: rick Date: Sat, 8 Feb 2025 18:29:11 +0800 Subject: [PATCH 1/5] feat: add webhook bearer auth support --- pkg/mock/in_memory.go | 870 ++++++++++++++++++++++-------------------- pkg/mock/types.go | 57 +-- 2 files changed, 492 insertions(+), 435 deletions(-) diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index c5ef0700..d607a69b 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -16,470 +16,520 @@ limitations under the License. package mock import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "strings" - "sync" - "time" - - "github.com/swaggest/openapi-go/openapi3" - "github.com/swaggest/rest/gorillamux" - - "github.com/linuxsuren/api-testing/pkg/version" - - "github.com/linuxsuren/api-testing/pkg/logging" - "github.com/linuxsuren/api-testing/pkg/render" - "github.com/linuxsuren/api-testing/pkg/util" - - "github.com/gorilla/mux" + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/swaggest/openapi-go/openapi3" + "github.com/swaggest/rest/gorillamux" + + "github.com/linuxsuren/api-testing/pkg/version" + + "github.com/linuxsuren/api-testing/pkg/logging" + "github.com/linuxsuren/api-testing/pkg/render" + "github.com/linuxsuren/api-testing/pkg/util" + + "github.com/gorilla/mux" ) var ( - memLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("memory") + memLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("memory") ) type inMemoryServer struct { - data map[string][]map[string]interface{} - mux *mux.Router - listener net.Listener - port int - prefix string - wg sync.WaitGroup - ctx context.Context - cancelFunc context.CancelFunc - reader Reader - metrics RequestMetrics + data map[string][]map[string]interface{} + mux *mux.Router + listener net.Listener + port int + prefix string + wg sync.WaitGroup + ctx context.Context + cancelFunc context.CancelFunc + reader Reader + metrics RequestMetrics } func NewInMemoryServer(ctx context.Context, port int) DynamicServer { - ctx, cancel := context.WithCancel(ctx) - return &inMemoryServer{ - port: port, - wg: sync.WaitGroup{}, - ctx: ctx, - cancelFunc: cancel, - metrics: NewNoopMetrics(), - } + ctx, cancel := context.WithCancel(ctx) + return &inMemoryServer{ + port: port, + wg: sync.WaitGroup{}, + ctx: ctx, + cancelFunc: cancel, + metrics: NewNoopMetrics(), + } } func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler http.Handler, err error) { - s.reader = reader - // init the data - s.data = make(map[string][]map[string]interface{}) - s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter() - s.prefix = prefix - handler = s.mux - s.metrics.AddMetricsHandler(s.mux) - err = s.Load() - return + s.reader = reader + // init the data + s.data = make(map[string][]map[string]interface{}) + s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter() + s.prefix = prefix + handler = s.mux + s.metrics.AddMetricsHandler(s.mux) + err = s.Load() + return } func (s *inMemoryServer) Load() (err error) { - var server *Server - if server, err = s.reader.Parse(); err != nil { - return - } - - memLogger.Info("start to run all the APIs from objects", "count", len(server.Objects)) - for _, obj := range server.Objects { - memLogger.Info("start mock server from object", "name", obj.Name) - s.startObject(obj) - s.initObjectData(obj) - } - - memLogger.Info("start to run all the APIs from items", "count", len(server.Items)) - for _, item := range server.Items { - s.startItem(item) - } - - memLogger.Info("start webhook servers", "count", len(server.Webhooks)) - for _, item := range server.Webhooks { - if err = s.startWebhook(&item); err != nil { - return - } - } - - s.handleOpenAPI() - - for _, proxy := range server.Proxies { - memLogger.Info("start to proxy", "target", proxy.Target) - 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, err = render.Render("proxy api", api, s) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to render proxy api") - return - } - memLogger.Info("redirect to", "target", api) - - targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to create proxy request") - return - } - - resp, err := http.DefaultClient.Do(targetReq) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to do proxy request") - return - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to read response body") - return - } - - for k, v := range resp.Header { - w.Header().Add(k, v[0]) - } - w.Write(data) - }) - } - return + var server *Server + if server, err = s.reader.Parse(); err != nil { + return + } + + memLogger.Info("start to run all the APIs from objects", "count", len(server.Objects)) + for _, obj := range server.Objects { + memLogger.Info("start mock server from object", "name", obj.Name) + s.startObject(obj) + s.initObjectData(obj) + } + + memLogger.Info("start to run all the APIs from items", "count", len(server.Items)) + for _, item := range server.Items { + s.startItem(item) + } + + memLogger.Info("start webhook servers", "count", len(server.Webhooks)) + for _, item := range server.Webhooks { + if err = s.startWebhook(&item); err != nil { + return + } + } + + s.handleOpenAPI() + + for _, proxy := range server.Proxies { + memLogger.Info("start to proxy", "target", proxy.Target) + 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, err = render.Render("proxy api", api, s) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to render proxy api") + return + } + memLogger.Info("redirect to", "target", api) + + targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to create proxy request") + return + } + + resp, err := http.DefaultClient.Do(targetReq) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to do proxy request") + return + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to read response body") + return + } + + for k, v := range resp.Header { + w.Header().Add(k, v[0]) + } + w.Write(data) + }) + } + return } func (s *inMemoryServer) Start(reader Reader, prefix string) (err error) { - var handler http.Handler - if handler, err = s.SetupHandler(reader, prefix); err == nil { - if s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)); err == nil { - go func() { - err = http.Serve(s.listener, handler) - }() - } - } - return + var handler http.Handler + if handler, err = s.SetupHandler(reader, prefix); err == nil { + if s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)); err == nil { + go func() { + err = http.Serve(s.listener, handler) + }() + } + } + return } func (s *inMemoryServer) EnableMetrics() { - s.metrics = NewInMemoryMetrics() + s.metrics = NewInMemoryMetrics() } func (s *inMemoryServer) startObject(obj Object) { - // create a simple CRUD server - s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) { - fmt.Println("mock server received request", req.URL.Path) - s.metrics.RecordRequest(req.URL.Path) - method := req.Method - w.Header().Set(util.ContentType, util.JSON) - - switch method { - case http.MethodGet: - // list all items - allItems := s.data[obj.Name] - filteredItems := make([]map[string]interface{}, 0) - - for i, item := range allItems { - exclude := false - - for k, v := range req.URL.Query() { - if len(v) == 0 { - continue - } - - if val, ok := item[k]; ok && val != v[0] { - exclude = true - break - } - } - - if !exclude { - filteredItems = append(filteredItems, allItems[i]) - } - } - - if len(filteredItems) != len(allItems) { - allItems = filteredItems - } - - data, err := json.Marshal(allItems) - writeResponse(w, data, err) - case http.MethodPost: - // create an item - if data, err := io.ReadAll(req.Body); err == nil { - objData := map[string]interface{}{} - - jsonErr := json.Unmarshal(data, &objData) - if jsonErr != nil { - memLogger.Info(jsonErr.Error()) - return - } - - s.data[obj.Name] = append(s.data[obj.Name], objData) - - _, _ = w.Write(data) - } else { - memLogger.Info("failed to read from body", "error", err) - } - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } - }) - - // handle a single object - s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) { - s.metrics.RecordRequest(req.URL.Path) - w.Header().Set(util.ContentType, util.JSON) - objects := s.data[obj.Name] - if objects != nil { - name := mux.Vars(req)["name"] - var data []byte - for _, obj := range objects { - if obj["name"] == name { - - data, _ = json.Marshal(obj) - break - } - } - - if len(data) == 0 { - w.WriteHeader(http.StatusNotFound) - return - } - - method := req.Method - switch method { - case http.MethodGet: - writeResponse(w, data, nil) - case http.MethodPut: - objData := map[string]interface{}{} - if data, err := io.ReadAll(req.Body); err == nil { - - jsonErr := json.Unmarshal(data, &objData) - if jsonErr != nil { - memLogger.Info(jsonErr.Error()) - return - } - for i, item := range s.data[obj.Name] { - if item["name"] == name { - s.data[obj.Name][i] = objData - break - } - } - _, _ = w.Write(data) - } - case http.MethodDelete: - for i, item := range s.data[obj.Name] { - if item["name"] == name { - if len(s.data[obj.Name]) == i+1 { - s.data[obj.Name] = s.data[obj.Name][:i] - } else { - s.data[obj.Name] = append(s.data[obj.Name][:i], s.data[obj.Name][i+1]) - } - - writeResponse(w, []byte(`{"msg": "deleted"}`), nil) - } - } - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } - - } - }) + // create a simple CRUD server + s.mux.HandleFunc("/"+obj.Name, func(w http.ResponseWriter, req *http.Request) { + fmt.Println("mock server received request", req.URL.Path) + s.metrics.RecordRequest(req.URL.Path) + method := req.Method + w.Header().Set(util.ContentType, util.JSON) + + switch method { + case http.MethodGet: + // list all items + allItems := s.data[obj.Name] + filteredItems := make([]map[string]interface{}, 0) + + for i, item := range allItems { + exclude := false + + for k, v := range req.URL.Query() { + if len(v) == 0 { + continue + } + + if val, ok := item[k]; ok && val != v[0] { + exclude = true + break + } + } + + if !exclude { + filteredItems = append(filteredItems, allItems[i]) + } + } + + if len(filteredItems) != len(allItems) { + allItems = filteredItems + } + + data, err := json.Marshal(allItems) + writeResponse(w, data, err) + case http.MethodPost: + // create an item + if data, err := io.ReadAll(req.Body); err == nil { + objData := map[string]interface{}{} + + jsonErr := json.Unmarshal(data, &objData) + if jsonErr != nil { + memLogger.Info(jsonErr.Error()) + return + } + + s.data[obj.Name] = append(s.data[obj.Name], objData) + + _, _ = w.Write(data) + } else { + memLogger.Info("failed to read from body", "error", err) + } + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + + // handle a single object + s.mux.HandleFunc(fmt.Sprintf("/%s/{name}", obj.Name), func(w http.ResponseWriter, req *http.Request) { + s.metrics.RecordRequest(req.URL.Path) + w.Header().Set(util.ContentType, util.JSON) + objects := s.data[obj.Name] + if objects != nil { + name := mux.Vars(req)["name"] + var data []byte + for _, obj := range objects { + if obj["name"] == name { + + data, _ = json.Marshal(obj) + break + } + } + + if len(data) == 0 { + w.WriteHeader(http.StatusNotFound) + return + } + + method := req.Method + switch method { + case http.MethodGet: + writeResponse(w, data, nil) + case http.MethodPut: + objData := map[string]interface{}{} + if data, err := io.ReadAll(req.Body); err == nil { + + jsonErr := json.Unmarshal(data, &objData) + if jsonErr != nil { + memLogger.Info(jsonErr.Error()) + return + } + for i, item := range s.data[obj.Name] { + if item["name"] == name { + s.data[obj.Name][i] = objData + break + } + } + _, _ = w.Write(data) + } + case http.MethodDelete: + for i, item := range s.data[obj.Name] { + if item["name"] == name { + if len(s.data[obj.Name]) == i+1 { + s.data[obj.Name] = s.data[obj.Name][:i] + } else { + s.data[obj.Name] = append(s.data[obj.Name][:i], s.data[obj.Name][i+1]) + } + + writeResponse(w, []byte(`{"msg": "deleted"}`), nil) + } + } + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + + } + }) } func (s *inMemoryServer) startItem(item Item) { - method := util.EmptyThenDefault(item.Request.Method, http.MethodGet) - memLogger.Info("register mock service", "method", method, "path", item.Request.Path, "encoder", item.Response.Encoder) + method := util.EmptyThenDefault(item.Request.Method, http.MethodGet) + memLogger.Info("register mock service", "method", method, "path", item.Request.Path, "encoder", item.Response.Encoder) - var headerSlices []string - for k, v := range item.Request.Header { - headerSlices = append(headerSlices, k, v) - } + var headerSlices []string + for k, v := range item.Request.Header { + headerSlices = append(headerSlices, k, v) + } - adHandler := &advanceHandler{item: &item, metrics: s.metrics} - s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...) + adHandler := &advanceHandler{item: &item, metrics: s.metrics} + s.mux.HandleFunc(item.Request.Path, adHandler.handle).Methods(strings.Split(method, ",")...).Headers(headerSlices...) } type advanceHandler struct { - item *Item - metrics RequestMetrics + item *Item + metrics RequestMetrics } func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) { - h.metrics.RecordRequest(req.URL.Path) - memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path, - "encoder", h.item.Response.Encoder) - - var err error - if h.item.Response.Encoder == "base64" { - h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(h.item.Response.Body) - } else if h.item.Response.Encoder == "url" { - var resp *http.Response - if resp, err = http.Get(h.item.Response.Body); err == nil { - h.item.Response.BodyData, err = io.ReadAll(resp.Body) - } - } else { - h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item) - } - - if err == nil { - h.item.Param = mux.Vars(req) - if h.item.Param == nil { - h.item.Param = make(map[string]string) - } - h.item.Param["Host"] = req.Host - if h.item.Response.Header == nil { - h.item.Response.Header = make(map[string]string) - } - 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)) - for k, v := range h.item.Response.Header { - hv, hErr := render.Render("mock-server-header", v, &h.item) - if hErr != nil { - hv = v - memLogger.Error(hErr, "failed render mock-server-header", "value", v) - } - - w.Header().Set(k, hv) - } - w.WriteHeader(util.ZeroThenDefault(h.item.Response.StatusCode, http.StatusOK)) - } - - writeResponse(w, h.item.Response.BodyData, err) + h.metrics.RecordRequest(req.URL.Path) + memLogger.Info("receiving mock request", "name", h.item.Name, "method", req.Method, "path", req.URL.Path, + "encoder", h.item.Response.Encoder) + + var err error + if h.item.Response.Encoder == "base64" { + h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(h.item.Response.Body) + } else if h.item.Response.Encoder == "url" { + var resp *http.Response + if resp, err = http.Get(h.item.Response.Body); err == nil { + h.item.Response.BodyData, err = io.ReadAll(resp.Body) + } + } else { + h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item) + } + + if err == nil { + h.item.Param = mux.Vars(req) + if h.item.Param == nil { + h.item.Param = make(map[string]string) + } + h.item.Param["Host"] = req.Host + if h.item.Response.Header == nil { + h.item.Response.Header = make(map[string]string) + } + 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)) + for k, v := range h.item.Response.Header { + hv, hErr := render.Render("mock-server-header", v, &h.item) + if hErr != nil { + hv = v + memLogger.Error(hErr, "failed render mock-server-header", "value", v) + } + + w.Header().Set(k, hv) + } + w.WriteHeader(util.ZeroThenDefault(h.item.Response.StatusCode, http.StatusOK)) + } + + writeResponse(w, h.item.Response.BodyData, err) } func writeResponse(w http.ResponseWriter, data []byte, err error) { - if err == nil { - w.Write(data) - } else { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - } + if err == nil { + w.Write(data) + } else { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + } } func (s *inMemoryServer) initObjectData(obj Object) { - if obj.Sample == "" { - return - } - - defaultCount := 1 - if obj.InitCount == nil { - obj.InitCount = &defaultCount - } - - for i := 0; i < *obj.InitCount; i++ { - objData, jsonErr := jsonStrToInterface(obj.Sample) - if jsonErr == nil { - s.data[obj.Name] = append(s.data[obj.Name], objData) - } else { - memLogger.Info(jsonErr.Error()) - } - } + if obj.Sample == "" { + return + } + + defaultCount := 1 + if obj.InitCount == nil { + obj.InitCount = &defaultCount + } + + for i := 0; i < *obj.InitCount; i++ { + objData, jsonErr := jsonStrToInterface(obj.Sample) + if jsonErr == nil { + s.data[obj.Name] = append(s.data[obj.Name], objData) + } else { + memLogger.Info(jsonErr.Error()) + } + } } func (s *inMemoryServer) startWebhook(webhook *Webhook) (err error) { - if webhook.Timer == "" || webhook.Name == "" { - return - } - - var duration time.Duration - duration, err = time.ParseDuration(webhook.Timer) - if err != nil { - memLogger.Error(err, "Error parsing webhook timer") - return - } - - s.wg.Add(1) - go func(wh *Webhook) { - defer s.wg.Done() - - memLogger.Info("start webhook server", "name", wh.Name) - timer := time.NewTimer(duration) - for { - timer.Reset(duration) - select { - case <-s.ctx.Done(): - 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) - } - } - } - }(webhook) - return + if webhook.Timer == "" || webhook.Name == "" { + return + } + + var duration time.Duration + duration, err = time.ParseDuration(webhook.Timer) + if err != nil { + memLogger.Error(err, "Error parsing webhook timer") + return + } + + s.wg.Add(1) + go func(wh *Webhook) { + defer s.wg.Done() + + memLogger.Info("start webhook server", "name", wh.Name) + timer := time.NewTimer(duration) + for { + timer.Reset(duration) + select { + case <-s.ctx.Done(): + 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 + } + + var bearerToken string + bearerToken, err = getBearerToken(s.ctx, wh.Request) + if err != nil { + memLogger.Error(err, "Error when render bearer token") + continue + } + + req, err := http.NewRequestWithContext(s.ctx, method, api, payload) + if err != nil { + memLogger.Error(err, "Error when create request") + continue + } + + 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 { + memLogger.Error(err, "Error when sending webhook") + } else { + data, _ := io.ReadAll(resp.Body) + memLogger.V(7).Info("received from webhook", "code", resp.StatusCode, "response", string(data)) + } + } + } + }(webhook) + return +} + +type bearerToken struct { + Token string `json:"token"` +} + +func getBearerToken(ctx context.Context, request RequestWithAuth) (token string, err error) { + if request.BearerAPI == "" { + memLogger.Info("bearer token is not set") + 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 - reflector := openapi3.NewReflector() - reflector.SpecSchema().SetTitle("Mock Server API") - reflector.SpecSchema().SetVersion(version.GetVersion()) - reflector.SpecSchema().SetDescription("Powered by https://github.com/linuxsuren/api-testing") - - // Walk the router with OpenAPI collector - c := gorillamux.NewOpenAPICollector(reflector) - - _ = s.mux.Walk(c.Walker) - - // Get the resulting schema - if jsonData, err := reflector.Spec.MarshalJSON(); err == nil { - w.Header().Set(util.ContentType, util.JSON) - _, _ = w.Write(jsonData) - } else { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(err.Error())) - } - }) + s.mux.HandleFunc("/api.json", func(w http.ResponseWriter, req *http.Request) { + // Setup OpenAPI schema + reflector := openapi3.NewReflector() + reflector.SpecSchema().SetTitle("Mock Server API") + reflector.SpecSchema().SetVersion(version.GetVersion()) + reflector.SpecSchema().SetDescription("Powered by https://github.com/linuxsuren/api-testing") + + // Walk the router with OpenAPI collector + c := gorillamux.NewOpenAPICollector(reflector) + + _ = s.mux.Walk(c.Walker) + + // Get the resulting schema + if jsonData, err := reflector.Spec.MarshalJSON(); err == nil { + w.Header().Set(util.ContentType, util.JSON) + _, _ = w.Write(jsonData) + } else { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } + }) } func jsonStrToInterface(jsonStr string) (objData map[string]interface{}, err error) { - if jsonStr, err = render.Render("init object", jsonStr, nil); err == nil { - objData = map[string]interface{}{} - err = json.Unmarshal([]byte(jsonStr), &objData) - } - return + if jsonStr, err = render.Render("init object", jsonStr, nil); err == nil { + objData = map[string]interface{}{} + err = json.Unmarshal([]byte(jsonStr), &objData) + } + return } func (s *inMemoryServer) GetPort() string { - return util.GetPort(s.listener) + return util.GetPort(s.listener) } func (s *inMemoryServer) Stop() (err error) { - if s.listener != nil { - err = s.listener.Close() - } else { - memLogger.Info("listener is nil") - } - if s.cancelFunc != nil { - s.cancelFunc() - } - s.wg.Wait() - return + if s.listener != nil { + err = s.listener.Close() + } else { + memLogger.Info("listener is nil") + } + if s.cancelFunc != nil { + s.cancelFunc() + } + s.wg.Wait() + return } diff --git a/pkg/mock/types.go b/pkg/mock/types.go index bed7c569..fb00c11b 100644 --- a/pkg/mock/types.go +++ b/pkg/mock/types.go @@ -16,47 +16,54 @@ limitations under the License. package mock type Object struct { - Name string `yaml:"name" json:"name"` - InitCount *int `yaml:"initCount" json:"initCount"` - Sample string `yaml:"sample" json:"sample"` + Name string `yaml:"name" json:"name"` + InitCount *int `yaml:"initCount" json:"initCount"` + Sample string `yaml:"sample" json:"sample"` } type Item struct { - Name string `yaml:"name" json:"name"` - Request Request `yaml:"request" json:"request"` - Response Response `yaml:"response" json:"response"` - Param map[string]string + Name string `yaml:"name" json:"name"` + Request Request `yaml:"request" json:"request"` + Response Response `yaml:"response" json:"response"` + Param map[string]string } type Request struct { - Path string `yaml:"path" json:"path"` - Method string `yaml:"method" json:"method"` - Header map[string]string `yaml:"header" json:"header"` - Body string `yaml:"body" json:"body"` + Path string `yaml:"path" json:"path"` + Method string `yaml:"method" json:"method"` + Header map[string]string `yaml:"header" json:"header"` + 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"` - Header map[string]string `yaml:"header" json:"header"` - StatusCode int `yaml:"statusCode" json:"statusCode"` - BodyData []byte + Encoder string `yaml:"encoder" json:"encoder"` + Body string `yaml:"body" json:"body"` + Header map[string]string `yaml:"header" json:"header"` + StatusCode int `yaml:"statusCode" json:"statusCode"` + BodyData []byte } type Webhook struct { - Name string `yaml:"name" json:"name"` - Timer string `yaml:"timer" json:"timer"` - Request Request `yaml:"request" json:"request"` + Name string `yaml:"name" json:"name"` + Timer string `yaml:"timer" json:"timer"` + Request RequestWithAuth `yaml:"request" json:"request"` } type Proxy struct { - Path string `yaml:"path" json:"path"` - Target string `yaml:"target" json:"target"` + Path string `yaml:"path" json:"path"` + Target string `yaml:"target" json:"target"` } type Server struct { - Objects []Object `yaml:"objects" json:"objects"` - Items []Item `yaml:"items" json:"items"` - Proxies []Proxy `yaml:"proxies" json:"proxies"` - Webhooks []Webhook `yaml:"webhooks" json:"webhooks"` + Objects []Object `yaml:"objects" json:"objects"` + Items []Item `yaml:"items" json:"items"` + Proxies []Proxy `yaml:"proxies" json:"proxies"` + Webhooks []Webhook `yaml:"webhooks" json:"webhooks"` } From 787694089bce7ecd6decc77b26c5a4e508dd85eb Mon Sep 17 00:00:00 2001 From: rick Date: Tue, 11 Feb 2025 20:50:28 +0800 Subject: [PATCH 2/5] add tpl func randFloat --- docs/site/content/zh/latest/tasks/mock/simple.yaml | 10 +++++++--- pkg/render/template.go | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/site/content/zh/latest/tasks/mock/simple.yaml b/docs/site/content/zh/latest/tasks/mock/simple.yaml index 7fea3daa..3b6990f9 100644 --- a/docs/site/content/zh/latest/tasks/mock/simple.yaml +++ b/docs/site/content/zh/latest/tasks/mock/simple.yaml @@ -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, + "number": {{randInt 100 199}}, + "float": {{randFloat 0.0 1.0}}, "message": "{{.Response.Header.server}}", "author": "someone", - "status": "success" - }] + "status": "success", + "created": "{{ now.Format "2006-01-02T15:04:05Z07:00" }}" + }], + "uptime": "{{uptime}}" } - name: base64 request: diff --git a/pkg/render/template.go b/pkg/render/template.go index 59ebd578..ac391b59 100644 --- a/pkg/render/template.go +++ b/pkg/render/template.go @@ -30,6 +30,7 @@ import ( mathrand "math/rand" "strings" "text/template" + "time" "github.com/linuxsuren/api-testing/pkg/version" @@ -179,6 +180,11 @@ 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 { @@ -189,8 +195,15 @@ var advancedFuncs = []AdvancedFunc{{ 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() + }, }} +var uptime = time.Now() + // GetAdvancedFuncs returns all the advanced functions func GetAdvancedFuncs() []AdvancedFunc { return advancedFuncs From c3cf711df10f8b5e548b98eb7b7dbcbfff7769ef Mon Sep 17 00:00:00 2001 From: rick Date: Fri, 14 Feb 2025 09:39:42 +0800 Subject: [PATCH 3/5] refactor run webhook --- pkg/mock/in_memory.go | 91 +++++++++++++++++++++----------------- pkg/mock/testdata/api.yaml | 2 +- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index d607a69b..85530efc 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -403,53 +403,63 @@ 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 + if err = runWebhook(s.ctx, s, wh); err != nil { + memLogger.Error(err, "Error when run webhook") } + } + } + }(webhook) + return +} - 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 - } +func runWebhook(ctx context.Context, objCtx interface{}, wh *Webhook) (err error) { + client := http.DefaultClient - var bearerToken string - bearerToken, err = getBearerToken(s.ctx, wh.Request) - if err != nil { - memLogger.Error(err, "Error when render bearer token") - continue - } + 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 + } - req, err := http.NewRequestWithContext(s.ctx, method, api, payload) - if err != nil { - memLogger.Error(err, "Error when create request") - continue - } + 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 + } - if bearerToken != "" { - memLogger.V(7).Info("set bearer token", "token", bearerToken) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) - } + var bearerToken string + bearerToken, err = getBearerToken(ctx, wh.Request) + if err != nil { + memLogger.Error(err, "Error when render bearer token") + return + } - for k, v := range wh.Request.Header { - req.Header.Set(k, v) - } + var req *http.Request + req, err = http.NewRequestWithContext(ctx, method, api, payload) + if err != nil { + memLogger.Error(err, "Error when create request") + return + } - resp, err := client.Do(req) - if err != nil { - memLogger.Error(err, "Error when sending webhook") - } else { - data, _ := io.ReadAll(resp.Body) - memLogger.V(7).Info("received from webhook", "code", resp.StatusCode, "response", string(data)) - } - } - } - }(webhook) + 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 } @@ -459,7 +469,6 @@ type bearerToken struct { func getBearerToken(ctx context.Context, request RequestWithAuth) (token string, err error) { if request.BearerAPI == "" { - memLogger.Info("bearer token is not set") return } diff --git a/pkg/mock/testdata/api.yaml b/pkg/mock/testdata/api.yaml index b5c5b83c..f134f9ed 100644 --- a/pkg/mock/testdata/api.yaml +++ b/pkg/mock/testdata/api.yaml @@ -55,7 +55,7 @@ proxies: - path: /v1/invalid-template target: http://localhost:{{.GetPort} webhooks: - - timer: 1ms + - timer: 1m name: baidu request: method: GET From 8974df321d1879915b3459d82c743c7f27efd275 Mon Sep 17 00:00:00 2001 From: rick Date: Fri, 14 Feb 2025 10:05:35 +0800 Subject: [PATCH 4/5] add template func uptimeSeconds --- pkg/render/template.go | 391 +++++++++++++++++++++-------------------- 1 file changed, 198 insertions(+), 193 deletions(-) diff --git a/pkg/render/template.go b/pkg/render/template.go index ac391b59..f9e095e9 100644 --- a/pkg/render/template.go +++ b/pkg/render/template.go @@ -16,214 +16,219 @@ limitations under the License. package render import ( - "bytes" - "context" - "crypto/md5" - "crypto/rand" - "crypto/sha256" - _ "embed" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "io" - mathrand "math/rand" - "strings" - "text/template" - "time" - - "github.com/linuxsuren/api-testing/pkg/version" - - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" - - "github.com/Masterminds/sprig/v3" - "github.com/linuxsuren/api-testing/pkg/secret" - "github.com/linuxsuren/api-testing/pkg/util" - "gopkg.in/yaml.v3" + "bytes" + "context" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + _ "embed" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + mathrand "math/rand" + "strings" + "text/template" + "time" + + "github.com/linuxsuren/api-testing/pkg/version" + + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + "github.com/Masterminds/sprig/v3" + "github.com/linuxsuren/api-testing/pkg/secret" + "github.com/linuxsuren/api-testing/pkg/util" + "gopkg.in/yaml.v3" ) var secretGetter secret.SecretGetter // SetSecretGetter set the secret getter func SetSecretGetter(getter secret.SecretGetter) { - if getter == nil { - getter = &nonSecretGetter{ - err: fmt.Errorf("no secret server"), - } - } - secretGetter = getter + if getter == nil { + getter = &nonSecretGetter{ + err: fmt.Errorf("no secret server"), + } + } + secretGetter = getter } // Render render then return the result func Render(name, text string, ctx interface{}) (result string, err error) { - var data []byte - if data, err = RenderAsBytes(name, text, ctx); err == nil { - result = string(data) - } - return + var data []byte + if data, err = RenderAsBytes(name, text, ctx); err == nil { + result = string(data) + } + return } func RenderAsBytes(name, text string, ctx interface{}) (data []byte, err error) { - var tpl *template.Template - if tpl, err = template.New(name). - Funcs(FuncMap()). - Parse(text); err == nil { - buf := new(bytes.Buffer) - if err = tpl.Execute(buf, ctx); err == nil { - data = buf.Bytes() - } - } - return + var tpl *template.Template + if tpl, err = template.New(name). + Funcs(FuncMap()). + Parse(text); err == nil { + buf := new(bytes.Buffer) + if err = tpl.Execute(buf, ctx); err == nil { + data = buf.Bytes() + } + } + return } func RenderAsReader(name, text string, ctx interface{}) (reader io.Reader, err error) { - var data []byte - if data, err = RenderAsBytes(name, text, ctx); err == nil { - reader = bytes.NewReader(data) - } - return + var data []byte + if data, err = RenderAsBytes(name, text, ctx); err == nil { + reader = bytes.NewReader(data) + } + return } // FuncMap returns all the supported functions func FuncMap() template.FuncMap { - funcs := sprig.FuncMap() - for _, item := range GetAdvancedFuncs() { - if item.FuncName == "" || item.Func == nil { - continue - } - funcs[item.FuncName] = item.Func - } - funcs["rasEncryptWithPublicKey"] = rasEncryptWithPublicKey - funcs["randImage"] = generateRandomImage - funcs["randPdf"] = generateRandomPdf - funcs["randZip"] = generateRandomZip - funcs["readFile"] = readFile - return funcs + funcs := sprig.FuncMap() + for _, item := range GetAdvancedFuncs() { + if item.FuncName == "" || item.Func == nil { + continue + } + funcs[item.FuncName] = item.Func + } + funcs["rasEncryptWithPublicKey"] = rasEncryptWithPublicKey + funcs["randImage"] = generateRandomImage + funcs["randPdf"] = generateRandomPdf + funcs["randZip"] = generateRandomZip + funcs["readFile"] = readFile + return funcs } // FuncUsage returns the usage of target template function func FuncUsage(funcName string) (usage string) { - usageMap := make(map[string]string) - if err := yaml.Unmarshal(templateUsage, usageMap); err == nil { - usage = usageMap[funcName] - } - return + usageMap := make(map[string]string) + if err := yaml.Unmarshal(templateUsage, usageMap); err == nil { + usage = usageMap[funcName] + } + return } // RenderThenPrint renders the template then prints the result func RenderThenPrint(name, text string, ctx interface{}, w io.Writer) (err error) { - var report string - if report, err = Render(name, text, ctx); err == nil { - _, err = fmt.Fprint(w, report) - } - return + var report string + if report, err = Render(name, text, ctx); err == nil { + _, err = fmt.Fprint(w, report) + } + return } var advancedFuncs = []AdvancedFunc{{ - FuncName: "generateJSONString", - Func: generateJSONString, - GoDogExper: `^生成对象,字段包含 (.*)$`, - Generator: generateJSONObject, + FuncName: "generateJSONString", + Func: generateJSONString, + GoDogExper: `^生成对象,字段包含 (.*)$`, + Generator: generateJSONObject, }, { - FuncName: "randomKubernetesName", - Func: func() string { - return util.String(8) - }, - GoDogExper: `^动态k8s名称(.*)$`, - Generator: func(ctx context.Context, fields string) (err error) { - writeWithContext(ctx, `{{randomKubernetesName}}`) - return - }, + FuncName: "randomKubernetesName", + Func: func() string { + return util.String(8) + }, + GoDogExper: `^动态k8s名称(.*)$`, + Generator: func(ctx context.Context, fields string) (err error) { + writeWithContext(ctx, `{{randomKubernetesName}}`) + return + }, }, { - GoDogExper: `^生成随机字符串,长度 (.*)$`, - Generator: func(ctx context.Context, fields string) (err error) { - writeWithContext(ctx, `{{randAlpha `+fields+`}}`) - return - }, + GoDogExper: `^生成随机字符串,长度 (.*)$`, + Generator: func(ctx context.Context, fields string) (err error) { + writeWithContext(ctx, `{{randAlpha `+fields+`}}`) + return + }, }, { - FuncName: "secretValue", - Func: func(name string) string { - val, err := secretGetter.GetSecret(name) - if err == nil { - return val.Value - } - return err.Error() - }, + FuncName: "secretValue", + Func: func(name string) string { + val, err := secretGetter.GetSecret(name) + if err == nil { + return val.Value + } + return err.Error() + }, }, { - FuncName: "md5", - Func: func(text string) string { - hash := md5.Sum([]byte(text)) - return hex.EncodeToString(hash[:]) - }, + FuncName: "md5", + Func: func(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) + }, }, { - FuncName: "base64", - Func: func(text string) string { - return base64.StdEncoding.EncodeToString([]byte(text)) - }, + FuncName: "base64", + Func: func(text string) string { + return base64.StdEncoding.EncodeToString([]byte(text)) + }, }, { - FuncName: "base64Decode", - Func: func(text string) string { - result, err := base64.StdEncoding.DecodeString(text) - if err == nil { - return string(result) - } else { - return err.Error() - } - }, + FuncName: "base64Decode", + Func: func(text string) string { + result, err := base64.StdEncoding.DecodeString(text) + if err == nil { + return string(result) + } else { + return err.Error() + } + }, }, { - FuncName: "sha256sumBytes", - Func: func(data []byte) string { - h := sha256.New() - h.Write(data) - return hex.EncodeToString(h.Sum(nil)) - }, + FuncName: "sha256sumBytes", + Func: func(data []byte) string { + h := sha256.New() + 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: "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: "randEnum", + Func: func(items ...string) string { + return items[mathrand.Intn(len(items))] + }, }, { - FuncName: "randEmail", - Func: func() string { - return fmt.Sprintf("%s@%s.com", util.String(3), util.String(3)) - }, + 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: "uptime", + Func: func() string { + return time.Since(uptime).String() + }, +}, { + FuncName: "uptimeSeconds", + Func: func() float64 { + return time.Since(uptime).Seconds() + }, }} var uptime = time.Now() // GetAdvancedFuncs returns all the advanced functions func GetAdvancedFuncs() []AdvancedFunc { - return advancedFuncs + return advancedFuncs } func GetEngineVersion() (ver string) { - ver, _ = version.GetModVersion("github.com/Masterminds/sprig", "") - return + ver, _ = version.GetModVersion("github.com/Masterminds/sprig", "") + return } func generateJSONString(fields []string) (result string) { - data := make(map[string]string) - for _, item := range fields { - data[item] = "random" - } - - if json, err := json.Marshal(data); err == nil { - result = string(json) - } - return + data := make(map[string]string) + for _, item := range fields { + data[item] = "random" + } + + if json, err := json.Marshal(data); err == nil { + result = string(json) + } + return } type ContextKey string @@ -233,56 +238,56 @@ var ContextBufferKey ContextKey = "ContextBufferKey" // generateJSONObject generates a json object // For instance: {{generateJSONString "hello" "world"}} func generateJSONObject(ctx context.Context, fields string) (err error) { - items := strings.Split(fields, ",") - funcExp := "{{generateJSONString" - for _, item := range items { - funcExp += " \"" + strings.TrimSpace(item) + "\"" - } - funcExp += "}}" - - writeWithContext(ctx, funcExp) - return + items := strings.Split(fields, ",") + funcExp := "{{generateJSONString" + for _, item := range items { + funcExp += " \"" + strings.TrimSpace(item) + "\"" + } + funcExp += "}}" + + writeWithContext(ctx, funcExp) + return } func writeWithContext(ctx context.Context, text string) { - buf := ctx.Value(ContextBufferKey) - writer, ok := buf.(io.Writer) - if ok && writer != nil { - _, _ = writer.Write([]byte(text)) - } + buf := ctx.Value(ContextBufferKey) + writer, ok := buf.(io.Writer) + if ok && writer != nil { + _, _ = writer.Write([]byte(text)) + } } // AdvancedFunc represents an advanced function type AdvancedFunc struct { - FuncName string - Func interface{} - GoDogExper string - Generator func(ctx context.Context, fields string) (err error) + FuncName string + Func interface{} + GoDogExper string + Generator func(ctx context.Context, fields string) (err error) } // rasEncryptWithPublicKey encrypts the given content with the provided public key func rasEncryptWithPublicKey(content, key string) (string, error) { - block, _ := pem.Decode([]byte(key)) - if block == nil { - return "", errors.New("failed to parse PEM block containing the public key") - } - - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return "", fmt.Errorf("failed to parse DER encoded public key: %s", err) - } - - rsaPub, ok := pub.(*rsa.PublicKey) - if !ok { - return "", errors.New("key type is not RSA") - } - - encryptedData, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPub, []byte(content)) - if err != nil { - return "", fmt.Errorf("failed to encrypt with RSA public key: %s", err) - } - - return base64.StdEncoding.EncodeToString(encryptedData), nil + block, _ := pem.Decode([]byte(key)) + if block == nil { + return "", errors.New("failed to parse PEM block containing the public key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse DER encoded public key: %s", err) + } + + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return "", errors.New("key type is not RSA") + } + + encryptedData, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPub, []byte(content)) + if err != nil { + return "", fmt.Errorf("failed to encrypt with RSA public key: %s", err) + } + + return base64.StdEncoding.EncodeToString(encryptedData), nil } //go:embed data/templateUsage.yaml From a89ce378f6c23ea44b15968b060a6e67efff00b1 Mon Sep 17 00:00:00 2001 From: rick Date: Sat, 15 Feb 2025 10:43:04 +0800 Subject: [PATCH 5/5] add random enum with weight feature --- .../content/zh/latest/tasks/mock/simple.yaml | 6 +- docs/site/content/zh/latest/tasks/template.md | 26 +- pkg/render/template.go | 421 ++++++++++-------- pkg/render/template_test.go | 26 +- 4 files changed, 276 insertions(+), 203 deletions(-) diff --git a/docs/site/content/zh/latest/tasks/mock/simple.yaml b/docs/site/content/zh/latest/tasks/mock/simple.yaml index 3b6990f9..04e585cf 100644 --- a/docs/site/content/zh/latest/tasks/mock/simple.yaml +++ b/docs/site/content/zh/latest/tasks/mock/simple.yaml @@ -12,12 +12,12 @@ items: { "count": 1, "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": {{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}}" diff --git a/docs/site/content/zh/latest/tasks/template.md b/docs/site/content/zh/latest/tasks/template.md index f3dc49f0..148c19d3 100644 --- a/docs/site/content/zh/latest/tasks/template.md +++ b/docs/site/content/zh/latest/tasks/template.md @@ -10,4 +10,28 @@ title = "用例模板" ``` 182{{shuffle "09876543"}} -``` \ No newline at end of file +``` + +## 带权重的随机枚举 + +下面的代码以 80% 的概率返回 `open`,以 20% 的概率返回 `closed`: + +``` +{{randWeightEnum (weightObject 4 "open") (weightObject 1 "closed")}} +``` + +## 时间 + +下面的代码可以生成当前时间,并制定时间格式: + +``` +{{ now.Format "2006-01-02T15:04:05Z07:00" }} +``` + +## 环境变量 + +下面的代码可以获取环境变量 `SHELL` 的值,在需要使用一个全局变量的时候,可以使用这个模板函数: + +``` +{{ env "SHELL" }} +``` diff --git a/pkg/render/template.go b/pkg/render/template.go index f9e095e9..d456060e 100644 --- a/pkg/render/template.go +++ b/pkg/render/template.go @@ -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. @@ -16,219 +16,244 @@ limitations under the License. package render import ( - "bytes" - "context" - "crypto/md5" - "crypto/rand" - "crypto/sha256" - _ "embed" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "io" - mathrand "math/rand" - "strings" - "text/template" - "time" - - "github.com/linuxsuren/api-testing/pkg/version" - - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" - - "github.com/Masterminds/sprig/v3" - "github.com/linuxsuren/api-testing/pkg/secret" - "github.com/linuxsuren/api-testing/pkg/util" - "gopkg.in/yaml.v3" + "bytes" + "context" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + _ "embed" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + mathrand "math/rand" + "strings" + "text/template" + "time" + + "github.com/linuxsuren/api-testing/pkg/version" + + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + "github.com/Masterminds/sprig/v3" + "github.com/linuxsuren/api-testing/pkg/secret" + "github.com/linuxsuren/api-testing/pkg/util" + "gopkg.in/yaml.v3" ) var secretGetter secret.SecretGetter // SetSecretGetter set the secret getter func SetSecretGetter(getter secret.SecretGetter) { - if getter == nil { - getter = &nonSecretGetter{ - err: fmt.Errorf("no secret server"), - } - } - secretGetter = getter + if getter == nil { + getter = &nonSecretGetter{ + err: fmt.Errorf("no secret server"), + } + } + secretGetter = getter } // Render render then return the result func Render(name, text string, ctx interface{}) (result string, err error) { - var data []byte - if data, err = RenderAsBytes(name, text, ctx); err == nil { - result = string(data) - } - return + var data []byte + if data, err = RenderAsBytes(name, text, ctx); err == nil { + result = string(data) + } + return } func RenderAsBytes(name, text string, ctx interface{}) (data []byte, err error) { - var tpl *template.Template - if tpl, err = template.New(name). - Funcs(FuncMap()). - Parse(text); err == nil { - buf := new(bytes.Buffer) - if err = tpl.Execute(buf, ctx); err == nil { - data = buf.Bytes() - } - } - return + var tpl *template.Template + if tpl, err = template.New(name). + Funcs(FuncMap()). + Parse(text); err == nil { + buf := new(bytes.Buffer) + if err = tpl.Execute(buf, ctx); err == nil { + data = buf.Bytes() + } + } + return } func RenderAsReader(name, text string, ctx interface{}) (reader io.Reader, err error) { - var data []byte - if data, err = RenderAsBytes(name, text, ctx); err == nil { - reader = bytes.NewReader(data) - } - return + var data []byte + if data, err = RenderAsBytes(name, text, ctx); err == nil { + reader = bytes.NewReader(data) + } + return } // FuncMap returns all the supported functions func FuncMap() template.FuncMap { - funcs := sprig.FuncMap() - for _, item := range GetAdvancedFuncs() { - if item.FuncName == "" || item.Func == nil { - continue - } - funcs[item.FuncName] = item.Func - } - funcs["rasEncryptWithPublicKey"] = rasEncryptWithPublicKey - funcs["randImage"] = generateRandomImage - funcs["randPdf"] = generateRandomPdf - funcs["randZip"] = generateRandomZip - funcs["readFile"] = readFile - return funcs + funcs := sprig.FuncMap() + for _, item := range GetAdvancedFuncs() { + if item.FuncName == "" || item.Func == nil { + continue + } + funcs[item.FuncName] = item.Func + } + funcs["rasEncryptWithPublicKey"] = rasEncryptWithPublicKey + funcs["randImage"] = generateRandomImage + funcs["randPdf"] = generateRandomPdf + funcs["randZip"] = generateRandomZip + funcs["readFile"] = readFile + return funcs } // FuncUsage returns the usage of target template function func FuncUsage(funcName string) (usage string) { - usageMap := make(map[string]string) - if err := yaml.Unmarshal(templateUsage, usageMap); err == nil { - usage = usageMap[funcName] - } - return + usageMap := make(map[string]string) + if err := yaml.Unmarshal(templateUsage, usageMap); err == nil { + usage = usageMap[funcName] + } + return } // RenderThenPrint renders the template then prints the result func RenderThenPrint(name, text string, ctx interface{}, w io.Writer) (err error) { - var report string - if report, err = Render(name, text, ctx); err == nil { - _, err = fmt.Fprint(w, report) - } - return + var report string + if report, err = Render(name, text, ctx); err == nil { + _, err = fmt.Fprint(w, report) + } + return } var advancedFuncs = []AdvancedFunc{{ - FuncName: "generateJSONString", - Func: generateJSONString, - GoDogExper: `^生成对象,字段包含 (.*)$`, - Generator: generateJSONObject, + FuncName: "generateJSONString", + Func: generateJSONString, + GoDogExper: `^生成对象,字段包含 (.*)$`, + Generator: generateJSONObject, }, { - FuncName: "randomKubernetesName", - Func: func() string { - return util.String(8) - }, - GoDogExper: `^动态k8s名称(.*)$`, - Generator: func(ctx context.Context, fields string) (err error) { - writeWithContext(ctx, `{{randomKubernetesName}}`) - return - }, + FuncName: "randomKubernetesName", + Func: func() string { + return util.String(8) + }, + GoDogExper: `^动态k8s名称(.*)$`, + Generator: func(ctx context.Context, fields string) (err error) { + writeWithContext(ctx, `{{randomKubernetesName}}`) + return + }, }, { - GoDogExper: `^生成随机字符串,长度 (.*)$`, - Generator: func(ctx context.Context, fields string) (err error) { - writeWithContext(ctx, `{{randAlpha `+fields+`}}`) - return - }, + GoDogExper: `^生成随机字符串,长度 (.*)$`, + Generator: func(ctx context.Context, fields string) (err error) { + writeWithContext(ctx, `{{randAlpha `+fields+`}}`) + return + }, }, { - FuncName: "secretValue", - Func: func(name string) string { - val, err := secretGetter.GetSecret(name) - if err == nil { - return val.Value - } - return err.Error() - }, + FuncName: "secretValue", + Func: func(name string) string { + val, err := secretGetter.GetSecret(name) + if err == nil { + return val.Value + } + return err.Error() + }, }, { - FuncName: "md5", - Func: func(text string) string { - hash := md5.Sum([]byte(text)) - return hex.EncodeToString(hash[:]) - }, + FuncName: "md5", + Func: func(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) + }, }, { - FuncName: "base64", - Func: func(text string) string { - return base64.StdEncoding.EncodeToString([]byte(text)) - }, + FuncName: "base64", + Func: func(text string) string { + return base64.StdEncoding.EncodeToString([]byte(text)) + }, }, { - FuncName: "base64Decode", - Func: func(text string) string { - result, err := base64.StdEncoding.DecodeString(text) - if err == nil { - return string(result) - } else { - return err.Error() - } - }, + FuncName: "base64Decode", + Func: func(text string) string { + result, err := base64.StdEncoding.DecodeString(text) + if err == nil { + return string(result) + } else { + return err.Error() + } + }, }, { - FuncName: "sha256sumBytes", - Func: func(data []byte) string { - h := sha256.New() - h.Write(data) - return hex.EncodeToString(h.Sum(nil)) - }, + FuncName: "sha256sumBytes", + Func: func(data []byte) string { + h := sha256.New() + 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: "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: "randEnum", + Func: func(items ...string) string { + return items[mathrand.Intn(len(items))] + }, }, { - FuncName: "randEmail", - Func: func() string { - return fmt.Sprintf("%s@%s.com", util.String(3), util.String(3)) - }, + FuncName: "weightObject", + Func: func(weight int, object interface{}) WeightEnum { + return WeightEnum{ + Weight: weight, + Object: object, + } + }, }, { - FuncName: "uptime", - Func: func() string { - return time.Since(uptime).String() - }, + 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: "uptimeSeconds", - Func: func() float64 { - return time.Since(uptime).Seconds() - }, + 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 + return advancedFuncs } func GetEngineVersion() (ver string) { - ver, _ = version.GetModVersion("github.com/Masterminds/sprig", "") - return + ver, _ = version.GetModVersion("github.com/Masterminds/sprig", "") + return } func generateJSONString(fields []string) (result string) { - data := make(map[string]string) - for _, item := range fields { - data[item] = "random" - } - - if json, err := json.Marshal(data); err == nil { - result = string(json) - } - return + data := make(map[string]string) + for _, item := range fields { + data[item] = "random" + } + + if json, err := json.Marshal(data); err == nil { + result = string(json) + } + return } type ContextKey string @@ -238,56 +263,56 @@ var ContextBufferKey ContextKey = "ContextBufferKey" // generateJSONObject generates a json object // For instance: {{generateJSONString "hello" "world"}} func generateJSONObject(ctx context.Context, fields string) (err error) { - items := strings.Split(fields, ",") - funcExp := "{{generateJSONString" - for _, item := range items { - funcExp += " \"" + strings.TrimSpace(item) + "\"" - } - funcExp += "}}" - - writeWithContext(ctx, funcExp) - return + items := strings.Split(fields, ",") + funcExp := "{{generateJSONString" + for _, item := range items { + funcExp += " \"" + strings.TrimSpace(item) + "\"" + } + funcExp += "}}" + + writeWithContext(ctx, funcExp) + return } func writeWithContext(ctx context.Context, text string) { - buf := ctx.Value(ContextBufferKey) - writer, ok := buf.(io.Writer) - if ok && writer != nil { - _, _ = writer.Write([]byte(text)) - } + buf := ctx.Value(ContextBufferKey) + writer, ok := buf.(io.Writer) + if ok && writer != nil { + _, _ = writer.Write([]byte(text)) + } } // AdvancedFunc represents an advanced function type AdvancedFunc struct { - FuncName string - Func interface{} - GoDogExper string - Generator func(ctx context.Context, fields string) (err error) + FuncName string + Func interface{} + GoDogExper string + Generator func(ctx context.Context, fields string) (err error) } // rasEncryptWithPublicKey encrypts the given content with the provided public key func rasEncryptWithPublicKey(content, key string) (string, error) { - block, _ := pem.Decode([]byte(key)) - if block == nil { - return "", errors.New("failed to parse PEM block containing the public key") - } - - pub, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return "", fmt.Errorf("failed to parse DER encoded public key: %s", err) - } - - rsaPub, ok := pub.(*rsa.PublicKey) - if !ok { - return "", errors.New("key type is not RSA") - } - - encryptedData, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPub, []byte(content)) - if err != nil { - return "", fmt.Errorf("failed to encrypt with RSA public key: %s", err) - } - - return base64.StdEncoding.EncodeToString(encryptedData), nil + block, _ := pem.Decode([]byte(key)) + if block == nil { + return "", errors.New("failed to parse PEM block containing the public key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", fmt.Errorf("failed to parse DER encoded public key: %s", err) + } + + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return "", errors.New("key type is not RSA") + } + + encryptedData, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPub, []byte(content)) + if err != nil { + return "", fmt.Errorf("failed to encrypt with RSA public key: %s", err) + } + + return base64.StdEncoding.EncodeToString(encryptedData), nil } //go:embed data/templateUsage.yaml diff --git a/pkg/render/template_test.go b/pkg/render/template_test.go index 543fe813..753f7c43 100644 --- a/pkg/render/template_test.go +++ b/pkg/render/template_test.go @@ -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) {