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

This commit is contained in:
zwjamnsss 2017-09-21 12:13:17 +08:00
commit 765889462d
18 changed files with 969 additions and 144 deletions

View File

@ -1,6 +1,7 @@
import React from 'react';
import moment from 'moment';
import constants from './constants/variable';
import Mock from 'mockjs'
const Roles = {
0 : 'admin',
@ -71,6 +72,40 @@ exports.debounce = (func, wait) => {
};
};
exports.simpleJsonPathParse = function (key, json){
if(!key || typeof key !== 'string' || key.indexOf('$.') !== 0 || key.length <= 2){
return null;
}
let keys = key.substr(2).split(".");
keys = keys.filter(item=>{
return item;
})
for(let i=0, l = keys.length; i< l; i++){
try{
let m = keys[i].match(/(.*?)\[([0-9]+)\]/)
if(m){
json = json[m[1]][m[2]];
}else{
json = json[keys[i]];
}
}catch(e){
json = null;
break;
}
}
return json;
}
exports.handleMockWord =(word) =>{
if(!word || typeof word !== 'string' || word[0] !== '@') return word;
return Mock.mock(word);
}
// 从 Javascript 对象中选取随机属性
exports.pickRandomProperty = (obj) => {
let result;

View File

@ -10,6 +10,7 @@ import URL from 'url';
const MockExtra = require('common/mock-extra.js')
import './Postman.scss';
import json5 from 'json5'
import { handleMockWord } from '../../common.js'
function json_parse(data) {
try {
@ -19,23 +20,8 @@ function json_parse(data) {
}
}
function isValidJson(json) {
if (!json) return false;
if (typeof json === 'object') return true;
try {
if (typeof json === 'string') {
json5.parse(json);
return true;
}
} catch (e) {
return false;
}
}
function isJsonData(headers, res) {
if (isValidJson(res)) {
return true;
}
function isJsonData(headers) {
if (!headers || typeof headers !== 'object') return false;
let isResJson = false;
Object.keys(headers).map(key => {
@ -75,8 +61,9 @@ export default class Run extends Component {
bodyType: '',
bodyOther: '',
loading: false,
validRes: null,
hasPlugin: true
validRes: [],
hasPlugin: true,
test_status: null
}
constructor(props) {
@ -136,8 +123,13 @@ export default class Run extends Component {
req_body_form = [],
basepath = '',
env = [],
case_env = ''
case_env = '',
test_status = '',
test_res_body = '',
test_report = [],
test_res_header=''
} = data;
// case 任意编辑 pathname不管项目的 basepath
const pathname = (type === 'inter' ? (basepath + url) : url).replace(/\/+/g, '/');
@ -168,12 +160,21 @@ export default class Run extends Component {
bodyOther: req_body_other,
caseEnv: case_env || (env[0] && env[0].name),
bodyType: req_body_type || 'form',
loading: false
loading: false,
test_status: test_status,
validRes: test_report,
res: test_res_body,
resHeader: test_res_header
}, () => {
if (req_body_type && req_body_type !== 'file' && req_body_type !== 'form') {
this.loadBodyEditor()
}
if(test_res_body){
this.bindAceEditor();
}
});
}
@autobind
@ -192,7 +193,7 @@ export default class Run extends Component {
const href = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
pathname: urlObj.pathname ? urlObj.pathname + path : path,
pathname: urlObj.pathname ? URL.resolve(urlObj.pathname, path) : path,
query: this.getQueryObj(query)
});
@ -212,7 +213,7 @@ export default class Run extends Component {
}
const { res_body, res_body_type } = that.props.data;
let validRes = '';
let validRes = [];
let query = {};
that.state.query.forEach(item => {
query[item.name] = item.value;
@ -233,8 +234,17 @@ export default class Run extends Component {
validRes = Mock.valid(tpl, res)
}
message.success('请求完成')
that.setState({ res, resHeader: header, validRes })
if (Array.isArray(validRes) && validRes.length > 0) {
message.warn('请求完成, 返回数据跟接口定义不匹配');
validRes = validRes.map(item => {
return item.message
})
that.setState({ res, resHeader: header, validRes, test_status: 'invalid' })
} else if (Array.isArray(validRes) && validRes.length === 0) {
message.success('请求完成');
that.setState({ res, resHeader: header, validRes: ['验证通过'], test_status: 'ok' })
}
that.setState({ loading: false })
that.bindAceEditor()
} catch (e) {
@ -250,7 +260,7 @@ export default class Run extends Component {
message.error(e.message)
}
message.error('请求异常')
that.setState({ res: err || '请求失败', resHeader: header, validRes: null })
that.setState({ res: err || '请求失败', resHeader: header, validRes: [], test_status: 'error' })
that.setState({ loading: false })
that.bindAceEditor()
}
@ -428,11 +438,12 @@ export default class Run extends Component {
const obj = {};
arr.forEach(item => {
if (item.name && item.type !== 'file') {
obj[item.name] = item.value || '';
obj[item.name] = handleMockWord(item.value);
}
})
return obj;
}
getFiles(bodyForm) {
const files = {};
bodyForm.forEach(item => {
@ -446,7 +457,7 @@ export default class Run extends Component {
const queryObj = {};
query.forEach(item => {
if (item.name) {
queryObj[item.name] = item.value || '';
queryObj[item.name] = handleMockWord(item.value);
}
})
return queryObj;
@ -498,11 +509,10 @@ export default class Run extends Component {
}
render() {
const { method, domains, pathParam, pathname, query, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes, res } = this.state;
const { method, domains, pathParam, pathname, query, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes } = this.state;
HTTP_METHOD[method] = HTTP_METHOD[method] || {}
const hasPlugin = this.state.hasPlugin;
let isResJson = isJsonData(resHeader, res);
let isResJson = isJsonData(resHeader);
let path = pathname;
pathParam.forEach(item => {
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
@ -510,16 +520,10 @@ export default class Run extends Component {
const search = decodeURIComponent(URL.format({ query: this.getQueryObj(query) }));
let validResView;
if (!validRes) {
validResView = '请定义返回json'
}
if (Array.isArray(validRes) && validRes.length > 0) {
validResView = validRes.map((item, index) => {
return <div key={index}>{item.message}</div>
})
} else if (Array.isArray(validRes)) {
validResView = <p>验证通过</p>
}
validResView = validRes.map((item, index) => {
return <div key={index}>{item}</div>
})
@ -569,7 +573,7 @@ export default class Run extends Component {
domains.map((item, index) => (<Option value={item.name} key={index}>{item.name + '' + item.domain}</Option>))
}
</Select>
<Input disabled value={path + search} onChange={this.changePath} spellCheck="false" style={{ flexBasis: 180, flexGrow: 1 }} />
</InputGroup>
@ -587,7 +591,7 @@ export default class Run extends Component {
onClick={this.props.save}
type="primary"
style={{ marginLeft: 10 }}
>{this.props.type === 'inter' ? '保存' : '更新'}</Button>
>{this.props.type === 'inter' ? '保存' : '保存'}</Button>
</Tooltip>
</div>

View File

@ -49,8 +49,8 @@
transition: all .2s;
}
.delete-group:hover, .edit-group:hover {
color: $color-blue;
background-color: $color-white;
background-color: $color-blue;
border: 1px solid $color-blue;
}
}
.group-operate {

View File

@ -0,0 +1,93 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Row, Col, Tabs } from 'antd'
const TabPane = Tabs.TabPane;
function json_format(json) {
return JSON.stringify(json, null, ' ')
}
const CaseReport = function (props) {
let body = json_format(props.body);
let headers = json_format(props.headers, null, ' ');
let res_header = json_format(props.res_header, null, ' ');
let res_body = json_format(props.res_body);
let validRes = props.validRes.map((item, index) => {
return <div key={index}>{item.message}</div>
})
return <div className="report">
<Tabs defaultActiveKey="request" >
<TabPane className="case-report-pane" tab="Request" key="request">
<Row className="case-report">
<Col className="case-report-title" span="6">Url</Col>
<Col span="18">{props.url}</Col>
</Row>
{props.query ?
<Row className="case-report">
<Col className="case-report-title" span="6">Query</Col>
<Col span="18">{props.query}</Col>
</Row>
: null
}
{props.headers ?
<Row className="case-report">
<Col className="case-report-title" span="6">Headers</Col>
<Col span="18"><pre>{headers}</pre></Col>
</Row>
: null
}
{props.body ?
<Row className="case-report">
<Col className="case-report-title" span="6">Body</Col>
<Col span="18"><pre>{body}</pre></Col>
</Row>
: null
}
</TabPane>
<TabPane className="case-report-pane" tab="Response" key="response">
{props.res_header ?
<Row className="case-report">
<Col className="case-report-title" span="6">Headers</Col>
<Col span="18"><pre>{res_header}</pre></Col>
</Row>
: null
}
{props.res_body ?
<Row className="case-report">
<Col className="case-report-title" span="6">Body</Col>
<Col span="18"><pre>{res_body}</pre></Col>
</Row>
: null
}
</TabPane>
<TabPane className="case-report-pane" tab="验证结果" key="valid">
{props.validRes ?
<Row className="case-report">
<Col className="case-report-title" span="6">验证结果</Col>
<Col span="18">{validRes}</Col>
</Row>
: null
}
</TabPane>
</Tabs>
</div>
}
CaseReport.propTypes = {
url: PropTypes.string,
body: PropTypes.any,
headers: PropTypes.object,
res_header: PropTypes.object,
res_body: PropTypes.any,
query: PropTypes.string,
validRes: PropTypes.array
}
export default CaseReport;

View File

@ -96,6 +96,7 @@ export default class InterfaceCaseContent extends Component {
}
updateCase = async () => {
const {
caseEnv: case_env,
pathname: path,
@ -107,9 +108,10 @@ export default class InterfaceCaseContent extends Component {
bodyForm: req_body_form,
bodyOther: req_body_other
} = this.postman.state;
const {editCasename: casename} = this.state;
const {_id: id} = this.props.currCase;
const res = await axios.post('/api/col/up_case', {
let params = {
id,
casename,
case_env,
@ -121,7 +123,20 @@ export default class InterfaceCaseContent extends Component {
req_body_type,
req_body_form,
req_body_other
});
};
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'){
params.test_res_body = JSON.stringify(params.test_res_body, null, ' ');
}
const res = await axios.post('/api/col/up_case', params);
if (this.props.currCase.casename !== casename) {
this.props.fetchInterfaceColList(this.props.match.params.id);
}

View File

@ -3,9 +3,31 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'
import { withRouter } from 'react-router'
import { Link } from 'react-router-dom'
import { Table, Tooltip } from 'antd'
import { Tooltip, Icon, Button, Spin, Modal, message } from 'antd'
import { fetchInterfaceColList, fetchCaseList, setColData } from '../../../../reducer/modules/interfaceCol'
import { formatTime } from '../../../../common.js'
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
import { handleMockWord, simpleJsonPathParse } from '../../../../common.js'
// import { formatTime } from '../../../../common.js'
import * as Table from 'reactabular-table';
import * as dnd from 'reactabular-dnd';
import * as resolve from 'table-resolver';
import axios from 'axios'
import URL from 'url';
import Mock from 'mockjs'
import json5 from 'json5'
import CaseReport from './CaseReport.js'
const MockExtra = require('common/mock-extra.js')
function json_parse(data) {
try {
return json5.parse(data)
} catch (e) {
return data
}
}
@connect(
state => {
@ -14,7 +36,8 @@ import { formatTime } from '../../../../common.js'
currColId: state.interfaceCol.currColId,
currCaseId: state.interfaceCol.currCaseId,
isShowCol: state.interfaceCol.isShowCol,
currCaseList: state.interfaceCol.currCaseList
currCaseList: state.interfaceCol.currCaseList,
currProject: state.project.currProject
}
},
{
@ -24,7 +47,8 @@ import { formatTime } from '../../../../common.js'
}
)
@withRouter
export default class InterfaceColContent extends Component {
@DragDropContext(HTML5Backend)
class InterfaceColContent extends Component {
static propTypes = {
match: PropTypes.object,
@ -36,11 +60,22 @@ export default class InterfaceColContent extends Component {
currCaseList: PropTypes.array,
currColId: PropTypes.number,
currCaseId: PropTypes.number,
isShowCol: PropTypes.bool
isShowCol: PropTypes.bool,
currProject: PropTypes.object
}
constructor(props) {
super(props)
super(props);
this.reports = {};
this.records = {};
this.state = {
rows: [],
reports: {},
visible: false,
curCaseid: null
};
this.onRow = this.onRow.bind(this);
this.onMoveRow = this.onMoveRow.bind(this);
}
async componentWillMount() {
@ -49,73 +84,362 @@ export default class InterfaceColContent extends Component {
const params = this.props.match.params;
const { actionId } = params;
currColId = +actionId ||
result.payload.data.data.find(item => +item._id === +currColId) && +currColId ||
result.payload.data.data[0]._id;
result.payload.data.data.find(item => +item._id === +currColId) && +currColId ||
result.payload.data.data[0]._id;
this.props.history.push('/project/' + params.id + '/interface/col/' + currColId)
if(currColId && currColId != 0){
this.props.fetchCaseList(currColId);
this.props.setColData({currColId: +currColId, isShowCol: true})
if (currColId && currColId != 0) {
await this.props.fetchCaseList(currColId);
this.props.setColData({ currColId: +currColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
}
componentWillReceiveProps(nextProps) {
handleColdata = (rows) => {
rows = rows.map((item) => {
item.id = item._id;
item._test_status = item.test_status;
return item;
})
rows = rows.sort((n, o) => {
return n.index > o.index
})
this.setState({
rows: rows
})
}
executeTests = async () => {
for (let i = 0, l = this.state.rows.length, newRows, curitem; i < l; i++) {
let { rows } = this.state;
curitem = Object.assign({}, rows[i], { test_status: 'loading' });
newRows = [].concat([], rows);
newRows[i] = curitem;
this.setState({
rows: newRows
})
let status = 'error';
try {
let result = await this.handleTest(curitem);
if (result.code === 400) {
status = 'error';
} else if (result.code === 0) {
status = 'ok';
} else if (result.code === 1) {
status = 'invalid'
}
this.reports[curitem._id] = result;
this.records[curitem._id] = result.res_body;
} catch (e) {
status = 'error';
console.error(e);
}
curitem = Object.assign({}, rows[i], { test_status: status });
newRows = [].concat([], rows);
newRows[i] = curitem;
this.setState({
rows: newRows
})
}
}
handleTest = (interfaceData) => {
const { currProject } = this.props;
const { 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}`);
});
const domains = currProject.env.concat();
const urlObj = URL.parse(domains.find(item => item.name === case_env).domain);
const href = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
pathname: urlObj.pathname ? URL.resolve(urlObj.pathname, path) : path,
query: this.getQueryObj(interfaceData.req_query)
});
return new Promise((resolve, reject) => {
let result = { code: 400, msg: '数据异常', validRes: [] };
let that = this;
window.crossRequest({
url: href,
method: interfaceData.method,
headers: that.getHeadersObj(interfaceData.req_headers),
data: interfaceData.req_body_type === 'form' ? that.arrToObj(interfaceData.req_body_form) : interfaceData.req_body_other,
success: (res, header) => {
res = json_parse(res);
result.url = href;
result.method = interfaceData.method;
result.headers = that.getHeadersObj(interfaceData.req_headers);
result.body = interfaceData.req_body_type === 'form' ? that.arrToObj(interfaceData.req_body_form) : interfaceData.req_body_other
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 = 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: (res) => {
result.code = 400;
result.msg = '请求异常'
reject(res)
}
})
})
}
handleVarWord(val){
return simpleJsonPathParse(val, this.records)
}
handleValue(val){
if(!val || typeof val !== 'string'){
return val;
}else if(val[0] === '@'){
return handleMockWord(val);
}else if(val.indexOf('$.') === 0){
return this.handleVarWord(val);
}
return val;
}
arrToObj =(arr) =>{
arr = arr || [];
const obj = {};
arr.forEach(item => {
if (item.name && item.type !== 'file') {
obj[item.name] = this.handleValue(item.value);
}
})
return obj;
}
getQueryObj =(query)=> {
query = query || [];
const queryObj = {};
query.forEach(item => {
if (item.name) {
queryObj[item.name] = this.handleValue(item.value);
}
})
return queryObj;
}
getHeadersObj = (headers) =>{
headers = headers || [];
const headersObj = {};
headers.forEach(item => {
if (item.name && item.value) {
headersObj[item.name] = item.value;
}
})
return headersObj;
}
onRow(row) {
return {
rowId: row.id,
onMove: this.onMoveRow
};
}
onMoveRow({ sourceRowId, targetRowId }) {
let rows = dnd.moveRows({
sourceRowId,
targetRowId
})(this.state.rows);
let changes = [];
rows.forEach((item, index) => {
changes.push({
id: item._id,
index: index
})
})
axios.post('/api/col/up_col_index', changes).then()
if (rows) {
this.setState({ rows });
}
}
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)) {
this.props.history.push('/project/' + id + '/interface/col/' + interfaceColList[0]._id)
} else if (oldColId !== newColId) {
if(newColId && newColId != 0){
this.props.fetchCaseList(newColId);
this.props.setColData({currColId: +newColId, isShowCol: true})
if (newColId && newColId != 0) {
await this.props.fetchCaseList(newColId);
this.props.setColData({ currColId: +newColId, isShowCol: true })
this.handleColdata(this.props.currCaseList)
}
}
}
openReport = (id) => {
if (!this.reports[id]) {
return message.warn('还没有生成报告')
}
this.setState({
visible: true,
curCaseid: id
})
}
handleCancel = () => {
this.setState({
visible: false
});
}
render() {
const { currCaseList } = this.props;
const columns = [{
title: '用例名称',
dataIndex: 'casename',
key: 'casename',
render: (text, item)=>{
return <Link to={"/project/" + item.project_id + "/interface/case/" + item._id} >{text}</Link>
property: 'casename',
header: {
label: '用例名称'
},
cell: {
formatters: [
(text, { rowData }) => {
let record = rowData;
return <Link to={"/project/" + record.project_id + "/interface/case/" + record._id}>{record.casename}</Link>
}
]
}
}, {
title: '接口路径',
dataIndex: 'path',
key: 'path',
render: (path, record) => {
return (
<Tooltip title="跳转到对应接口">
<Link to={`/project/${record.project_id}/interface/api/${record.interface_id}`}>{path}</Link>
</Tooltip>
)
header: {
label: 'key',
formatters: [() => {
return <Tooltip title="每个用例都有一个独一无二的key可用来获取匹配的接口响应数据">
Key</Tooltip>
}]
},
cell: {
formatters: [
(value, { rowData }) => {
return <span>{rowData._id}</span>
}]
}
}, {
title: '请求方法',
dataIndex: 'method',
key: 'method'
}, {
title: '更新时间',
dataIndex: 'up_time',
key: 'up_time',
render: (item) => {
return <span>{formatTime(item)}</span>
property: 'test_status',
header: {
label: '状态'
},
cell: {
formatters: [(value, { rowData }) => {
switch (rowData.test_status) {
case 'ok':
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
case 'error':
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>
default:
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
}
}]
}
}];
}, {
property: 'path',
header: {
label: '接口路径'
},
cell: {
formatters: [
(text, { rowData }) => {
let record = rowData;
return (
<Tooltip title="跳转到对应接口">
<Link to={`/project/${record.project_id}/interface/api/${record.interface_id}`}>{record.path}</Link>
</Tooltip>
)
}
]
}
}, {
header: {
label: '测试报告'
},
cell: {
formatters: [(text, { rowData }) => {
return <Button onClick={() => this.openReport(rowData.id)}>报告</Button>
}]
}
}
];
const { rows } = this.state;
const components = {
header: {
cell: dnd.Header
},
body: {
row: dnd.Row
}
};
const resolvedColumns = resolve.columnChildren({ columns });
const resolvedRows = resolve.resolve({
columns: resolvedColumns,
method: resolve.nested
})(rows);
return (
<div>
<div style={{padding:"16px"}}>
<h2 style={{marginBottom: '10px'}}>测试集合</h2>
<Table dataSource={currCaseList} columns={columns} pagination={false} rowKey="_id"/>
</div>
<div className="interface-col">
<h2 style={{ marginBottom: '10px', display: 'inline-block' }}>测试集合</h2>
<Button type="primary" style={{ float: 'right' }} onClick={this.executeTests}>开始测试</Button>
<Table.Provider
components={components}
columns={resolvedColumns}
style={{ width: '100%', lineHeight: '36px' }}
>
<Table.Header
style={{ textAlign: 'left' }}
headerRows={resolve.headerRows({ columns })}
/>
<Table.Body
rows={resolvedRows}
rowKey="id"
onRow={this.onRow}
/>
</Table.Provider>
<Modal
title="测试报告"
width="660px"
style={{ minHeight: '500px' }}
visible={this.state.visible}
onCancel={this.handleCancel}
footer={null}
>
<CaseReport {...this.reports[this.state.curCaseid]} />
</Modal>
</div>
)
}
}
export default InterfaceColContent

View File

@ -30,3 +30,65 @@
}
}
}
.container {
display: block;
width: 100%;
padding: 10px;
}
/*
* note that styling gu-mirror directly is a bad practice because it's too generic.
* you're better off giving the draggable elements a unique class and styling that directly!
*/
.gu-mirror {
padding: 10px;
background-color: rgba(0, 0, 0, 0.2);
transition: opacity 0.4s ease-in-out;
}
.container div {
cursor: move;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
margin-bottom: 10px;
}
.container div:last-child {
margin-bottom: 0;
}
.gu-mirror {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
.container .ex-moved {
background-color: #e74c3c;
}
.container.ex-over {
background-color: rgba(255, 255, 255, 0.3);
}
.handle {
padding: 0 5px;
margin-right: 5px;
background-color: rgba(0, 0, 0, 0.4);
cursor: move;
}
.report{
min-height: 400px;
.case-report-pane{
margin-top: 10px;
}
.case-report{
margin: 10px;
.case-report-title{
font-size: 14px;
font-weight: bold;
text-align: right;
padding-right: 20px;
}
}
}
.interface-col{
padding: 16px;
}

View File

@ -59,7 +59,8 @@ export default class Run extends Component {
bodyForm: req_body_form,
bodyOther: req_body_other
} = this.postman.state;
const res = await axios.post('/api/col/add_case', {
let params = {
interface_id,
casename: caseName,
col_id: colId,
@ -73,7 +74,20 @@ export default class Run extends Component {
req_body_type,
req_body_form,
req_body_other
});
};
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'){
params.test_res_body = JSON.stringify(params.test_res_body, null, ' ');
}
const res = await axios.post('/api/col/add_case', params);
if (res.data.errcode) {
message.error(res.data.errmsg)
} else {
@ -85,7 +99,6 @@ export default class Run extends Component {
render () {
const { currInterface, currProject } = this.props;
const data = Object.assign({}, currInterface, currProject, {_id: currInterface._id})
return (
<div>
<Postman data={data} type="inter" saveTip="保存到集合" save={() => this.setState({saveCaseModalVisible: true})} ref={this.savePostmanRef} />

View File

@ -10,7 +10,7 @@ class advMockModel extends baseModel {
return {
interface_id: { type: Number, required: true },
project_id: {type: Number, required: true},
enable: {type: Boolean, default: false}, //1表示开启0关闭
enable: {type: Boolean, default: false},
mock_script: String,
uid: String,
up_time: Number
@ -25,7 +25,6 @@ class advMockModel extends baseModel {
}
delByInterfaceId(interface_id) {
console.log(interface_id);
return this.model.deleteOne({
interface_id: interface_id
});

View File

@ -1,10 +1,19 @@
const controller = require('./controller');
const advModel = require('./model.js');
const yapi = require('yapi.js');
const mongoose = require('mongoose');
module.exports = function(){
yapi.connect.then(function () {
let Col = mongoose.connection.db.collection('adv_mock')
Col.ensureIndex({
interface_id: 1
})
Col.ensureIndex({
project_id: 1
})
})
this.bindHook('add_router', function(addRouter){
addRouter({
controller: controller,

57
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "yapi",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3196,6 +3196,22 @@
"randombytes": "2.0.5"
}
},
"disposables": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz",
"integrity": "sha1-BkcnoltU9QK9griaot+4358bOeM="
},
"dnd-core": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.5.1.tgz",
"integrity": "sha1-F49a1lJs4C3VlQjxFVNfe/wM6U4=",
"requires": {
"asap": "2.0.6",
"invariant": "2.2.2",
"lodash": "4.17.4",
"redux": "3.7.2"
}
},
"dns-equal": {
"version": "1.0.0",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/dns-equal/-/dns-equal-1.0.0.tgz",
@ -11438,6 +11454,27 @@
}
}
},
"react-dnd": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.5.1.tgz",
"integrity": "sha1-7O8dnYR4y3av1Zdc1RI+98O+v+U=",
"requires": {
"disposables": "1.0.1",
"dnd-core": "2.5.1",
"hoist-non-react-statics": "2.3.1",
"invariant": "2.2.2",
"lodash": "4.17.4",
"prop-types": "15.5.10"
}
},
"react-dnd-html5-backend": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.1.tgz",
"integrity": "sha1-02VuUUsMRpkCpIX/+nX4aE4hx3c=",
"requires": {
"lodash": "4.17.4"
}
},
"react-dock": {
"version": "0.2.4",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/react-dock/-/react-dock-0.2.4.tgz",
@ -12182,6 +12219,19 @@
"slick-carousel": "1.7.1"
}
},
"reactabular-dnd": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/reactabular-dnd/-/reactabular-dnd-8.9.0.tgz",
"integrity": "sha1-B5LeSto2H5Qlj4pqGg1svYPSpaA="
},
"reactabular-table": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/reactabular-table/-/reactabular-table-8.9.0.tgz",
"integrity": "sha1-RvbO9jnNm9SyuvHxTiaG+ys14oQ=",
"requires": {
"classnames": "2.2.5"
}
},
"read-all-stream": {
"version": "3.1.0",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/read-all-stream/-/read-all-stream-3.1.0.tgz",
@ -13644,6 +13694,11 @@
}
}
},
"table-resolver": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/table-resolver/-/table-resolver-3.2.0.tgz",
"integrity": "sha512-DQrDHFdJPnvIhyjAcTqF4vhu/Uhp5eNRst9Url9KmBNqxYSMrPXOJoxhU7HPCd3efi1Hua7lMIDnBAphsdhPQw=="
},
"tapable": {
"version": "0.2.8",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/tapable/-/tapable-0.2.8.tgz",

View File

@ -78,10 +78,14 @@
"rc-queue-anim": "^1.2.0",
"rc-scroll-anim": "^1.0.7",
"react": "^15.6.1",
"react-dnd": "^2.5.1",
"react-dnd-html5-backend": "^2.5.1",
"react-dom": "^15.6.1",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
"react-scripts": "1.0.10",
"reactabular-dnd": "^8.9.0",
"reactabular-table": "^8.9.0",
"redux": "^3.7.1",
"redux-promise": "^0.5.3",
"redux-thunk": "^2.2.0",
@ -89,6 +93,7 @@
"sha1": "^1.1.1",
"string-replace-webpack-plugin": "^0.1.3",
"style-loader": "^0.18.2",
"table-resolver": "^3.2.0",
"underscore": "^1.8.3",
"universal-cookie": "^2.0.8",
"url": "^0.11.0",

View File

@ -5,6 +5,7 @@ const yapi = require('./yapi.js');
const commons = require('./utils/commons');
yapi.commons = commons;
const dbModule = require('./utils/db.js');
yapi.connect = dbModule.connect();
const mockServer = require('./middleware/mockServer.js');
const plugins = require('./plugin.js');
const websockify = require('koa-websocket');
@ -17,7 +18,7 @@ const router = require('./router.js');
let indexFile = process.argv[2] === 'dev' ? 'dev.html' : 'index.html';
yapi.connect = dbModule.connect();
const app = websockify(new Koa());
yapi.app = app;
app.use(mockServer);
@ -27,6 +28,8 @@ app.use(router.allowedMethods());
websocket(app);
app.use( async (ctx, next) => {
if( /^\/(?!api)[a-zA-Z0-9\/\-_]*$/.test(ctx.path) ){
ctx.path = "/"

View File

@ -119,7 +119,7 @@ class interfaceColController extends baseController{
if(!id || id == 0){
return ctx.body = yapi.commons.resReturn(null, 407, 'col_id不能为空')
}
let result = await this.caseModel.list(id, 'all');
let resultList = await this.caseModel.list(id, 'all');
let colData = await this.colModel.get(id);
let project = await this.projectModel.getBaseInfo(colData.project_id);
@ -129,20 +129,27 @@ class interfaceColController extends baseController{
}
}
for(let index=0; index< result.length; index++){
result[index] = result[index].toObject();
let interfaceData = await this.interfaceModel.getBaseinfo(result[index].interface_id);
if(!interfaceData){
await this.caseModel.del(result[index]._id);
result[index] = undefined;
for(let index=0; index< resultList.length; index++){
let result = resultList[index].toObject();
let data = await this.interfaceModel.get(result.interface_id);
if(!data){
await this.caseModel.del(result._id);
continue;
}
let projectData = await this.projectModel.getBaseInfo(interfaceData.project_id);
result[index].path = projectData.basepath + interfaceData.path;
result[index].method = interfaceData.method;
let projectData = await this.projectModel.getBaseInfo(data.project_id);
result.path = projectData.basepath + data.path;
result.method = data.method;
result.req_body_type = data.req_body_type;
result.req_headers = data.req_headers;
result.res_body = data.res_body;
result.res_body_type = data.res_body_type;
result.req_body_form = this.handleParamsValue(data.req_body_form, result.req_body_form)
result.req_query = this.handleParamsValue(data.req_query, result.req_query)
result.req_params = this.handleParamsValue(data.req_params, result.req_params)
resultList[index] = result;
}
ctx.body = yapi.commons.resReturn(result);
ctx.body = yapi.commons.resReturn(resultList);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
@ -316,7 +323,7 @@ class interfaceColController extends baseController{
return ctx.body = yapi.commons.resReturn(null, 400, '不存在的case');
}
result = result.toObject();
let data = await this.interfaceModel.get(result.interface_id);
let data = await this.interfaceModel.get(result.interface_id);
if(!data){
return ctx.body = yapi.commons.resReturn(null, 400, '找不到对应的接口,请联系管理员')
}
@ -327,8 +334,7 @@ class interfaceColController extends baseController{
result.req_body_type = data.req_body_type;
result.req_headers = data.req_headers;
result.res_body = data.res_body;
result.res_body_type = data.res_body_type;
result.res_body_type = data.res_body_type;
result.req_body_form = this.handleParamsValue(data.req_body_form, result.req_body_form)
result.req_query = this.handleParamsValue(data.req_query, result.req_query)
result.req_params = this.handleParamsValue(data.req_params, result.req_params)
@ -341,6 +347,9 @@ class interfaceColController extends baseController{
handleParamsValue(params, val){
let value = {};
try{
params = params.toObject();
}catch(e){ }
if(params.length === 0 || val.length === 0){
return params;
}
@ -349,7 +358,7 @@ class interfaceColController extends baseController{
})
params.forEach((item, index)=>{
if(!value[item.name] || typeof value[item.name] !== 'object') return null;
params[index].value = value[item.name].value;
params[index].value = value[item.name].value;
})
return params;
}
@ -412,9 +421,8 @@ class interfaceColController extends baseController{
if(!params || !Array.isArray(params)){
ctx.body = yapi.commons.resReturn(null, 400, "请求参数必须是数组")
}
// let caseName = "";
params.forEach((item) => {
if(item.id && item.index){
if(item.id){
this.caseModel.upCaseIndex(item.id, item.index).then((res) => {}, (err) => {
yapi.commons.log(err.message, 'error')
})
@ -422,15 +430,6 @@ class interfaceColController extends baseController{
});
// let username = this.getUsername();
// yapi.commons.saveLog({
// content: `用户 "${username}" 更新了接口集 "${params.col_name}"`,
// type: 'project',
// uid: this.getUid(),
// username: username,
// typeid: params.project_id
// });
return ctx.body = yapi.commons.resReturn('成功!')
}catch(e){
ctx.body = yapi.commons.resReturn(null, 400, e.message)

164
server/controllers/test.js Normal file
View File

@ -0,0 +1,164 @@
const yapi = require('../yapi.js');
const baseController = require('./base.js');
class interfaceColController extends baseController{
constructor(ctx) {
super(ctx);
}
/**
* 测试 get
* @interface /test/get
* @method GET
* @returns {Object}
* @example
*/
async testGet(ctx){
try {
let query = ctx.query;
ctx.body = yapi.commons.resReturn(query);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 post
* @interface /test/post
* @method POST
* @returns {Object}
* @example
*/
async testPost(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn(params);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 单文件上传
* @interface /test/single/upload
* @method POST
* @returns {Object}
* @example
*/
async testSingleUpload(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn({res: '上传成功'});
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 文件上传
* @interface /test/files/upload
* @method POST
* @returns {Object}
* @example
*/
async testFilesUpload(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn({res: '上传成功'});
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 put
* @interface /test/put
* @method PUT
* @returns {Object}
* @example
*/
async testPut(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn(params);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 delete
* @interface /test/delete
* @method DELETE
* @returns {Object}
* @example
*/
async testDelete(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn(params);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 head
* @interface /test/head
* @method HEAD
* @returns {Object}
* @example
*/
async testHead(ctx){
try{
let query = ctx.query;
ctx.body = yapi.commons.resReturn(query);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 options
* @interface /test/options
* @method OPTIONS
* @returns {Object}
* @example
*/
async testOptions(ctx){
try{
let query = ctx.query;
ctx.body = yapi.commons.resReturn(query);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
/**
* 测试 patch
* @interface /test/patch
* @method PATCH
* @returns {Object}
* @example
*/
async testPatch(ctx){
try{
let params = ctx.request.body;
ctx.body = yapi.commons.resReturn(params);
}catch(e){
ctx.body = yapi.commons.resReturn(null, 402, e.message);
}
}
}
module.exports = interfaceColController

View File

@ -1,5 +1,7 @@
const yapi = require('../yapi.js');
const baseModel = require('./base.js');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
class interfaceCase extends baseModel {
getName() {
@ -17,25 +19,21 @@ class interfaceCase extends baseModel {
add_time: Number,
up_time: Number,
case_env: { type: String },
// path: { type: String },
// method: { type: String },
req_params: [{
name: String, value: String
}],
req_query: [{
name: String, value: String
}],
// req_headers: [{
// name: String, value: String
// }],
// req_body_type: {
// type: String,
// enum: ['form', 'json', 'text', 'xml']
// },
req_body_form: [{
name: String, value: String
}],
req_body_other: String
req_body_other: String,
test_res_body: String,
test_status: {type: String, enum: ['ok', 'invalid', 'error', '']},
test_report: [],
test_res_header: Schema.Types.Mixed
};
}

View File

@ -3,6 +3,7 @@ const interfaceController = require('./controllers/interface.js');
const groupController = require('./controllers/group.js');
const userController = require('./controllers/user.js');
const interfaceColController = require('./controllers/interfaceCol.js');
const testController = require('./controllers/test.js');
const yapi = require('./yapi.js');
const projectController = require('./controllers/project.js');
@ -47,6 +48,10 @@ let INTERFACE_CONFIG = {
col: {
prefix: '/col/',
controller: interfaceColController
},
test: {
prefix: '/test/',
controller: testController
}
};
@ -347,7 +352,44 @@ let routerConfig = {
path: "del_case",
method: "get"
}
]
],
"test": [{
action: "testPost",
path: "post",
method: "post"
}, {
action: "testGet",
path: "get",
method: "get"
}, {
action: "testPut",
path: "put",
method: "put"
}, {
action: "testDelete",
path: "delete",
method: "del"
}, {
action: "testHead",
path: "head",
method: "head"
}, {
action: "testOptions",
path: "options",
method: "options"
}, {
action: "testPatch",
path: "patch",
method: "patch"
}, {
action: "testFilesUpload",
path: "files/upload",
method: "post"
}, {
action: "testSingleUpload",
path: "single/upload",
method: "post"
}]
}
let pluginsRouterPath = [];

11
ykit.js
View File

@ -69,13 +69,18 @@ module.exports = {
'react-router-dom',
'prop-types',
'axios',
'moment'
'moment',
'react-dnd-html5-backend',
'react-dnd',
'reactabular-table',
'reactabular-dnd',
'table-resolver'
],
lib2: [
'brace',
'mockjs',
'json5'
'json5',
'url'
]
}
},