diff --git a/docs/api-testing-mock-schema.json b/docs/api-testing-mock-schema.json index 7af68faa..6b2b86fd 100644 --- a/docs/api-testing-mock-schema.json +++ b/docs/api-testing-mock-schema.json @@ -60,11 +60,19 @@ "type": "object", "properties": { "encoder": { - "type": "string" + "type": "string", + "enum": [ + "base64", + "url", + "raw" + ] }, "body": { "type": "string" }, + "bodyFromFile": { + "type": "string" + }, "header": { "type": "object", "description": "HTTP response headers. Common headers include 'Content-Type', 'Cache-Control', 'Set-Cookie', etc.", diff --git a/docs/site/content/zh/latest/tasks/mock.md b/docs/site/content/zh/latest/tasks/mock.md index 37f894fd..7952175b 100644 --- a/docs/site/content/zh/latest/tasks/mock.md +++ b/docs/site/content/zh/latest/tasks/mock.md @@ -102,7 +102,11 @@ items: curl http://localhost:6060/mock/api/v1/repos/atest/prs -v ``` -另外,为了满足复杂的场景,还可以对 Response Body 做特定的解码,目前支持:`base64`、`url`: +#### 编码器 + +另外,为了满足复杂的场景,还可以对 Response Body 做特定的解码,目前支持:`base64`、`url`、`raw`: + +> encoder 为 `raw` 时,表示不进行处理 ```yaml #!api-testing-mock @@ -136,6 +140,63 @@ items: encoder: url ``` +如果你的响应内容比较大,或者保存在一个本地文件中,那么你可以这么写: + +```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: + bodyFromFile: /tmp/baidu.html +``` + +通过下面的方式也可以生成图片: + +```yaml +items: +- name: image + request: + path: /v1/image + response: + header: + Content-Type: image/png + body: | + {{ randImage 300 300 }} +``` + +#### 条件判断 + +对于查询类的 API,通常会接收参数,并根据参数的不同,返回相应的数据。这时候,可以用到条件判断的表达式: + +```yaml +items: + - name: cats + request: + path: /api/v1/cats/{size} + response: + header: + Content-Type: application/json + body: | + {{if eq .Param.size "big"}} + { + "name": "big cat" + } + {{else if eq .Param.size "middle"}} + { + "name": "middle cat" + } + {{else if eq .Param.size "small"}} + { + "name": "small cat" + } + {{end}} +``` + +## 代理 + 在实际情况中,往往是向已有系统或平台添加新的 API,此时要 Mock 所有已经存在的 API 就既没必要也需要很多工作量。因此,我们提供了一种简单的方式,即可以增加**代理**的方式把已有的 API 请求转发到实际的地址,只对新增的 API 进行 Mock 处理。如下所示: ```yaml @@ -160,7 +221,7 @@ proxies: target: http://192.168.123.58:9200 ``` -## TCP 协议代理 +### TCP 协议代理 ```yaml proxies: @@ -170,7 +231,7 @@ proxies: target: 192.168.123.58:33060 ``` -## 代理多个服务 +### 代理多个服务 ```shell atest mock-compose bin/compose.yaml diff --git a/pkg/mock/in_memory.go b/pkg/mock/in_memory.go index 7296aad2..0315f5e6 100644 --- a/pkg/mock/in_memory.go +++ b/pkg/mock/in_memory.go @@ -24,6 +24,7 @@ import ( "io" "net" "net/http" + "os" "sort" "strings" "sync" @@ -450,6 +451,15 @@ func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) { 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) @@ -458,9 +468,21 @@ func (h *advanceHandler) handle(w http.ResponseWriter, req *http.Request) { 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 { - fmt.Printf("failed to render body: %v", err) + 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") + } } } diff --git a/pkg/mock/in_memory_test.go b/pkg/mock/in_memory_test.go index f6977859..591ac5f4 100644 --- a/pkg/mock/in_memory_test.go +++ b/pkg/mock/in_memory_test.go @@ -24,10 +24,14 @@ import ( "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() @@ -167,6 +171,13 @@ func TestInMemoryServer(t *testing.T) { 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"), "/") diff --git a/pkg/mock/server.go b/pkg/mock/server.go index 1ddb05a1..1b034f70 100644 --- a/pkg/mock/server.go +++ b/pkg/mock/server.go @@ -1,5 +1,5 @@ /* -Copyright 2024 API Testing Authors. +Copyright 2024-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. diff --git a/pkg/mock/testdata/api.yaml b/pkg/mock/testdata/api.yaml index 2b9758ad..1e2978fe 100644 --- a/pkg/mock/testdata/api.yaml +++ b/pkg/mock/testdata/api.yaml @@ -30,6 +30,12 @@ items: response: body: aGVsbG8= encoder: base64 + - name: readResponseFromFile + request: + path: /v1/readResponseFromFile + response: + encoder: raw + bodyFromFile: testdata/api.yaml - name: prList request: path: /v1/repos/{repo}/prs @@ -50,6 +56,14 @@ items: "status": "success" }] } + - name: image + request: + path: /v1/image + response: + header: + Content-Type: image/png + body: | + {{ randImage 300 300 }} proxies: - path: /v1/myProjects target: http://localhost:{{.GetPort}} diff --git a/pkg/mock/types.go b/pkg/mock/types.go index 5b9f456f..5b0f365b 100644 --- a/pkg/mock/types.go +++ b/pkg/mock/types.go @@ -44,11 +44,12 @@ type RequestWithAuth struct { } 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"` + 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 { diff --git a/pkg/server/remote_server.go b/pkg/server/remote_server.go index 16788328..8f6d55e6 100644 --- a/pkg/server/remote_server.go +++ b/pkg/server/remote_server.go @@ -1405,7 +1405,9 @@ func (s *mockServerController) Reload(ctx context.Context, in *MockConfig) (repl } server := mock.NewInMemoryServer(ctx, int(in.GetPort())).WithTLS(dServer.GetTLS()) - server.Start(s.mockWriter, in.Prefix) + if err = server.Start(s.mockWriter, in.Prefix); err != nil { + return + } server.WithLogWriter(s) s.loader = server }