diff --git a/docs/site/content/zh/latest/tasks/mock.md b/docs/site/content/zh/latest/tasks/mock.md index 7952175b..cec85f0b 100644 --- a/docs/site/content/zh/latest/tasks/mock.md +++ b/docs/site/content/zh/latest/tasks/mock.md @@ -195,6 +195,35 @@ items: {{end}} ``` +下面是一个根据请求 Payload 给出不同响应的例子: + +```yaml +items: + - name: createBook + request: + path: /v1/books + method: POST + body: | + { + "size": "big" + } + response: + body: | + {{if eq (fromJson .Param._payload).size "big"}} + { + "name": "big book" + } + {{else if eq (fromJson .Param._payload).size "small"}} + { + "name": "small book" + } + {{else}} + { + "name": "unknown book" + } + {{end}} +``` + ## 代理 在实际情况中,往往是向已有系统或平台添加新的 API,此时要 Mock 所有已经存在的 API 就既没必要也需要很多工作量。因此,我们提供了一种简单的方式,即可以增加**代理**的方式把已有的 API 请求转发到实际的地址,只对新增的 API 进行 Mock 处理。如下所示: diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index 0315f5e6..6077ccf0 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -16,719 +16,726 @@ limitations under the License. package mock import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "sort" - "strings" - "sync" - "time" - - jsonpatch "github.com/evanphx/json-patch" - "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" + "os" + "sort" + "strings" + "sync" + "time" + + jsonpatch "github.com/evanphx/json-patch" + "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 - certFile, keyFile string - 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 + certFile, keyFile string + 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) WithTLS(certFile, keyFile string) DynamicServer { - s.certFile = certFile - s.keyFile = keyFile - return s + s.certFile = certFile + s.keyFile = keyFile + return s } func (s *inMemoryServer) WithLogWriter(writer io.Writer) DynamicServer { - if writer != nil { - memLogger = memLogger.WithNameAndWriter("stream", writer) - } - return s + if writer != nil { + memLogger = memLogger.WithNameAndWriter("stream", writer) + } + return s } func (s *inMemoryServer) GetTLS() (string, string) { - return s.certFile, s.keyFile + return s.certFile, s.keyFile } 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 { - continue - } - } - - s.handleOpenAPI() - - for i, proxy := range server.Proxies { - memLogger.Info("start to proxy", "target", proxy.Target) - switch proxy.Protocol { - case "http", "": - s.httpProxy(&proxy) - case "tcp": - s.tcpProxy(&server.Proxies[i]) - default: - memLogger.Error(fmt.Errorf("unsupported protocol: %s", proxy.Protocol), "failed to start proxy") - } - } - 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 { + continue + } + } + + s.handleOpenAPI() + + for i, proxy := range server.Proxies { + memLogger.Info("start to proxy", "target", proxy.Target) + switch proxy.Protocol { + case "http", "": + s.httpProxy(&proxy) + case "tcp": + s.tcpProxy(&server.Proxies[i]) + default: + memLogger.Error(fmt.Errorf("unsupported protocol: %s", proxy.Protocol), "failed to start proxy") + } + } + return } func (s *inMemoryServer) httpProxy(proxy *Proxy) { - s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) { - if !strings.HasSuffix(proxy.Target, "/") { - proxy.Target += "/" - } - targetPath := strings.TrimPrefix(req.URL.Path, s.prefix) - targetPath = strings.TrimPrefix(targetPath, "/") - - apiRaw := fmt.Sprintf("%s%s", proxy.Target, targetPath) - api, err := render.Render("proxy api", apiRaw, s) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to render proxy api", "api", apiRaw) - return - } - memLogger.Info("redirect to", "target", api) - - var requestBody []byte - if requestBody, err = io.ReadAll(req.Body); err != nil { - w.WriteHeader(http.StatusInternalServerError) - } - - if proxy.RequestAmend.BodyPatch != "" && len(requestBody) > 0 { - var patch jsonpatch.Patch - if patch, err = jsonpatch.DecodePatch([]byte(proxy.RequestAmend.BodyPatch)); err != nil { - return - } - - fmt.Println("before patch:", string(requestBody)) - if requestBody, err = patch.Apply(requestBody); err != nil { - fmt.Println(err) - return - } - fmt.Println("after patch:", string(requestBody)) - } - - targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, bytes.NewBuffer(requestBody)) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - memLogger.Error(err, "failed to create proxy request") - return - } - - for k, v := range req.Header { - targetReq.Header.Add(k, v[0]) - } - - 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) - }) + s.mux.HandleFunc(proxy.Path, func(w http.ResponseWriter, req *http.Request) { + if !strings.HasSuffix(proxy.Target, "/") { + proxy.Target += "/" + } + targetPath := strings.TrimPrefix(req.URL.Path, s.prefix) + targetPath = strings.TrimPrefix(targetPath, "/") + + apiRaw := fmt.Sprintf("%s%s", proxy.Target, targetPath) + api, err := render.Render("proxy api", apiRaw, s) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to render proxy api", "api", apiRaw) + return + } + memLogger.Info("redirect to", "target", api) + + var requestBody []byte + if requestBody, err = io.ReadAll(req.Body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + + if proxy.RequestAmend.BodyPatch != "" && len(requestBody) > 0 { + var patch jsonpatch.Patch + if patch, err = jsonpatch.DecodePatch([]byte(proxy.RequestAmend.BodyPatch)); err != nil { + return + } + + fmt.Println("before patch:", string(requestBody)) + if requestBody, err = patch.Apply(requestBody); err != nil { + fmt.Println(err) + return + } + fmt.Println("after patch:", string(requestBody)) + } + + targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, bytes.NewBuffer(requestBody)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + memLogger.Error(err, "failed to create proxy request") + return + } + + for k, v := range req.Header { + targetReq.Header.Add(k, v[0]) + } + + 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) + }) } func (s *inMemoryServer) tcpProxy(proxy *Proxy) { - fmt.Println("start to proxy", proxy.Port) - lisener, err := net.Listen("tcp", fmt.Sprintf(":%d", proxy.Port)) - if err != nil { - memLogger.Error(err, "failed to listen") - return - } - fmt.Printf("proxy local: %d, target: %s\n", proxy.Port, proxy.Target) - defer lisener.Close() - - for { - conn, err := lisener.Accept() - if err != nil { - memLogger.Error(err, "failed to accept") - continue - } - - fmt.Println("accept connection") - go handleConnection(conn, proxy.Target) - } + fmt.Println("start to proxy", proxy.Port) + lisener, err := net.Listen("tcp", fmt.Sprintf(":%d", proxy.Port)) + if err != nil { + memLogger.Error(err, "failed to listen") + return + } + fmt.Printf("proxy local: %d, target: %s\n", proxy.Port, proxy.Target) + defer lisener.Close() + + for { + conn, err := lisener.Accept() + if err != nil { + memLogger.Error(err, "failed to accept") + continue + } + + fmt.Println("accept connection") + go handleConnection(conn, proxy.Target) + } } func handleConnection(clientConn net.Conn, targetAddr string) { - defer clientConn.Close() + defer clientConn.Close() - targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) - if err != nil { - fmt.Printf("Failed to connect to target server: %v\n", err) - return - } - defer targetConn.Close() + targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) + if err != nil { + fmt.Printf("Failed to connect to target server: %v\n", err) + return + } + defer targetConn.Close() - fmt.Printf("Connection established between %s and %s\n", clientConn.RemoteAddr(), targetConn.RemoteAddr()) + fmt.Printf("Connection established between %s and %s\n", clientConn.RemoteAddr(), targetConn.RemoteAddr()) - go io.Copy(clientConn, targetConn) - go io.Copy(targetConn, clientConn) + go io.Copy(clientConn, targetConn) + go io.Copy(targetConn, clientConn) - select {} + select {} } 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() { - if s.certFile != "" && s.keyFile != "" { - if err = http.ServeTLS(s.listener, handler, s.certFile, s.keyFile); err != nil { - memLogger.Error(err, "failed to start TLS mock server") - } - } else { - memLogger.Info("start HTTP mock server") - 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() { + if s.certFile != "" && s.keyFile != "" { + if err = http.ServeTLS(s.listener, handler, s.certFile, s.keyFile); err != nil { + memLogger.Error(err, "failed to start TLS mock server") + } + } else { + memLogger.Info("start HTTP mock server") + 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) { - memLogger.Info("mock server received request", "path", 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) { + memLogger.Info("mock server received request", "path", 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) - - var headerSlices []string - for k, v := range item.Request.Header { - headerSlices = append(headerSlices, k, v) - } - - adHandler := &advanceHandler{ - item: &item, - metrics: s.metrics, - mu: sync.Mutex{}, - } - existedRoute := s.mux.GetRoute(item.Name) - if existedRoute == nil { - s.mux.NewRoute().Name(item.Name).Methods(strings.Split(method, ",")...).Headers(headerSlices...).Path(item.Request.Path).HandlerFunc(adHandler.handle) - } else { - existedRoute.HandlerFunc(adHandler.handle) - } + 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) + } + + adHandler := &advanceHandler{ + item: &item, + metrics: s.metrics, + mu: sync.Mutex{}, + } + existedRoute := s.mux.GetRoute(item.Name) + if existedRoute == nil { + s.mux.NewRoute().Name(item.Name).Methods(strings.Split(method, ",")...).Headers(headerSlices...).Path(item.Request.Path).HandlerFunc(adHandler.handle) + } else { + existedRoute.HandlerFunc(adHandler.handle) + } } type advanceHandler struct { - item *Item - metrics RequestMetrics - mu sync.Mutex + item *Item + metrics RequestMetrics + mu sync.Mutex } func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) { - h.mu.Lock() - defer h.mu.Unlock() - - 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) - - 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()) - 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) - } - - if h.item.Response.BodyFromFile != "" { - // read from file - if data, readErr := os.ReadFile(h.item.Response.BodyFromFile); readErr != nil { - memLogger.Error(readErr, "failed to read file", "file", h.item.Response.BodyFromFile) - } else { - h.item.Response.Body = string(data) - } - } - - 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 if h.item.Response.Encoder == "raw" { - h.item.Response.BodyData = []byte(h.item.Response.Body) - } else { - if h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item); err != nil { - memLogger.Error(err, "failed to render body") - } - } - - if strings.HasPrefix(h.item.Response.Header[util.ContentType], "image/") { - if strings.HasPrefix(string(h.item.Response.BodyData), util.ImageBase64Prefix) { - // decode base64 image data - imgData := strings.TrimPrefix(string(h.item.Response.BodyData), util.ImageBase64Prefix) - if h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(imgData); err != nil { - memLogger.Error(err, "failed to decode base64 image data") - } - } - } - - if err == nil { - h.item.Response.Header[util.ContentLength] = fmt.Sprintf("%d", len(h.item.Response.BodyData)) - w.Header().Set(util.ContentLength, h.item.Response.Header[util.ContentLength]) - } - - writeResponse(w, h.item.Response.BodyData, err) + h.mu.Lock() + defer h.mu.Unlock() + + 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) + + h.item.Param = make(map[string]interface{}) + for k, v := range mux.Vars(req) { + h.item.Param[k] = v + } + payloadBuf := bytes.Buffer{} + if _, copyErr := io.Copy(&payloadBuf, req.Body); copyErr != nil { + memLogger.Error(copyErr, "failed to read request body") + } else { + h.item.Param["_payload"] = payloadBuf.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()) + 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) + } + + if h.item.Response.BodyFromFile != "" { + // read from file + if data, readErr := os.ReadFile(h.item.Response.BodyFromFile); readErr != nil { + memLogger.Error(readErr, "failed to read file", "file", h.item.Response.BodyFromFile) + } else { + h.item.Response.Body = string(data) + } + } + + 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 if h.item.Response.Encoder == "raw" { + h.item.Response.BodyData = []byte(h.item.Response.Body) + } else { + if h.item.Response.BodyData, err = render.RenderAsBytes("start-item", h.item.Response.Body, h.item); err != nil { + memLogger.Error(err, "failed to render body") + } + } + + if strings.HasPrefix(h.item.Response.Header[util.ContentType], "image/") { + if strings.HasPrefix(string(h.item.Response.BodyData), util.ImageBase64Prefix) { + // decode base64 image data + imgData := strings.TrimPrefix(string(h.item.Response.BodyData), util.ImageBase64Prefix) + if h.item.Response.BodyData, err = base64.StdEncoding.DecodeString(imgData); err != nil { + memLogger.Error(err, "failed to decode base64 image data") + } + } + } + + if err == nil { + h.item.Response.Header[util.ContentLength] = fmt.Sprintf("%d", len(h.item.Response.BodyData)) + w.Header().Set(util.ContentLength, h.item.Response.Header[util.ContentLength]) + } + + 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: - if err = runWebhook(s.ctx, s, wh); err != nil { - memLogger.Error(err, "Error when run webhook") - } - } - } - }(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: + if err = runWebhook(s.ctx, s, wh); err != nil { + memLogger.Error(err, "Error when run webhook") + } + } + } + }(webhook) + return } func runWebhook(ctx context.Context, objCtx interface{}, wh *Webhook) (err error) { - rawParams := make(map[string]string, len(wh.Param)) - paramKeys := make([]string, 0, len(wh.Param)) - for k, v := range wh.Param { - paramKeys = append(paramKeys, k) - rawParams[k] = v - } - sort.Strings(paramKeys) - - for _, k := range paramKeys { - v, vErr := render.Render("mock webhook server param", wh.Param[k], wh) - if vErr == nil { - wh.Param[k] = v - } - } - - 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 - } - wh.Param = rawParams - - 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, template: %s", err, wh.Request.Path) - return - } - - switch wh.Request.Protocol { - case "syslog": - err = sendSyslogWebhookRequest(ctx, wh, api, payload) - default: - err = sendHTTPWebhookRequest(ctx, wh, api, payload) - } - return + rawParams := make(map[string]string, len(wh.Param)) + paramKeys := make([]string, 0, len(wh.Param)) + for k, v := range wh.Param { + paramKeys = append(paramKeys, k) + rawParams[k] = v + } + sort.Strings(paramKeys) + + for _, k := range paramKeys { + v, vErr := render.Render("mock webhook server param", wh.Param[k], wh) + if vErr == nil { + wh.Param[k] = v + } + } + + 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 + } + wh.Param = rawParams + + 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, template: %s", err, wh.Request.Path) + return + } + + switch wh.Request.Protocol { + case "syslog": + err = sendSyslogWebhookRequest(ctx, wh, api, payload) + default: + err = sendHTTPWebhookRequest(ctx, wh, api, payload) + } + return } func sendSyslogWebhookRequest(ctx context.Context, wh *Webhook, api string, payload io.Reader) (err error) { - var conn net.Conn - if conn, err = net.Dial("udp", api); err == nil { - _, err = io.Copy(conn, payload) - } - return + var conn net.Conn + if conn, err = net.Dial("udp", api); err == nil { + _, err = io.Copy(conn, payload) + } + return } func sendHTTPWebhookRequest(ctx context.Context, wh *Webhook, api string, payload io.Reader) (err error) { - method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost) - client := http.DefaultClient - - var bearerToken string - bearerToken, err = getBearerToken(ctx, wh.Request) - if err != nil { - memLogger.Error(err, "Error when render bearer token") - return - } - - var req *http.Request - req, err = http.NewRequestWithContext(ctx, method, api, payload) - if err != nil { - memLogger.Error(err, "Error when create request") - return - } - - if bearerToken != "" { - memLogger.V(7).Info("set bearer token", "token", bearerToken) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) - } - - for k, v := range wh.Request.Header { - req.Header.Set(k, v) - } - - memLogger.Info("send webhook request", "api", api) - resp, err := client.Do(req) - if err != nil { - err = fmt.Errorf("error when sending webhook: %v", err) - } else { - if resp.StatusCode != http.StatusOK { - memLogger.Info("unexpected status", "code", resp.StatusCode) - } - - data, _ := io.ReadAll(resp.Body) - memLogger.V(7).Info("received from webhook", "code", resp.StatusCode, "response", string(data)) - } - return + method := util.EmptyThenDefault(wh.Request.Method, http.MethodPost) + client := http.DefaultClient + + var bearerToken string + bearerToken, err = getBearerToken(ctx, wh.Request) + if err != nil { + memLogger.Error(err, "Error when render bearer token") + return + } + + var req *http.Request + req, err = http.NewRequestWithContext(ctx, method, api, payload) + if err != nil { + memLogger.Error(err, "Error when create request") + return + } + + if bearerToken != "" { + memLogger.V(7).Info("set bearer token", "token", bearerToken) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) + } + + for k, v := range wh.Request.Header { + req.Header.Set(k, v) + } + + memLogger.Info("send webhook request", "api", api) + resp, err := client.Do(req) + if err != nil { + err = fmt.Errorf("error when sending webhook: %v", err) + } else { + if resp.StatusCode != http.StatusOK { + memLogger.Info("unexpected status", "code", resp.StatusCode) + } + + 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"` + Token string `json:"token"` } func getBearerToken(ctx context.Context, request RequestWithAuth) (token string, err error) { - if request.BearerAPI == "" { - return - } - - if request.BearerAPI, err = render.Render("bearer token request", request.BearerAPI, &request); err != nil { - 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 + if request.BearerAPI == "" { + return + } + + if request.BearerAPI, err = render.Render("bearer token request", request.BearerAPI, &request); err != nil { + 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 { - if err = s.listener.Close(); err != nil { - memLogger.Error(err, "failed to close listener") - } - } else { - memLogger.Info("listener is nil") - } - if s.cancelFunc != nil { - s.cancelFunc() - } - s.wg.Wait() - return + if s.listener != nil { + if err = s.listener.Close(); err != nil { + memLogger.Error(err, "failed to close listener") + } + } else { + memLogger.Info("listener is nil") + } + if s.cancelFunc != nil { + s.cancelFunc() + } + s.wg.Wait() + return } diff --git a/pkg/mock/in_memory_test.go b/pkg/mock/in_memory_test.go index 591ac5f4..ef1933e3 100644 --- a/pkg/mock/in_memory_test.go +++ b/pkg/mock/in_memory_test.go @@ -16,238 +16,269 @@ limitations under the License. package mock import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "strings" - "testing" - - _ "embed" - "github.com/linuxsuren/api-testing/pkg/util" - "github.com/stretchr/testify/assert" + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + + _ "embed" + "github.com/linuxsuren/api-testing/pkg/util" + "github.com/stretchr/testify/assert" ) //go:embed testdata/api.yaml var mockFile []byte func TestInMemoryServer(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - server.EnableMetrics() + server := NewInMemoryServer(context.Background(), 0) + server.EnableMetrics() - err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock") - assert.NoError(t, err) - defer func() { - server.Stop() - }() + err := server.Start(NewLocalFileReader("testdata/api.yaml"), "/mock") + assert.NoError(t, err) + defer func() { + server.Stop() + }() - api := "http://localhost:" + server.GetPort() + "/mock" + api := "http://localhost:" + server.GetPort() + "/mock" - _, err = http.Post(api+"/team", "", bytes.NewBufferString(`{ + _, err = http.Post(api+"/team", "", bytes.NewBufferString(`{ "name": "test", "members": [] }`)) - assert.NoError(t, err) - - var resp *http.Response - resp, err = http.Get(api + "/team") - if assert.NoError(t, err) { - data, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, `[{"name":"someone"},{"members":[],"name":"test"}]`, string(data)) - } - - t.Run("check the /api.json", func(t *testing.T) { - var resp *http.Response - resp, err = http.Get(api + "/api.json") - if assert.NoError(t, err) { - data, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.NotEmpty(t, string(data)) - } - }) - - t.Run("list with filter", func(t *testing.T) { - var resp *http.Response - resp, err = http.Get(api + "/team?name=someone") - if assert.NoError(t, err) { - data, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, `[{"name":"someone"}]`, string(data)) - } - }) - - t.Run("update object", func(t *testing.T) { - updateReq, err := http.NewRequest(http.MethodPut, api+"/team/test", bytes.NewBufferString(`{ + assert.NoError(t, err) + + var resp *http.Response + resp, err = http.Get(api + "/team") + if assert.NoError(t, err) { + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, `[{"name":"someone"},{"members":[],"name":"test"}]`, string(data)) + } + + t.Run("check the /api.json", func(t *testing.T) { + var resp *http.Response + resp, err = http.Get(api + "/api.json") + if assert.NoError(t, err) { + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NotEmpty(t, string(data)) + } + }) + + t.Run("list with filter", func(t *testing.T) { + var resp *http.Response + resp, err = http.Get(api + "/team?name=someone") + if assert.NoError(t, err) { + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, `[{"name":"someone"}]`, string(data)) + } + }) + + t.Run("update object", func(t *testing.T) { + updateReq, err := http.NewRequest(http.MethodPut, api+"/team/test", bytes.NewBufferString(`{ "name": "test", "members": [{ "name": "rick" }] }`)) - assert.NoError(t, err) - resp, err = http.DefaultClient.Do(updateReq) - assert.NoError(t, err) - }) - - t.Run("get a single object", func(t *testing.T) { - resp, err = http.Get(api + "/team/test") - assert.NoError(t, err) - - var data []byte - data, err = io.ReadAll(resp.Body) - assert.NoError(t, err) - - assert.Equal(t, `{"members":[{"name":"rick"}],"name":"test"}`, string(data)) - }) - - // delete object - delReq, err := http.NewRequest(http.MethodDelete, api+"/team/test", nil) - assert.NoError(t, err) - resp, err = http.DefaultClient.Do(delReq) - assert.NoError(t, err) - - t.Run("check if deleted", func(t *testing.T) { - var resp *http.Response - resp, err = http.Get(api + "/team") - if assert.NoError(t, err) { - data, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, `[{"name":"someone"}]`, string(data)) - } - - resp, err = http.Get(api + "/team/test") - if assert.NoError(t, err) { - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - } - }) - - t.Run("invalid request method", func(t *testing.T) { - delReq, err := http.NewRequest("fake", api+"/team", nil) - assert.NoError(t, err) - resp, err = http.DefaultClient.Do(delReq) - assert.NoError(t, err) - assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) - }) - - t.Run("only accept GET method in getting a single object", func(t *testing.T) { - wrongMethodReq, err := http.NewRequest(http.MethodPut, api+"/team", nil) - assert.NoError(t, err) - resp, err = http.DefaultClient.Do(wrongMethodReq) - assert.NoError(t, err) - assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) - }) - - t.Run("mock item", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, api+"/v1/repos/test/prs", nil) - assert.NoError(t, err) - req.Header.Set("name", "rick") - - resp, err = http.DefaultClient.Do(req) - assert.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, "194", resp.Header.Get(util.ContentLength)) - assert.Equal(t, "mock", resp.Header.Get("Server")) - assert.NotEmpty(t, resp.Header.Get(headerMockServer)) - - data, _ := io.ReadAll(resp.Body) - assert.True(t, strings.Contains(string(data), `"message": "mock"`), string(data)) - }) - - t.Run("miss match header", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, api+"/v1/repos/test/prs", nil) - assert.NoError(t, err) - - resp, err = http.DefaultClient.Do(req) - assert.NoError(t, err) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - }) - - t.Run("base64 encoder", func(t *testing.T) { - resp, err = http.Get(api + "/v1/base64") - assert.NoError(t, err) - data, _ := io.ReadAll(resp.Body) - assert.Equal(t, "hello", string(data)) - }) - - t.Run("read response from file", func(t *testing.T) { - resp, err = http.Get(api + "/v1/readResponseFromFile") - assert.NoError(t, err) - data, _ := io.ReadAll(resp.Body) - assert.Equal(t, mockFile, data) - }) - - t.Run("not found config file", func(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - err := server.Start(NewLocalFileReader("fake"), "/") - assert.Error(t, err) - }) - - t.Run("invalid webhook", func(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - err := server.Start(NewInMemoryReader(`webhooks: + assert.NoError(t, err) + resp, err = http.DefaultClient.Do(updateReq) + assert.NoError(t, err) + }) + + t.Run("get a single object", func(t *testing.T) { + resp, err = http.Get(api + "/team/test") + assert.NoError(t, err) + + var data []byte + data, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + + assert.Equal(t, `{"members":[{"name":"rick"}],"name":"test"}`, string(data)) + }) + + // delete object + delReq, err := http.NewRequest(http.MethodDelete, api+"/team/test", nil) + assert.NoError(t, err) + resp, err = http.DefaultClient.Do(delReq) + assert.NoError(t, err) + + t.Run("check if deleted", func(t *testing.T) { + var resp *http.Response + resp, err = http.Get(api + "/team") + if assert.NoError(t, err) { + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, `[{"name":"someone"}]`, string(data)) + } + + resp, err = http.Get(api + "/team/test") + if assert.NoError(t, err) { + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + } + }) + + t.Run("invalid request method", func(t *testing.T) { + delReq, err := http.NewRequest("fake", api+"/team", nil) + assert.NoError(t, err) + resp, err = http.DefaultClient.Do(delReq) + assert.NoError(t, err) + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + }) + + t.Run("only accept GET method in getting a single object", func(t *testing.T) { + wrongMethodReq, err := http.NewRequest(http.MethodPut, api+"/team", nil) + assert.NoError(t, err) + resp, err = http.DefaultClient.Do(wrongMethodReq) + assert.NoError(t, err) + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + }) + + t.Run("mock item", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, api+"/v1/repos/test/prs", nil) + assert.NoError(t, err) + req.Header.Set("name", "rick") + + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "194", resp.Header.Get(util.ContentLength)) + assert.Equal(t, "mock", resp.Header.Get("Server")) + assert.NotEmpty(t, resp.Header.Get(headerMockServer)) + + data, _ := io.ReadAll(resp.Body) + assert.True(t, strings.Contains(string(data), `"message": "mock"`), string(data)) + }) + + t.Run("miss match header", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, api+"/v1/repos/test/prs", nil) + assert.NoError(t, err) + + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("base64 encoder", func(t *testing.T) { + resp, err = http.Get(api + "/v1/base64") + assert.NoError(t, err) + data, _ := io.ReadAll(resp.Body) + assert.Equal(t, "hello", string(data)) + }) + + t.Run("read response from file", func(t *testing.T) { + resp, err = http.Get(api + "/v1/readResponseFromFile") + assert.NoError(t, err) + data, _ := io.ReadAll(resp.Body) + assert.Equal(t, mockFile, data) + }) + + t.Run("not found config file", func(t *testing.T) { + server := NewInMemoryServer(context.Background(), 0) + err := server.Start(NewLocalFileReader("fake"), "/") + assert.Error(t, err) + }) + + t.Run("invalid webhook", func(t *testing.T) { + server := NewInMemoryServer(context.Background(), 0) + err := server.Start(NewInMemoryReader(`webhooks: - timer: aa name: fake`), "/") - assert.Error(t, err) - }) + assert.Error(t, err) + }) - t.Run("missing name or timer in webhook", func(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - err := server.Start(NewInMemoryReader(`webhooks: + t.Run("missing name or timer in webhook", func(t *testing.T) { + server := NewInMemoryServer(context.Background(), 0) + err := server.Start(NewInMemoryReader(`webhooks: - timer: 1s`), "/") - assert.Error(t, err) - }) + assert.Error(t, err) + }) - t.Run("invalid webhook payload", func(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - err := server.Start(NewInMemoryReader(`webhooks: + t.Run("invalid webhook payload", func(t *testing.T) { + server := NewInMemoryServer(context.Background(), 0) + err := server.Start(NewInMemoryReader(`webhooks: - name: invalid timer: 1ms request: body: "{{.fake"`), "/") - assert.Error(t, err) - }) + assert.Error(t, err) + }) - t.Run("invalid webhook api template", func(t *testing.T) { - server := NewInMemoryServer(context.Background(), 0) - err := server.Start(NewInMemoryReader(`webhooks: + t.Run("invalid webhook api template", func(t *testing.T) { + server := NewInMemoryServer(context.Background(), 0) + err := server.Start(NewInMemoryReader(`webhooks: - name: invalid timer: 1ms request: body: "{}" path: "{{.fake"`), "/") - assert.NoError(t, err) - }) - - t.Run("proxy", func(t *testing.T) { - resp, err = http.Get(api + "/v1/myProjects") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - - resp, err = http.Get(api + "/v1/invalid-template") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - }) - - t.Run("metrics", func(t *testing.T) { - resp, err = http.Get(api + "/metrics") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("go template support in response body", func(t *testing.T) { - repoName := "myRepo" - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v1/repos/%s/prs", api, repoName), nil) - assert.NoError(t, err) - - var resp *http.Response - req.Header.Set("name", "rick") - resp, err = http.DefaultClient.Do(req) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - - data, _ := io.ReadAll(resp.Body) - assert.Contains(t, string(data), repoName) - }) + assert.NoError(t, err) + }) + + t.Run("proxy", func(t *testing.T) { + resp, err = http.Get(api + "/v1/myProjects") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + resp, err = http.Get(api + "/v1/invalid-template") + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + }) + + t.Run("metrics", func(t *testing.T) { + resp, err = http.Get(api + "/metrics") + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) + + t.Run("go template support in response body", func(t *testing.T) { + repoName := "myRepo" + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v1/repos/%s/prs", api, repoName), nil) + assert.NoError(t, err) + + var resp *http.Response + req.Header.Set("name", "rick") + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + data, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(data), repoName) + }) + + t.Run("conditional response body", func(t *testing.T) { + for _, tt := range []struct { + name string + param string + }{ + { + name: "big", + }, + { + name: "small", + }, + { + name: "unknown", + }, + } { + t.Run(tt.name, func(t *testing.T) { + payload := bytes.NewBufferString(fmt.Sprintf(`{"size": "%s"}`, tt.name)) + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v1/books", api), payload) + assert.NoError(t, err) + + var resp *http.Response + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + data, _ := io.ReadAll(resp.Body) + assert.Equal(t, fmt.Sprintf("{\n \"name\": \"%s book\"\n}", tt.name), strings.TrimSpace(string(data))) + }) + } + }) } diff --git a/pkg/mock/testdata/api.yaml b/pkg/mock/testdata/api.yaml index 1e2978fe..b847a882 100644 --- a/pkg/mock/testdata/api.yaml +++ b/pkg/mock/testdata/api.yaml @@ -64,6 +64,25 @@ items: Content-Type: image/png body: | {{ randImage 300 300 }} + - name: createBook + request: + path: /v1/books + method: POST + response: + body: | + {{if eq (fromJson .Param._payload).size "big"}} + { + "name": "big book" + } + {{else if eq (fromJson .Param._payload).size "small"}} + { + "name": "small book" + } + {{else}} + { + "name": "unknown book" + } + {{end}} proxies: - path: /v1/myProjects target: http://localhost:{{.GetPort}} diff --git a/pkg/mock/types.go b/pkg/mock/types.go index 5b0f365b..06b490bb 100644 --- a/pkg/mock/types.go +++ b/pkg/mock/types.go @@ -16,65 +16,65 @@ 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]interface{} } type Request struct { - Protocol string `yaml:"protocol" json:"protocol"` - 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"` + Protocol string `yaml:"protocol" json:"protocol"` + 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"` + 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"` - BodyFromFile string `yaml:"bodyFromFile" json:"bodyFromFile"` - 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"` + BodyFromFile string `yaml:"bodyFromFile" json:"bodyFromFile"` + 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"` - Param map[string]string `yaml:"param" json:"param"` - Request RequestWithAuth `yaml:"request" json:"request"` + Name string `yaml:"name" json:"name"` + Timer string `yaml:"timer" json:"timer"` + Param map[string]string `yaml:"param" json:"param"` + Request RequestWithAuth `yaml:"request" json:"request"` } type Proxy struct { - Prefix string `yaml:"prefix" json:"prefix"` - Port int `yaml:"port" json:"port"` - Path string `yaml:"path" json:"path"` - Target string `yaml:"target" json:"target"` - RequestAmend RequestAmend `yaml:"requestAmend" json:"requestAmend"` - Protocol string `yaml:"protocol" json:"protocol"` + Prefix string `yaml:"prefix" json:"prefix"` + Port int `yaml:"port" json:"port"` + Path string `yaml:"path" json:"path"` + Target string `yaml:"target" json:"target"` + RequestAmend RequestAmend `yaml:"requestAmend" json:"requestAmend"` + Protocol string `yaml:"protocol" json:"protocol"` } type RequestAmend struct { - BodyPatch string `yaml:"bodyPatch" json:"bodyPatch"` + BodyPatch string `yaml:"bodyPatch" json:"bodyPatch"` } 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"` }