Merge branch 'dev' of gitlab.corp.qunar.com:mfe/yapi into dev

This commit is contained in:
zwjamnsss 2017-10-24 21:02:44 +08:00
commit 394acf8522
9 changed files with 303 additions and 140 deletions

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 {
@ -197,7 +198,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}`);

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,15 @@ 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 +167,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) {
@ -74,7 +77,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,8 +91,7 @@ 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) {
@ -112,6 +117,7 @@ class InterfaceColContent extends Component {
})
}
}, 500)
}
componentWillUnmount() {
@ -161,20 +167,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 = {};
}
}
@ -195,7 +201,7 @@ class InterfaceColContent extends Component {
}
}
handleTest = (interfaceData) => {
handleTest = async (interfaceData) => {
const { currProject } = this.props;
const { case_env } = interfaceData;
let path = URL.resolve(currProject.basepath, interfaceData.path);
@ -204,13 +210,13 @@ class InterfaceColContent extends Component {
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
});
const domains = currProject.env.concat();
let currDomain = domains.find(item => item.name === case_env);
if(!currDomain){
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 +228,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 +378,7 @@ class InterfaceColContent extends Component {
return obj;
}
getQueryObj = (query) => {
query = query || [];
@ -378,18 +427,29 @@ class InterfaceColContent extends Component {
}
async componentWillReceiveProps(nextProps) {
const { interfaceColList } = nextProps;
const { actionId: oldColId, id } = this.props.match.params
//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 !== this.currColId){
this.currColId = newColId;
if (newColId && newColId != 0) {
await this.props.fetchCaseList(newColId);
this.props.setColData({ currColId: +newColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
}
// 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)
// }
// }
}
openReport = (id) => {
@ -400,7 +460,60 @@ 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 +524,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({
@ -501,22 +614,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>
}]
}
}
@ -544,14 +664,14 @@ class InterfaceColContent extends Component {
<div style={{ display: 'inline-block', margin: 0, marginBottom: '16px' }}>
<Select value={currColEnv} style={{ width: "320px" }} onChange={this.colEnvChange}>
{
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 +704,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

@ -1,4 +1,4 @@
var strRegex = /\${([a-zA-Z0-9_\.]+)\}/g;
var strRegex = /\${(body|query)([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,7 +63,11 @@ 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];
if(!name) return str;
var names = name.split(varSplit);
var data = context;
names.forEach(function (n) {
@ -75,7 +79,7 @@ function mock(mockJSON, context) {
}
});
return data;
});
}
return str;
}
}

View File

@ -334,10 +334,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 +342,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();
@ -595,6 +591,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

@ -34,8 +34,9 @@ class interfaceCase extends baseModel {
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

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