mock平台

mockServer.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. const yapi = require('../yapi.js');
  2. const projectModel = require('../models/project.js');
  3. const interfaceModel = require('../models/interface.js');
  4. const mockExtra = require('../../common/mock-extra.js');
  5. const { schemaValidator } = require('../../common/utils.js');
  6. const _ = require('underscore');
  7. const Mock = require('mockjs');
  8. const variable = require('../../client/constants/variable.js')
  9. /**
  10. *
  11. * @param {*} apiPath /user/tom
  12. * @param {*} apiRule /user/:username
  13. */
  14. function matchApi(apiPath, apiRule) {
  15. let apiRules = apiRule.split('/');
  16. let apiPaths = apiPath.split('/');
  17. let pathParams = {
  18. __weight: 0
  19. };
  20. if (apiPaths.length !== apiRules.length) {
  21. return false;
  22. }
  23. for (let i = 0; i < apiRules.length; i++) {
  24. if (apiRules[i]) {
  25. apiRules[i] = apiRules[i].trim();
  26. } else {
  27. continue;
  28. }
  29. if (
  30. apiRules[i].length > 2 &&
  31. apiRules[i][0] === '{' &&
  32. apiRules[i][apiRules[i].length - 1] === '}'
  33. ) {
  34. pathParams[apiRules[i].substr(1, apiRules[i].length - 2)] = apiPaths[i];
  35. } else if (apiRules[i].indexOf(':') === 0) {
  36. pathParams[apiRules[i].substr(1)] = apiPaths[i];
  37. } else if (
  38. apiRules[i].length > 2 &&
  39. apiRules[i].indexOf('{') > -1 &&
  40. apiRules[i].indexOf('}') > -1
  41. ) {
  42. let params = [];
  43. apiRules[i] = apiRules[i].replace(/\{(.+?)\}/g, function(src, match) {
  44. params.push(match);
  45. return '([^\\/\\s]+)';
  46. });
  47. apiRules[i] = new RegExp(apiRules[i]);
  48. if (!apiRules[i].test(apiPaths[i])) {
  49. return false;
  50. }
  51. let matchs = apiPaths[i].match(apiRules[i]);
  52. params.forEach((item, index) => {
  53. pathParams[item] = matchs[index + 1];
  54. });
  55. } else {
  56. if (apiRules[i] !== apiPaths[i]) {
  57. return false;
  58. }else{
  59. pathParams.__weight++;
  60. }
  61. }
  62. }
  63. return pathParams;
  64. }
  65. function parseCookie(str) {
  66. if (!str || typeof str !== 'string') {
  67. return str;
  68. }
  69. if (str.split(';')[0]) {
  70. let c = str.split(';')[0].split('=');
  71. return { name: c[0], value: c[1] || '' };
  72. }
  73. return null;
  74. }
  75. function handleCorsRequest(ctx) {
  76. let header = ctx.request.header;
  77. ctx.set('Access-Control-Allow-Origin', header.origin);
  78. ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEADER, PATCH, OPTIONS');
  79. ctx.set('Access-Control-Allow-Headers', header['access-control-request-headers']);
  80. ctx.set('Access-Control-Allow-Credentials', true);
  81. ctx.set('Access-Control-Max-Age', 1728000);
  82. ctx.body = 'ok';
  83. }
  84. // 必填字段是否填写好
  85. function mockValidator(interfaceData, ctx) {
  86. let i,
  87. j,
  88. l,
  89. len,
  90. noRequiredArr = [];
  91. let method = interfaceData.method.toUpperCase() || 'GET';
  92. // query 判断
  93. for (i = 0, l = interfaceData.req_query.length; i < l; i++) {
  94. let curQuery = interfaceData.req_query[i];
  95. if (curQuery && typeof curQuery === 'object' && curQuery.required === '1') {
  96. if (!ctx.query[curQuery.name]) {
  97. noRequiredArr.push(curQuery.name);
  98. }
  99. }
  100. }
  101. // form 表单判断
  102. if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'form') {
  103. for (j = 0, len = interfaceData.req_body_form.length; j < len; j++) {
  104. let curForm = interfaceData.req_body_form[j];
  105. if (curForm && typeof curForm === 'object' && curForm.required === '1') {
  106. if (
  107. ctx.request.body[curForm.name] ||
  108. (ctx.request.body.fields && ctx.request.body.fields[curForm.name]) ||
  109. (ctx.request.body.files && ctx.request.body.files[curForm.name])
  110. ) {
  111. continue;
  112. }
  113. noRequiredArr.push(curForm.name);
  114. }
  115. }
  116. }
  117. let validResult;
  118. // json schema 判断
  119. if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'json' && interfaceData.req_body_is_json_schema === true) {
  120. const schema = yapi.commons.json_parse(interfaceData.req_body_other);
  121. const params = yapi.commons.json_parse(ctx.request.body);
  122. validResult = schemaValidator(schema, params);
  123. }
  124. if (noRequiredArr.length > 0 || (validResult && !validResult.valid)) {
  125. let message = `错误信息:`;
  126. message += noRequiredArr.length > 0 ? `缺少必须字段 ${noRequiredArr.join(',')} ` : '';
  127. message += validResult && !validResult.valid ? `shema 验证请求参数 ${validResult.message}` : '';
  128. return {
  129. valid: false,
  130. message
  131. };
  132. }
  133. return { valid: true };
  134. }
  135. module.exports = async (ctx, next) => {
  136. // no used variable 'hostname' & 'config'
  137. // let hostname = ctx.hostname;
  138. // let config = yapi.WEBCONFIG;
  139. let path = ctx.path;
  140. let header = ctx.request.header;
  141. if (path.indexOf('/mock/') !== 0) {
  142. if (next) await next();
  143. return true;
  144. }
  145. let paths = path.split('/');
  146. let projectId = paths[2];
  147. paths.splice(0, 3);
  148. path = '/' + paths.join('/');
  149. ctx.set('Access-Control-Allow-Origin', header.origin);
  150. ctx.set('Access-Control-Allow-Credentials', true);
  151. // ctx.set('Access-Control-Allow-Origin', '*');
  152. if (!projectId) {
  153. return (ctx.body = yapi.commons.resReturn(null, 400, 'projectId不能为空'));
  154. }
  155. let projectInst = yapi.getInst(projectModel),
  156. project;
  157. try {
  158. project = await projectInst.get(projectId);
  159. } catch (e) {
  160. return (ctx.body = yapi.commons.resReturn(null, 403, e.message));
  161. }
  162. if (!project) {
  163. return (ctx.body = yapi.commons.resReturn(null, 400, '不存在的项目'));
  164. }
  165. let interfaceData, newpath;
  166. let interfaceInst = yapi.getInst(interfaceModel);
  167. try {
  168. newpath = path.substr(project.basepath.length);
  169. interfaceData = await interfaceInst.getByPath(project._id, newpath, ctx.method);
  170. let queryPathInterfaceData = await interfaceInst.getByQueryPath(project._id, newpath, ctx.method);
  171. //处理query_path情况 url 中有 ?params=xxx
  172. if (!interfaceData || interfaceData.length != queryPathInterfaceData.length) {
  173. let i,
  174. l,
  175. j,
  176. len,
  177. curQuery,
  178. match = false;
  179. for (i = 0, l = queryPathInterfaceData.length; i < l; i++) {
  180. match = false;
  181. let currentInterfaceData = queryPathInterfaceData[i];
  182. curQuery = currentInterfaceData.query_path;
  183. if (!curQuery || typeof curQuery !== 'object' || !curQuery.path) {
  184. continue;
  185. }
  186. for (j = 0, len = curQuery.params.length; j < len; j++) {
  187. if (ctx.query[curQuery.params[j].name] !== curQuery.params[j].value) {
  188. continue;
  189. }
  190. if (j === len - 1) {
  191. match = true;
  192. }
  193. }
  194. if (match) {
  195. interfaceData = [currentInterfaceData];
  196. break;
  197. }
  198. // if (i === l - 1) {
  199. // interfaceData = [];
  200. // }
  201. }
  202. }
  203. //处理动态路由
  204. if (!interfaceData || interfaceData.length === 0) {
  205. let newData = await interfaceInst.getVar(project._id, ctx.method);
  206. let findInterface;
  207. let weight = 0;
  208. _.each(newData, item => {
  209. let m = matchApi(newpath, item.path);
  210. if (m !== false) {
  211. if(m.__weight >= weight){
  212. findInterface = item;
  213. }
  214. delete m.__weight;
  215. ctx.request.query = Object.assign(m, ctx.request.query);
  216. return true;
  217. }
  218. return false;
  219. });
  220. if (!findInterface) {
  221. //非正常跨域预检请求回应
  222. if (ctx.method === 'OPTIONS' && ctx.request.header['access-control-request-method']) {
  223. return handleCorsRequest(ctx);
  224. }
  225. return (ctx.body = yapi.commons.resReturn(
  226. null,
  227. 404,
  228. `不存在的api, 当前请求path为 ${newpath}, 请求方法为 ${
  229. ctx.method
  230. } ,请确认是否定义此请求。`
  231. ));
  232. }
  233. interfaceData = [await interfaceInst.get(findInterface._id)];
  234. }
  235. if (interfaceData.length > 1) {
  236. return (ctx.body = yapi.commons.resReturn(null, 405, '存在多个api,请检查数据库'));
  237. } else {
  238. interfaceData = interfaceData[0];
  239. }
  240. // 必填字段是否填写好
  241. if (project.strice) {
  242. const validResult = mockValidator(interfaceData, ctx);
  243. if (!validResult.valid) {
  244. return (ctx.body = yapi.commons.resReturn(
  245. null,
  246. 404,
  247. `接口字段验证不通过, ${validResult.message}`
  248. ));
  249. }
  250. }
  251. let res;
  252. // mock 返回值处理
  253. res = interfaceData.res_body;
  254. try {
  255. if (interfaceData.res_body_type === 'json') {
  256. if (interfaceData.res_body_is_json_schema === true) {
  257. //json-schema
  258. const schema = yapi.commons.json_parse(interfaceData.res_body);
  259. res = yapi.commons.schemaToJson(schema, {
  260. alwaysFakeOptionals: true
  261. });
  262. } else {
  263. // console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data'))
  264. // 处理 format-data
  265. if (
  266. _.isString(ctx.request.header['content-type']) &&
  267. ctx.request.header['content-type'].indexOf('multipart/form-data') > -1
  268. ) {
  269. ctx.request.body = ctx.request.body.fields;
  270. }
  271. // console.log('body', ctx.request.body)
  272. res = mockExtra(yapi.commons.json_parse(interfaceData.res_body), {
  273. query: ctx.request.query,
  274. body: ctx.request.body,
  275. params: Object.assign({}, ctx.request.query, ctx.request.body)
  276. });
  277. // console.log('res',res)
  278. }
  279. try {
  280. res = Mock.mock(res);
  281. } catch (e) {
  282. console.log('err', e.message);
  283. yapi.commons.log(e, 'error');
  284. }
  285. }
  286. let context = {
  287. projectData: project,
  288. interfaceData: interfaceData,
  289. ctx: ctx,
  290. mockJson: res,
  291. resHeader: {},
  292. httpCode: 200,
  293. delay: 0
  294. };
  295. if (project.is_mock_open && project.project_mock_script) {
  296. // 项目层面的mock脚本解析
  297. let script = project.project_mock_script;
  298. yapi.commons.handleMockScript(script, context);
  299. }
  300. await yapi.emitHook('mock_after', context);
  301. let handleMock = new Promise(resolve => {
  302. setTimeout(() => {
  303. resolve(true);
  304. }, context.delay);
  305. });
  306. await handleMock;
  307. if (context.resHeader && typeof context.resHeader === 'object') {
  308. for (let i in context.resHeader) {
  309. let cookie;
  310. if (i === 'Set-Cookie') {
  311. if (context.resHeader[i] && typeof context.resHeader[i] === 'string') {
  312. cookie = parseCookie(context.resHeader[i]);
  313. if (cookie && typeof cookie === 'object') {
  314. ctx.cookies.set(cookie.name, cookie.value, {
  315. maxAge: 864000000,
  316. httpOnly: false
  317. });
  318. }
  319. } else if (context.resHeader[i] && Array.isArray(context.resHeader[i])) {
  320. context.resHeader[i].forEach(item => {
  321. cookie = parseCookie(item);
  322. if (cookie && typeof cookie === 'object') {
  323. ctx.cookies.set(cookie.name, cookie.value, {
  324. maxAge: 864000000,
  325. httpOnly: false
  326. });
  327. }
  328. });
  329. }
  330. } else {
  331. ctx.set(i, context.resHeader[i]);
  332. }
  333. }
  334. }
  335. ctx.status = context.httpCode;
  336. ctx.body = context.mockJson;
  337. return;
  338. } catch (e) {
  339. yapi.commons.log(e, 'error');
  340. return (ctx.body = {
  341. errcode: 400,
  342. errmsg: '解析出错,请检查。Error: ' + e.message,
  343. data: null
  344. });
  345. }
  346. } catch (e) {
  347. yapi.commons.log(e, 'error');
  348. return (ctx.body = yapi.commons.resReturn(null, 409, e.message));
  349. }
  350. };