diff --git a/docs/api-testing-mock-schema.json b/docs/api-testing-mock-schema.json index d67f3083..296b413e 100644 --- a/docs/api-testing-mock-schema.json +++ b/docs/api-testing-mock-schema.json @@ -10,18 +10,7 @@ "properties": { "name": {"type": "string"}, "initCount": {"type": "integer"}, - "sample": {"type": "string"}, - "fields": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "kind": {"type": "string"} - }, - "required": ["name", "kind"] - } - } + "sample": {"type": "string"} }, "required": ["name"] } @@ -66,6 +55,17 @@ "required": ["name", "request", "response"] } }, + "proxies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "target": {"type": "string"} + }, + "required": ["path", "target"] + } + }, "webhooks": { "type": "array", "items": { diff --git a/docs/site/content/zh/about/index.md b/docs/site/content/zh/about/index.md index 0a5aaab4..32de6487 100644 --- a/docs/site/content/zh/about/index.md +++ b/docs/site/content/zh/about/index.md @@ -5,7 +5,7 @@ linkTitle: 关于 {{% blocks/cover title="About API Testing" height="auto" %}} -API Testing 是一个接口调试和测试工具的开源项目。 +API Testing (atest)是一个接口调试和测试工具的开源项目。 请继续阅读以了解更多信息,或访问我们的 [文档](/latest/) 开始使用! @@ -20,4 +20,3 @@ API Testing 是一个接口调试和测试工具的开源项目。 // TBD. {{% /blocks/section %}} - diff --git a/docs/site/content/zh/latest/tasks/mock-server.md b/docs/site/content/zh/latest/tasks/mock-server.md deleted file mode 100644 index 2e10ccbd..00000000 --- a/docs/site/content/zh/latest/tasks/mock-server.md +++ /dev/null @@ -1,17 +0,0 @@ -+++ -title = "Mock Server 功能使用" -+++ - -## Get started - -您可以通过执行下面的命令 mock 一个容器仓库服务[container registry](https://distribution.github.io/distribution/): - -```shell -atest mock --prefix / mock/image-registry.yaml -``` - -之后,您可以通过使用如下的命令使用 mock 功能。 - -```shell -docker pull localhost:6060/repo/name:tag -``` diff --git a/docs/site/content/zh/latest/tasks/mock.md b/docs/site/content/zh/latest/tasks/mock.md index d025c739..5b988be7 100644 --- a/docs/site/content/zh/latest/tasks/mock.md +++ b/docs/site/content/zh/latest/tasks/mock.md @@ -16,52 +16,76 @@ atest mock --prefix / --port 9090 mock.yaml 在 UI 上可以实现和命令行相同的功能,并可以通过页面编辑的方式修改、加载 Mock 服务配置。 +## Mock Docker Registry + +您可以通过执行下面的命令 mock 一个容器仓库服务[container registry](https://distribution.github.io/distribution/): + +```shell +atest mock --prefix / mock/image-registry.yaml +``` + +之后,您可以通过使用如下的命令使用 mock 功能。 + +```shell +docker pull localhost:6060/repo/name:tag +``` + ## 语法 从整体上来看,我们的写法和 HTTP 的名称基本保持一致,用户无需再了解额外的名词。此外,提供两种描述 Mock 服务的方式: -* 针对某个数据对象的 CRUD -* 任意 HTTP 服务 +* 面向对象的 CRUD +* 自定义 HTTP 服务 -下面是一个具体的例子: +### 面对对象 ```yaml #!api-testing-mock # yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json objects: - - name: repo - fields: - - name: name - kind: string - - name: url - kind: string - name: projects initCount: 3 sample: | { - "name": "api-testing", + "name": "atest", "color": "{{ randEnum "blue" "read" "pink" }}" } +``` + +上面 `projects` 的配置,会自动提供该对象的 CRUD(创建、查找、更新、删除)的 API,你可以通过 `atest` 或类似工具发出 HTTP 请求。例如: + +```shell +curl http://localhost:6060/mock/projects + +curl http://localhost:6060/mock/projects/atest + +curl http://localhost:6060/mock/projects -X POST -d '{"name": "new"}' + +curl http://localhost:6060/mock/projects -X PUT -d '{"name": "new", "remark": "this is a project"}' + +curl http://localhost:6060/mock/projects/atest -X DELETE +``` + +> `initCount` 是指按照 `sample` 给定的数据初始化多少个对象;如果没有指定的话,则默认值为 1. + +### 自定义 + +```yaml +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json items: - - name: base64 - request: - path: /v1/base64 - response: - body: aGVsbG8= - encoder: base64 - name: prList request: - path: /v1/repos/{repo}/prs - header: - name: rick + path: /api/v1/repos/{repo}/prs response: header: server: mock + Content-Type: application/json body: | { "count": 1, "items": [{ - "title": "fix: there is a bug on page {{ randEnum "one" }}", + "title": "fix: there is a bug on page {{ randEnum "one", "two" }}", "number": 123, "message": "{{.Response.Header.server}}", "author": "someone", @@ -69,3 +93,61 @@ items: }] } ``` + +启动 Mock 服务后,我们就可以发起如下的请求: + +```shell +curl http://localhost:6060/mock/api/v1/repos/atest/prs -v +``` + +另外,为了满足复杂的场景,还可以对 Response Body 做特定的解码,目前支持:`base64`、`url`: + +```yaml +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json +items: + - name: base64 + request: + path: /api/v1/base64 + response: + body: aGVsbG8= + encoder: base64 +``` + +上面 Body 的内容是经过 `base64` 编码的,这可以用于不希望直接明文显示,或者是图片的场景: + +```shell +curl http://localhost:6060/mock/api/v1/base64 +``` + +如果你的 Body 内容可以通过另外一个 HTTP 请求(GET)获得,那么你可以这么写: + +```yaml +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json +items: + - name: baidu + request: + path: /api/v1/baidu + response: + body: https://baidu.com + encoder: url +``` + +在实际情况中,往往是向已有系统或平台添加新的 API,此时要 Mock 所有已经存在的 API 就既没必要也需要很多工作量。因此,我们提供了一种简单的方式,即可以增加**代理**的方式把已有的 API 请求转发到实际的地址,只对新增的 API 进行 Mock 处理。如下所示: + +```yaml +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json +proxies: + - path: /api/v1/{part} + target: http://atest.localhost:8080 +``` + +当我们发起如下的请求时,实际请求的地址为 `http://atest.localhost:8080/api/v1/projects` + +```shell +curl http://localhost:6060/mock/api/v1/projects +``` + +> 更多 URL 中通配符的用法,请参考 https://github.com/gorilla/mux diff --git a/docs/mock/image-registry.yaml b/docs/site/content/zh/latest/tasks/mock/image-registry.yaml similarity index 100% rename from docs/mock/image-registry.yaml rename to docs/site/content/zh/latest/tasks/mock/image-registry.yaml diff --git a/docs/site/content/zh/latest/tasks/mock/object.yaml b/docs/site/content/zh/latest/tasks/mock/object.yaml new file mode 100644 index 00000000..be3b9ab6 --- /dev/null +++ b/docs/site/content/zh/latest/tasks/mock/object.yaml @@ -0,0 +1,10 @@ +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json +objects: + - name: projects + initCount: 3 + sample: | + { + "name": "atest", + "color": "{{ randEnum "blue" "read" "pink" }}" + } diff --git a/docs/site/content/zh/latest/tasks/mock/simple.yaml b/docs/site/content/zh/latest/tasks/mock/simple.yaml new file mode 100644 index 00000000..7fea3daa --- /dev/null +++ b/docs/site/content/zh/latest/tasks/mock/simple.yaml @@ -0,0 +1,35 @@ +#!api-testing-mock +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-mock-schema.json +items: + - name: prList + request: + path: /api/v1/repos/{repo}/prs + response: + header: + server: mock + body: | + { + "count": 1, + "items": [{ + "title": "fix: there is a bug on page {{ randEnum "one" }}", + "number": 123, + "message": "{{.Response.Header.server}}", + "author": "someone", + "status": "success" + }] + } + - name: base64 + request: + path: /api/v1/base64 + response: + body: aGVsbG8= + encoder: base64 + - name: baidu + request: + path: /v1/baidu + response: + body: https://baidu.com + encoder: url +proxies: + - path: /api/v1/{part} + target: http://atest.localhost:8080 diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index c51ca5e0..f7750d77 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -48,6 +48,7 @@ type inMemoryServer struct { mux *mux.Router listener net.Listener port int + prefix string wg sync.WaitGroup ctx context.Context cancelFunc context.CancelFunc @@ -69,6 +70,7 @@ func (s *inMemoryServer) SetupHandler(reader Reader, prefix string) (handler htt // init the data s.data = make(map[string][]map[string]interface{}) s.mux = mux.NewRouter().PathPrefix(prefix).Subrouter() + s.prefix = prefix handler = s.mux err = s.Load() return @@ -100,27 +102,56 @@ func (s *inMemoryServer) Load() (err error) { } 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)) + memLogger.Info("redirect to", "target", api) + + targetReq, err := http.NewRequestWithContext(req.Context(), req.Method, api, req.Body) + if err != nil { + memLogger.Error(err, "failed to create proxy request") + return + } + + resp, err := http.DefaultClient.Do(targetReq) + if err != nil { + memLogger.Error(err, "failed to do proxy request") + return + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + 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 { - return - } - - if s.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.port)); err != nil { - return + 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) + }() + } } - go func() { - err = http.Serve(s.listener, handler) - }() return } 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) method := req.Method w.Header().Set(util.ContentType, util.JSON) @@ -134,7 +165,7 @@ func (s *inMemoryServer) startObject(obj Object) { exclude := false for k, v := range req.URL.Query() { - if v == nil || len(v) == 0 { + if len(v) == 0 { continue } @@ -168,32 +199,6 @@ func (s *inMemoryServer) startObject(obj Object) { s.data[obj.Name] = append(s.data[obj.Name], objData) - _, _ = w.Write(data) - } else { - memLogger.Info("failed to read from body", "error", err) - } - case http.MethodDelete: - // delete 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 - } - - for i, item := range s.data[obj.Name] { - if objData["name"] == item["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]) - } - break - } - } - _, _ = w.Write(data) } else { memLogger.Info("failed to read from body", "error", err) @@ -220,33 +225,52 @@ func (s *inMemoryServer) startObject(obj Object) { memLogger.Info("failed to read from body", "error", err) } default: - w.WriteHeader(http.StatusBadRequest) + w.WriteHeader(http.StatusMethodNotAllowed) } }) - // get a single object + // handle a single object s.mux.HandleFunc(fmt.Sprintf("/%s/{name:[a-z]+}", obj.Name), func(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - 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, err := json.Marshal(obj) - writeResponse(w, data, err) - return + 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.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) + } + } }) - return } func (s *inMemoryServer) startItem(item Item) { diff --git a/pkg/mock/in_memory_test.go b/pkg/mock/in_memory_test.go index 5edfe999..b1594471 100644 --- a/pkg/mock/in_memory_test.go +++ b/pkg/mock/in_memory_test.go @@ -95,10 +95,7 @@ func TestInMemoryServer(t *testing.T) { }) // delete object - delReq, err := http.NewRequest(http.MethodDelete, api+"/team", bytes.NewBufferString(`{ - "name": "test", - "members": [] - }`)) + delReq, err := http.NewRequest(http.MethodDelete, api+"/team/test", nil) assert.NoError(t, err) resp, err = http.DefaultClient.Do(delReq) assert.NoError(t, err) @@ -111,6 +108,11 @@ func TestInMemoryServer(t *testing.T) { 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) { @@ -118,11 +120,11 @@ func TestInMemoryServer(t *testing.T) { assert.NoError(t, err) resp, err = http.DefaultClient.Do(delReq) assert.NoError(t, err) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + 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/test", nil) + wrongMethodReq, err := http.NewRequest(http.MethodPut, api+"/team/someone", nil) assert.NoError(t, err) resp, err = http.DefaultClient.Do(wrongMethodReq) assert.NoError(t, err) diff --git a/pkg/mock/types.go b/pkg/mock/types.go index d88248d9..bed7c569 100644 --- a/pkg/mock/types.go +++ b/pkg/mock/types.go @@ -16,15 +16,9 @@ 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"` - Fields []Field `yaml:"fields" json:"fields"` -} - -type Field struct { - Name string `yaml:"name" json:"name"` - Kind string `yaml:"kind" json:"kind"` + Name string `yaml:"name" json:"name"` + InitCount *int `yaml:"initCount" json:"initCount"` + Sample string `yaml:"sample" json:"sample"` } type Item struct { @@ -55,8 +49,14 @@ type Webhook struct { Request Request `yaml:"request" json:"request"` } +type Proxy struct { + 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"` }