Skip to content

Commit

Permalink
feat: support to config mock server on ui (#552)
Browse files Browse the repository at this point in the history
* doc: add document of atest extension

* feat: support to config mock server on ui

* add more document

* Fix code scanning alert no. 61: Incorrect conversion between integer types

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: Rick <[email protected]>

---------

Signed-off-by: Rick <[email protected]>
Co-authored-by: rick <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 7d9a97f commit bbaf91f
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 359 deletions.
26 changes: 13 additions & 13 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func createServerCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c
flags.StringArrayVarP(&opt.mockConfig, "mock-config", "", nil, "The mock config files")
flags.StringVarP(&opt.mockPrefix, "mock-prefix", "", "/mock", "The mock server API prefix")
flags.StringVarP(&opt.extensionRegistry, "extension-registry", "", "docker.io", "The extension registry URL")
flags.DurationVarP(&opt.downloadTimeout, "download-timeout", "", time.Second*10, "The timeout of extension download")

// gc related flags
flags.IntVarP(&opt.gcPercent, "gc-percent", "", 100, "The GC percent of Go")
Expand Down Expand Up @@ -129,6 +130,7 @@ type serverOption struct {
configDir string
skyWalking string
extensionRegistry string
downloadTimeout time.Duration

auth string
oauthProvider string
Expand Down Expand Up @@ -251,6 +253,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {

extDownloader := downloader.NewStoreDownloader()
extDownloader.WithRegistry(o.extensionRegistry)
extDownloader.WithTimeout(o.downloadTimeout)
storeExtMgr := server.NewStoreExtManager(o.execer)
storeExtMgr.WithDownloader(extDownloader)
remoteServer := server.NewRemoteServer(loader, remote.NewGRPCloaderFromStore(), secretServer, storeExtMgr, o.configDir, o.grpcMaxRecvMsgSize)
Expand All @@ -264,9 +267,16 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
}

// create mock server controller
mockInMemoryReader := mock.NewInMemoryReader("")
var mockWriter mock.ReaderAndWriter
if len(o.mockConfig) > 0 {
cmd.Println("currently only one mock config is supported, will take the first one")
mockWriter = mock.NewLocalFileReader(o.mockConfig[0])
} else {
mockWriter = mock.NewInMemoryReader("")
}

dynamicMockServer := mock.NewInMemoryServer(0)
mockServerController := server.NewMockServerController(mockInMemoryReader, dynamicMockServer)
mockServerController := server.NewMockServerController(mockWriter, dynamicMockServer, o.httpPort)

clean := make(chan os.Signal, 1)
signal.Notify(clean, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
Expand Down Expand Up @@ -363,17 +373,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
combineHandlers := server.NewDefaultCombineHandler()
combineHandlers.PutHandler("", mux)

if len(o.mockConfig) > 0 {
cmd.Println("currently only one mock config is supported, will take the first one")
var mockServerHandler http.Handler
if mockServerHandler, err = mock.NewInMemoryServer(0).
SetupHandler(mock.NewLocalFileReader(o.mockConfig[0]), o.mockPrefix); err != nil {
return
}
combineHandlers.PutHandler(o.mockPrefix, mockServerHandler)
}

if handler, hErr := dynamicMockServer.SetupHandler(mockInMemoryReader, o.mockPrefix+"/server"); hErr != nil {
if handler, hErr := dynamicMockServer.SetupHandler(mockWriter, o.mockPrefix+"/server"); hErr != nil {
err = hErr
return
} else {
Expand Down
2 changes: 1 addition & 1 deletion console/atest-ui/src/components/EditButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ElInput } from 'element-plus'
import type { InputInstance } from 'element-plus'
const props = defineProps({
value: String,
value: String || Number,
})
const emit = defineEmits(['changed'])
Expand Down
24 changes: 20 additions & 4 deletions console/atest-ui/src/views/MockManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ import { ref } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { API } from './net';
import {useI18n} from "vue-i18n";
import EditButton from '../components/EditButton.vue'
const { t } = useI18n()
const mockConfig = ref('');
interface MockConfig {
Config: string
Prefix: string
Port: number
}
const mockConfig = ref({} as MockConfig);
const link = ref('')
API.GetMockConfig((d) => {
mockConfig.value = d.Config
mockConfig.value = d
link.value = window.location.origin + d.Prefix + "/api.json"
})
const prefixChanged = (p: string) => {
mockConfig.value.Prefix = p
}
const portChanged = (p: number) => {
mockConfig.value.Port = p
}
const tabActive = ref('yaml')
const insertSample = () => {
mockConfig.value = `objects:
mockConfig.value.Config = `objects:
- name: projects
initCount: 3
sample: |
Expand All @@ -39,10 +51,14 @@ items:
<el-divider direction="vertical" />
<el-link target="_blank" :href="link">{{ link }}</el-link> <!-- Noncompliant -->
</div>
<div>
API Prefix:<EditButton :value="mockConfig.Prefix" @changed="prefixChanged"/>
Port:<EditButton :value="mockConfig.Port" @changed="portChanged"/>
</div>
<div>
<el-tabs v-model="tabActive">
<el-tab-pane label="YAML" name="yaml">
<Codemirror v-model="mockConfig" />
<Codemirror v-model="mockConfig.Config" />
</el-tab-pane>
</el-tabs>
</div>
Expand Down
2 changes: 1 addition & 1 deletion console/atest-ui/src/views/SecretManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { FormInstance } from 'element-plus'
import { API } from './net'
import type { Secret } from './net'
import { UIAPI } from './net-vue'
Expand Down
6 changes: 2 additions & 4 deletions console/atest-ui/src/views/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,13 @@ function GetSuggestedAPIs(name: string,
.then(callback)
}

function ReloadMockServer(config: string) {
function ReloadMockServer(config: any) {
const requestOptions = {
method: 'POST',
headers: {
'X-Auth': getToken()
},
body: JSON.stringify({
Config: config
})
body: JSON.stringify(config)
}
fetch(`/api/v1/mock/reload`, requestOptions)
.then(DefaultResponseProcess)
Expand Down
8 changes: 0 additions & 8 deletions docs/site/content/zh/latest/releases/v0.0.1.md

This file was deleted.

33 changes: 33 additions & 0 deletions docs/site/content/zh/latest/tasks/extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
+++
title = "插件"
+++

`atest` 会把非核心、可扩展的功能以插件(extension)的形式实现。下面介绍有哪些插件,以及如何使用:

> 在不同的系统中,插件有着不同的表述,例如:extension、plugin 等。
| 类型 | 名称 | 描述 |
|------|------|------|
| 存储 | [orm](https://github.com/LinuxSuRen/atest-ext-store-orm) | 保存数据到关系型数据库中,例如:MySQL |
| 存储 | [s3](https://github.com/LinuxSuRen/atest-ext-store-s3) | 保存数据到对象存储中 |
| 存储 | [etcd](https://github.com/LinuxSuRen/atest-ext-store-etcd) | 保存数据到 Etcd 数据库中 |
| 存储 | [git](https://github.com/LinuxSuRen/atest-ext-store-git) | 保存数据到 Git 仓库中 |
| 存储 | [mongodb](https://github.com/LinuxSuRen/atest-ext-store-mongodb) | 保存数据到 MongDB 中 |

> `atest` 也是唯一支持如此丰富的存储的接口开发、测试的开源工具。
## 下载插件

我们建议通过如下的命令来下载插件:

```shell
atest extension orm
```

上面的命令,会识别当前的操作系统,自动下载最新版本的插件。当然,用户可以通过自行编译、手动下载的方式获取插件二进制文件。

`atest` 可以从任意支持 OCI 的镜像仓库中(命令参数说明中给出了支持的镜像服务地址)下载插件,也可以指定下载超时时间:

```shell
atest extension orm --registry ghcr.io --timeout 2ms
```
71 changes: 71 additions & 0 deletions docs/site/content/zh/latest/tasks/mock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
+++
title = "Mock 服务"
+++

Mock 服务在前后端并行开发、系统对接、设备对接场景下能起到非常好的作用,可以极大地降低团队之间、系统之间的耦合度。

用户可以通过命令行终端(CLI)、Web UI 的方式来使用 Mock 服务。

## 命令行

```shell
atest mock --prefix / --port 9090 mock.yaml
```

## Web

在 UI 上可以实现和命令行相同的功能,并可以通过页面编辑的方式修改、加载 Mock 服务配置。

## 语法

从整体上来看,我们的写法和 HTTP 的名称基本保持一致,用户无需再了解额外的名词。此外,提供两种描述 Mock 服务的方式:

* 针对某个数据对象的 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",
"color": "{{ randEnum "blue" "read" "pink" }}"
}
items:
- name: base64
request:
path: /v1/base64
response:
body: aGVsbG8=
encoder: base64
- name: prList
request:
path: /v1/repos/{repo}/prs
header:
name: rick
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"
}]
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ spec:

`ca`为 CA 证书的路径,`key`为与`cert`对应的私钥,这两项填写后代表启用 mTLS。(mTLS 尚未实现)

当`insecure`为`false`时,`cert`和`serverName`为必填项。
当`insecure`为`false`时,`cert`和`serverName`为必填项。
18 changes: 18 additions & 0 deletions docs/site/content/zh/latest/tasks/verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
+++
title = "测试用例验证"
+++

`atest` 采用 https://expr.medv.io 对 HTTP 请求响应的验证,比如:返回的数据列表长度验证、具体值的验证等等。下面给出一些例子:

> 需要注意的是,`data` 指的是 HTTP Response Body(响应体)的 JSON 对象。
## 数组长度判断

```yaml
- name: projectKinds
request:
api: /api/resources/projectKinds
expect:
verify:
- len(data.data) == 6
```
2 changes: 2 additions & 0 deletions pkg/mock/in_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ func (s *inMemoryServer) GetPort() string {
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()
Expand Down
10 changes: 8 additions & 2 deletions pkg/mock/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ package mock

import (
"errors"
"github.com/linuxsuren/api-testing/docs"
"os"

"github.com/linuxsuren/api-testing/docs"

"gopkg.in/yaml.v3"
)

Expand All @@ -37,7 +38,7 @@ type localFileReader struct {
data []byte
}

func NewLocalFileReader(file string) Reader {
func NewLocalFileReader(file string) ReaderAndWriter {
return &localFileReader{file: file}
}

Expand All @@ -48,6 +49,11 @@ func (r *localFileReader) Parse() (server *Server, err error) {
return
}

func (r *localFileReader) Write(data []byte) {
r.data = data
_ = os.WriteFile(r.file, r.data, 0644)
}

func (r *localFileReader) GetData() []byte {
return r.data
}
Expand Down
38 changes: 31 additions & 7 deletions pkg/server/remote_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"path/filepath"
reflect "reflect"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -1243,28 +1244,51 @@ func (s *server) PProf(ctx context.Context, in *PProfRequest) (reply *PProfData,
// Start starts the mock server
type mockServerController struct {
UnimplementedMockServer
mockWriter mock.ReaderAndWriter
loader mock.Loadable
reader mock.Reader
mockWriter mock.ReaderAndWriter
loader mock.Loadable
reader mock.Reader
prefix string
combinePort int
}

func NewMockServerController(mockWriter mock.ReaderAndWriter, loader mock.Loadable) MockServer {
func NewMockServerController(mockWriter mock.ReaderAndWriter, loader mock.Loadable, combinePort int) MockServer {
return &mockServerController{
mockWriter: mockWriter,
loader: loader,
mockWriter: mockWriter,
loader: loader,
prefix: "/mock/server",
combinePort: combinePort,
}
}

func (s *mockServerController) Reload(ctx context.Context, in *MockConfig) (reply *Empty, err error) {
s.mockWriter.Write([]byte(in.Config))
s.prefix = in.Prefix
if dServer, ok := s.loader.(mock.DynamicServer); ok && dServer.GetPort() != strconv.Itoa(int(in.GetPort())) {
if strconv.Itoa(s.combinePort) != dServer.GetPort() {
if stopErr := dServer.Stop(); stopErr != nil {
remoteServerLogger.Info("failed to stop old server", "error", stopErr)
} else {
remoteServerLogger.Info("old server stopped", "port", dServer.GetPort())
}
}

server := mock.NewInMemoryServer(int(in.GetPort()))
server.Start(s.mockWriter, in.Prefix)
s.loader = server
}
err = s.loader.Load()
return
}
func (s *mockServerController) GetConfig(ctx context.Context, in *Empty) (reply *MockConfig, err error) {
reply = &MockConfig{
Prefix: "/mock/server",
Prefix: s.prefix,
Config: string(s.mockWriter.GetData()),
}
if dServer, ok := s.loader.(mock.DynamicServer); ok {
if port, pErr := strconv.ParseInt(dServer.GetPort(), 10, 32); pErr == nil {
reply.Port = int32(port)
}
}
return
}

Expand Down
Loading

0 comments on commit bbaf91f

Please sign in to comment.