fork from bc4552c5a8
This commit is contained in:
386
server/middleware/mockServer.js
Normal file
386
server/middleware/mockServer.js
Normal file
@@ -0,0 +1,386 @@
|
||||
const yapi = require('../yapi.js');
|
||||
const projectModel = require('../models/project.js');
|
||||
const interfaceModel = require('../models/interface.js');
|
||||
const mockExtra = require('../../common/mock-extra.js');
|
||||
const { schemaValidator } = require('../../common/utils.js');
|
||||
const _ = require('underscore');
|
||||
const Mock = require('mockjs');
|
||||
const variable = require('../../client/constants/variable.js')
|
||||
/**
|
||||
*
|
||||
* @param {*} apiPath /user/tom
|
||||
* @param {*} apiRule /user/:username
|
||||
*/
|
||||
function matchApi(apiPath, apiRule) {
|
||||
let apiRules = apiRule.split('/');
|
||||
let apiPaths = apiPath.split('/');
|
||||
let pathParams = {
|
||||
__weight: 0
|
||||
};
|
||||
|
||||
if (apiPaths.length !== apiRules.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < apiRules.length; i++) {
|
||||
if (apiRules[i]) {
|
||||
apiRules[i] = apiRules[i].trim();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
apiRules[i].length > 2 &&
|
||||
apiRules[i][0] === '{' &&
|
||||
apiRules[i][apiRules[i].length - 1] === '}'
|
||||
) {
|
||||
pathParams[apiRules[i].substr(1, apiRules[i].length - 2)] = apiPaths[i];
|
||||
} else if (apiRules[i].indexOf(':') === 0) {
|
||||
pathParams[apiRules[i].substr(1)] = apiPaths[i];
|
||||
} else if (
|
||||
apiRules[i].length > 2 &&
|
||||
apiRules[i].indexOf('{') > -1 &&
|
||||
apiRules[i].indexOf('}') > -1
|
||||
) {
|
||||
let params = [];
|
||||
apiRules[i] = apiRules[i].replace(/\{(.+?)\}/g, function(src, match) {
|
||||
params.push(match);
|
||||
return '([^\\/\\s]+)';
|
||||
});
|
||||
apiRules[i] = new RegExp(apiRules[i]);
|
||||
if (!apiRules[i].test(apiPaths[i])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let matchs = apiPaths[i].match(apiRules[i]);
|
||||
|
||||
params.forEach((item, index) => {
|
||||
pathParams[item] = matchs[index + 1];
|
||||
});
|
||||
} else {
|
||||
if (apiRules[i] !== apiPaths[i]) {
|
||||
return false;
|
||||
}else{
|
||||
pathParams.__weight++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pathParams;
|
||||
}
|
||||
|
||||
function parseCookie(str) {
|
||||
if (!str || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
if (str.split(';')[0]) {
|
||||
let c = str.split(';')[0].split('=');
|
||||
return { name: c[0], value: c[1] || '' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleCorsRequest(ctx) {
|
||||
let header = ctx.request.header;
|
||||
ctx.set('Access-Control-Allow-Origin', header.origin);
|
||||
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEADER, PATCH, OPTIONS');
|
||||
ctx.set('Access-Control-Allow-Headers', header['access-control-request-headers']);
|
||||
ctx.set('Access-Control-Allow-Credentials', true);
|
||||
ctx.set('Access-Control-Max-Age', 1728000);
|
||||
ctx.body = 'ok';
|
||||
}
|
||||
// 必填字段是否填写好
|
||||
function mockValidator(interfaceData, ctx) {
|
||||
let i,
|
||||
j,
|
||||
l,
|
||||
len,
|
||||
noRequiredArr = [];
|
||||
let method = interfaceData.method.toUpperCase() || 'GET';
|
||||
// query 判断
|
||||
for (i = 0, l = interfaceData.req_query.length; i < l; i++) {
|
||||
let curQuery = interfaceData.req_query[i];
|
||||
if (curQuery && typeof curQuery === 'object' && curQuery.required === '1') {
|
||||
if (!ctx.query[curQuery.name]) {
|
||||
noRequiredArr.push(curQuery.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
// form 表单判断
|
||||
if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'form') {
|
||||
for (j = 0, len = interfaceData.req_body_form.length; j < len; j++) {
|
||||
let curForm = interfaceData.req_body_form[j];
|
||||
if (curForm && typeof curForm === 'object' && curForm.required === '1') {
|
||||
if (
|
||||
ctx.request.body[curForm.name] ||
|
||||
(ctx.request.body.fields && ctx.request.body.fields[curForm.name]) ||
|
||||
(ctx.request.body.files && ctx.request.body.files[curForm.name])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
noRequiredArr.push(curForm.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let validResult;
|
||||
// json schema 判断
|
||||
if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'json' && interfaceData.req_body_is_json_schema === true) {
|
||||
const schema = yapi.commons.json_parse(interfaceData.req_body_other);
|
||||
const params = yapi.commons.json_parse(ctx.request.body);
|
||||
validResult = schemaValidator(schema, params);
|
||||
}
|
||||
if (noRequiredArr.length > 0 || (validResult && !validResult.valid)) {
|
||||
let message = `错误信息:`;
|
||||
message += noRequiredArr.length > 0 ? `缺少必须字段 ${noRequiredArr.join(',')} ` : '';
|
||||
message += validResult && !validResult.valid ? `schema 验证请求参数 ${validResult.message}` : '';
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
// no used variable 'hostname' & 'config'
|
||||
// let hostname = ctx.hostname;
|
||||
// let config = yapi.WEBCONFIG;
|
||||
let path = ctx.path;
|
||||
let header = ctx.request.header;
|
||||
|
||||
if (path.indexOf('/mock/') !== 0) {
|
||||
if (next) await next();
|
||||
return true;
|
||||
}
|
||||
|
||||
let paths = path.split('/');
|
||||
let projectId = paths[2];
|
||||
paths.splice(0, 3);
|
||||
path = '/' + paths.join('/');
|
||||
|
||||
ctx.set('Access-Control-Allow-Origin', header.origin);
|
||||
ctx.set('Access-Control-Allow-Credentials', true);
|
||||
|
||||
// ctx.set('Access-Control-Allow-Origin', '*');
|
||||
|
||||
if (!projectId) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, 'projectId不能为空'));
|
||||
}
|
||||
|
||||
let projectInst = yapi.getInst(projectModel),
|
||||
project;
|
||||
try {
|
||||
project = await projectInst.get(projectId);
|
||||
} catch (e) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 403, e.message));
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 400, '不存在的项目'));
|
||||
}
|
||||
|
||||
let interfaceData, newpath;
|
||||
let interfaceInst = yapi.getInst(interfaceModel);
|
||||
|
||||
try {
|
||||
newpath = path.substr(project.basepath.length);
|
||||
interfaceData = await interfaceInst.getByPath(project._id, newpath, ctx.method);
|
||||
let queryPathInterfaceData = await interfaceInst.getByQueryPath(project._id, newpath, ctx.method);
|
||||
//处理query_path情况 url 中有 ?params=xxx
|
||||
if (!interfaceData || interfaceData.length != queryPathInterfaceData.length) {
|
||||
|
||||
let i,
|
||||
l,
|
||||
j,
|
||||
len,
|
||||
curQuery,
|
||||
match = false;
|
||||
for (i = 0, l = queryPathInterfaceData.length; i < l; i++) {
|
||||
match = false;
|
||||
let currentInterfaceData = queryPathInterfaceData[i];
|
||||
curQuery = currentInterfaceData.query_path;
|
||||
if (!curQuery || typeof curQuery !== 'object' || !curQuery.path) {
|
||||
continue;
|
||||
}
|
||||
for (j = 0, len = curQuery.params.length; j < len; j++) {
|
||||
if (ctx.query[curQuery.params[j].name] !== curQuery.params[j].value) {
|
||||
continue;
|
||||
}
|
||||
if (j === len - 1) {
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
interfaceData = [currentInterfaceData];
|
||||
break;
|
||||
}
|
||||
// if (i === l - 1) {
|
||||
// interfaceData = [];
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//处理动态路由
|
||||
if (!interfaceData || interfaceData.length === 0) {
|
||||
let newData = await interfaceInst.getVar(project._id, ctx.method);
|
||||
|
||||
let findInterface;
|
||||
let weight = 0;
|
||||
_.each(newData, item => {
|
||||
let m = matchApi(newpath, item.path);
|
||||
if (m !== false) {
|
||||
if(m.__weight >= weight){
|
||||
findInterface = item;
|
||||
}
|
||||
delete m.__weight;
|
||||
ctx.request.query = Object.assign(m, ctx.request.query);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!findInterface) {
|
||||
//非正常跨域预检请求回应
|
||||
if (ctx.method === 'OPTIONS' && ctx.request.header['access-control-request-method']) {
|
||||
return handleCorsRequest(ctx);
|
||||
}
|
||||
|
||||
return (ctx.body = yapi.commons.resReturn(
|
||||
null,
|
||||
404,
|
||||
`不存在的api, 当前请求path为 ${newpath}, 请求方法为 ${
|
||||
ctx.method
|
||||
} ,请确认是否定义此请求。`
|
||||
));
|
||||
}
|
||||
interfaceData = [await interfaceInst.get(findInterface._id)];
|
||||
}
|
||||
|
||||
if (interfaceData.length > 1) {
|
||||
return (ctx.body = yapi.commons.resReturn(null, 405, '存在多个api,请检查数据库'));
|
||||
} else {
|
||||
interfaceData = interfaceData[0];
|
||||
}
|
||||
|
||||
// 必填字段是否填写好
|
||||
if (project.strice) {
|
||||
const validResult = mockValidator(interfaceData, ctx);
|
||||
if (!validResult.valid) {
|
||||
return (ctx.body = yapi.commons.resReturn(
|
||||
null,
|
||||
404,
|
||||
`接口字段验证不通过, ${validResult.message}`
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let res;
|
||||
// mock 返回值处理
|
||||
res = interfaceData.res_body;
|
||||
try {
|
||||
if (interfaceData.res_body_type === 'json') {
|
||||
if (interfaceData.res_body_is_json_schema === true) {
|
||||
//json-schema
|
||||
const schema = yapi.commons.json_parse(interfaceData.res_body);
|
||||
res = yapi.commons.schemaToJson(schema, {
|
||||
alwaysFakeOptionals: true
|
||||
});
|
||||
} else {
|
||||
// console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data'))
|
||||
// 处理 format-data
|
||||
|
||||
if (
|
||||
_.isString(ctx.request.header['content-type']) &&
|
||||
ctx.request.header['content-type'].indexOf('multipart/form-data') > -1
|
||||
) {
|
||||
ctx.request.body = ctx.request.body.fields;
|
||||
}
|
||||
// console.log('body', ctx.request.body)
|
||||
|
||||
res = mockExtra(yapi.commons.json_parse(interfaceData.res_body), {
|
||||
query: ctx.request.query,
|
||||
body: ctx.request.body,
|
||||
params: Object.assign({}, ctx.request.query, ctx.request.body)
|
||||
});
|
||||
// console.log('res',res)
|
||||
}
|
||||
|
||||
try {
|
||||
res = Mock.mock(res);
|
||||
} catch (e) {
|
||||
console.log('err', e.message);
|
||||
yapi.commons.log(e, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
let context = {
|
||||
projectData: project,
|
||||
interfaceData: interfaceData,
|
||||
ctx: ctx,
|
||||
mockJson: res,
|
||||
resHeader: {},
|
||||
httpCode: 200,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
if (project.is_mock_open && project.project_mock_script) {
|
||||
// 项目层面的mock脚本解析
|
||||
let script = project.project_mock_script;
|
||||
await yapi.commons.handleMockScript(script, context);
|
||||
}
|
||||
|
||||
await yapi.emitHook('mock_after', context);
|
||||
|
||||
let handleMock = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, context.delay);
|
||||
});
|
||||
await handleMock;
|
||||
if (context.resHeader && typeof context.resHeader === 'object') {
|
||||
for (let i in context.resHeader) {
|
||||
let cookie;
|
||||
if (i === 'Set-Cookie') {
|
||||
if (context.resHeader[i] && typeof context.resHeader[i] === 'string') {
|
||||
cookie = parseCookie(context.resHeader[i]);
|
||||
if (cookie && typeof cookie === 'object') {
|
||||
ctx.cookies.set(cookie.name, cookie.value, {
|
||||
maxAge: 864000000,
|
||||
httpOnly: false
|
||||
});
|
||||
}
|
||||
} else if (context.resHeader[i] && Array.isArray(context.resHeader[i])) {
|
||||
context.resHeader[i].forEach(item => {
|
||||
cookie = parseCookie(item);
|
||||
if (cookie && typeof cookie === 'object') {
|
||||
ctx.cookies.set(cookie.name, cookie.value, {
|
||||
maxAge: 864000000,
|
||||
httpOnly: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ctx.set(i, context.resHeader[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.status = context.httpCode;
|
||||
ctx.body = context.mockJson;
|
||||
return;
|
||||
} catch (e) {
|
||||
yapi.commons.log(e, 'error');
|
||||
return (ctx.body = {
|
||||
errcode: 400,
|
||||
errmsg: '解析出错,请检查。Error: ' + e.message,
|
||||
data: null
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
yapi.commons.log(e, 'error');
|
||||
return (ctx.body = yapi.commons.resReturn(null, 409, e.message));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user