feat: merge dev

This commit is contained in:
suxiaoxin 2017-10-27 15:16:44 +08:00
commit 2cfd28bd81
65 changed files with 2094 additions and 511 deletions

View File

@ -1,6 +1,9 @@
## 1.1.3
### Bug Fixed
* 修复了切换集合环境的 Bug
* 修复了 mockServer 拿不到 Post 请求 Body
* 修复了接口调试 pathParams 无法使用 mock 参数和变量参数
## 1.1.2

View File

@ -26,12 +26,14 @@ YApi 是<strong>高效</strong>、<strong>易用</strong>、<strong>功能强大
#### 升级
升级项目版本是非常容易的,并且不会影响已有的项目数据,只会同步 vendors 目录下的源码文件。
cd {项目目录}
yapi ls //查看版本号列表
yapi update //更新到最新版本
yapi update -v {Version} //更新到指定版本
### 在线 Demo
<p><a target="_blank" href="http://yapi.demo.qunar.com">yapi.demo.qunar.com</a></p>

View File

@ -1,7 +1,9 @@
import React from 'react';
import moment from 'moment';
import constants from './constants/variable';
import constants from './constants/variable'
import Mock from 'mockjs'
import json5 from 'json5'
import MockExtra from 'common/mock-extra.js'
const Roles = {
0 : 'admin',
@ -194,6 +196,10 @@ function handleMockWord(word) {
return Mock.mock(word);
}
exports.getMockText = (mockTpl) => {
return JSON.stringify(Mock.mock(MockExtra(json5.parse(mockTpl), {})), null, " ")
}
/**
* 合并后新的对象属性与 Obj 一致nextObj 有对应属性则取 nextObj 属性值否则取 Obj 属性值
* @param {Object} Obj 旧对象

View File

@ -11,6 +11,7 @@ const MockExtra = require('common/mock-extra.js')
import './Postman.scss';
import json5 from 'json5'
import { handleMockWord, isJson } from '../../common.js'
import _ from "underscore"
function json_parse(data) {
try {
@ -137,7 +138,6 @@ export default class Run extends Component {
case_env = '',
test_status = '',
test_res_body = '',
test_report = [],
test_res_header = '',
mock_verify = false
} = data;
@ -174,7 +174,7 @@ export default class Run extends Component {
bodyType: req_body_type || 'form',
loading: false,
test_status: test_status,
validRes: test_report,
validRes: [],
res: test_res_body,
resHeader: test_res_header,
resMockTest: mock_verify
@ -197,7 +197,7 @@ export default class Run extends Component {
return;
}
const { headers, bodyForm, pathParam, bodyOther, caseEnv, domains, method, pathname, query, bodyType } = this.state;
const urlObj = URL.parse(domains.find(item => item.name === caseEnv).domain);
const urlObj = URL.parse(_.find(domains, item => item.name === caseEnv).domain);
let path = pathname
pathParam.forEach(item => {
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
@ -236,6 +236,7 @@ export default class Run extends Component {
data: reqBody,
files: bodyType === 'form' ? this.getFiles(bodyForm) : {},
file: bodyType === 'file' ? 'single-file' : null,
timeout: 8240000, //因浏览器限制,超时时间最多为两分钟
success: (res, header, third) => {
console.log('suc', third);
this.setState({
@ -377,6 +378,7 @@ export default class Run extends Component {
}
pathParam[index].name = v;
} else {
pathParam[index].value = v;
}
this.setState({ pathParam, pathname: newPathname });
@ -569,7 +571,8 @@ export default class Run extends Component {
let isResJson = isJsonData(resHeader);
let path = pathname;
pathParam.forEach(item => {
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
let val = handleMockWord(item.value);
path = path.replace(`:${item.name}`, val || `:${item.name}`);
});
const search = decodeURIComponent(URL.format({ query: this.getQueryObj(query) }));

View File

@ -164,5 +164,10 @@ export default {
{ name: '挑选(枚举)', mock: '@pick' },
{ name: '打乱数组', mock: '@shuffle' },
{ name: '协议', mock: '@protocol' }
]
],
IP_REGEXP: /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/,
docHref: {
adv_mock_case: 'https://yapi.ymfe.org/adv_mock.html#Mock_期望',
adv_mock_script: 'https://yapi.ymfe.org/adv_mock.html#自定义_Mock_脚本'
}
}

View File

@ -18,6 +18,7 @@ import {
setGroupList,
fetchGroupMsg
} from '../../../reducer/modules/group.js'
import _ from 'underscore'
import './GroupList.scss';
@ -185,7 +186,8 @@ export default class GroupList extends Component {
@autobind
selectGroup(e) {
const groupId = e.key;
const currGroup = this.props.groupList.find((group) => { return +group._id === +groupId });
//const currGroup = this.props.groupList.find((group) => { return +group._id === +groupId });
const currGroup = _.find(this.props.groupList, (group) => { return +group._id === +groupId });
this.props.setCurrGroup(currGroup);
this.props.history.replace(`${currGroup._id}`);
this.props.fetchNewsData(groupId, "group", 1, 10)

View File

@ -76,8 +76,8 @@ export default class InterfaceCaseContent extends Component {
let currColId = this.getColId(result.payload.data.data, currCaseId);
this.props.history.push('/project/' + params.id + '/interface/case/' + currCaseId)
await this.props.fetchCaseData(currCaseId)
this.props.setColData({currCaseId: +currCaseId, currColId, isShowCol: false})
this.setState({editCasename: this.props.currCase.casename})
this.props.setColData({ currCaseId: +currCaseId, currColId, isShowCol: false })
this.setState({ editCasename: this.props.currCase.casename })
}
async componentWillReceiveProps(nextProps) {
@ -87,8 +87,8 @@ export default class InterfaceCaseContent extends Component {
let currColId = this.getColId(interfaceColList, newCaseId);
if (oldCaseId !== newCaseId) {
await this.props.fetchCaseData(newCaseId);
this.props.setColData({currCaseId: +newCaseId, currColId, isShowCol: false})
this.setState({editCasename: this.props.currCase.casename})
this.props.setColData({ currCaseId: +newCaseId, currColId, isShowCol: false })
this.setState({ editCasename: this.props.currCase.casename })
}
}
@ -97,7 +97,7 @@ export default class InterfaceCaseContent extends Component {
}
updateCase = async () => {
const {
caseEnv: case_env,
pathname: path,
@ -110,9 +110,9 @@ export default class InterfaceCaseContent extends Component {
bodyOther: req_body_other,
resMockTest: mock_verify
} = this.postman.state;
const {editCasename: casename} = this.state;
const {_id: id} = this.props.currCase;
const { editCasename: casename } = this.state;
const { _id: id } = this.props.currCase;
let params = {
id,
casename,
@ -127,15 +127,14 @@ export default class InterfaceCaseContent extends Component {
req_body_other,
mock_verify
};
if(this.postman.state.test_status !== 'error'){
if (this.postman.state.test_status !== 'error') {
params.test_res_body = this.postman.state.res;
params.test_report = this.postman.state.validRes;
params.test_status = this.postman.state.test_status;
params.test_res_header = this.postman.state.resHeader;
}
if(params.test_res_body && typeof params.test_res_body === 'object'){
if (params.test_res_body && typeof params.test_res_body === 'object') {
params.test_res_body = JSON.stringify(params.test_res_body, null, ' ');
}
@ -167,30 +166,18 @@ export default class InterfaceCaseContent extends Component {
render() {
const { currCase, currProject } = this.props;
const { isEditingCasename, editCasename } = this.state;
const data = Object.assign({}, currCase, currProject, {_id: currCase._id});
const data = Object.assign({}, currCase, currProject, { _id: currCase._id });
return (
<div style={{padding: '6px 0'}} className="case-content">
<div style={{ padding: '6px 0' }} className="case-content">
<div className="case-title">
{!isEditingCasename && <Tooltip title="点击编辑" placement="bottom"><div className="case-name" onClick={this.triggerEditCasename}>
{currCase.casename}
</div></Tooltip>}
{isEditingCasename && <div className="edit-case-name">
<Input value={editCasename} onChange={e => this.setState({editCasename: e.target.value})} style={{fontSize: 18}} />
{/*<Button
title="Enter"
onClick={this.saveCasename}
type="primary"
style={{ marginLeft: 8 }}
>保存</Button>
<Button
title="Esc"
onClick={this.cancelEditCasename}
type="primary"
style={{ marginLeft: 8 }}
>取消</Button>*/}
<Input value={editCasename} onChange={e => this.setState({ editCasename: e.target.value })} style={{ fontSize: 18 }} />
</div>}
<span className="inter-link" style={{margin: '0px 8px 0px 6px', fontSize: 12}}>
<span className="inter-link" style={{ margin: '0px 8px 0px 6px', fontSize: 12 }}>
<Link className="text" to={`/project/${currProject._id}/interface/api/${currCase.interface_id}`}>对应接口</Link>
</span>
</div>

View File

@ -4,11 +4,12 @@ import PropTypes from 'prop-types'
import { withRouter } from 'react-router'
import { Link } from 'react-router-dom'
import constants from '../../../../constants/variable.js'
import { Tooltip, Icon, Button, Spin, Modal, message ,Select} from 'antd'
import { Tooltip, Icon, Button, Spin, Modal, message, Select, Switch } from 'antd'
import { fetchInterfaceColList, fetchCaseList, setColData } from '../../../../reducer/modules/interfaceCol'
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
import { isJson, handleMockWord, simpleJsonPathParse } from '../../../../common.js'
import mockEditor from '../InterfaceList/mockEditor';
import * as Table from 'reactabular-table';
import * as dnd from 'reactabular-dnd';
import * as resolve from 'table-resolver';
@ -17,6 +18,8 @@ import URL from 'url';
import Mock from 'mockjs'
import json5 from 'json5'
import CaseReport from './CaseReport.js'
import _ from 'underscore'
const MockExtra = require('common/mock-extra.js')
const Option = Select.Option;
function json_parse(data) {
@ -28,6 +31,14 @@ function json_parse(data) {
}
const HTTP_METHOD = constants.HTTP_METHOD;
function handleReport(json){
try{
return JSON.parse(json);
}catch(e){
return {};
}
}
@connect(
state => {
@ -74,7 +85,10 @@ class InterfaceColContent extends Component {
visible: false,
curCaseid: null,
hasPlugin: false,
currColEnv: ''
currColEnv: '',
advVisible: false,
curScript: '',
enableScript: false
};
this.onRow = this.onRow.bind(this);
this.onMoveRow = this.onMoveRow.bind(this);
@ -85,12 +99,15 @@ class InterfaceColContent extends Component {
let { currColId } = this.props;
const params = this.props.match.params;
const { actionId } = params;
currColId = +actionId ||
result.payload.data.data.find(item => +item._id === +currColId) && +currColId ||
this.currColId = currColId = +actionId ||
result.payload.data.data[0]._id;
this.props.history.push('/project/' + params.id + '/interface/col/' + currColId)
if (currColId && currColId != 0) {
await this.props.fetchCaseList(currColId);
let result = await this.props.fetchCaseList(currColId);
if(result.payload.data.errcode === 0){
this.reports = handleReport(result.payload.data.colData.test_report);
}
this.props.setColData({ currColId: +currColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
@ -112,6 +129,7 @@ class InterfaceColContent extends Component {
})
}
}, 500)
}
componentWillUnmount() {
@ -119,21 +137,16 @@ class InterfaceColContent extends Component {
}
handleColdata = (rows) => {
let { currColEnv } = this.state;
const colEnv = this.props.currProject.env;
currColEnv = currColEnv || colEnv[0].name;
rows = rows.map((item) => {
item.id = item._id;
item._test_status = item.test_status;
item.case_env = currColEnv;
return item;
})
rows = rows.sort((n, o) => {
return n.index - o.index;
})
this.setState({
rows: rows,
currColEnv
rows: rows
})
}
@ -161,20 +174,20 @@ class InterfaceColContent extends Component {
status = 'error';
result = e;
}
let query = this.arrToObj(curitem.req_query);
if(!query || typeof query !== 'object'){
if (!query || typeof query !== 'object') {
query = {};
}
let body = {};
if(HTTP_METHOD[curitem.method].request_body){
if(curitem.req_body_type === 'form'){
if (HTTP_METHOD[curitem.method].request_body) {
if (curitem.req_body_type === 'form') {
body = this.arrToObj(curitem.req_body_form);
}else {
} else {
body = isJson(curitem.req_body_other);
}
if(!body || typeof body !== 'object'){
if (!body || typeof body !== 'object') {
body = {};
}
}
@ -193,24 +206,29 @@ class InterfaceColContent extends Component {
rows: newRows
})
}
await axios.post('/api/col/up_col', { col_id: this.props.currColId, test_report: JSON.stringify(this.reports) })
}
handleTest = (interfaceData) => {
handleTest = async (interfaceData) => {
const { currProject } = this.props;
const { case_env } = interfaceData;
let { case_env } = interfaceData;
let path = URL.resolve(currProject.basepath, interfaceData.path);
interfaceData.req_params = interfaceData.req_params || [];
interfaceData.req_params.forEach(item => {
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
let val = this.handleValue(item.value);
path = path.replace(`:${item.name}`, val || `:${item.name}`);
});
const domains = currProject.env.concat();
let currDomain = domains.find(item => item.name === case_env);
if(!currDomain){
case_env = this.state.currColEnv ? this.state.currColEnv : case_env ;
let currDomain = _.find(domains, item => item.name === case_env);
if (!currDomain) {
currDomain = domains[0];
}
const urlObj = URL.parse(currDomain.domain);
if(urlObj.pathname){
if(urlObj.pathname[urlObj.pathname.length - 1] !== '/'){
if (urlObj.pathname) {
if (urlObj.pathname[urlObj.pathname.length - 1] !== '/') {
urlObj.pathname += '/'
}
}
@ -222,89 +240,132 @@ class InterfaceColContent extends Component {
query: this.getQueryObj(interfaceData.req_query)
});
let result = { code: 400, msg: '数据异常', validRes: [] };
let that = this;
return new Promise((resolve, reject) => {
let result = { code: 400, msg: '数据异常', validRes: [] };
let that = this;
result.url = href;
result.method = interfaceData.method;
result.headers = that.getHeadersObj(interfaceData.req_headers);
if(interfaceData.req_body_type === 'form'){
result.body = that.arrToObj(interfaceData.req_body_form)
}else{
let reqBody = isJson(interfaceData.req_body_other);
if(reqBody === false){
result.body = this.handleValue(interfaceData.req_body_other)
}else{
result.body = JSON.stringify(this.handleJson(reqBody))
}
result.url = href;
result.method = interfaceData.method;
result.headers = that.getHeadersObj(interfaceData.req_headers);
if (interfaceData.req_body_type === 'form') {
result.body = that.arrToObj(interfaceData.req_body_form)
} else {
let reqBody = isJson(interfaceData.req_body_other);
if (reqBody === false) {
result.body = this.handleValue(interfaceData.req_body_other)
} else {
result.body = JSON.stringify(this.handleJson(reqBody))
}
window.crossRequest({
}
try{
let data =await this.crossRequest({
url: href,
method: interfaceData.method,
headers: that.getHeadersObj(interfaceData.req_headers),
data: result.body,
success: (res, header) => {
res = json_parse(res);
result.res_header = header;
result.res_body = res;
if (res && typeof res === 'object') {
let tpl = MockExtra(json_parse(interfaceData.res_body), {
query: interfaceData.req_query,
body: interfaceData.req_body_form
})
let validRes = [];
if (interfaceData.mock_verify) {
validRes = Mock.valid(tpl, res);
}
if (validRes.length === 0) {
result.code = 0;
result.validRes = [{ message: '验证通过' }];
resolve(result);
} else if (validRes.length > 0) {
result.code = 1;
result.validRes = validRes;
resolve(result)
}
} else {
reject(result)
}
},
error: (err, header) => {
try {
err = json_parse(err);
} catch (e) {
console.log(e)
}
err = err || '请求异常';
result.code = 400;
result.res_header = header;
result.res_body = err;
reject(result)
}
data: result.body
})
let res = data.res.body = json_parse(data.res.body);
let header = data.res.header;
result.res_header = header;
result.res_body = res;
let validRes = [];
if (res && typeof res === 'object') {
if (interfaceData.mock_verify) {
let tpl = MockExtra(json_parse(interfaceData.res_body), {
query: interfaceData.req_query,
body: interfaceData.req_body_form
})
validRes = Mock.valid(tpl, res);
}
}
let responseData = Object.assign({}, {
status:data.res.status,
body: res,
header: data.res.header,
statusText: data.res.statusText
})
await that.handleScriptTest(interfaceData, responseData, validRes);
if (validRes.length === 0) {
result.code = 0;
result.validRes = [{ message: '验证通过' }];
} else if (validRes.length > 0) {
result.code = 1;
result.validRes = validRes;
}
return result;
}catch(data){
if(data.err){
data.err = data.err || '请求异常';
try {
data.err = json_parse( data.err);
} catch (e) {
console.log(e)
}
result.res_body = data.err;
result.res_header = data.header;
}else{
result.res_body = data.message;
}
result.code = 400;
return result;
}
}
crossRequest = (options)=>{
return new Promise((resolve, reject)=>{
options.success = function(res, header, data){
resolve(data);
}
options.error = function(err, header){
reject({
err,
header
})
}
window.crossRequest(options);
})
}
handleJson = (data)=>{
if(!data){
//response, validRes
handleScriptTest =async (interfaceData,response, validRes)=>{
if(interfaceData.enable_script !== true){
return null;
}
try{
let test = await axios.post('/api/col/run_script', {
response: response,
records: this.records,
script: interfaceData.test_script
})
if(test.data.errcode !== 0){
validRes.push({
message: test.data.data[0]
})
}
}catch(err){
console.log(err);
validRes.push({
message: err.message
})
}
}
handleJson = (data) => {
if (!data) {
return data;
}
if(typeof data === 'string'){
if (typeof data === 'string') {
return this.handleValue(data);
}else if(typeof data === 'object'){
for(let i in data){
} else if (typeof data === 'object') {
for (let i in data) {
data[i] = this.handleJson(data[i]);
}
}else{
} else {
return data;
}
return data;
return data;
}
handleValue = (val) => {
@ -329,7 +390,7 @@ class InterfaceColContent extends Component {
return obj;
}
getQueryObj = (query) => {
query = query || [];
@ -370,7 +431,6 @@ class InterfaceColContent extends Component {
index: index
})
})
axios.post('/api/col/up_col_index', changes).then()
if (rows) {
this.setState({ rows });
@ -378,17 +438,12 @@ class InterfaceColContent extends Component {
}
async componentWillReceiveProps(nextProps) {
const { interfaceColList } = nextProps;
const { actionId: oldColId, id } = this.props.match.params
let newColId = nextProps.match.params.actionId
if (!interfaceColList.find(item => +item._id === +newColId)&&interfaceColList[0]._id) {
this.props.history.push('/project/' + id + '/interface/col/' + interfaceColList[0]._id)
} else if ((oldColId !== newColId) || interfaceColList !== this.props.interfaceColList) {
if (newColId && newColId != 0) {
await this.props.fetchCaseList(newColId);
this.props.setColData({ currColId: +newColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
let newColId = !isNaN(nextProps.match.params.actionId) ? +nextProps.match.params.actionId : 0;
if(newColId && this.currColId && newColId !== this.currColId){
this.currColId = newColId;
await this.props.fetchCaseList(newColId);
this.props.setColData({ currColId: +newColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
}
@ -400,7 +455,57 @@ class InterfaceColContent extends Component {
visible: true,
curCaseid: id
})
}
openAdv = (id) => {
let findCase = _.find(this.props.currCaseList, item=> item.id === id)
this.setState({
enableScript: findCase.enable_script,
curScript: findCase.test_script,
advVisible: true,
curCaseid: id
}, () => {
let that = this;
if(that.Editor){
that.Editor.setValue(this.state.curScript);
}else{
that.Editor = mockEditor({
container: 'case-script',
data: this.state.curScript,
onChange: function (d) {
that.setState({
curScript: d.text
})
}
})
}
})
}
handleAdvCancel = () => {
this.setState({
advVisible: false
});
}
handleAdvOk = async () => {
const {curCaseid, enableScript, curScript} = this.state;
const res = await axios.post('/api/col/up_case', {
id: curCaseid,
test_script: curScript,
enable_script: enableScript
});
if(res.data.errcode === 0){
message.success('更新成功');
}
this.setState({
advVisible: false
});
let currColId = this.currColId;
await this.props.fetchCaseList(currColId);
this.props.setColData({ currColId: +currColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
handleCancel = () => {
@ -411,7 +516,7 @@ class InterfaceColContent extends Component {
colEnvChange = (envName) => {
let rows = [...this.state.rows];
for(var i in rows){
for (var i in rows) {
rows[i].case_env = envName;
}
this.setState({
@ -470,15 +575,19 @@ class InterfaceColContent extends Component {
},
cell: {
formatters: [(value, { rowData }) => {
switch (rowData.test_status) {
case 'ok':
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
case 'error':
let id = rowData._id;
let code = this.reports[id] ? this.reports[id].code : 0;
if(rowData.test_status === 'loading'){
return <div ><Spin /></div>
}
switch (code) {
case 0:
return <div ><Tooltip title="Pass"><Icon style={{ color: '#00a854' }} type="check-circle" /></Tooltip></div>
case 400:
return <div ><Tooltip title="请求异常"><Icon type="info-circle" style={{ color: '#f04134' }} /></Tooltip></div>
case 'invalid':
return <div ><Tooltip title="返回数据校验未通过"><Icon type="exclamation-circle" style={{ color: '#ffbf00' }} /></Tooltip></div>
case 'loading':
return <div ><Spin /></div>
case 1:
return <div ><Tooltip title="验证失败"><Icon type="exclamation-circle" style={{ color: '#ffbf00' }} /></Tooltip></div>
default:
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
}
@ -501,22 +610,29 @@ class InterfaceColContent extends Component {
}
]
}
}, {
},
{
header: {
label: '测试报告'
label: '操作'
},
props: {
style: {
width: '100px'
width: '200px'
}
},
cell: {
formatters: [(text, { rowData }) => {
if (!this.reports[rowData.id]) {
return null;
let reportFun = () => {
if (!this.reports[rowData.id]) {
return null;
}
return <Button onClick={() => this.openReport(rowData.id)}>测试报告</Button>
}
return <Button onClick={() => this.openReport(rowData.id)}>报告</Button>
return <div className="interface-col-table-action">
<Button onClick={() => this.openAdv(rowData.id)} type="primary">高级</Button>
{reportFun()}
</div>
}]
}
}
@ -542,16 +658,17 @@ class InterfaceColContent extends Component {
<Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip>
</a></h2>
<div style={{ display: 'inline-block', margin: 0, marginBottom: '16px' }}>
<Select value={currColEnv} style={{ width: "320px" }} onChange={this.colEnvChange}>
<Select value={currColEnv} style={{ width: "320px" }} onChange={this.colEnvChange}>
<Option key="default" value="" >默认使用 case 详情页面保存的 domain</Option>
{
colEnv.map((item)=>{
return <Option key={item._id} value={item.name}>{item.name+": "+item.domain}</Option>;
colEnv.map((item) => {
return <Option key={item._id} value={item.name}>{item.name + ": " + item.domain}</Option>;
})
}
</Select>
</div>
{this.state.hasPlugin?
<Button type="primary" style={{ float: 'right' }} onClick={this.executeTests}>开始测试</Button>:
{this.state.hasPlugin ?
<Button type="primary" style={{ float: 'right' }} onClick={this.executeTests}>开始测试</Button> :
<Tooltip title="请安装 cross-request Chrome 插件">
<Button disabled type="primary" style={{ float: 'right' }} >开始测试</Button>
</Tooltip>
@ -584,6 +701,21 @@ class InterfaceColContent extends Component {
>
<CaseReport {...this.reports[this.state.curCaseid]} />
</Modal>
<Modal
title="自定义测试脚本"
width="660px"
style={{ minHeight: '500px' }}
visible={this.state.advVisible}
onCancel={this.handleAdvCancel}
onOk={this.handleAdvOk}
>
<h3>
是否开启:&nbsp;
<Switch checked={this.state.enableScript} onChange={e=>this.setState({enableScript: e})} />
</h3>
<div className="case-script" id="case-script" style={{ minHeight: 500 }}></div>
</Modal>
</div>
)
}

View File

@ -115,4 +115,14 @@
.interface-col-table-body td{
padding-left:5px;
}
.interface-col-table-action button{
margin-right: 5px;
padding: 5px 10px;
max-width: 90px;
}
}
.case-script{
margin: 10px
}

View File

@ -211,7 +211,7 @@ class InterfaceEditForm extends Component {
this.setState({
req_radio_type: HTTP_METHOD[this.state.method].request_body ? 'req-body' : 'req-query'
})
let that = this, mockPreview, resBodyEditor;
let that = this;
const initReqBody = that.state.req_body_other;
const initResBody = that.state.res_body;
mockEditor({
@ -225,24 +225,20 @@ class InterfaceEditForm extends Component {
}
})
resBodyEditor = mockEditor({
this.resBodyEditor = mockEditor({
container: 'res_body_json',
data: that.state.res_body,
onChange: function (d) {
if (d.format === true) {
mockPreview.setValue(d.mockText)
}
that.setState({
res_body: d.text,
res_body_mock: d.mockText
res_body: d.text
});
EditFormContext.props.changeEditStatus(initResBody !== d.text);
}
})
mockPreview = mockEditor({
this.mockPreview = mockEditor({
container: 'mock-preview',
data: resBodyEditor.curData.mockText,
data: '',
readOnly: true
})
@ -281,9 +277,28 @@ class InterfaceEditForm extends Component {
this.setState(newValue)
}
handleJsonType = (key) => {
handleMockPreview = ()=>{
let str = '';
try{
if(this.resBodyEditor.curData.format === true){
str = JSON.stringify(this.resBodyEditor.curData.mockData(), null , ' ');
}else{
str = '解析出错: ' + this.resBodyEditor.curData.format;
}
}catch(err){
str = '解析出错: ' + err.message;
}
this.mockPreview.setValue(
str
)
}
handleJsonType = (key) => {
key = key || 'tpl';
if(key === 'preview'){
this.handleMockPreview()
}
this.setState({
jsonType: key
})
@ -742,7 +757,7 @@ class InterfaceEditForm extends Component {
<TabPane tab="模板" key="tpl">
</TabPane>
<TabPane tab="预览" key="preview">
<TabPane tab="预览" key="preview">
</TabPane>

View File

@ -68,10 +68,8 @@ function run(options) {
var obj = json5.parse(json);
curData.format = true;
curData.jsonData = obj;
curData.mockData = Mock.mock(MockExtra(obj, {}));
curData.mockText = JSON.stringify(curData.mockData, null, " ");
curData.mockData = ()=>Mock.mock(MockExtra(obj, {})); //为防止时时 mock 导致页面卡死的问题,改成函数式需要用到再计算
} catch (e) {
curData.format = e.message;
}
}

View File

@ -156,6 +156,20 @@ hooks = {
type: 'listener',
mulit: true,
listener: []
},
/*
* 添加 reducer
* @param Object reducerModules
*
* @info
* importDataModule = {};
*
*/
add_reducer: {
type: 'listener',
mulit: true,
listener: []
}
};

View File

@ -13,7 +13,7 @@ export default (state = initialState, action) => {
case FETCH_MOCK_COL:
return {
...state,
list: action.payload.data.data
list: action.payload.data
}
default:
return state

View File

@ -8,9 +8,10 @@ import news from './news.js'
import addInterface from './addInterface.js'
import menu from './menu.js'
import follow from './follow.js'
import mockCol from './mockCol.js'
export default combineReducers({
import { emitHook } from 'client/plugin.js'
const reducerModules = {
group,
user,
inter,
@ -19,6 +20,9 @@ export default combineReducers({
news,
addInterface,
menu,
follow,
mockCol
})
follow
}
emitHook('add_reducer', reducerModules);
export default combineReducers(reducerModules)

View File

@ -1,4 +1,5 @@
const path = require('path');
const _ =require('underscore');
function getPluginConfig(name, type) {
let pluginConfig;
@ -18,33 +19,98 @@ function getPluginConfig(name, type) {
}
}
module.exports = {
/**
function isObj(object) {
return object && typeof (object) == 'object' && Object.prototype.toString.call(object).toLowerCase() == "[object object]";
}
function isArray(object) {
return object && typeof (object) == 'object' && object.constructor == Array;
}
function getLength(object) {
return Object.keys(object).length;
}
function Compare(objA, objB) {
if (!isObj(objA) && !isObj(objB)){
return objA === objB;
}
if (!isObj(objA) || !isObj(objB)) return false;
if (getLength(objA) != getLength(objB)) return false;
return CompareObj(objA, objB, true);
}
function CompareObj(objA, objB, flag) {
for (var key in objA) {
if (!flag)
break;
if (!objB.hasOwnProperty(key)) { flag = false; break; }
if (!isArray(objA[key])) {
if (objB[key] != objA[key]) { flag = false; break; }
} else {
if (!isArray(objB[key])) { flag = false; break; }
var oA = objA[key], oB = objB[key];
if (oA.length != oB.length) { flag = false; break; }
for (var k in oA) {
if (!flag)
break;
flag = CompareObj(oA[k], oB[k], flag);
}
}
}
return flag;
}
/**
* type @string enum[plugin, ext] plugin是外部插件ext是内部插件
*/
initPlugins: function (plugins, type) {
if (!plugins) {
return [];
}
if (typeof plugins !== 'object' || !Array.isArray(plugins)) {
throw new Error('插件配置有误,请检查', plugins);
}
return plugins.map(item => {
let pluginConfig;
if (item && typeof item === 'string') {
pluginConfig = getPluginConfig(item, type);
return Object.assign({}, pluginConfig, { name: item, enable: true })
} else if (item && typeof item === 'object') {
pluginConfig = getPluginConfig(item.name, type);
return Object.assign({},
pluginConfig,
{
name: item.name,
options: item.options,
enable: item.enable === false ? false : true
})
}
})
exports.initPlugins = function (plugins, type) {
if (!plugins) {
return [];
}
if (typeof plugins !== 'object' || !Array.isArray(plugins)) {
throw new Error('插件配置有误,请检查', plugins);
}
plugins = plugins.map(item => {
let pluginConfig;
if (item && typeof item === 'string') {
pluginConfig = getPluginConfig(item, type);
return Object.assign({}, pluginConfig, { name: item, enable: true })
} else if (item && typeof item === 'object') {
pluginConfig = getPluginConfig(item.name, type);
return Object.assign({},
pluginConfig,
{
name: item.name,
options: item.options,
enable: item.enable === false ? false : true
})
}
})
plugins = plugins.filter(item=>{
return item.enable === true && (item.server || item.client)
})
return _.uniq(plugins, item=>item.name)
}
exports.jsonEqual = Compare;
exports.isDeepMatch = function(obj, properties){
if(!properties || typeof properties !== 'object'){
return false;
}
if(!obj || typeof obj !== 'object'){
return true;
}
let match = true;
for(var i in properties){
if(!Compare(obj[i], properties[i])){
match = false;
break;
}
}
return match;
}

View File

@ -1,4 +1,4 @@
var strRegex = /\${([a-zA-Z0-9_\.]+)\}/g;
var strRegex = /\${([a-zA-Z]+)\.?([a-zA-Z0-9_\.]*)\}/i;
var varSplit = '.';
var mockSplit = '|';
var Mock = require('mockjs');
@ -33,7 +33,7 @@ function mock(mockJSON, context) {
c[i] = (p[i].constructor === Array) ? [] : {};
parse(p[i], c[i]);
} else if(p[i] && typeof p[i] === 'string'){
p[i] = handleStr(p[i]);
p[i] = handleStr(p[i]);
var filters = i.split(mockSplit), newFilters = [].concat(filters);
c[i] = p[i];
if (filters.length > 1) {
@ -63,9 +63,17 @@ function mock(mockJSON, context) {
if (typeof str !== 'string' || str.indexOf('{') === -1 || str.indexOf('}') === -1 || str.indexOf('$') === -1) {
return str;
}
str = str.replace(strRegex, function (matchs, name) {
let matchs = str.match(strRegex);
if(matchs){
let name = matchs[1] + (matchs[2]? '.' + matchs[2] : '');
if(!name) return str;
var names = name.split(varSplit);
var data = context;
if(typeof context[names[0]] === undefined){
return str;
}
names.forEach(function (n) {
if (data === '') return '';
if (n in data) {
@ -75,7 +83,7 @@ function mock(mockJSON, context) {
}
});
return data;
});
}
return str;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

101
doc/page/usage/adv_mock.md Normal file
View File

@ -0,0 +1,101 @@
## Mock 期望
在测试时很多时候需要根据不同的请求参数、IP 返回不同的 HTTP Code、HTTP 头和 JSON 数据。
Mock 期望就是根据设置的请求过滤规则,返回期望数据。
### 使用方法
1. 进入接口详情页,点击『高级 Mock』选项。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case1.png"/></div>
2. 点击『添加期望』,填写过滤规则以及期望返回数据,点击『确定』保存。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case3.png"/></div>
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case4.png"/></div>
3. 然后尝试在浏览器里发送符合规则的请求,查看返回的数据是否符合期望。
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case5.png"/></div>
### 期望填写
* 期望名称:给此条期望命名
请求
* IP 过滤:请求的 IP 是设置的地址才可能返回期望。默认 IP 过滤关闭,任何 IP 地址都可能返回期望。
* 参数过滤:请求必须包含设置的参数,并且值相等才可能返回期望。参数可以在 Body 或 Query 中。
响应
* HTTP Code期望响应的 HTTP 状态码
* 延时:期望响应的延迟时间
* HTTP 头:期望响应带有的 HTTP 头
* 返回 JSON期望返回的 JSON 数据
## 自定义 Mock 脚本
在前端开发阶段,对于某些接口,业务相对复杂,而 UI 端也需要根据接口返回的不同内容去做相应的处理。
YApi 提供了写 JS 脚本方式处理这一问题,可以根据用户请求的参数修改返回内容。
### 全局变量
请求
- `header` 请求的 HTTP 头
- `params` 请求参数,包括 Body、Query 中所有参数
- `cookie` 请求带的 Cookies
响应
- `mockJson`
接口定义的响应数据 Mock 模板
- `resHeader`
响应的 HTTP 头
- `httpCode`
响应的 HTTP 状态码
- `delay`
Mock 响应延时,单位为 ms
- `Random`
Mock.Random 方法,详细使用方法请查看 <a href="https://github.com/nuysoft/Mock/wiki/Mock.Random">Wiki</a>
### 使用方法
1. 首先开启此功能
2. Mock 脚本就是用 JavaScript 对 `mockJson` 变量修改,请避免被全局变量(httpCode, resHeader, delay)的修改
### 示例1, 根据请求参数重写 mockJson
```
if(params.type == 1){
mockJson.errcode = 400;
mockJson.errmsg = 'error;
}
if(header.token == 't'){
mockJson.errcode = 300;
mockJson.errmsg = 'error;
}
if(cookie.type == 'a'){
mockJson.errcode = 500;
mockJson.errmsg = 'error;
}
```
### 示例2, 生成高度自定义数据内容
```
var a = [1,1,1,1,1,1,1,1,1,1]
mockJson = {
errcode: 0,
email: Random.email('qq.com'),
data: a.map(function(item){
return Random.city() + '银行'
})
}
```
## Mock 优先级说明
请求 Mock 数据时规则匹配优先级Mock 期望 > 自定义 Mock 脚本 > 普通 Mock。
如果前面匹配到 Mock 数据,后面 Mock 则不返回。

View File

@ -1,9 +1,9 @@
## 介绍
<p style='text-indent:2em;line-height:1.8em'>在平时的开发过程中经常遇到的一个问题是每次调试接口都需要重新填写参数YApi测试集可以保存之前填写的参数方便下次的调试。YApi测试集还可以一次性测试所有接口每个的请求参数可以通过前面已请求的接口数据读取或填写mock随机字符串</p>
<p style='text-indent:2em;line-height:1.8em'>在平时的开发过程中经常遇到的一个问题是每次调试接口都需要重新填写参数YApi测试集不但能够保存之前填写的参数,方便下次的调试,还可以一次性测试所有接口每个的请求参数可以通过前面已请求的接口数据读取或填写mock随机字符串,通过设置断言脚本验证返回数据的正确性,</p>
## 测试列表
<img class="doc-img" style="width:100%" src="./images/usage/case-list.jpg" />
<img class="doc-img" style="width: 618px;" src="./images/usage/case-list.gif" />
在测试列表可以看到每个测试用例的 key,还有 开始测试、报告等功能
@ -48,3 +48,33 @@ $.{key}.{params|body}.{path}
其中 **$.** 是使用 **动态变量** 的标志,$.269.**params** 即表示 key 值为 269 用例的请求参数,$.269.**body** 即表示 key 值为 269 用例的返回值。
> Tips: 上下拖动测试集合的列表项可以调整测试的顺序。
### 高级
可通过写断言脚本,实现精准测试,支持 js 所有语法
#### 公共变量
* assert
断言函数,详细 api可查看 <a target="_blank" href="https://nodejs.org/dist/latest-v8.x/docs/api/assert.html">document</a>
* status
http 状态码
* body
返回 response body
* header
返回 response header
* records
记录的 http 请求信息,假设需要获取 key为555的接口参数或者响应数据可通过 records[555].params 或 records[555].body 获取
#### 示例
```
assert.equal(body.errcode, 0)
assert.equal(body.data.group_name, 'testGroup')
```

View File

@ -1,16 +1,16 @@
## Mock介绍
<p style='text-indent:2em;line-height:1.8em'>YApi的Mock功能可以根据用户的输入接口信息如协议、URL、接口名、请求头、请求参数、mock规则([点击到Mock规则](#mock)生成Mock接口这些接口会自动生成模拟数据创建者可以自由构造需要的数据。而且与常见的Mock方式如将Mock写在代码里和JS拦截等相比yapi的Mock在使用场景和效率和复杂度上是相差甚远的正是由于yapi的Mock是一个第三方平台那么在团队开发时任何人都可以权限许可下创建、修改接口信息等操作这对于团队开发是很有好处的。 </p>
<p style='text-indent:2em;line-height:1.8em'>YApi的 Mock 功能可以根据用户的输入接口信息如协议、URL、接口名、请求头、请求参数、mock 规则([点击到 Mock 规则](#mock))生成 Mock 接口,这些接口会自动生成模拟数据,创建者可以自由构造需要的数据。而且与常见的 Mock 方式如将 Mock 写在代码里和JS拦截等相比 yapi Mock 在使用场景和效率和复杂度上是相差甚远的,正是由于 yapi Mock 是一个第三方平台,那么在团队开发时任何人都可以权限许可下创建、修改接口信息等操作,这对于团队开发是很有好处的。 </p>
**mock地址解析**yapi平台网址+mock+**您的项目id**+**接口实际请求path**
**mock地址解析**`YApi平台网址 + mock + 您的项目id + 接口实际请求path`
假设你 YApi 的部署地址为http://yapi.xxx.com,然后后面的都可以用这个地址作为示例
假设你 YApi 的部署地址为http://yapi.xxx.com 然后用这个地址作为示例
mockd地址 http://yapi.xxx.com/mock/29/api/hackathon/login
mockd 地址: http://yapi.xxx.com/mock/29/api/hackathon/login
项目id可以在项目设置里查看到
> 注:项目 id 可以在项目设置里查看到
## 定义mock数据示例
## 定义 mock 数据示例
```
{
"status|0-1": 0, //接口状态
@ -36,7 +36,7 @@
## YApi-Mock 跟 Mockjs 区别
<a href="http://mockjs.com">Mockjs 官网</a>
<a href="http://mockjs.com/examples.html">Mockjs 官网</a>
1 因为 yapi 基于 json 定义 mock ,无法使用 mockjs 原有的函数功能,正则表达式需要基于 rule 书写,示例如下:
@ -48,7 +48,7 @@
```
2 支持替换请求的query,body参数
2 支持替换请求的 query, body 参数
```
{
@ -58,9 +58,11 @@
```
## 如何使用Mock?
### 1 在js代码直接请求yapi提供的mock地址不用担心跨域问题
在代码直接请求yapi提供的mock地址以jQuery为例
## 如何使用 Mock
### 1 在 js 代码直接请求yapi提供的 mock 地址(不用担心跨域问题)
在代码直接请求 yapi 提供的 mock 地址,以 jQuery 为例:
````javascript
let prefix = 'http://yapi.xxx.com/mock/2817'
@ -73,7 +75,7 @@ $.post(prefix+'/baseapi/path', {username: 'xxx'}, function(res){
优点:不用修改项目代码
#### 2.1 基于nginx反向代理
#### 2.1 基于 nginx 反向代理
```` nginx
location /baseapi
@ -82,7 +84,7 @@ proxy_pass http://yapi.xxx.com/mock/2817/baseapi; #baseapi后面没有"/"
}
````
#### 2.2 基于ykit mock功能
#### 2.2 基于 ykit mock功能
```javascript
{
@ -97,7 +99,7 @@ proxy_pass http://yapi.xxx.com/mock/2817/baseapi; #baseapi后面没有"/"
#### 2.3 基于ykit Jerry代理
#### 2.3 基于 ykit Jerry 代理
假设您本地服务器访问地址是: http://xxx.com
@ -105,31 +107,10 @@ proxy_pass http://yapi.xxx.com/mock/2817/baseapi; #baseapi后面没有"/"
<span id="mock"></span>
## 高级Mock
在前端开发阶段,对于某些接口,业务相对复杂,而 UI 端也需要根据接口返回的不同内容去做相应的处理
YApi 提供了写 js 脚本方式处理这一问题,可以根据用户请求的参数修改返回内容。
### 全局变量
1. mockJson
2. query
3. body
4. header
5. cookie
### 使用方法
1. 首先开启此功能
2. mock脚本就是用 javascript 对 mockJson 变量修改
### 示例:
<img class="doc-img" style="width: 80%" src="./images/usage/adv-mock.jpg" />
## Mock语法规范
>了解更多Mock详情[Mock.js 官方文档](https://github.com/nuysoft/Mock/wiki/Syntax-Specification)
## Mock 语法规范
>了解更多Mock详情[Mock.js 官方文档](http://mockjs.com/examples.html)
Mock.js 的语法规范包括两部分:

View File

@ -8,6 +8,18 @@ body, h1, h2, h3, h4, h5, h6 {
margin-top: 3em;
}
.doc-img-wrapper {
background: #f7f7f7;
padding: 16px;
display: inline-block;
text-align: center;
width: 80%;
position: relative;
border-radius: 4px;
}
.doc-img-r {
display: block;
}
.doc-img {
width: 50%;
display: block;

View File

@ -3,8 +3,10 @@ import React, { Component } from 'react'
import axios from 'axios'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom';
import { Form, Switch, Button, message, Icon, Tooltip } from 'antd';
import { Form, Switch, Button, message, Icon, Tooltip, Radio } from 'antd';
import MockCol from './MockCol/MockCol.js'
import mockEditor from 'client/containers/Project/Interface/InterfaceList/mockEditor';
import constants from '../../client/constants/variable.js'
const FormItem = Form.Item;
@ -18,7 +20,8 @@ class AdvMock extends Component {
super(props);
this.state = {
enable: false,
mock_script: ''
mock_script: '',
tab: 'case'
}
}
@ -74,6 +77,12 @@ class AdvMock extends Component {
})
}
handleTapChange = (e) => {
this.setState({
tab: e.target.value
})
}
render() {
const formItemLayout = {
labelCol: {
@ -91,27 +100,39 @@ class AdvMock extends Component {
}
}
};
const { tab } = this.state;
const isShowCase = tab === 'case';
return <div style={{ padding: '20px 10px' }}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={<span>是否开启&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://yapi.ymfe.org/mock.html#高级Mock" ><Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip></a></span>}
{...formItemLayout}
>
<Switch checked={this.state.enable} onChange={this.onChange} checkedChildren="开" unCheckedChildren="关" />
</FormItem>
<div style={{textAlign: 'center', marginBottom: 20}}>
<Radio.Group value={tab} size="large" onChange={this.handleTapChange}>
<Radio.Button value="case">期望</Radio.Button>
<Radio.Button value="script">脚本</Radio.Button>
</Radio.Group>
</div>
<div style={{display: isShowCase ? 'none' : ''}}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={<span>是否开启&nbsp;<a target="_blank" rel="noopener noreferrer" href={constants.docHref.adv_mock_script} ><Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip></a></span>}
{...formItemLayout}
>
<Switch checked={this.state.enable} onChange={this.onChange} checkedChildren="开" unCheckedChildren="关" />
</FormItem>
<FormItem
label="Mock脚本"
{...formItemLayout}
>
<div id="mock-script" style={{ minHeight: '500px' }} ></div>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">保存</Button>
</FormItem>
<FormItem
label="Mock脚本"
{...formItemLayout}
>
<div id="mock-script" style={{ minHeight: '500px' }} ></div>
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">保存</Button>
</FormItem>
</Form>
</Form>
</div>
<div style={{display: isShowCase ? '' : 'none'}}>
<MockCol/>
</div>
</div>
}
}

View File

@ -1,23 +1,33 @@
import React, { Component } from 'react'
// import axios from 'axios'
import PropTypes from 'prop-types'
import { Button, Form, Input, Switch, Select, Icon, Modal } from 'antd';
import { Button, Form, Input, Switch, Select, Icon, Modal, Col, Row, InputNumber, AutoComplete } from 'antd';
import { safeAssign } from '../../../client/common.js';
import { httpCodes } from '../index.js';
import mockEditor from '../../../client/containers/Project/Interface/InterfaceList/mockEditor';
import constants from '../../../client/constants/variable.js'
import { httpCodes } from '../index.js'
import { connect } from 'react-redux'
import './CaseDesModal.scss'
const Option = Select.Option;
const FormItem = Form.Item;
// const RadioButton = Radio.Button;
// const RadioGroup = Radio.Group;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
labelCol: { span: 5 },
wrapperCol: { span: 12 }
};
const formItemLayoutWithOutLabel = {
wrapperCol: { span: 12, offset: 5 }
};
@connect(
state => {
return {
currInterface: state.inter.curdata
}
}
)
@Form.create()
export default class CaseDesModal extends Component {
static propTypes = {
@ -26,7 +36,14 @@ export default class CaseDesModal extends Component {
onCancel: PropTypes.func,
onOk: PropTypes.func,
isAdd: PropTypes.bool,
visible: PropTypes.bool
visible: PropTypes.bool,
currInterface: PropTypes.object
}
state = {
headers: [],
paramsArr: [],
paramsForm: 'form'
}
constructor(props) {
@ -34,6 +51,12 @@ export default class CaseDesModal extends Component {
}
preProcess = caseData => {
try {
caseData = JSON.parse(JSON.stringify(caseData))
} catch (error) {
console.log(error)
}
// caseModel
// const a = {
// interface_id: { type: Number, required: true },
// project_id: {type: Number, required: true},
@ -51,7 +74,7 @@ export default class CaseDesModal extends Component {
// up_time: Number,
// res_body: {type: String, required: true}
// }
const initCaseData = {
const initCaseData = {
ip: '',
ip_enable: false,
name: '',
@ -59,19 +82,32 @@ export default class CaseDesModal extends Component {
deplay: 0,
headers: [{name: '', value: ''}],
paramsArr: [{name: '', value: ''}],
params: '',
res_body: ''
}
caseData.paramsArr = caseData.params && caseData.params.length ? Object.keys(caseData.params).map(key => {
const paramsArr = caseData.params && Object.keys(caseData.params).length ? Object.keys(caseData.params).map(key => {
return { name: key, value: caseData.params[key] }
}).filter(item => {
if (typeof item.value === 'object') {
this.setState({ paramsForm: 'json' })
}
return typeof item.value !== 'object'
}) : [{name: '', value: ''}];
caseData.headers = caseData.headers && caseData.headers.length ? caseData.headers : [{name: '', value: ''}];
caseData = safeAssign(initCaseData, caseData);
const headers = caseData.headers && caseData.headers.length ? caseData.headers : [{name: '', value: ''}];
caseData.code = ''+caseData.code;
caseData.params = JSON.stringify(caseData.params, null, 2);
this.setState({
headers,
paramsArr
})
caseData = safeAssign(initCaseData, { ...caseData, headers, paramsArr });
return caseData;
}
endProcess = caseData => {
const headers = [];
const params = {};
const { paramsForm } = this.state;
caseData.headers.forEach(item => {
if (item.name) {
headers.push({
@ -86,29 +122,164 @@ export default class CaseDesModal extends Component {
}
})
caseData.headers = headers;
caseData.params = params;
if (paramsForm === 'form') {
caseData.params = params;
} else {
try {
caseData.params = JSON.parse(caseData.params)
} catch (error) {
console.log(error)
}
}
delete caseData.paramsArr;
return caseData;
}
componentDidMount() {
this.props.form.setFieldsValue(this.preProcess(this.props.caseData))
this.shouldLoadEditor = true
}
componentDidUpdate() {
if (this.shouldLoadEditor) {
this.loadBodyEditor()
this.loadParamsEditor()
this.shouldLoadEditor = false
}
}
componentWillReceiveProps(nextProps) {
if (this.props.caseData !== nextProps.caseData) {
this.props.form.setFieldsValue(this.preProcess(this.props.caseData))
if (
this.props.caseData !== nextProps.caseData ||
this.props.visible !== nextProps.visible
) {
this.props.form.setFieldsValue(this.preProcess(nextProps.caseData))
this.shouldLoadEditor = true
}
}
handleOk = () => {
const form = this.props.form;
this.props.onOk(this.endProcess(form.getFieldsValue()));
form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.onOk(this.endProcess(values));
}
})
}
addValues = (key) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.concat({ name: '', value: ''});
this.setState({ [key]: values })
setFieldsValue({ [key]: values })
}
removeValues = (key, index) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.filter((item, index2) => index !== index2);
this.setState({ [key]: values })
setFieldsValue({ [key]: values })
}
getParamsKey = () => {
let { req_query, req_body_form, req_body_type } = this.props.currInterface;
const keys = [];
req_query.forEach(item => {
keys.push(item.name)
})
if (req_body_type === 'form') {
req_body_form.forEach(item => {
keys.push(item.name)
})
}
return keys
}
loadBodyEditor = () => {
const that = this;
const { setFieldsValue } = this.props.form;
this.props.visible && mockEditor({
container: 'res_body_json',
data: that.props.caseData.res_body,
onChange: function (d) {
if (d.format !== true) return false;
setFieldsValue({ res_body: d.text })
}
});
}
loadParamsEditor = () => {
const that = this;
const { setFieldsValue } = this.props.form;
this.props.visible && mockEditor({
container: 'case_modal_params',
data: that.props.caseData.params,
onChange: function (d) {
if (d.format !== true) return false;
setFieldsValue({ params: d.text })
}
});
}
jsonValidator = (rule, value, callback) => {
try {
JSON.parse(value)
callback()
} catch (error) {
callback(new Error())
}
}
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const { isAdd, visible, onCancel } = this.props;
const { headers, paramsArr, paramsForm } = this.state;
const valuesTpl = (name, values, title) => {
getFieldDecorator(name)
const dataSource = name === 'headers' ? constants.HTTP_REQUEST_HEADER : this.getParamsKey();
const display = (name === 'paramsArr' && paramsForm === 'json') ? 'none': ''
return values.map((item, index) => (
<div key={index} className={name} style={{ display }}>
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`${name}[${index}].name`, { initialValue: item.name })(
<AutoComplete
dataSource={dataSource}
filterOption={(inputValue, option) => option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1}
/>
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`${name}[${index}].value`, { initialValue: item.value })(
<Input />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues(name, index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
))
}
getFieldDecorator('params')
return (
<Modal
@ -116,7 +287,10 @@ export default class CaseDesModal extends Component {
visible={visible}
maskClosable={false}
onOk={this.handleOk}
width={780}
onCancel={() => onCancel()}
afterClose={() => this.setState({paramsForm: 'form'})}
className="case-des-modal"
>
<Form>
<FormItem
@ -129,23 +303,88 @@ export default class CaseDesModal extends Component {
<Input placeholder="请输入期望名称" />
)}
</FormItem>
<FormItem {...formItemLayout} label="IP 过滤">
{getFieldDecorator('ip_enable', {
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(
<Switch />
)}
{getFieldDecorator('ip')(
<Input placeholder="请输入过滤的 IP 地址" />
)}
<h2 className="sub-title">请求</h2>
<FormItem {...formItemLayout} label="IP 过滤" className="ip-filter">
<Col span={6} className="ip-switch">
<FormItem>
{getFieldDecorator('ip_enable', {
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(
<Switch />
)}
</FormItem>
</Col>
<Col span={18}>
<div style={{display: getFieldValue('ip_enable') ? '' : 'none'}} className="ip">
<FormItem>
{getFieldDecorator('ip', getFieldValue('ip_enable') ? {
rules: [{ pattern: constants.IP_REGEXP, message: '请填写正确的 IP 地址', required: true }]
} : {})(
<Input placeholder="请输入过滤的 IP 地址" />
)}
</FormItem>
</div>
</Col>
</FormItem>
<div className="params-form">
<FormItem {...formItemLayoutWithOutLabel}>
<Switch
checkedChildren="JSON"
unCheckedChildren="JSON"
checked={paramsForm === 'json'}
onChange={bool => {
this.setState({ paramsForm: bool ? 'json' : 'form' }, () => {
if (paramsForm === 'json') {
this.loadParamsEditor()
}
})
}}
/>
{
// <RadioGroup
// value={paramsForm}
// size="small"
// onChange={e => this.setState({ paramsForm: e.target.value })}
// >
// <RadioButton value="form">Form</RadioButton>
// <RadioButton value="json">JSON</RadioButton>
// </RadioGroup>
}
</FormItem>
</div>
{
valuesTpl('paramsArr', paramsArr, '参数过滤')
}
<FormItem wrapperCol={{ span: 6, offset: 5 }} style={{display: paramsForm === 'form' ? '': 'none'}}>
<Button size="default" type="primary" onClick={() => this.addValues('paramsArr')} style={{ width: '100%' }}>
<Icon type="plus" /> 添加参数
</Button>
</FormItem>
<FormItem {...formItemLayout} wrapperCol={{ span: 17 }} label="参数过滤" style={{display: paramsForm === 'form' ? 'none': ''}}>
<div id="case_modal_params" style={{
minHeight: "300px",
border: "1px solid #d9d9d9",
borderRadius: 4
}} ></div>
<FormItem
{...formItemLayoutWithOutLabel}
>
{getFieldDecorator('params', paramsForm === 'json' ? {
rules: [{ validator: this.jsonValidator, message: '请输入正确的 JSON' }]
} : {})(
<Input style={{display: 'none'}} />
)}
</FormItem>
</FormItem>
<h2 className="sub-title">响应</h2>
<FormItem
{...formItemLayout}
label="HTTP CODE"
required
label="HTTP Code"
>
{getFieldDecorator('code')(
<Select search>
<Select showSearch>
{
httpCodes.map(code => <Option key={''+code} value={''+code}>{''+code}</Option>)
}
@ -160,87 +399,33 @@ export default class CaseDesModal extends Component {
initialValue: 0,
rules: [{ required: true, message: '请输入延时时间!', type: 'integer' }]
})(
<Input placeholder="请输入延时时间" />
<InputNumber placeholder="请输入延时时间" min={0}/>
)}
<span>ms</span>
</FormItem>
{
getFieldDecorator('headers', { initialValue: [] }) &&
getFieldValue('headers').map((item, index) => (
<div key={index}>
<FormItem
{...formItemLayout}
label={index ? '' : 'HTTP 头'}
>
{getFieldDecorator(`headers[${index}].name`)(
<Input />
)}
</FormItem>
<FormItem
{...formItemLayout}
>
{getFieldDecorator(`headers[${index}].value`)(
<Input />
)}
{getFieldValue('headers').length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeHeaders(index)}
/>
) : null}
</FormItem>
</div>
))
valuesTpl('headers', headers, 'HTTP 头')
}
<FormItem>
<Button type="dashed" onClick={this.addHeaders} style={{ width: '60%' }}>
<FormItem wrapperCol={{ span: 6, offset: 5 }}>
<Button size="default" type="primary" onClick={() => this.addValues('headers')} style={{ width: '100%' }}>
<Icon type="plus" /> 添加 HTTP
</Button>
</FormItem>
{
getFieldDecorator('paramsArr', { initialValue: [] }) &&
getFieldValue('paramsArr').map((item, index) => (
<div key={index}>
<FormItem
{...formItemLayout}
label={index ? '' : '参数'}
>
{getFieldDecorator(`paramsArr[${index}].name`)(
<Input />
)}
</FormItem>
<FormItem
>
{getFieldDecorator(`paramsArr[${index}].value`)(
<Input />
)}
{getFieldValue('paramsArr').length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeParams(index)}
/>
) : null}
</FormItem>
</div>
))
}
<FormItem>
<Button type="dashed" onClick={this.addParams} style={{ width: '60%' }}>
<Icon type="plus" /> 添加参数
</Button>
</FormItem>
<FormItem
{...formItemLayout}
label="返回 JSON"
>
{getFieldDecorator('res_body', {
rules: [{ required: true, message: '请输入期望名称!' }]
})(
<Input placeholder="返回 JSON" />
)}
<FormItem {...formItemLayout} wrapperCol={{ span: 17 }} label="返回 JSON" required>
<div id="res_body_json" style={{
minHeight: "300px",
border: "1px solid #d9d9d9",
borderRadius: 4
}} ></div>
<FormItem
{...formItemLayoutWithOutLabel}
>
{getFieldDecorator('res_body', {
rules: [{ validator: this.jsonValidator, message: '请输入正确的返回 JSON' }]
})(
<Input style={{display: 'none'}} />
)}
</FormItem>
</FormItem>
</Form>
</Modal>

View File

@ -0,0 +1,20 @@
.case-des-modal {
.ant-modal-body {
max-height: 520px;
overflow-y: scroll;
}
.ip-filter .ip>.ant-form-item, .ip-filter .ip-switch>.ant-form-item {
margin-bottom: 0;
}
.headers>.ant-form-item, .paramsArr>.ant-form-item, .params-form>.ant-form-item {
margin-bottom: 0;
}
.sub-title {
clear: both;
font-weight: normal;
margin-top: .48rem;
margin-bottom: .16rem;
// border-left: 3px solid #2395f1;
padding-left: 32px;
}
}

View File

@ -3,15 +3,17 @@ import { connect } from 'react-redux'
import axios from 'axios'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom';
import { Table, Button, message } from 'antd';
import { fetchMockCol } from '../../../client/reducer/modules/mockCol'
import { formatTime } from '../../../client/common.js';
import { Table, Button, message, Popconfirm, Tooltip, Icon } from 'antd';
import { fetchMockCol } from 'client/reducer/modules/mockCol'
import { formatTime, getMockText } from 'client/common.js';
import constants from 'client/constants/variable.js'
import CaseDesModal from './CaseDesModal';
@connect(
state => {
return {
list: state.mockCol.list
list: state.mockCol.list,
currInterface: state.inter.curdata
}
},
{
@ -22,6 +24,7 @@ import CaseDesModal from './CaseDesModal';
export default class MockCol extends Component {
static propTypes = {
list: PropTypes.array,
currInterface: PropTypes.object,
match: PropTypes.object,
fetchMockCol: PropTypes.func
}
@ -42,7 +45,8 @@ export default class MockCol extends Component {
}
handleOk = async (caseData) => {
const interface_id = this.props.match.params.action;
const { caseData: currcase } = this.state;
const interface_id = this.props.match.params.actionId;
const project_id = this.props.match.params.id;
caseData = Object.assign({
...caseData,
@ -50,11 +54,25 @@ export default class MockCol extends Component {
project_id: project_id
})
if (!this.state.isAdd) {
caseData.id = 0;
caseData.id = currcase._id;
}
axios.post('/api/plugin/advmock/case/save', caseData).then(res => {
await axios.post('/api/plugin/advmock/case/save', caseData).then(async res => {
if (res.data.errcode === 0) {
message.success(this.state.isAdd ? '添加成功' : '保存成功');
await this.props.fetchMockCol(interface_id);
this.setState({ caseDesModalVisible: false })
} else {
message.error(res.data.errmsg);
}
})
}
deleteCase = async (id) => {
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/del', {id}).then(async res => {
if (res.data.errcode === 0) {
message.success('删除成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
@ -67,36 +85,103 @@ export default class MockCol extends Component {
render() {
const data = this.props.list;
const { list: data, currInterface } = this.props;
const { isAdd, caseData, caseDesModalVisible } = this.state;
const initCaseData = {
ip: '',
ip_enable: false,
name: currInterface.title,
code: '200',
deplay: 0,
headers: [{name: '', value: ''}],
paramsArr: [{name: '', value: ''}],
res_body: getMockText(currInterface.res_body)
}
let ipFilters = [];
let ipObj = {};
let userFilters = [];
let userObj = {};
data.forEach(item => {
ipObj[item.ip_enable ? item.ip : ''] = '';
userObj[item.username] = '';
})
ipFilters = Object.keys(Object.assign(ipObj)).map(value => {
if (!value) {
value = '无过滤'
}
return { text: value, value }
})
userFilters = Object.keys(Object.assign(userObj)).map(value => { return { text: value, value } })
const columns = [{
title: '期望名称',
dataIndex: 'name',
key: 'name',
render: text => <a href="#">{text}</a>
key: 'name'
}, {
title: 'ip',
dataIndex: 'ip',
key: 'ip'
key: 'ip',
render: (text, recode) => {
if (!recode.ip_enable) {
text = '';
}
return text;
},
onFilter: (value, record) => (record.ip === value && record.ip_enable) || (value === '无过滤' && !record.ip_enable),
filters: ipFilters
}, {
title: '创建人',
dataIndex: 'username',
key: 'username'
key: 'username',
onFilter: (value, record) => record.username === value,
filters: userFilters
}, {
title: '编辑时间',
key: 'action',
dataIndex: 'up_time',
key: 'up_time',
render: text => formatTime(text)
}, {
title: '操作',
dataIndex: 'address',
key: 'address'
dataIndex: '_id',
key: '_id',
render: (_id, recode) => {
return (
<div>
<span style={{marginRight: 5}}>
<Button size="small" onClick={() => this.setState({
isAdd: false,
caseDesModalVisible: true,
caseData: recode
})}>编辑</Button>
</span>
<span>
<Popconfirm
title="你确定要删除这条期望?"
onConfirm={() => this.deleteCase(_id)}
okText="确定"
cancelText="取消"
>
<Button size="small" onClick={() => {}}>删除</Button>
</Popconfirm>
</span>
</div>
)
}
}];
return (
<div style={{ padding: '20px 10px' }}>
<Button type="primary" onClick={() => this.setState({isAdd: true, caseDesModalVisible: true})}>添加期望</Button>
<Table columns={columns} dataSource={data} />
<div>
<div style={{marginBottom: 8}}>
<Button type="primary" onClick={() => this.setState({
isAdd: true,
caseDesModalVisible: true,
caseData: initCaseData
})}>添加期望</Button>
<a target="_blank" rel="noopener noreferrer" href={constants.docHref.adv_mock_case} style={{marginLeft: 8}} >
<Tooltip title="点击查看文档"><Icon type="question-circle-o" /></Tooltip>
</a>
</div>
<Table columns={columns} dataSource={data} pagination={false} rowKey='_id' />
<CaseDesModal
visible={caseDesModalVisible}
isAdd={isAdd}

View File

@ -0,0 +1,30 @@
import axios from 'axios'
// Actions
const FETCH_MOCK_COL = 'yapi/mockCol/FETCH_MOCK_COL';
// Reducer
const initialState = {
list: []
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_MOCK_COL:
return {
...state,
list: action.payload.data
}
default:
return state
}
}
// Action Creators
export async function fetchMockCol(interfaceId) {
let result = await axios.get('/api/plugin/advmock/case/list?interface_id=' + interfaceId);
return {
type: FETCH_MOCK_COL,
payload: result.data
}
}

View File

@ -1,4 +1,5 @@
import AdvMock from './MockCol/MockCol.js'
import AdvMock from './AdvMock'
import mockCol from './MockCol/mockColReducer.js'
module.exports = function(){
this.bindHook('interface_tab', function(tabs){
@ -7,4 +8,7 @@ module.exports = function(){
component: AdvMock
}
})
this.bindHook('add_reducer', function(reducerModules){
reducerModules.mockCol = mockCol;
})
}

View File

@ -2,6 +2,7 @@ const baseController = require('controllers/base.js');
const advModel = require('./advMockModel.js');
const yapi = require('yapi.js');
const caseModel = require('./caseModel.js');
const userModel = require('../../server/models/user.js');
const config = require('./index.js');
class advMockController extends baseController{
@ -9,6 +10,7 @@ class advMockController extends baseController{
super(ctx);
this.Model = yapi.getInst(advModel);
this.caseModel = yapi.getInst(caseModel);
this.userModel = yapi.getInst(userModel);
}
async getMock(ctx){
@ -56,6 +58,11 @@ class advMockController extends baseController{
return ctx.body = yapi.commons.resReturn(null, 400, '缺少 interface_id');
}
let result = await this.caseModel.list(id);
for(let i = 0, len = result.length; i < len; i++) {
let userinfo = await this.userModel.findById(result[i].uid);
result[i] = result[i].toObject();
result[i].username = userinfo.username;
}
ctx.body = yapi.commons.resReturn(result);
}
@ -68,6 +75,7 @@ class advMockController extends baseController{
let result = await this.caseModel.get({
_id: id
})
ctx.body = yapi.commons.resReturn(result);
}
@ -107,10 +115,12 @@ class advMockController extends baseController{
ip_enable: data.ip_enable
}
if(data.params && typeof data.params === 'object'){
if(data.params && typeof data.params === 'object' && Object.keys(data.params).length >0){
for(let i in data.params){
findRepeatParams['params.' + i] = data.params[i];
}
}else{
findRepeatParams.params = null;
}
if(data.ip_enable){
@ -118,8 +128,7 @@ class advMockController extends baseController{
}
findRepeat = await this.caseModel.get(findRepeatParams);
if(findRepeat){
if(findRepeat && findRepeat._id !== params.id){
return ctx.body = yapi.commons.resReturn(null,400, '已存在的期望');
}
@ -138,7 +147,8 @@ class advMockController extends baseController{
if(!id){
return ctx.body =yapi.commons.resReturn(null, 408, '缺少 id');
}
ctx.body = await this.caseModel.del(id);
let result = await this.caseModel.del(id);
return ctx.body = yapi.commons.resReturn(result);
}

View File

@ -4,6 +4,9 @@ const caseModel = require('./caseModel.js');
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const _ = require('underscore');
const path = require('path');
const lib = require(path.resolve(yapi.WEBROOT, 'common/lib.js' ));
const Mock = require('mockjs');
function arrToObj(arr){
let obj = {};
@ -45,7 +48,7 @@ module.exports = function(){
let matchList = [];
listWithIp.forEach(item=>{
let params = item.params;
if(_.isMatch(reqParams, params)){
if(lib.isDeepMatch(reqParams, params)){
matchList.push(item);
}
})
@ -56,7 +59,7 @@ module.exports = function(){
}).select('_id params')
list.forEach(item=>{
let params = item.params;
if(_.isMatch(reqParams, item.params)){
if(lib.isDeepMatch(reqParams, item.params)){
matchList.push(item);
}
})
@ -152,7 +155,7 @@ module.exports = function(){
let caseData = await checkCase(context.ctx, interfaceId);
if(caseData){
let data = await handleByCase(caseData, context);
context.mockJson = data.res_body;
context.mockJson = yapi.commons.json_parse(data.res_body);
context.resHeader = arrToObj(data.headers);
context.httpCode = data.code;
context.delay = data.delay;
@ -172,7 +175,8 @@ module.exports = function(){
params: Object.assign({}, context.ctx.query, context.ctx.request.body),
resHeader: context.resHeader,
httpCode: context.httpCode,
delay: context.httpCode
delay: context.httpCode,
Random: Mock.Random
}
sandbox.cookie = {};

View File

@ -1,6 +1,6 @@
{
"name": "yapi",
"version": "1.1.2",
"version": "1.2.0",
"description": "YAPI",
"main": "index.js",
"scripts": {
@ -39,7 +39,7 @@
"koa-websocket": "^4.0.0",
"mockjs": "^1.0.1-beta3",
"moment": "^2.18.1",
"mongoose": "^4.12.3",
"mongoose": "4.7.0",
"mongoose-auto-increment": "^5.0.1",
"nodemailer": "^4.0.1",
"ora": "^1.3.0",

View File

@ -36,7 +36,7 @@ class interfaceController extends baseController {
* @param {Boolean} [req_headers[].required] 是否是必须默认为否
* @param {String} [req_headers[].desc] header描述
* @param {String} [req_body_type] 请求参数方式["form", "json", "text", "xml"]四种
* @param {Array} [req_params] name, desc两个参数
* @param {Array} [req_params] 路径参数 name, desc两个参数
* @param {Mixed} [req_body_form] 请求参数,如果请求方式是form参数是Array数组其他格式请求参数是字符串
* @param {String} [req_body_form[].name] 请求参数名
* @param {String} [req_body_form[].value] 请求参数值可填写生成规则mock@email随机生成一条email

View File

@ -154,7 +154,9 @@ class interfaceColController extends baseController{
resultList = resultList.sort((a,b)=>{
return a.index - b.index;
});
ctx.body = yapi.commons.resReturn(resultList);
let ctxBody = yapi.commons.resReturn(resultList);
ctxBody.colData = colData;
ctx.body = ctxBody;
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
@ -334,10 +336,6 @@ class interfaceColController extends baseController{
return ctx.body = yapi.commons.resReturn(null, 400, '用例id不能为空');
}
// if(!params.casename){
// return ctx.body = yapi.commons.resReturn(null, 400, '用例名称不能为空');
// }
let caseData = await this.caseModel.get(params.id);
let auth = await this.checkAuth(caseData.project_id, 'project', 'edit');
if (!auth) {
@ -346,9 +344,9 @@ class interfaceColController extends baseController{
params.uid = this.getUid();
//不允许修改接口id和项目id
delete params.interface_id;
delete params.project_id;
// delete params.col_id;
let result = await this.caseModel.up(params.id, params);
let username = this.getUsername();
@ -454,20 +452,22 @@ class interfaceColController extends baseController{
try{
let params = ctx.request.body;
let id = params.col_id;
if(!id){
return ctx.body = yapi.commons.resReturn(null, 400, '缺少 col_id 参数');
}
let colData = await this.colModel.get(id);
if(!colData){
return ctx.body = yapi.commons.resReturn(null, 400, '不存在');
}
let auth = await this.checkAuth(colData.project_id, 'project', 'edit')
if (!auth) {
return ctx.body = yapi.commons.resReturn(null, 400, '没有权限');
}
let result = await this.colModel.up(params.col_id, {
name: params.name,
desc: params.desc,
up_time: yapi.commons.time()
});
delete params.col_id;
let result = await this.colModel.up(id, params);
let username = this.getUsername();
yapi.commons.saveLog({
contnet: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${colData.project_id}/interface/col/${params.col_id}">${params.name}</a> 的信息`,
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${colData.project_id}/interface/col/${id}">${colData.name}</a> 的信息`,
type: 'project',
uid: this.getUid(),
username: username,
@ -595,6 +595,28 @@ class interfaceColController extends baseController{
}
}
async runCaseScript(ctx){
let params = ctx.request.body;
let script = params.script;
if(!script){
return ctx.body = yapi.commons.resReturn('ok');
}
try{
let a = yapi.commons.sandbox({
assert: require('assert'),
status: params.response.status,
body: params.response.body,
header: params.response.header,
records: params.records
}, script);
return ctx.body = yapi.commons.resReturn('ok');
}catch(err){
let errArr = err.stack.split("\n");
return ctx.body = yapi.commons.resReturn(errArr, 400, err.message)
}
}
}

View File

@ -32,10 +32,10 @@ class interfaceCase extends baseModel {
req_body_other: String,
test_res_body: String,
test_status: {type: String, enum: ['ok', 'invalid', 'error', '']},
test_report: [],
test_res_header: Schema.Types.Mixed,
mock_verify: {type: Boolean, default: false}
mock_verify: {type: Boolean, default: false},
enable_script: {type: Boolean, default: false},
test_script: String
};
}

View File

@ -13,10 +13,11 @@ class interfaceCol extends baseModel {
project_id: { type: Number, required: true },
desc: String,
add_time: Number,
up_time: Number
up_time: Number,
test_report: {type:String, default: '{}'}
};
}
save(data) {
let m = new this.model(data);
return m.save();
@ -37,7 +38,7 @@ class interfaceCol extends baseModel {
list(project_id) {
return this.model.find({
project_id: project_id
}).exec();
}).select('name uid project_id desc add_time up_time').exec();
}
del(id) {

View File

@ -366,6 +366,10 @@ let routerConfig = {
action: "delCase",
path: "del_case",
method: "get"
},{
action: "runCaseScript",
path: "run_script",
method: "post"
}
],
"test": [{

256
static/doc/adv_mock.html Normal file
View File

@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
<meta name="format-detection" content="telephone=no,email=no" />
<meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1">
<meta name="description" content="description of your site">
<meta name="author" content="author of the site">
<title>YApi 使用手册</title>
<link rel="stylesheet" href="source/main.css" />
<link rel="stylesheet" href="styles/theme.css" />
</head>
<body>
<div class="ydoc">
<header class="ydoc-header">
<div class="ydoc-header-area">
<a href="http://ued.qunar.com/ymfe/" class="navbar-brand">YMFE</a>
<button class="ydocIcon navbar-toggle">&#xf020;</button>
<nav class="ydoc-nav">
<ul class="navbar-left">
<li class="active">
<a href="index.html">使用手册</a>
</li>
<li class="">
<a href="devops.html">内网部署</a>
</li>
<li class="">
<a href="plugin.html">插件Wiki</a>
</li>
<li class="">
<a href="qa.html">常见问题</a>
</li>
<li class="">
<a href="releases.html">版本记录</a>
</li>
<li class="">
<a href="http://yapi.demo.qunar.com/" target="_blank">demo站点</a>
</li>
<li class="">
<a href="api.html"></a>
</li>
</ul>
</nav>
</div>
</header>
<!-- <header style="height:20px"></header> -->
<!-- Docs page layout -->
<div class="ydoc-banner-bg">
<div class="ydoc-banner ">
<div class="ydoc-banner-area">
<h1 >YApi</h1>
<p class="desc ">高效、易用、功能强大的api管理平台旨在为开发、产品、测试人员提供更优雅的接口管理服务。</p>
</div>
</div>
<div class="ydoc-container">
<div class="ydoc-container-content ">
<div class="content-left staticsidenav" role="complementary">
<nav class="docs-sidebar hidden-print hidden-xs hidden-sm">
<ul class="nav docs-sidenav">
<!-- <li > -->
<li >
<a href="getfamiliar.html">认识 YApi</a>
</li>
<!-- <li > -->
<li >
<a href="quickstart.html">创建第一个API</a>
</li>
<!-- <li > -->
<li >
<a href="manage.html">管理分组与项目</a>
</li>
<!-- <li > -->
<li >
<a href="project.html">项目操作</a>
</li>
<!-- <li > -->
<li >
<a href="interface.html">接口操作</a>
</li>
<!-- <li > -->
<li >
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li class="active" class="active" > -->
<li class="active" >
<a href="adv_mock.html">高级 Mock</a>
</li>
<ul class="nav docs-sidenav-extend" >
<li >
<a href="#Mock_期望">Mock 期望</a>
</li>
<li >
<a href="#自定义_Mock_脚本">自定义 Mock 脚本</a>
</li>
<li >
<a href="#Mock_优先级说明">Mock 优先级说明</a>
</li>
</ul>
<!-- <li > -->
<li >
<a href="case.html">使用测试集</a>
</li>
<!-- <li > -->
<li >
<a href="data.html">数据导入</a>
</li>
</ul>
</nav>
</div>
<div class="content-right markdown-body use-sidebar" role="main">
<h2 class="subject" id="Mock_期望">Mock 期望 <a class="hashlink" href="#Mock_期望">#</a></h2><p>在测试时很多时候需要根据不同的请求参数、IP 返回不同的 HTTP Code、HTTP 头和 JSON 数据。</p>
<p>Mock 期望就是根据设置的请求过滤规则,返回期望数据。</p>
<h3 class="subject" id="使用方法">使用方法 <a class="hashlink" href="#使用方法">#</a></h3><ol>
<li>进入接口详情页,点击『高级 Mock』选项。<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case1.png"/></div></li><li>点击『添加期望』,填写过滤规则以及期望返回数据,点击『确定』保存。<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case3.png"/></div>
<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case4.png"/></div></li><li>然后尝试在浏览器里发送符合规则的请求,查看返回的数据是否符合期望。<div class="doc-img-wrapper"><img class="doc-img-r" src="./images/usage/adv-mock-case5.png"/></div>
</li></ol>
<h3 class="subject" id="期望填写">期望填写 <a class="hashlink" href="#期望填写">#</a></h3><ul>
<li>期望名称:给此条期望命名</li></ul>
<p>请求</p>
<ul>
<li>IP 过滤:请求的 IP 是设置的地址才可能返回期望。默认 IP 过滤关闭,任何 IP 地址都可能返回期望。</li><li>参数过滤:请求必须包含设置的参数,并且值相等才可能返回期望。参数可以在 Body 或 Query 中。</li></ul>
<p>响应</p>
<ul>
<li>HTTP Code期望响应的 HTTP 状态码</li><li>延时:期望响应的延迟时间</li><li>HTTP 头:期望响应带有的 HTTP 头</li><li>返回 JSON期望返回的 JSON 数据</li></ul>
<h2 class="subject" id="自定义_Mock_脚本">自定义 Mock 脚本 <a class="hashlink" href="#自定义_Mock_脚本">#</a></h2><p>在前端开发阶段,对于某些接口,业务相对复杂,而 UI 端也需要根据接口返回的不同内容去做相应的处理。</p>
<p>YApi 提供了写 JS 脚本方式处理这一问题,可以根据用户请求的参数修改返回内容。</p>
<h3 class="subject" id="全局变量">全局变量 <a class="hashlink" href="#全局变量">#</a></h3><p>请求</p>
<ul>
<li><code>header</code> 请求的 HTTP 头</li><li><code>params</code> 请求参数,包括 Body、Query 中所有参数</li><li><code>cookie</code> 请求带的 Cookies</li></ul>
<p>响应</p>
<ul>
<li><p><code>mockJson</code>
接口定义的响应数据 Mock 模板</p>
</li><li><p><code>resHeader</code>
响应的 HTTP 头</p>
</li><li><p><code>httpCode</code>
响应的 HTTP 状态码</p>
</li><li><p><code>delay</code>
Mock 响应延时,单位为 ms</p>
</li><li><p><code>Random</code>
Mock.Random 方法,详细使用方法请查看 <a href="https://github.com/nuysoft/Mock/wiki/Mock.Random">Wiki</a></p>
</li></ul>
<h3 class="subject" id="使用方法">使用方法 <a class="hashlink" href="#使用方法">#</a></h3><ol>
<li>首先开启此功能</li><li>Mock 脚本就是用 JavaScript 对 <code>mockJson</code> 变量修改,请避免被全局变量(httpCode, resHeader, delay)的修改</li></ol>
<h3 class="subject" id="示例1,_根据请求参数重写_mockJson">示例1, 根据请求参数重写 mockJson <a class="hashlink" href="#示例1,_根据请求参数重写_mockJson">#</a></h3><pre><code>if(params.type == <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">{</span>
mockJson.errcode = <span class="token number">400</span><span class="token punctuation">;</span>
mockJson.errmsg = 'error<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
if(header.token == 't'<span class="token punctuation">)</span><span class="token punctuation">{</span>
mockJson.errcode = <span class="token number">300</span><span class="token punctuation">;</span>
mockJson.errmsg = 'error<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
if(cookie.type == 'a'<span class="token punctuation">)</span><span class="token punctuation">{</span>
mockJson.errcode = <span class="token number">500</span><span class="token punctuation">;</span>
mockJson.errmsg = 'error<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre><h3 class="subject" id="示例2,_生成高度自定义数据内容">示例2, 生成高度自定义数据内容 <a class="hashlink" href="#示例2,_生成高度自定义数据内容">#</a></h3><pre><code>var a = <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">]</span>
mockJson = <span class="token punctuation">{</span>
errcode<span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
email<span class="token operator">:</span> Random.email('qq.com'<span class="token punctuation">)</span><span class="token punctuation">,</span>
data<span class="token operator">:</span> a.map(function(item<span class="token punctuation">)</span><span class="token punctuation">{</span>
return Random.city(<span class="token punctuation">)</span> + '银行'
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code></pre><h2 class="subject" id="Mock_优先级说明">Mock 优先级说明 <a class="hashlink" href="#Mock_优先级说明">#</a></h2><p>请求 Mock 数据时规则匹配优先级Mock 期望 &gt; 自定义 Mock 脚本 &gt; 普通 Mock。</p>
<p>如果前面匹配到 Mock 数据,后面 Mock 则不返回。</p>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="copyright">
&copy; 2016 <a href="http://ued.qunar.com/ymfe/">YMFE</a> Team. Build by <a href="http://ued.qunar.com/ydoc/">ydoc</a>.
</div>
</footer>
</div>
<div class="open-panel"></div>
<div class="mask"></div>
<script src="source/main.js"></script>
<script src="source/app.js"></script>
</body>
</html>

View File

@ -642,7 +642,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#227" target="_blank">./server/controllers/group.js:227</a>
<a href="./static/server/controllers/group.js.html#213" target="_blank">./server/controllers/group.js:213</a>
</p>
@ -728,7 +728,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#279" target="_blank">./server/controllers/group.js:279</a>
<a href="./static/server/controllers/group.js.html#265" target="_blank">./server/controllers/group.js:265</a>
</p>
@ -790,7 +790,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#305" target="_blank">./server/controllers/group.js:305</a>
<a href="./static/server/controllers/group.js.html#291" target="_blank">./server/controllers/group.js:291</a>
</p>
@ -864,7 +864,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#356" target="_blank">./server/controllers/group.js:356</a>
<a href="./static/server/controllers/group.js.html#342" target="_blank">./server/controllers/group.js:342</a>
</p>
@ -926,7 +926,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#418" target="_blank">./server/controllers/group.js:418</a>
<a href="./static/server/controllers/group.js.html#404" target="_blank">./server/controllers/group.js:404</a>
</p>
@ -998,7 +998,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/group.js.html#458" target="_blank">./server/controllers/group.js:458</a>
<a href="./static/server/controllers/group.js.html#448" target="_blank">./server/controllers/group.js:448</a>
</p>
@ -3379,7 +3379,7 @@
<tr>
<td>req_params</td>
<td>Array</td>
<td>name, desc两个参数</td>
<td>路径参数 name, desc两个参数</td>
<td>
</td>
@ -3694,7 +3694,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interface.js.html#326" target="_blank">./server/controllers/interface.js:326</a>
<a href="./static/server/controllers/interface.js.html#329" target="_blank">./server/controllers/interface.js:329</a>
</p>
@ -3927,7 +3927,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interface.js.html#530" target="_blank">./server/controllers/interface.js:530</a>
<a href="./static/server/controllers/interface.js.html#533" target="_blank">./server/controllers/interface.js:533</a>
</p>
@ -3999,7 +3999,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interface.js.html#729" target="_blank">./server/controllers/interface.js:729</a>
<a href="./static/server/controllers/interface.js.html#732" target="_blank">./server/controllers/interface.js:732</a>
</p>
@ -4507,7 +4507,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interfaceCol.js.html#162" target="_blank">./server/controllers/interfaceCol.js:162</a>
<a href="./static/server/controllers/interfaceCol.js.html#164" target="_blank">./server/controllers/interfaceCol.js:164</a>
</p>
@ -4689,7 +4689,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interfaceCol.js.html#304" target="_blank">./server/controllers/interfaceCol.js:304</a>
<a href="./static/server/controllers/interfaceCol.js.html#306" target="_blank">./server/controllers/interfaceCol.js:306</a>
</p>
@ -4859,7 +4859,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interfaceCol.js.html#373" target="_blank">./server/controllers/interfaceCol.js:373</a>
<a href="./static/server/controllers/interfaceCol.js.html#371" target="_blank">./server/controllers/interfaceCol.js:371</a>
</p>
@ -4921,7 +4921,7 @@
<p>
<small class="text-muted">源码位置:</small>
<a href="./static/server/controllers/interfaceCol.js.html#440" target="_blank">./server/controllers/interfaceCol.js:440</a>
<a href="./static/server/controllers/interfaceCol.js.html#438" target="_blank">./server/controllers/interfaceCol.js:438</a>
</p>

View File

@ -124,7 +124,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li class="active" class="active" > -->
@ -161,9 +167,9 @@
<div class="content-right markdown-body use-sidebar" role="main">
<h2 class="subject" id="介绍">介绍 <a class="hashlink" href="#介绍">#</a></h2><p style='text-indent:2em;line-height:1.8em'>在平时的开发过程中经常遇到的一个问题是每次调试接口都需要重新填写参数YApi测试集可以保存之前填写的参数方便下次的调试。YApi测试集还可以一次性测试所有接口每个的请求参数可以通过前面已请求的接口数据读取或填写mock随机字符串</p>
<h2 class="subject" id="介绍">介绍 <a class="hashlink" href="#介绍">#</a></h2><p style='text-indent:2em;line-height:1.8em'>在平时的开发过程中经常遇到的一个问题是每次调试接口都需要重新填写参数YApi测试集不但能够保存之前填写的参数,方便下次的调试,还可以一次性测试所有接口每个的请求参数可以通过前面已请求的接口数据读取或填写mock随机字符串,通过设置断言脚本验证返回数据的正确性,</p>
<h2 class="subject" id="测试列表">测试列表 <a class="hashlink" href="#测试列表">#</a></h2><p><img class="doc-img" style="width:100%" src="./images/usage/case-list.jpg" /></p>
<h2 class="subject" id="测试列表">测试列表 <a class="hashlink" href="#测试列表">#</a></h2><p><img class="doc-img" style="width: 618px;" src="./images/usage/case-list.gif" /></p>
<p>在测试列表可以看到每个测试用例的 key,还有 开始测试、报告等功能</p>
<p>点击开始测试会按照 case 定义的参数从上往下一个一个进行测试,如果顺序有问题,可以拖动调整</p>
<p>测试完成之后,点击报告查看该次请求的结果</p>
@ -191,7 +197,22 @@
<blockquote>
<p>Tips: 上下拖动测试集合的列表项可以调整测试的顺序。</p>
</blockquote>
<h3 class="subject" id="高级">高级 <a class="hashlink" href="#高级">#</a></h3><p>可通过写断言脚本,实现精准测试,支持 js 所有语法</p>
<h4 class="subject" id="公共变量">公共变量 <a class="hashlink" href="#公共变量">#</a></h4><ul>
<li><p>assert </p>
<p>断言函数,详细 api可查看 <a target="_blank" href="https://nodejs.org/dist/latest-v8.x/docs/api/assert.html">document</a></p>
</li><li><p>status</p>
<p>http 状态码</p>
</li><li><p>body </p>
<p>返回 response body</p>
</li><li><p>header </p>
<p>返回 response header</p>
</li><li><p>records </p>
<p>记录的 http 请求信息,假设需要获取 key为555的接口参数或者响应数据可通过 records[555].params 或 records[555].body 获取 </p>
</li></ul>
<h4 class="subject" id="示例">示例 <a class="hashlink" href="#示例">#</a></h4><pre><code>assert.equal(body.errcode<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span>
assert.equal(body.data.group_name<span class="token punctuation">,</span> 'testGroup'<span class="token punctuation">)</span>
</code></pre>
</div>
</div>

View File

@ -124,7 +124,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -144,7 +144,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

View File

@ -124,7 +124,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -136,7 +136,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -144,7 +144,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -124,7 +124,7 @@
<!-- <li class="active" class="active" > -->
<li class="active" >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<ul class="nav docs-sidenav-extend" >
@ -134,7 +134,7 @@
</li>
<li >
<a href="#定义mock数据示例">定义mock数据示例</a>
<a href="#定义_mock_数据示例">定义 mock 数据示例</a>
</li>
<li >
@ -142,19 +142,21 @@
</li>
<li >
<a href="#如何使用Mock?">如何使用Mock?</a>
<a href="#如何使用_Mock">如何使用 Mock</a>
</li>
<li >
<a href="#高级Mock">高级Mock</a>
</li>
<li >
<a href="#Mock语法规范">Mock语法规范</a>
<a href="#Mock_语法规范">Mock 语法规范</a>
</li>
</ul>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->
<li >
@ -173,13 +175,15 @@
<div class="content-right markdown-body use-sidebar" role="main">
<h2 class="subject" id="Mock介绍">Mock介绍 <a class="hashlink" href="#Mock介绍">#</a></h2> <p style='text-indent:2em;line-height:1.8em'>YApi的Mock功能可以根据用户的输入接口信息如协议、URL、接口名、请求头、请求参数、mock规则(<a href="#mock">点击到Mock规则</a>生成Mock接口这些接口会自动生成模拟数据创建者可以自由构造需要的数据。而且与常见的Mock方式如将Mock写在代码里和JS拦截等相比yapi的Mock在使用场景和效率和复杂度上是相差甚远的正是由于yapi的Mock是一个第三方平台那么在团队开发时任何人都可以权限许可下创建、修改接口信息等操作这对于团队开发是很有好处的。 </p>
<h2 class="subject" id="Mock介绍">Mock介绍 <a class="hashlink" href="#Mock介绍">#</a></h2> <p style='text-indent:2em;line-height:1.8em'>YApi的 Mock 功能可以根据用户的输入接口信息如协议、URL、接口名、请求头、请求参数、mock 规则(<a href="#mock">点击到 Mock 规则</a>)生成 Mock 接口,这些接口会自动生成模拟数据,创建者可以自由构造需要的数据。而且与常见的 Mock 方式如将 Mock 写在代码里和JS拦截等相比 yapi Mock 在使用场景和效率和复杂度上是相差甚远的,正是由于 yapi Mock 是一个第三方平台,那么在团队开发时任何人都可以权限许可下创建、修改接口信息等操作,这对于团队开发是很有好处的。 </p>
<p> <strong>mock地址解析</strong>yapi平台网址+mock+<strong>您的项目id</strong>+<strong>接口实际请求path</strong></p>
<p> 假设你 YApi 的部署地址为:<a href="http://yapi.xxx.com然后后面的都可以用这个地址作为示例">http://yapi.xxx.com然后后面的都可以用这个地址作为示例</a></p>
<pre><code>mockd地址 http<span class="token operator">:</span>//yapi.xxx.com/mock/<span class="token number">29</span>/api/hackathon/login
</code></pre><p>项目id可以在项目设置里查看到</p>
<h2 class="subject" id="定义mock数据示例">定义mock数据示例 <a class="hashlink" href="#定义mock数据示例">#</a></h2><pre><code><span class="token punctuation">{</span>
<p> <strong>mock地址解析</strong><code>YApi平台网址 + mock + 您的项目id + 接口实际请求path</code></p>
<p> 假设你 YApi 的部署地址为:<a href="http://yapi.xxx.com">http://yapi.xxx.com</a> 然后用这个地址作为示例</p>
<pre><code>mockd 地址: http<span class="token operator">:</span>//yapi.xxx.com/mock/<span class="token number">29</span>/api/hackathon/login
</code></pre><blockquote>
<p>注:项目 id 可以在项目设置里查看到</p>
</blockquote>
<h2 class="subject" id="定义_mock_数据示例">定义 mock 数据示例 <a class="hashlink" href="#定义_mock_数据示例">#</a></h2><pre><code><span class="token punctuation">{</span>
<span class="token property">"status|0-1"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span> //接口状态
<span class="token property">"message"</span><span class="token operator">:</span> <span class="token string">"请求完成"</span><span class="token punctuation">,</span> //消息提示
<span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
@ -198,48 +202,41 @@
<span class="token punctuation">}</span>
<span class="token punctuation">]</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre><h2 class="subject" id="YApi-Mock_跟_Mockjs_区别">YApi-Mock 跟 Mockjs 区别 <a class="hashlink" href="#YApi-Mock_跟_Mockjs_区别">#</a></h2><p><a href="http://mockjs.com">Mockjs 官网</a></p>
</code></pre><h2 class="subject" id="YApi-Mock_跟_Mockjs_区别">YApi-Mock 跟 Mockjs 区别 <a class="hashlink" href="#YApi-Mock_跟_Mockjs_区别">#</a></h2><p><a href="http://mockjs.com/examples.html">Mockjs 官网</a></p>
<p>1 因为 yapi 基于 json 定义 mock ,无法使用 mockjs 原有的函数功能,正则表达式需要基于 rule 书写,示例如下:</p>
<pre><code><span class="token punctuation">{</span>
<span class="token property">"name|regexp"</span><span class="token operator">:</span> <span class="token string">"[a-z0-9_]+?"</span><span class="token punctuation">,</span>
<span class="token property">"type|regexp"</span><span class="token operator">:</span> <span class="token string">"json|text|xml"</span> //枚举数据类型可这样实现
<span class="token punctuation">}</span>
</code></pre><p>2 支持替换请求的query,body参数</p>
</code></pre><p>2 支持替换请求的 query, body 参数</p>
<pre><code><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"${query.name}"</span><span class="token punctuation">,</span> //请求的url是/path?name=xiaoming<span class="token punctuation">,</span> 返回的name字段是xiaoming
<span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"${body.type}"</span> //请求的requestBody type=<span class="token number">1</span><span class="token punctuation">,</span>返回的type字段是<span class="token number">1</span>
<span class="token punctuation">}</span>
</code></pre><h2 class="subject" id="如何使用Mock?">如何使用Mock? <a class="hashlink" href="#如何使用Mock?">#</a></h2><h3 class="subject" id="1_在js代码直接请求yapi提供的mock地址不用担心跨域问题">1 在js代码直接请求yapi提供的mock地址不用担心跨域问题 <a class="hashlink" href="#1_在js代码直接请求yapi提供的mock地址不用担心跨域问题">#</a></h3><p>在代码直接请求yapi提供的mock地址以jQuery为例</p>
</code></pre><h2 class="subject" id="如何使用_Mock">如何使用 Mock <a class="hashlink" href="#如何使用_Mock">#</a></h2><h3 class="subject" id="1_在_js_代码直接请求yapi提供的_mock_地址(不用担心跨域问题)">1 在 js 代码直接请求yapi提供的 mock 地址(不用担心跨域问题) <a class="hashlink" href="#1_在_js_代码直接请求yapi提供的_mock_地址(不用担心跨域问题)">#</a></h3><p>在代码直接请求 yapi 提供的 mock 地址,以 jQuery 为例:</p>
<pre><code class="lang-javascript"><span class="token keyword">let</span> prefix <span class="token operator">=</span> <span class="token string">'http://yapi.xxx.com/mock/2817'</span>
$<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span>prefix<span class="token operator">+</span><span class="token string">'/baseapi/path'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>username<span class="token punctuation">:</span> <span class="token string">'xxx'</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span>res<span class="token punctuation">)</span><span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>res<span class="token punctuation">)</span> <span class="token comment" spellcheck="true">//返回上图预览部分的数据</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
</code></pre>
<h3 class="subject" id="2_基于本地服务器反向代理">2 基于本地服务器反向代理 <a class="hashlink" href="#2_基于本地服务器反向代理">#</a></h3><p>优点:不用修改项目代码</p>
<h4 class="subject" id="2.1_基于nginx反向代理">2.1 基于nginx反向代理 <a class="hashlink" href="#2.1_基于nginx反向代理">#</a></h4><pre><code class="lang-nginx"><span class="token keyword">location</span> <span class="token operator">/</span>baseapi
<h4 class="subject" id="2.1_基于_nginx_反向代理">2.1 基于 nginx 反向代理 <a class="hashlink" href="#2.1_基于_nginx_反向代理">#</a></h4><pre><code class="lang-nginx"><span class="token keyword">location</span> <span class="token operator">/</span>baseapi
<span class="token punctuation">{</span>
<span class="token keyword">proxy_pass</span> <span class="token keyword">http</span><span class="token punctuation">:</span><span class="token operator">/</span><span class="token operator">/</span>yapi<span class="token punctuation">.</span>xxx<span class="token punctuation">.</span>com<span class="token operator">/</span>mock<span class="token operator">/</span><span class="token number">2817</span><span class="token operator">/</span>baseapi<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">#baseapi后面没有"/"</span>
<span class="token punctuation">}</span>
</code></pre>
<h4 class="subject" id="2.2_基于ykit_mock功能">2.2 基于ykit mock功能 <a class="hashlink" href="#2.2_基于ykit_mock功能">#</a></h4><pre><code class="lang-javascript"><span class="token punctuation">{</span>
<h4 class="subject" id="2.2_基于_ykit_mock功能">2.2 基于 ykit mock功能 <a class="hashlink" href="#2.2_基于_ykit_mock功能">#</a></h4><pre><code class="lang-javascript"><span class="token punctuation">{</span>
pattern<span class="token punctuation">:</span> <span class="token regex">/\/api\/(.*)/</span><span class="token punctuation">,</span>
responder<span class="token punctuation">:</span> <span class="token string">'http://yapi.xxx.com/mock/58/api/$1'</span>
<span class="token punctuation">}</span>
</code></pre>
<p>上面通过正则匹配,将所有接口转到 <a href="http://yapi.xxx.com">http://yapi.xxx.com</a> 上,比如 <code>http://localhost/api/user/status</code> 会成为 <code>http://yapi.xxx.com/mock/58/api/user/status</code></p>
<p>详细使用指南: <a target="_blank" href="https://ykit.ymfe.org/plugins-mock.html#获取远程数据_Map_Remote_">ykit-config-mock</a></p>
<h4 class="subject" id="2.3_基于ykit_Jerry代理">2.3 基于ykit Jerry代理 <a class="hashlink" href="#2.3_基于ykit_Jerry代理">#</a></h4><p>假设您本地服务器访问地址是: <a href="http://xxx.com">http://xxx.com</a></p>
<h4 class="subject" id="2.3_基于_ykit_Jerry_代理">2.3 基于 ykit Jerry 代理 <a class="hashlink" href="#2.3_基于_ykit_Jerry_代理">#</a></h4><p>假设您本地服务器访问地址是: <a href="http://xxx.com">http://xxx.com</a></p>
<p><img src="./images/ykit.jpg" /></p>
<p><span id="mock"></span></p>
<h2 class="subject" id="高级Mock">高级Mock <a class="hashlink" href="#高级Mock">#</a></h2><p>在前端开发阶段,对于某些接口,业务相对复杂,而 UI 端也需要根据接口返回的不同内容去做相应的处理</p>
<p>YApi 提供了写 js 脚本方式处理这一问题,可以根据用户请求的参数修改返回内容。</p>
<h3 class="subject" id="全局变量">全局变量 <a class="hashlink" href="#全局变量">#</a></h3><ol>
<li>mockJson</li><li>query</li><li>body</li><li>header</li><li>cookie</li></ol>
<h3 class="subject" id="使用方法">使用方法 <a class="hashlink" href="#使用方法">#</a></h3><ol>
<li>首先开启此功能</li><li>mock脚本就是用 javascript 对 mockJson 变量修改</li></ol>
<h3 class="subject" id="示例:">示例: <a class="hashlink" href="#示例:">#</a></h3><p><img class="doc-img" style="width: 80%" src="./images/usage/adv-mock.jpg" /></p>
<h2 class="subject" id="Mock语法规范">Mock语法规范 <a class="hashlink" href="#Mock语法规范">#</a></h2><blockquote>
<p>了解更多Mock详情<a href="https://github.com/nuysoft/Mock/wiki/Syntax-Specification">Mock.js 官方文档</a></p>
<h2 class="subject" id="Mock_语法规范">Mock 语法规范 <a class="hashlink" href="#Mock_语法规范">#</a></h2><blockquote>
<p>了解更多Mock详情<a href="http://mockjs.com/examples.html">Mock.js 官方文档</a></p>
</blockquote>
<p>Mock.js 的语法规范包括两部分:</p>
<p><a href="#DTD">1. 数据模板定义规范Data Template DefinitionDTD</a></p>

View File

@ -140,7 +140,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -140,7 +140,13 @@
<!-- <li > -->
<li >
<a href="mock.html">使用mock功能</a>
<a href="mock.html">普通 Mock</a>
</li>
<!-- <li > -->
<li >
<a href="adv_mock.html">高级 Mock</a>
</li>
<!-- <li > -->

View File

@ -91,6 +91,20 @@
<nav class="docs-sidebar hidden-print hidden-xs hidden-sm">
<ul class="nav docs-sidenav">
<!-- <li > -->
<li >
<a href="#1.1.3">1.1.3</a>
</li>
<ul class="nav docs-sidenav-extend" >
<li >
<a href="#Bug_Fixed">Bug Fixed</a>
</li>
</ul>
<!-- <li > -->
<li >
@ -179,7 +193,9 @@
<div class="content-right markdown-body use-sidebar" role="main">
<h2 class="subject" id="1.1.2">1.1.2 <a class="hashlink" href="#1.1.2">#</a></h2><h3 class="subject" id="Features">Features <a class="hashlink" href="#Features">#</a></h3><ul>
<h2 class="subject" id="1.1.3">1.1.3 <a class="hashlink" href="#1.1.3">#</a></h2><h3 class="subject" id="Bug_Fixed">Bug Fixed <a class="hashlink" href="#Bug_Fixed">#</a></h3><ul>
<li>修复了切换集合环境的 Bug</li><li>修复了 mockServer 拿不到 Post 请求 Body</li><li>修复了接口调试 pathParams 无法使用 mock 参数和变量参数</li></ul>
<h2 class="subject" id="1.1.2">1.1.2 <a class="hashlink" href="#1.1.2">#</a></h2><h3 class="subject" id="Features">Features <a class="hashlink" href="#Features">#</a></h3><ul>
<li>接口运行增加了 query 和 body 的 enable 选项,可选择是否请求该字段</li><li>Mock 支持了时间戳占位符 @timestamp</li><li>接口集运行页面可选择环境</li><li>接口集动态参数格式由原来的 $.{key}.{jsonPath} 改为 $.{key}.{body|params}.{jsonPath}</li></ul>
<h3 class="subject" id="Bug_Fixed">Bug Fixed <a class="hashlink" href="#Bug_Fixed">#</a></h3><ul>
<li>修复了接口集运行功能会忽略环境配置的 domain 路径</li><li>修复了动态路由 mock 返回结果不是该接口定义返回数据</li><li>修复了日志链接错误问题</li><li>修复了添加用户 loading 问题</li><li>修复了用户名编辑,前台未更新问题</li><li>修复了复制接口导致 GET 请求显示 request-body 问题</li><li>修复了接口集页面刷新后跳转到第一个接口集问题</li><li>修复了接口用例页面修改 header 参数值没有效果 bug</li><li>修复了接口集页面导入接口会导致 reqBody 清空 bug</li></ul>

View File

@ -205,20 +205,6 @@ class groupController extends baseController {
}
}
// params.role = ['owner', 'dev', 'guest'].find(v => v === params.role) || 'dev';
// var check = await groupInst.checkMemberRepeat(params.id, params.member_uid);
// if (check > 0) {
// return ctx.body = yapi.commons.resReturn(null, 400, '成员已存在');
// }
// let groupUserdata = await this.getUserdata(params.member_uid, params.role);
// if (groupUserdata === null) {
// return ctx.body = yapi.commons.resReturn(null, 400, '组长uid不存在')
// }
// if (groupUserdata._role === 'admin') {
// return ctx.body = yapi.commons.resReturn(null, 400, '不能邀请管理员')
// }
// delete groupUserdata._role;
try {
let result = await groupInst.addMember(params.id, add_members);
let username = this.getUsername();
@ -475,10 +461,14 @@ class groupController extends baseController {
await interfaceCaseInst.delByProjectId(p._id)
await interfaceColInst.delByProjectId(p._id)
})
await projectInst.delByGroupid(id);
if(projectList.length > 0){
await projectInst.delByGroupid(id);
}
let result = await groupInst.del(id);
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
console.error(err);
ctx.body = yapi.commons.resReturn(null, 402, err.message);
}
}

View File

@ -63,7 +63,7 @@ class interfaceController extends baseController {
* @param {Boolean} [req_headers[].required] 是否是必须,默认为否
* @param {String} [req_headers[].desc] header描述
* @param {String} [req_body_type] 请求参数方式,有["form", "json", "text", "xml"]四种
* @param {Array} [req_params] name, desc两个参数
* @param {Array} [req_params] 路径参数 name, desc两个参数
* @param {Mixed} [req_body_form] 请求参数,如果请求方式是form参数是Array数组其他格式请求参数是字符串
* @param {String} [req_body_form[].name] 请求参数名
* @param {String} [req_body_form[].value] 请求参数值可填写生成规则mock。如@email随机生成一条email
@ -327,6 +327,9 @@ class interfaceController extends baseController {
}
let project = await this.projectModel.getBaseInfo(project_id);
if(!project){
return ctx.body = yapi.commons.resReturn(null, 406, '不存在的项目');
}
if (project.project_type === 'private') {
if (await this.checkAuth(project._id, 'project', 'view') !== true) {
return ctx.body = yapi.commons.resReturn(null, 406, '没有权限');

View File

@ -181,7 +181,9 @@ class interfaceColController extends baseController{
resultList = resultList.sort((a,b)=>{
return a.index - b.index;
});
ctx.body = yapi.commons.resReturn(resultList);
let ctxBody = yapi.commons.resReturn(resultList);
ctxBody.colData = colData;
ctx.body = ctxBody;
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
@ -361,10 +363,6 @@ class interfaceColController extends baseController{
return ctx.body = yapi.commons.resReturn(null, 400, '用例id不能为空');
}
// if(!params.casename){
// return ctx.body = yapi.commons.resReturn(null, 400, '用例名称不能为空');
// }
let caseData = await this.caseModel.get(params.id);
let auth = await this.checkAuth(caseData.project_id, 'project', 'edit');
if (!auth) {
@ -373,9 +371,9 @@ class interfaceColController extends baseController{
params.uid = this.getUid();
//不允许修改接口id和项目id
delete params.interface_id;
delete params.project_id;
// delete params.col_id;
let result = await this.caseModel.up(params.id, params);
let username = this.getUsername();
@ -481,20 +479,22 @@ class interfaceColController extends baseController{
try{
let params = ctx.request.body;
let id = params.col_id;
if(!id){
return ctx.body = yapi.commons.resReturn(null, 400, '缺少 col_id 参数');
}
let colData = await this.colModel.get(id);
if(!colData){
return ctx.body = yapi.commons.resReturn(null, 400, '不存在');
}
let auth = await this.checkAuth(colData.project_id, 'project', 'edit')
if (!auth) {
return ctx.body = yapi.commons.resReturn(null, 400, '没有权限');
}
let result = await this.colModel.up(params.col_id, {
name: params.name,
desc: params.desc,
up_time: yapi.commons.time()
});
delete params.col_id;
let result = await this.colModel.up(id, params);
let username = this.getUsername();
yapi.commons.saveLog({
contnet: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${colData.project_id}/interface/col/${params.col_id}">${params.name}</a> 的信息`,
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${colData.project_id}/interface/col/${id}">${colData.name}</a> 的信息`,
type: 'project',
uid: this.getUid(),
username: username,
@ -622,6 +622,28 @@ class interfaceColController extends baseController{
}
}
async runCaseScript(ctx){
let params = ctx.request.body;
let script = params.script;
if(!script){
return ctx.body = yapi.commons.resReturn('ok');
}
try{
let a = yapi.commons.sandbox({
assert: require('assert'),
status: params.response.status,
body: params.response.body,
header: params.response.header,
records: params.records
}, script);
return ctx.body = yapi.commons.resReturn('ok');
}catch(err){
let errArr = err.stack.split("\n");
return ctx.body = yapi.commons.resReturn(errArr, 400, err.message)
}
}
}

View File

@ -8,6 +8,18 @@ body, h1, h2, h3, h4, h5, h6 {
margin-top: 3em;
}
.doc-img-wrapper {
background: #f7f7f7;
padding: 16px;
display: inline-block;
text-align: center;
width: 80%;
position: relative;
border-radius: 4px;
}
.doc-img-r {
display: block;
}
.doc-img {
width: 50%;
display: block;

View File

@ -0,0 +1,204 @@
import test from 'ava';
const jsm = require('../common/json-schema-mockjs.js');
test('jsmBase', t=>{
let json1 = {
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": ["firstName", "lastName"]
};
t.deepEqual(jsm(json1), {
firstName: '@string',
lastName: '@string',
age: "@integer"
});
})
test('jsmRef', t=>{
let json2 = {
"$ref": "#/definitions/Pet",
"definitions": {
"Order": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"petId": {
"type": "integer",
"format": "int64"
},
"quantity": {
"type": "integer",
"format": "int32"
},
"shipDate": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"description": "Order Status",
"enum": [
"placed",
"approved",
"delivered"
]
},
"complete": {
"type": "boolean",
"default": false
}
},
"xml": {
"name": "Order"
}
},
"Category": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Category"
}
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"username": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"userStatus": {
"type": "integer",
"format": "int32",
"description": "User Status"
}
},
"xml": {
"name": "User"
}
},
"Tag": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
}
},
"xml": {
"name": "Tag"
}
},
"Pet": {
"type": "object",
"required": [
"name",
"photoUrls"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"category": {
"$ref": "#/definitions/Category"
},
"name": {
"type": "string",
"example": "doggie"
},
"photoUrls": {
"type": "array",
"xml": {
"name": "photoUrl",
"wrapped": true
},
"items": {
"type": "string"
}
},
"status": {
"type": "string",
"description": "pet status in the store"
}
},
"xml": {
"name": "Pet"
}
},
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
}
}
const destJson2 = {
id: '@integer',
category: {
id: '@integer',
name: '@string'
},
name: '@string',
photoUrls: ['@string'],
status: '@string'
}
t.deepEqual(jsm(json2), destJson2);
})

143
test/lib.test.js Normal file
View File

@ -0,0 +1,143 @@
import test from 'ava';
const rewire = require("rewire");
const lib = rewire('../common/lib.js');
const initPlugins = lib.initPlugins;
test('initPlugins', t=>{
lib.__set__("getPluginConfig", function(){
return {
server: true,
client: true
}
})
let configs = initPlugins(['a', 'b'], 'exts');
t.deepEqual(configs, [{
name: 'a',
enable: true,
server: true,
client: true
}, {
name: 'b',
enable: true,
server: true,
client: true
}])
})
test('initPlugins2', t=>{
lib.__set__("getPluginConfig", function(){
return {
server: true,
client: false
}
})
let configs = initPlugins(['a', 'b'], 'exts');
t.deepEqual(configs, [{
name: 'a',
enable: true,
server: true,
client: false
}, {
name: 'b',
enable: true,
server: true,
client: false
}])
})
test('initPlugins3', t=>{
lib.__set__("getPluginConfig", function(){
return {
server: false,
client: true
}
})
let configs = initPlugins(['a', {name: 'a'}], 'exts');
t.deepEqual(configs, [{
name: 'a',
enable: true,
server: false,
client: true
}])
})
test('initPlugins3', t=>{
lib.__set__("getPluginConfig", function(){
return {
server: false,
client: true
}
})
let configs = initPlugins([{
name: 'a',
options: {
a:1,
t:{
c:3
}
}
}], 'exts');
t.deepEqual(configs, [{
name: 'a',
enable: true,
server: false,
client: true,
options: {
a:1,
t:{
c:3
}
}
}])
})
test('initPlugins3', t=>{
lib.__set__("getPluginConfig", function(){
return {
server: false,
client: false
}
})
let configs = initPlugins(['a', 'b'], 'exts');
t.deepEqual(configs, [])
})
test('testJsonEqual', t=>{
let json1 = {
a:"1",
b:2,
c:{
t:3,
x: [11,22]
}
};
let json2 = {
c:{
x: [11,22],
t:3
},
b:2,
a:"1"
}
t.true(lib.jsonEqual(json1, json1));
})
test('testJsonEqualBase', t=>{
t.true(lib.jsonEqual(1,1));
})
test('testJsonEqualBaseString', t=>{
t.true(lib.jsonEqual('2', '2'));
})
test('isDeepMatch', t=>{
t.true(lib.isDeepMatch({a:'aaaaa', b:2}, {a:'aaaaa'}))
})
test('isDeepMatch', t=>{
t.true(lib.isDeepMatch({a:1, b:2, c: {t:'ttt'}}, {c: {t:'ttt'}}))
})

107
test/mock-extra.test.js Normal file
View File

@ -0,0 +1,107 @@
import test from 'ava';
const mockExtra = require('../common/mock-extra.js');
test('mock-extra', t=>{
let data = '@string ${body.a}';
t.is(mockExtra(data), '@string ${body.a}');
let data2 = {
a:'@string',
b:{
t:'${body.a}'
}
}
t.deepEqual(mockExtra(data2,{
body: {
a: 3
}
}), {
a:'@string',
b:{
t:3
}
}, 'message');
//test object
let data3 = {
a:'@string',
b:{
t:'${body}'
}
}
t.deepEqual(mockExtra(data3,{
body: {
a: 3,
t: 5
}
}), {
a:'@string',
b:{
t:{
a: 3,
t: 5
}
}
}, 'message');
//test array
let data4 = {
a:'@string',
b:{
t:'${query.arr}'
}
}
t.deepEqual(mockExtra(data4, {query: {
arr: [1,2,3]
}}), {
a: '@string',
b:{
t: [1,2,3]
}
}, 'message');
//test var
let data5 = {
a:'@string',
b:{
t:'${ttt.arr}'
}
}
t.deepEqual(mockExtra(data5, {ttt: {
arr: [1,2,3]
}}), {
a: '@string',
b:{
t: [1,2,3]
}
}, 'message');
//test var
let data6 = {
a:'@string',
b:{
"ttt|regexp":'a|b'
}
}
//test regexp
t.deepEqual(mockExtra(data6, {ttt: {
arr: [1,2,3]
}}), {
a: '@string',
b:{
ttt: /a|b/
}
}, 'message');
})

View File

@ -2,6 +2,7 @@ import test from 'ava';
const rewire = require("rewire");
const mockServer = rewire('../../server/middleware/mockServer.js');
const matchApi = mockServer.__get__('matchApi');
const mockExtra = require('../../common/mock-extra.js');
test('matchApi', t => {
const apiRule = '/user/:username';
@ -20,4 +21,4 @@ test('matchApi', t => {
t.false(matchApi('/user/a/ttt2/b', apiRule_3))
});
});

View File

@ -49,9 +49,13 @@
"index": "interface",
"content": "./doc/page/usage/api.md"
},{
"name": "使用mock功能",
"name": "普通 Mock",
"index": "mock",
"content": "./doc/page/usage/mock.md"
},{
"name": "高级 Mock",
"index": "adv_mock",
"content": "./doc/page/usage/adv_mock.md"
},{
"name": "使用测试集",
"index": "case",