This commit is contained in:
2024-03-01 20:28:14 +08:00
commit 076c21dc36
491 changed files with 84482 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
import React, { Component } from 'react';
// import { connect } from 'react-redux'
import axios from 'axios';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Form, Switch, Button, message, Icon, Tooltip, Radio } from 'antd';
import MockCol from './MockCol/MockCol.js';
import mockEditor from 'client/components/AceEditor/mockEditor';
import constants from '../../client/constants/variable.js';
const FormItem = Form.Item;
class AdvMock extends Component {
static propTypes = {
form: PropTypes.object,
match: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
enable: false,
mock_script: '',
tab: 'case'
};
}
handleSubmit = e => {
e.preventDefault();
let projectId = this.props.match.params.id;
let interfaceId = this.props.match.params.actionId;
let params = {
project_id: projectId,
interface_id: interfaceId,
mock_script: this.state.mock_script,
enable: this.state.enable
};
axios.post('/api/plugin/advmock/save', params).then(res => {
if (res.data.errcode === 0) {
message.success('保存成功');
} else {
message.error(res.data.errmsg);
}
});
};
UNSAFE_componentWillMount() {
this.getAdvMockData();
}
async getAdvMockData() {
let interfaceId = this.props.match.params.actionId;
let result = await axios.get('/api/plugin/advmock/get?interface_id=' + interfaceId);
if (result.data.errcode === 0) {
let mockData = result.data.data;
this.setState({
enable: mockData.enable,
mock_script: mockData.mock_script
});
}
let that = this;
mockEditor({
container: 'mock-script',
data: that.state.mock_script,
onChange: function(d) {
that.setState({
mock_script: d.text
});
}
});
}
onChange = v => {
this.setState({
enable: v
});
};
handleTapChange = e => {
this.setState({
tab: e.target.value
});
};
render() {
const formItemLayout = {
labelCol: {
sm: { span: 4 }
},
wrapperCol: {
sm: { span: 16 }
}
};
const tailFormItemLayout = {
wrapperCol: {
sm: {
span: 16,
offset: 11
}
}
};
const { tab } = this.state;
const isShowCase = tab === 'case';
return (
<div style={{ padding: '20px 10px' }}>
<div style={{ textAlign: 'center', marginBottom: 20 }}>
<Radio.Group value={tab} size="large" onChange={this.handleTapChange}>
<Radio.Button value="case">期望</Radio.Button>
<Radio.Button value="script">脚本</Radio.Button>
</Radio.Group>
</div>
<div style={{ display: isShowCase ? 'none' : '' }}>
<Form onSubmit={this.handleSubmit}>
<FormItem
label={
<span>
是否开启&nbsp;<a
target="_blank"
rel="noopener noreferrer"
href={constants.docHref.adv_mock_script}
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</span>
}
{...formItemLayout}
>
<Switch
checked={this.state.enable}
onChange={this.onChange}
checkedChildren="开"
unCheckedChildren="关"
/>
</FormItem>
<FormItem label="Mock脚本" {...formItemLayout}>
<div id="mock-script" style={{ minHeight: '500px' }} />
</FormItem>
<FormItem {...tailFormItemLayout}>
<Button type="primary" htmlType="submit">
保存
</Button>
</FormItem>
</Form>
</div>
<div style={{ display: isShowCase ? '' : 'none' }}>
<MockCol />
</div>
</div>
);
}
}
module.exports = Form.create()(withRouter(AdvMock));

View File

@@ -0,0 +1,482 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Form,
Select,
InputNumber,
Switch,
Col,
message,
Row,
Input,
Button,
Icon,
AutoComplete,
Modal
} from 'antd';
const Option = Select.Option;
const FormItem = Form.Item;
import { safeAssign } from 'client/common.js';
import AceEditor from 'client/components/AceEditor/AceEditor';
import constants from 'client/constants/variable.js';
import { httpCodes } from '../index.js';
import './CaseDesModal.scss';
import { connect } from 'react-redux';
import json5 from 'json5';
const formItemLayout = {
labelCol: { span: 5 },
wrapperCol: { span: 12 }
};
const formItemLayoutWithOutLabel = {
wrapperCol: { span: 12, offset: 5 }
};
@connect(state => {
return {
currInterface: state.inter.curdata
};
})
class CaseDesForm extends Component {
static propTypes = {
form: PropTypes.object,
caseData: PropTypes.object,
currInterface: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func,
isAdd: PropTypes.bool,
visible: PropTypes.bool
};
// 初始化输入数据
preProcess = caseData => {
try {
caseData = JSON.parse(JSON.stringify(caseData));
} catch (error) {
console.log(error);
}
const initCaseData = {
ip: '',
ip_enable: false,
name: '',
code: '200',
delay: 0,
headers: [{ name: '', value: '' }],
paramsArr: [{ name: '', value: '' }],
params: {},
res_body: '',
paramsForm: 'form'
};
caseData.params = caseData.params || {};
const paramsArr = Object.keys(caseData.params).length
? Object.keys(caseData.params)
.map(key => {
return { name: key, value: caseData.params[key] };
})
.filter(item => {
if (typeof item.value === 'object') {
// this.setState({ paramsForm: 'json' })
caseData.paramsForm = 'json';
}
return typeof item.value !== 'object';
})
: [{ name: '', value: '' }];
const headers =
caseData.headers && caseData.headers.length ? caseData.headers : [{ name: '', value: '' }];
caseData.code = '' + caseData.code;
caseData.params = JSON.stringify(caseData.params, null, 2);
caseData = safeAssign(initCaseData, { ...caseData, headers, paramsArr });
return caseData;
};
constructor(props) {
super(props);
const { caseData } = this.props;
this.state = this.preProcess(caseData);
}
// 处理request_body编译器
handleRequestBody = d => {
this.setState({ res_body: d.text });
};
// 处理参数编译器
handleParams = d => {
this.setState({ params: d.text });
};
// 增加参数信息
addValues = key => {
const { getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.concat({ name: '', value: '' });
this.setState({ [key]: values });
};
// 删除参数信息
removeValues = (key, index) => {
const { setFieldsValue, getFieldValue } = this.props.form;
let values = getFieldValue(key);
values = values.filter((val, index2) => index !== index2);
setFieldsValue({ [key]: values });
this.setState({ [key]: values });
};
// 处理参数
getParamsKey = () => {
let {
req_query,
req_body_form,
req_body_type,
method,
req_body_other,
req_body_is_json_schema,
req_params
} = this.props.currInterface;
let keys = [];
req_query &&
Array.isArray(req_query) &&
req_query.forEach(item => {
keys.push(item.name);
});
req_params &&
Array.isArray(req_params) &&
req_params.forEach(item => {
keys.push(item.name);
});
if (constants.HTTP_METHOD[method.toUpperCase()].request_body && req_body_type === 'form') {
req_body_form &&
Array.isArray(req_body_form) &&
req_body_form.forEach(item => {
keys.push(item.name);
});
} else if (
constants.HTTP_METHOD[method.toUpperCase()].request_body &&
req_body_type === 'json' &&
req_body_other
) {
let bodyObj;
try {
// 针对json-schema的处理
if (req_body_is_json_schema) {
bodyObj = json5.parse(this.props.caseData.req_body_other);
} else {
bodyObj = json5.parse(req_body_other);
}
keys = keys.concat(Object.keys(bodyObj));
} catch (error) {
console.log(error);
}
}
return keys;
};
endProcess = caseData => {
const headers = [];
const params = {};
const { paramsForm } = this.state;
caseData.headers &&
Array.isArray(caseData.headers) &&
caseData.headers.forEach(item => {
if (item.name) {
headers.push({
name: item.name,
value: item.value
});
}
});
caseData.paramsArr &&
Array.isArray(caseData.paramsArr) &&
caseData.paramsArr.forEach(item => {
if (item.name) {
params[item.name] = item.value;
}
});
caseData.headers = headers;
if (paramsForm === 'form') {
caseData.params = params;
} else {
try {
caseData.params = json5.parse(caseData.params);
} catch (error) {
console.log(error);
message.error('请求参数 json 格式有误,请修改');
return false;
}
}
delete caseData.paramsArr;
return caseData;
};
handleOk = () => {
const form = this.props.form;
form.validateFieldsAndScroll((err, values) => {
if (!err) {
values.res_body = this.state.res_body;
values.params = this.state.params;
this.props.onOk(this.endProcess(values));
}
});
};
render() {
const { getFieldDecorator, getFieldValue } = this.props.form;
const { isAdd, visible, onCancel } = this.props;
const {
name,
code,
headers,
ip,
ip_enable,
params,
paramsArr,
paramsForm,
res_body,
delay
} = this.state;
this.props.form.initialValue;
const valuesTpl = (values, title) => {
const dataSource = this.getParamsKey();
const display = paramsForm === 'json' ? 'none' : '';
return values.map((item, index) => (
<div key={index} className="paramsArr" style={{ display }}>
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`paramsArr[${index}].name`, { initialValue: item.name })(
<AutoComplete
dataSource={dataSource}
placeholder="参数名称"
filterOption={(inputValue, option) =>
option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
/>
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`paramsArr[${index}].value`, { initialValue: item.value })(
<Input placeholder="参数值" />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues('paramsArr', index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
));
};
const headersTpl = (values, title) => {
const dataSource = constants.HTTP_REQUEST_HEADER;
return values.map((item, index) => (
<div key={index} className="headers">
<FormItem
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
wrapperCol={index === 0 ? { span: 19 } : { span: 19, offset: 5 }}
label={index ? '' : title}
>
<Row gutter={8}>
<Col span={10}>
<FormItem>
{getFieldDecorator(`headers[${index}].name`, { initialValue: item.name })(
<AutoComplete
dataSource={dataSource}
placeholder="参数名称"
filterOption={(inputValue, option) =>
option.props.children.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
/>
)}
</FormItem>
</Col>
<Col span={10}>
<FormItem>
{getFieldDecorator(`headers[${index}].value`, { initialValue: item.value })(
<Input placeholder="参数值" />
)}
</FormItem>
</Col>
<Col span={4}>
{values.length > 1 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
onClick={() => this.removeValues('headers', index)}
/>
) : null}
</Col>
</Row>
</FormItem>
</div>
));
};
return (
<Modal
title={isAdd ? '添加期望' : '编辑期望'}
visible={visible}
maskClosable={false}
onOk={this.handleOk}
width={780}
onCancel={() => onCancel()}
afterClose={() => this.setState({ paramsForm: 'form' })}
className="case-des-modal"
>
<Form onSubmit={this.handleOk}>
<h2 className="sub-title" style={{ marginTop: 0 }}>
基本信息
</h2>
<FormItem {...formItemLayout} label="期望名称">
{getFieldDecorator('name', {
initialValue: name,
rules: [{ required: true, message: '请输入期望名称!' }]
})(<Input placeholder="请输入期望名称" />)}
</FormItem>
<FormItem {...formItemLayout} label="IP 过滤" className="ip-filter">
<Col span={6} className="ip-switch">
<FormItem>
{getFieldDecorator('ip_enable', {
initialValue: ip_enable,
valuePropName: 'checked',
rules: [{ type: 'boolean' }]
})(<Switch />)}
</FormItem>
</Col>
<Col span={18}>
<div style={{ display: getFieldValue('ip_enable') ? '' : 'none' }} className="ip">
<FormItem>
{getFieldDecorator(
'ip',
getFieldValue('ip_enable')
? {
initialValue: ip,
rules: [
{
pattern: constants.IP_REGEXP,
message: '请填写正确的 IP 地址',
required: true
}
]
}
: {}
)(<Input placeholder="请输入过滤的 IP 地址" />)}
</FormItem>
</div>
</Col>
</FormItem>
<Row className="params-form" style={{ marginBottom: 8 }}>
<Col {...{ span: 12, offset: 5 }}>
<Switch
size="small"
checkedChildren="JSON"
unCheckedChildren="JSON"
checked={paramsForm === 'json'}
onChange={bool => {
this.setState({ paramsForm: bool ? 'json' : 'form' });
}}
/>
</Col>
</Row>
{valuesTpl(paramsArr, '参数过滤')}
<FormItem
wrapperCol={{ span: 6, offset: 5 }}
style={{ display: paramsForm === 'form' ? '' : 'none' }}
>
<Button
size="default"
type="primary"
onClick={() => this.addValues('paramsArr')}
style={{ width: '100%' }}
>
<Icon type="plus" /> 添加参数
</Button>
</FormItem>
<FormItem
{...formItemLayout}
wrapperCol={{ span: 17 }}
label="参数过滤"
style={{ display: paramsForm === 'form' ? 'none' : '' }}
>
<AceEditor className="pretty-editor" data={params} onChange={this.handleParams} />
<FormItem>
{getFieldDecorator(
'params',
paramsForm === 'json'
? {
rules: [
{ validator: this.jsonValidator, message: '请输入正确的 JSON 字符串!' }
]
}
: {}
)(<Input style={{ display: 'none' }} />)}
</FormItem>
</FormItem>
<h2 className="sub-title">响应</h2>
<FormItem {...formItemLayout} required label="HTTP Code">
{getFieldDecorator('code', {
initialValue: code
})(
<Select showSearch>
{httpCodes.map(code => (
<Option key={'' + code} value={'' + code}>
{'' + code}
</Option>
))}
</Select>
)}
</FormItem>
<FormItem {...formItemLayout} label="延时">
{getFieldDecorator('delay', {
initialValue: delay,
rules: [{ required: true, message: '请输入延时时间!', type: 'integer' }]
})(<InputNumber placeholder="请输入延时时间" min={0} />)}
<span>ms</span>
</FormItem>
{headersTpl(headers, 'HTTP 头')}
<FormItem wrapperCol={{ span: 6, offset: 5 }}>
<Button
size="default"
type="primary"
onClick={() => this.addValues('headers')}
style={{ width: '100%' }}
>
<Icon type="plus" /> 添加 HTTP
</Button>
</FormItem>
<FormItem {...formItemLayout} wrapperCol={{ span: 17 }} label="Body" required>
<FormItem>
<AceEditor
className="pretty-editor"
data={res_body}
mode={this.props.currInterface.res_body_type === 'json' ? null : 'text'}
onChange={this.handleRequestBody}
/>
</FormItem>
</FormItem>
</Form>
</Modal>
);
}
}
const CaseDesModal = Form.create()(CaseDesForm);
export default CaseDesModal;

View File

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

View File

@@ -0,0 +1,269 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Table, Button, message, Popconfirm, Tooltip, Icon } from 'antd';
import { fetchMockCol } from 'client/reducer/modules/mockCol';
import { formatTime } from 'client/common.js';
import constants from 'client/constants/variable.js';
import CaseDesModal from './CaseDesModal';
import { json5_parse } from '../../../client/common';
import _ from 'underscore';
@connect(
state => {
return {
list: state.mockCol.list,
currInterface: state.inter.curdata,
currProject: state.project.currProject
};
},
{
fetchMockCol
}
)
@withRouter
export default class MockCol extends Component {
static propTypes = {
list: PropTypes.array,
currInterface: PropTypes.object,
match: PropTypes.object,
fetchMockCol: PropTypes.func,
currProject: PropTypes.object
};
state = {
caseData: {},
caseDesModalVisible: false,
isAdd: false
};
constructor(props) {
super(props);
}
UNSAFE_componentWillMount() {
const interfaceId = this.props.match.params.actionId;
this.props.fetchMockCol(interfaceId);
}
openModal = (record, isAdd) => {
return async () => {
if (this.props.currInterface.res_body_is_json_schema && isAdd) {
let result = await axios.post('/api/interface/schema2json', {
schema: json5_parse(this.props.currInterface.res_body),
required: true
});
record.res_body = JSON.stringify(result.data);
}
// 参数过滤schema形式
if (this.props.currInterface.req_body_is_json_schema) {
let result = await axios.post('/api/interface/schema2json', {
schema: json5_parse(this.props.currInterface.req_body_other),
required: true
});
record.req_body_other = JSON.stringify(result.data);
}
this.setState({
isAdd: isAdd,
caseDesModalVisible: true,
caseData: record
});
};
};
handleOk = async caseData => {
if (!caseData) {
return null;
}
const { caseData: currcase } = this.state;
const interface_id = this.props.match.params.actionId;
const project_id = this.props.match.params.id;
caseData = Object.assign({
...caseData,
interface_id: interface_id,
project_id: project_id
});
if (!this.state.isAdd) {
caseData.id = currcase._id;
}
await axios.post('/api/plugin/advmock/case/save', caseData).then(async res => {
if (res.data.errcode === 0) {
message.success(this.state.isAdd ? '添加成功' : '保存成功');
await this.props.fetchMockCol(interface_id);
this.setState({ caseDesModalVisible: false });
} else {
message.error(res.data.errmsg);
}
});
};
deleteCase = async id => {
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/del', { id }).then(async res => {
if (res.data.errcode === 0) {
message.success('删除成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
});
};
// mock case 可以设置开启的关闭
openMockCase = async (id , enable=true)=> {
const interface_id = this.props.match.params.actionId;
await axios.post('/api/plugin/advmock/case/hide', {
id,
enable: !enable
}).then(async res => {
if (res.data.errcode === 0) {
message.success('修改成功');
await this.props.fetchMockCol(interface_id);
} else {
message.error(res.data.errmsg);
}
})
}
render() {
const { list: data, currInterface } = this.props;
const { isAdd, caseData, caseDesModalVisible } = this.state;
const role = this.props.currProject.role;
const isGuest = role === 'guest';
const initCaseData = {
ip: '',
ip_enable: false,
name: currInterface.title,
code: '200',
delay: 0,
headers: [{ name: '', value: '' }],
params: {},
res_body: currInterface.res_body
};
let ipFilters = [];
let ipObj = {};
let userFilters = [];
let userObj = {};
_.isArray(data) &&
data.forEach(item => {
ipObj[item.ip_enable ? item.ip : ''] = '';
userObj[item.username] = '';
});
ipFilters = Object.keys(Object.assign(ipObj)).map(value => {
if (!value) {
value = '无过滤';
}
return { text: value, value };
});
userFilters = Object.keys(Object.assign(userObj)).map(value => {
return { text: value, value };
});
const columns = [
{
title: '期望名称',
dataIndex: 'name',
key: 'name'
},
{
title: 'ip',
dataIndex: 'ip',
key: 'ip',
render: (text, recode) => {
if (!recode.ip_enable) {
text = '';
}
return text;
},
onFilter: (value, record) =>
(record.ip === value && record.ip_enable) || (value === '无过滤' && !record.ip_enable),
filters: ipFilters
},
{
title: '创建人',
dataIndex: 'username',
key: 'username',
onFilter: (value, record) => record.username === value,
filters: userFilters
},
{
title: '编辑时间',
dataIndex: 'up_time',
key: 'up_time',
render: text => formatTime(text)
},
{
title: '操作',
dataIndex: '_id',
key: '_id',
render: (_id, recode) => {
// console.log(recode)
return (
!isGuest && (
<div>
<span style={{ marginRight: 5 }}>
<Button size="small" onClick={this.openModal(recode)}>
编辑
</Button>
</span>
<span style={{ marginRight: 5 }}>
<Popconfirm
title="你确定要删除这条期望?"
onConfirm={() => this.deleteCase(_id)}
okText="确定"
cancelText="取消"
>
<Button size="small" onClick={() => {}}>
删除
</Button>
</Popconfirm>
</span>
<span>
<Button size="small" onClick={() => this.openMockCase(_id, recode.case_enable)}>
{recode.case_enable ? <span>已开启</span> : <span></span>}
</Button>
</span>
</div>
)
);
}
}
];
return (
<div>
<div style={{ marginBottom: 8 }}>
<Button type="primary" onClick={this.openModal(initCaseData, true)} disabled={isGuest}>
添加期望
</Button>
<a
target="_blank"
rel="noopener noreferrer"
href={constants.docHref.adv_mock_case}
style={{ marginLeft: 8 }}
>
<Tooltip title="点击查看文档">
<Icon type="question-circle-o" />
</Tooltip>
</a>
</div>
<Table columns={columns} dataSource={data} pagination={false} rowKey="_id" />
{caseDesModalVisible && (
<CaseDesModal
visible={caseDesModalVisible}
isAdd={isAdd}
caseData={caseData}
onOk={this.handleOk}
onCancel={() => this.setState({ caseDesModalVisible: false })}
ref={this.saveFormRef}
/>
)}
</div>
);
}
}

View File

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

View File

@@ -0,0 +1,61 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
class advMockModel extends baseModel {
getName() {
return 'adv_mock';
}
getSchema() {
return {
interface_id: { type: Number, required: true },
project_id: {type: Number, required: true},
enable: {type: Boolean, default: false},
mock_script: String,
uid: String,
up_time: Number
};
}
get(interface_id) {
return this.model.findOne({
interface_id: interface_id
});
}
delByInterfaceId(interface_id) {
return this.model.remove({
interface_id: interface_id
});
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
up(data) {
data.up_time = yapi.commons.time();
return this.model.update({
interface_id: data.interface_id
}, {
uid: data.uid,
up_time: data.up_time,
mock_script: data.mock_script,
enable: data.enable
}, {
upsert: true
})
}
}
module.exports = advMockModel;

View File

@@ -0,0 +1,76 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
const mongoose = require('mongoose');
class caseModel extends baseModel {
getName() {
return 'adv_mock_case';
}
getSchema() {
return {
interface_id: { type: Number, required: true },
project_id: {type: Number, required: true},
ip: {type: String},
ip_enable: {type: Boolean, default: false},
name: {type: String, required: true},
code: {type: Number, default: 200},
delay: {type: Number, default: 0},
headers: [{
name: {type: String, required: true},
value: {type: String}
}],
params: mongoose.Schema.Types.Mixed,
uid: String,
up_time: Number,
res_body: {type: String, required: true},
case_enable: {type: Boolean, default: true}
};
}
get(data) {
return this.model.findOne(data);
}
list(id){
return this.model.find({
interface_id: id
})
}
delByInterfaceId(interface_id) {
return this.model.remove({
interface_id: interface_id
});
}
delByProjectId(project_id){
return this.model.remove({
project_id: project_id
})
}
save(data) {
data.up_time = yapi.commons.time();
let m = new this.model(data);
return m.save();
}
up(data) {
let id = data.id;
delete data.id;
data.up_time = yapi.commons.time();
return this.model.update({
_id: id
}, data)
}
del(id){
return this.model.remove({
_id: id
})
}
}
module.exports = caseModel;

View File

@@ -0,0 +1,14 @@
import AdvMock from './AdvMock'
import mockCol from './MockCol/mockColReducer.js'
module.exports = function(){
this.bindHook('interface_tab', function(tabs){
tabs.advMock = {
name: '高级Mock',
component: AdvMock
}
})
this.bindHook('add_reducer', function(reducerModules){
reducerModules.mockCol = mockCol;
})
}

View File

@@ -0,0 +1,186 @@
const baseController = require('controllers/base.js');
const advModel = require('./advMockModel.js');
const yapi = require('yapi.js');
const caseModel = require('./caseModel.js');
const userModel = require('models/user.js');
const config = require('./index.js');
class advMockController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(advModel);
this.caseModel = yapi.getInst(caseModel);
this.userModel = yapi.getInst(userModel);
}
async getMock(ctx) {
let id = ctx.query.interface_id;
let mockData = await this.Model.get(id);
if (!mockData) {
return (ctx.body = yapi.commons.resReturn(null, 408, 'mock脚本不存在'));
}
return (ctx.body = yapi.commons.resReturn(mockData));
}
async upMock(ctx) {
let params = ctx.request.body;
try {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 40033, '没有权限'));
}
if (!params.interface_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少interface_id'));
}
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少project_id'));
}
let data = {
interface_id: params.interface_id,
mock_script: params.mock_script || '',
project_id: params.project_id,
uid: this.getUid(),
enable: params.enable === true ? true : false
};
let result;
let mockData = await this.Model.get(data.interface_id);
if (mockData) {
result = await this.Model.up(data);
} else {
result = await this.Model.save(data);
}
return (ctx.body = yapi.commons.resReturn(result));
} catch (e) {
return (ctx.body = yapi.commons.resReturn(null, 400, e.message));
}
}
async list(ctx) {
try {
let id = ctx.query.interface_id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '缺少 interface_id'));
}
let result = await this.caseModel.list(id);
for (let i = 0, len = result.length; i < len; i++) {
let userinfo = await this.userModel.findById(result[i].uid);
result[i] = result[i].toObject();
// if (userinfo) {
result[i].username = userinfo.username;
// }
}
ctx.body = yapi.commons.resReturn(result);
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
async getCase(ctx) {
let id = ctx.query.id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '缺少 id'));
}
let result = await this.caseModel.get({
_id: id
});
ctx.body = yapi.commons.resReturn(result);
}
async saveCase(ctx) {
let params = ctx.request.body;
if (!params.interface_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少interface_id'));
}
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少project_id'));
}
if (!params.res_body) {
return (ctx.body = yapi.commons.resReturn(null, 408, '请输入 Response Body'));
}
let data = {
interface_id: params.interface_id,
project_id: params.project_id,
ip_enable: params.ip_enable,
name: params.name,
params: params.params || [],
uid: this.getUid(),
code: params.code || 200,
delay: params.delay || 0,
headers: params.headers || [],
up_time: yapi.commons.time(),
res_body: params.res_body,
ip: params.ip
};
data.code = isNaN(data.code) ? 200 : +data.code;
data.delay = isNaN(data.delay) ? 0 : +data.delay;
if (config.httpCodes.indexOf(data.code) === -1) {
return (ctx.body = yapi.commons.resReturn(null, 408, '非法的 httpCode'));
}
let findRepeat, findRepeatParams;
findRepeatParams = {
project_id: data.project_id,
interface_id: data.interface_id,
ip_enable: data.ip_enable
};
if (data.params && typeof data.params === 'object' && Object.keys(data.params).length > 0) {
for (let i in data.params) {
findRepeatParams['params.' + i] = data.params[i];
}
}
if (data.ip_enable) {
findRepeatParams.ip = data.ip;
}
findRepeat = await this.caseModel.get(findRepeatParams);
if (findRepeat && findRepeat._id !== params.id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '已存在的期望'));
}
let result;
if (params.id && !isNaN(params.id)) {
data.id = +params.id;
result = await this.caseModel.up(data);
} else {
result = await this.caseModel.save(data);
}
return (ctx.body = yapi.commons.resReturn(result));
}
async delCase(ctx) {
let id = ctx.request.body.id;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少 id'));
}
let result = await this.caseModel.del(id);
return (ctx.body = yapi.commons.resReturn(result));
}
async hideCase(ctx) {
let id = ctx.request.body.id;
let enable = ctx.request.body.enable;
if (!id) {
return (ctx.body = yapi.commons.resReturn(null, 408, '缺少 id'));
}
let data = {
id,
case_enable: enable
};
let result = await this.caseModel.up(data);
return (ctx.body = yapi.commons.resReturn(result));
}
}
module.exports = advMockController;

View File

@@ -0,0 +1,7 @@
module.exports = {
server: true,
client: true,
httpCodes: [
100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,422,423,424,426,428,429,431,500,501,502,503,504,505,506,507,508,510,511
]
}

View File

@@ -0,0 +1,212 @@
const controller = require('./controller');
const advModel = require('./advMockModel.js');
const caseModel = require('./caseModel.js');
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const _ = require('underscore');
const path = require('path');
const lib = require(path.resolve(yapi.WEBROOT, 'common/lib.js'));
const Mock = require('mockjs');
const mockExtra = require(path.resolve(yapi.WEBROOT, 'common/mock-extra.js'));
function arrToObj(arr) {
let obj = { 'Set-Cookie': [] };
arr.forEach(item => {
if (item.name === 'Set-Cookie') {
obj['Set-Cookie'].push(item.value);
} else obj[item.name] = item.value;
});
return obj;
}
module.exports = function() {
yapi.connect.then(function() {
let Col = mongoose.connection.db.collection('adv_mock');
Col.createIndex({
interface_id: 1
});
Col.createIndex({
project_id: 1
});
let caseCol = mongoose.connection.db.collection('adv_mock_case');
caseCol.createIndex({
interface_id: 1
});
caseCol.createIndex({
project_id: 1
});
});
async function checkCase(ctx, interfaceId) {
let reqParams = Object.assign({}, ctx.query, ctx.request.body);
let caseInst = yapi.getInst(caseModel);
// let ip = ctx.ip.match(/\d+.\d+.\d+.\d+/)[0];
// request.ip
let ip = yapi.commons.getIp(ctx);
// 数据库信息查询
// 过滤 开启IP
let listWithIp = await caseInst.model
.find({
interface_id: interfaceId,
ip_enable: true,
ip: ip
})
.select('_id params case_enable');
let matchList = [];
listWithIp.forEach(item => {
let params = item.params;
if (item.case_enable && lib.isDeepMatch(reqParams, params)) {
matchList.push(item);
}
});
// 其他数据
if (matchList.length === 0) {
let list = await caseInst.model
.find({
interface_id: interfaceId,
ip_enable: false
})
.select('_id params case_enable');
list.forEach(item => {
let params = item.params;
if (item.case_enable && lib.isDeepMatch(reqParams, params)) {
matchList.push(item);
}
});
}
if (matchList.length > 0) {
let maxItem = _.max(matchList, item => (item.params && Object.keys(item.params).length) || 0);
return maxItem;
}
return null;
}
async function handleByCase(caseData) {
let caseInst = yapi.getInst(caseModel);
let result = await caseInst.get({
_id: caseData._id
});
return result;
}
this.bindHook('add_router', function(addRouter) {
addRouter({
controller: controller,
method: 'get',
path: 'advmock/get',
action: 'getMock'
});
addRouter({
controller: controller,
method: 'post',
path: 'advmock/save',
action: 'upMock'
});
addRouter({
/**
* 保存期望
*/
controller: controller,
method: 'post',
path: 'advmock/case/save',
action: 'saveCase'
});
addRouter({
controller: controller,
method: 'get',
path: 'advmock/case/get',
action: 'getCase'
});
addRouter({
/**
* 获取期望列表
*/
controller: controller,
method: 'get',
path: 'advmock/case/list',
action: 'list'
});
addRouter({
/**
* 删除期望列表
*/
controller: controller,
method: 'post',
path: 'advmock/case/del',
action: 'delCase'
});
addRouter({
/**
* 隐藏期望列表
*/
controller: controller,
method: 'post',
path: 'advmock/case/hide',
action: 'hideCase'
});
});
this.bindHook('interface_del', async function(id) {
let inst = yapi.getInst(advModel);
await inst.delByInterfaceId(id);
});
this.bindHook('project_del', async function(id) {
let inst = yapi.getInst(advModel);
await inst.delByProjectId(id);
});
/**
* let context = {
projectData: project,
interfaceData: interfaceData,
ctx: ctx,
mockJson: res
}
*/
this.bindHook('mock_after', async function(context) {
let interfaceId = context.interfaceData._id;
let caseData = await checkCase(context.ctx, interfaceId);
// 只有开启高级mock才可用
if (caseData && caseData.case_enable) {
// 匹配到高级mock
let data = await handleByCase(caseData);
context.mockJson = yapi.commons.json_parse(data.res_body);
try {
context.mockJson = Mock.mock(
mockExtra(context.mockJson, {
query: context.ctx.query,
body: context.ctx.request.body,
params: Object.assign({}, context.ctx.query, context.ctx.request.body)
})
);
} catch (err) {
yapi.commons.log(err, 'error');
}
context.resHeader = arrToObj(data.headers);
context.httpCode = data.code;
context.delay = data.delay;
return true;
}
let inst = yapi.getInst(advModel);
let data = await inst.get(interfaceId);
if (!data || !data.enable || !data.mock_script) {
return context;
}
// mock 脚本
let script = data.mock_script;
await yapi.commons.handleMockScript(script, context);
});
};