feat: mocking plugin (#5940)

Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com>
Co-authored-by: Peter Zhu <starszcan@gmail.com>
Co-authored-by: Bisakh <bisakhmondal00@gmail.com>
Co-authored-by: 罗泽轩 <spacewanderlzx@gmail.com>
Co-authored-by: EH <jerryrdong@tencent.com>
This commit is contained in:
EH 2022-02-25 14:14:06 +08:00 committed by GitHub
parent bcabe5fa0a
commit a8528ad8ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1047 additions and 2 deletions

224
apisix/plugins/mocking.lua Normal file
View File

@ -0,0 +1,224 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local xml2lua = require("xml2lua")
local json = core.json
local math = math
local ngx = ngx
local ngx_re = ngx.re
local pairs = pairs
local string = string
local table = table
local type = type
local support_content_type = {
["application/xml"] = true,
["application/json"] = true,
["text/plain"] = true,
["text/html"] = true,
["text/xml"] = true
}
local schema = {
type = "object",
properties = {
-- specify response delay time,default 0ms
delay = { type = "integer", default = 0 },
-- specify response status,default 200
response_status = { type = "integer", default = 200, minimum = 100 },
-- specify response content type, support application/xml, text/plain
-- and application/json, default application/json
content_type = { type = "string", default = "application/json;charset=utf8" },
-- specify response body.
response_example = { type = "string" },
-- specify response json schema, if response_example is not nil, this conf will be ignore.
-- generate random response by json schema.
response_schema = { type = "object" },
with_mock_header = { type = "boolean", default = true }
},
anyOf = {
{ required = { "response_example" } },
{ required = { "response_schema" } }
}
}
local _M = {
version = 0.1,
priority = 10900,
name = "mocking",
schema = schema,
}
local function parse_content_type(content_type)
if not content_type then
return ""
end
local m = ngx_re.match(content_type, "([ -~]*);([ -~]*)", "jo")
if m and #m == 2 then
return m[1], m[2]
end
return content_type
end
function _M.check_schema(conf)
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end
local typ = parse_content_type(conf.content_type)
if not support_content_type[typ] then
return false, "unsupported content type!"
end
return true
end
local function gen_string(example)
if example and type(example) == "string" then
return example
end
local n = math.random(1, 10)
local list = {}
for i = 1, n do
table.insert(list, string.char(math.random(97, 122)))
end
return table.concat(list)
end
local function gen_number(example)
if example and type(example) == "number" then
return example
end
return math.random() * 10000
end
local function gen_integer(example)
if example and type(example) == "number" then
return math.floor(example)
end
return math.random(1, 10000)
end
local function gen_boolean(example)
if example and type(example) == "boolean" then
return example
end
local r = math.random(0, 1)
if r == 0 then
return false
end
return true
end
local gen_array, gen_object, gen_by_property
function gen_array(property)
local output = {}
if property.items == nil then
return nil
end
local v = property.items
local n = math.random(1, 3)
for i = 1, n do
table.insert(output, gen_by_property(v))
end
return output
end
function gen_object(property)
local output = {}
if not property.properties then
return output
end
for k, v in pairs(property.properties) do
output[k] = gen_by_property(v)
end
return output
end
function gen_by_property(property)
local typ = string.lower(property.type)
local example = property.example
if typ == "array" then
return gen_array(property)
end
if typ == "object" then
return gen_object(property)
end
if typ == "string" then
return gen_string(example)
end
if typ == "number" then
return gen_number(example)
end
if typ == "integer" then
return gen_integer(example)
end
if typ == "boolean" then
return gen_boolean(example)
end
return nil
end
function _M.access(conf)
local response_content = ""
if conf.response_example then
response_content = conf.response_example
else
local output = gen_object(conf.response_schema)
local typ = parse_content_type(conf.content_type)
if typ == "application/xml" or typ == "text/xml" then
response_content = xml2lua.toXml(output, "data")
elseif typ == "application/json" or typ == "text/plain" then
response_content = json.encode(output)
else
core.log.error("json schema body only support xml and json content type")
end
end
ngx.header["Content-Type"] = conf.content_type
if conf.with_mock_header then
ngx.header["x-mock-by"] = "APISIX/" .. core.version.VERSION
end
if conf.delay > 0 then
ngx.sleep(conf.delay)
end
return conf.response_status, core.utils.resolve_var(response_content)
end
return _M

View File

@ -330,6 +330,7 @@ plugins: # plugin list (sorted by priority)
- zipkin # priority: 11011
- request-id # priority: 11010
- fault-injection # priority: 11000
- mocking # priority: 10900
- serverless-pre-function # priority: 10000
#- batch-requests # priority: 4010
- cors # priority: 4000

View File

@ -52,7 +52,8 @@
"plugins/proxy-rewrite",
"plugins/grpc-transcode",
"plugins/grpc-web",
"plugins/fault-injection"
"plugins/fault-injection",
"plugins/mocking"
]
},
{

View File

@ -0,0 +1,234 @@
---
title: mocking
---
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->
## Summary
- [**Name**](#name)
- [**Attributes**](#attributes)
- [**How To Enable**](#how-to-enable)
- [**Test Plugin**](#test-plugin)
- [**Disable Plugin**](#disable-plugin)
## Name
Mock API plugin, When the plugin is bound, it returns random mock data in the specified format and is no longer forwarded to the upstreams.
## Attributes
| Name | Type | Requirement | Default | Valid | Description |
| ------------- | -------| ----- | ----- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| delay | integer | optional | | | Delay return time, in seconds |
| response_status | integer| optional | 200 | | response http status code |
| content_type | string | optional | application/json | | response header Content-Type。 |
| response_example| string | optional | | | response body |
| response_schema | object | optional | | | The jsonschema object for the response is specified. This property takes effect if the `response_example` is not specified |
| with_mock_header | boolean | optional | true | | Whether to return the response header: "x-mock-by: APISIX/{version}", returned by default, false does not return |
Supported field types: `string`, `number`, `integer`, `boolean`, `object`, `array`
Base data types (`string`, `number`, `integer`, `Boolean`) through configuration example attribute to specify the generated response value, random return not configured.
Here is a `jsonschema` example:
```json
{
"properties":{
"field0":{
"example":"abcd",
"type":"string"
},
"field1":{
"example":123.12,
"type":"number"
},
"field3":{
"properties":{
"field3_1":{
"type":"string"
},
"field3_2":{
"properties":{
"field3_2_1":{
"example":true,
"type":"boolean"
},
"field3_2_2":{
"items":{
"example":155.55,
"type":"integer"
},
"type":"array"
}
},
"type":"object"
}
},
"type":"object"
},
"field2":{
"items":{
"type":"string"
},
"type":"array"
}
},
"type":"object"
}
```
Here are the return objects that might be generated by this `jsonschema`:
```json
{
"field1": 123.12,
"field3": {
"field3_1": "LCFE0",
"field3_2": {
"field3_2_1": true,
"field3_2_2": [
155,
155
]
}
},
"field0": "abcd",
"field2": [
"sC"
]
}
```
## How To Enable
Here, use `route` as an example (`service` is used in the same way) to enable the `mocking` plugin on the specified `route`.
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"plugins": {
"mocking": {
"delay": 1,
"content_type": "application/json",
"response_status": 200,
"response_schema": {
"properties":{
"field0":{
"example":"abcd",
"type":"string"
},
"field1":{
"example":123.12,
"type":"number"
},
"field3":{
"properties":{
"field3_1":{
"type":"string"
},
"field3_2":{
"properties":{
"field3_2_1":{
"example":true,
"type":"boolean"
},
"field3_2_2":{
"items":{
"example":155.55,
"type":"integer"
},
"type":"array"
}
},
"type":"object"
}
},
"type":"object"
},
"field2":{
"items":{
"type":"string"
},
"type":"array"
}
},
"type":"object"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
## Test Plugin
the `mocking` plugin is configured as follows:
```json
{
"delay":0,
"content_type":"",
"with_mock_header":true,
"response_status":201,
"response_example":"{\"a\":1,\"b\":2}"
}
```
Use curl to access:
```shell
$ curl http://127.0.0.1:9080/test-mock -i
HTTP/1.1 201 Created
...
Content-Type: application/json;charset=utf8
x-mock-by: APISIX/2.10.0
...
{"a":1,"b":2}
```
## Disable Plugin
When you want to disable this plugin, it is very simple,
you can delete the corresponding JSON configuration in the plugin configuration,
no need to restart the service, it will take effect immediately:
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
This plugin has been disabled now. It works for other plugins.

View File

@ -52,7 +52,8 @@
"plugins/proxy-rewrite",
"plugins/grpc-transcode",
"plugins/grpc-web",
"plugins/fault-injection"
"plugins/fault-injection",
"plugins/mocking"
]
},
{

View File

@ -0,0 +1,234 @@
---
title: mocking
---
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->
## 目录
- [**简介**](#简介)
- [**属性**](#属性)
- [**如何启用**](#如何启用)
- [**测试插件**](#测试插件)
- [**禁用插件**](#禁用插件)
## 简介
Mock API 插件,绑定该插件后将随机返回指定格式的`mock`数据,不再转发到后端。
## 属性
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ------------- | -------| ----- | ----- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| delay | integer | 可选 | | | 延时返回的时间,单位为秒 |
| response_status | integer| 可选 | 200 | | 返回的响应 http status code |
| content_type | string | 可选 | application/json | | 返回的响应头的 Content-Type。 |
| response_example| string | 可选 | | | 返回的响应体,与`response_schema`字段二选一 |
| response_schema | object | 可选 | | | 指定响应的`jsonschema`对象,未指定`response_example`字段时生效,具体结构看后文说明 |
| with_mock_header | boolean | 可选 | true | | 是否返回响应头:"x-mock-by: APISIX/{version}",默认返回,指定为 false 则不返回 |
支持的字段类型:`string`, `number`, `integer`, `boolean`, `object`, `array`
基础数据类型(`string`,`number`,`integer`,`boolean`)可通过配置`example`属性指定生成的响应值,未配置时随机返回。
以下是一个`jsonschema`实例:
```json
{
"properties":{
"field0":{
"example":"abcd",
"type":"string"
},
"field1":{
"example":123.12,
"type":"number"
},
"field3":{
"properties":{
"field3_1":{
"type":"string"
},
"field3_2":{
"properties":{
"field3_2_1":{
"example":true,
"type":"boolean"
},
"field3_2_2":{
"items":{
"example":155.55,
"type":"integer"
},
"type":"array"
}
},
"type":"object"
}
},
"type":"object"
},
"field2":{
"items":{
"type":"string"
},
"type":"array"
}
},
"type":"object"
}
```
以下为该`jsonschema`可能生成的返回对象:
```json
{
"field1": 123.12,
"field3": {
"field3_1": "LCFE0",
"field3_2": {
"field3_2_1": true,
"field3_2_2": [
155,
155
]
}
},
"field0": "abcd",
"field2": [
"sC"
]
}
```
## 如何启用
这里以`route`为例(`service`的使用是同样的方法),在指定的 `route` 上启用 `mocking` 插件。
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"plugins": {
"mocking": {
"delay": 1,
"content_type": "application/json",
"response_status": 200,
"response_schema": {
"properties":{
"field0":{
"example":"abcd",
"type":"string"
},
"field1":{
"example":123.12,
"type":"number"
},
"field3":{
"properties":{
"field3_1":{
"type":"string"
},
"field3_2":{
"properties":{
"field3_2_1":{
"example":true,
"type":"boolean"
},
"field3_2_2":{
"items":{
"example":155.55,
"type":"integer"
},
"type":"array"
}
},
"type":"object"
}
},
"type":"object"
},
"field2":{
"items":{
"type":"string"
},
"type":"array"
}
},
"type":"object"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
## 测试插件
当`mocking`插件配置如下时:
```json
{
"delay":0,
"content_type":"",
"with_mock_header":true,
"response_status":201,
"response_example":"{\"a\":1,\"b\":2}"
}
```
curl访问将返回如下结果
```shell
$ curl http://127.0.0.1:9080/test-mock -i
HTTP/1.1 201 Created
Date: Fri, 14 Jan 2022 11:49:34 GMT
Content-Type: application/json;charset=utf8
Transfer-Encoding: chunked
Connection: keep-alive
x-mock-by: APISIX/2.10.0
Server: APISIX/2.10.0
{"a":1,"b":2}
```
## 移除插件
当你想去掉`mocking`插件的时候,很简单,在插件的配置中把对应的 json 配置删除即可,无须重启服务,即刻生效:
```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"methods": ["GET"],
"uri": "/index.html",
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
}
}'
```
现在就已经移除了`mocking`插件了。其他插件的开启和移除也是同样的方法。

View File

@ -74,6 +74,7 @@ dependencies = {
"lualdap = 1.2.6-1",
"lua-resty-rocketmq = 0.3.0-0",
"opentelemetry-lua = 0.1-2",
"xml2lua = 1.5-2",
}
build = {

1
t/admin/plugins.t vendored
View File

@ -68,6 +68,7 @@ ext-plugin-pre-req
zipkin
request-id
fault-injection
mocking
serverless-pre-function
cors
ip-restriction

348
t/plugin/mocking.t vendored Normal file
View File

@ -0,0 +1,348 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX 'no_plan';
repeat_each(1);
no_long_string();
no_shuffle();
no_root_location();
add_block_preprocessor(sub {
my ($block) = @_;
if (!$block->request) {
$block->set_value("request", "GET /t");
}
if (!$block->error_log && !$block->no_error_log) {
$block->set_value("no_error_log", "[error]\n[alert]");
}
});
run_tests;
__DATA__
=== TEST 1: set route(return response example:"hello world")
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_example": "hello world"
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 2: hit route(return response example:"hello world")
--- request
GET /hello
--- response_body chomp
hello world
=== TEST 3: set route(return response schema: string case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_schema": {
"type": "object",
"properties": {
"field1":{
"type":"string",
"example":"hello"
}
}
}
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 4: hit route(return response schema: string case)
--- request
GET /hello
--- response_body chomp
{"field1":"hello"}
=== TEST 5: set route(return response schema: integer case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_schema": {
"type": "object",
"properties": {
"field1":{
"type":"integer",
"example":4
}
}
}
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 6: hit route(return response schema: integer case)
--- request
GET /hello
--- response_body chomp
{"field1":4}
=== TEST 7: set route(return response schema: number case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_schema": {
"type": "object",
"properties": {
"field1":{
"type":"number",
"example":5.5
}
}
}
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 8: hit route(return response schema: number case)
--- request
GET /hello
--- response_body chomp
{"field1":5.5}
=== TEST 9: set route(return response schema: boolean case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_schema": {
"type": "object",
"properties": {
"field1":{
"type":"boolean",
"example":true
}
}
}
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 10: hit route(return response schema: boolean case)
--- request
GET /hello
--- response_body chomp
{"field1":true}
=== TEST 11: set route(return response schema: object case)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "text/plain",
"response_status": 200,
"response_schema": {
"type": "object",
"properties": {
"field1":{
"type":"object"
}
}
}
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 12: hit route(return response schema: object case)
--- request
GET /hello
--- response_body chomp
{"field1":{}}
=== TEST 13: set route(return response header: application/json)
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"mocking": {
"delay": 1,
"content_type": "application/json",
"response_status": 200,
"response_example": "{\"field1\":{}}"
}
},
"uri": "/hello"
}]]
)
if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- response_body
passed
=== TEST 14: hit route(return response header: application/json)
--- request
GET /hello
--- response_headers
Content-Type: application/json