diff --git a/client/common.js b/client/common.js index b46f1b2b..82f15b70 100755 --- a/client/common.js +++ b/client/common.js @@ -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; diff --git a/client/components/Postman/Postman.js b/client/components/Postman/Postman.js index b1c013c4..be00c59a 100755 --- a/client/components/Postman/Postman.js +++ b/client/components/Postman/Postman.js @@ -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
{item.message}
- }) - } else if (Array.isArray(validRes)) { - validResView =

验证通过

- } + validResView = validRes.map((item, index) => { + return
{item}
+ }) + @@ -569,7 +573,7 @@ export default class Run extends Component { domains.map((item, index) => ()) } - + @@ -587,7 +591,7 @@ export default class Run extends Component { onClick={this.props.save} type="primary" style={{ marginLeft: 10 }} - >{this.props.type === 'inter' ? '保存' : '更新'} + >{this.props.type === 'inter' ? '保存' : '保存'} diff --git a/client/containers/Group/GroupList/GroupList.scss b/client/containers/Group/GroupList/GroupList.scss index 48ca0b33..91e3c5b3 100755 --- a/client/containers/Group/GroupList/GroupList.scss +++ b/client/containers/Group/GroupList/GroupList.scss @@ -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 { diff --git a/client/containers/Project/Interface/InterfaceCol/CaseReport.js b/client/containers/Project/Interface/InterfaceCol/CaseReport.js new file mode 100644 index 00000000..78986d91 --- /dev/null +++ b/client/containers/Project/Interface/InterfaceCol/CaseReport.js @@ -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
{item.message}
+ }) + + return
+ + + + Url + {props.url} + + {props.query ? + + Query + {props.query} + + : null + } + + {props.headers ? + + Headers +
{headers}
+
+ : null + } + + {props.body ? + + Body +
{body}
+
+ : null + } +
+ + {props.res_header ? + + Headers +
{res_header}
+
+ : null + } + {props.res_body ? + + Body +
{res_body}
+
+ : null + } + +
+ + {props.validRes ? + + 验证结果 + {validRes} + + : null + } + +
+ + + +
+} + +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; \ No newline at end of file diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js b/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js index 0d5a84a4..9847f3c1 100755 --- a/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js +++ b/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js @@ -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); } diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js b/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js index 0e56fbd1..e80968e9 100755 --- a/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js +++ b/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js @@ -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 {text} + property: 'casename', + header: { + label: '用例名称' + }, + cell: { + formatters: [ + (text, { rowData }) => { + let record = rowData; + return {record.casename} + } + ] } }, { - title: '接口路径', - dataIndex: 'path', - key: 'path', - render: (path, record) => { - return ( - - {path} - - ) + header: { + label: 'key', + formatters: [() => { + return + Key + }] + }, + cell: { + formatters: [ + (value, { rowData }) => { + return {rowData._id} + }] } }, { - title: '请求方法', - dataIndex: 'method', - key: 'method' - }, { - title: '更新时间', - dataIndex: 'up_time', - key: 'up_time', - render: (item) => { - return {formatTime(item)} + property: 'test_status', + header: { + label: '状态' + }, + cell: { + formatters: [(value, { rowData }) => { + switch (rowData.test_status) { + case 'ok': + return
+ case 'error': + return
+ case 'invalid': + return
+ case 'loading': + return
+ default: + return
+ } + }] } - }]; + }, { + property: 'path', + header: { + label: '接口路径' + }, + cell: { + formatters: [ + (text, { rowData }) => { + let record = rowData; + return ( + + {record.path} + + ) + } + ] + } + }, { + header: { + label: '测试报告' + + }, + cell: { + formatters: [(text, { rowData }) => { + return + }] + } + } + ]; + 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 ( -
-
-

测试集合

- - +
+

测试集合

+ + + + + + + + +
) } } + +export default InterfaceColContent \ No newline at end of file diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss b/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss index da4ed49f..c84c48f0 100755 --- a/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss +++ b/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss @@ -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; +} diff --git a/client/containers/Project/Interface/InterfaceList/Run/Run.js b/client/containers/Project/Interface/InterfaceList/Run/Run.js index a6eba675..5e852aa6 100755 --- a/client/containers/Project/Interface/InterfaceList/Run/Run.js +++ b/client/containers/Project/Interface/InterfaceList/Run/Run.js @@ -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 (
this.setState({saveCaseModalVisible: true})} ref={this.savePostmanRef} /> diff --git a/exts/yapi-plugin-advanced-mock/model.js b/exts/yapi-plugin-advanced-mock/model.js index 6de524f6..d11d7c9f 100644 --- a/exts/yapi-plugin-advanced-mock/model.js +++ b/exts/yapi-plugin-advanced-mock/model.js @@ -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 }); diff --git a/exts/yapi-plugin-advanced-mock/server.js b/exts/yapi-plugin-advanced-mock/server.js index 75af1d1b..e2ace960 100644 --- a/exts/yapi-plugin-advanced-mock/server.js +++ b/exts/yapi-plugin-advanced-mock/server.js @@ -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, diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 37106811..ffadc16d 100755 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -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", diff --git a/package.json b/package.json index 7a45f5c3..7c2f4fb1 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/app.js b/server/app.js index 489761d3..ca6f803c 100755 --- a/server/app.js +++ b/server/app.js @@ -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 = "/" diff --git a/server/controllers/interfaceCol.js b/server/controllers/interfaceCol.js index 629bc3f6..1acd7d9b 100755 --- a/server/controllers/interfaceCol.js +++ b/server/controllers/interfaceCol.js @@ -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) diff --git a/server/controllers/test.js b/server/controllers/test.js new file mode 100644 index 00000000..0435b59f --- /dev/null +++ b/server/controllers/test.js @@ -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 diff --git a/server/models/interfaceCase.js b/server/models/interfaceCase.js index a12632de..bcc9c967 100755 --- a/server/models/interfaceCase.js +++ b/server/models/interfaceCase.js @@ -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 }; } diff --git a/server/router.js b/server/router.js index e274b8cf..0050e8c3 100755 --- a/server/router.js +++ b/server/router.js @@ -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 = []; diff --git a/ykit.js b/ykit.js index 59ec79bb..e1177ea1 100755 --- a/ykit.js +++ b/ykit.js @@ -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' ] } },