application.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. 'use strict';
  2. const debug = require('debug')('egg-mock:application');
  3. const mm = require('mm');
  4. const http = require('http');
  5. const fs = require('fs');
  6. const merge = require('merge-descriptors');
  7. const is = require('is-type-of');
  8. const assert = require('assert');
  9. const Transport = require('egg-logger').Transport;
  10. const mockHttpclient = require('../../lib/mock_httpclient');
  11. const supertestRequest = require('../../lib/supertest');
  12. const ORIGIN_TYPES = Symbol('egg-mock:originTypes');
  13. const BACKGROUND_TASKS = Symbol('Application#backgroundTasks');
  14. module.exports = {
  15. /**
  16. * mock Context
  17. * @function App#mockContext
  18. * @param {Object} data - ctx data
  19. * @return {Context} ctx
  20. * @example
  21. * ```js
  22. * const ctx = app.mockContext({
  23. * user: {
  24. * name: 'Jason'
  25. * }
  26. * });
  27. * console.log(ctx.user.name); // Jason
  28. *
  29. * // controller
  30. * module.exports = function*() {
  31. * this.body = this.user.name;
  32. * };
  33. * ```
  34. */
  35. mockContext(data) {
  36. data = data || {};
  37. if (this._customMockContext) {
  38. this._customMockContext(data);
  39. }
  40. // 使用者自定义mock,可以覆盖上面的 mock
  41. for (const key in data) {
  42. mm(this.context, key, data[key]);
  43. }
  44. const req = this.mockRequest(data);
  45. const res = new http.ServerResponse(req);
  46. return this.createContext(req, res);
  47. },
  48. /**
  49. * mock cookie session
  50. * @function App#mockSession
  51. * @param {Object} data - session object
  52. * @return {App} this
  53. */
  54. mockSession(data) {
  55. if (!data) {
  56. return this;
  57. }
  58. if (is.object(data) && !data.save) {
  59. Object.defineProperty(data, 'save', {
  60. value: () => {},
  61. enumerable: false,
  62. });
  63. }
  64. mm(this.context, 'session', data);
  65. return this;
  66. },
  67. /**
  68. * Mock service
  69. * @function App#mockService
  70. * @param {String} service - name
  71. * @param {String} methodName - method
  72. * @param {Object/Function/Error} fn - mock you data
  73. * @return {App} this
  74. */
  75. mockService(service, methodName, fn) {
  76. if (typeof service === 'string') {
  77. const arr = service.split('.');
  78. service = this.serviceClasses;
  79. for (const key of arr) {
  80. service = service[key];
  81. }
  82. service = service.prototype || service;
  83. }
  84. this._mockFn(service, methodName, fn);
  85. return this;
  86. },
  87. /**
  88. * mock service that return error
  89. * @function App#mockServiceError
  90. * @param {String} service - name
  91. * @param {String} methodName - method
  92. * @param {Error} [err] - error infomation
  93. * @return {App} this
  94. */
  95. mockServiceError(service, methodName, err) {
  96. if (typeof err === 'string') {
  97. err = new Error(err);
  98. } else if (!err) {
  99. // mockServiceError(service, methodName)
  100. err = new Error('mock ' + methodName + ' error');
  101. }
  102. this.mockService(service, methodName, err);
  103. return this;
  104. },
  105. _mockFn(obj, name, data) {
  106. const origin = obj[name];
  107. assert(is.function(origin), `property ${name} in original object must be function`);
  108. // keep origin properties' type to support mock multitimes
  109. if (!obj[ORIGIN_TYPES]) obj[ORIGIN_TYPES] = {};
  110. let type = obj[ORIGIN_TYPES][name];
  111. if (!type) {
  112. type = obj[ORIGIN_TYPES][name] = is.generatorFunction(origin) || is.asyncFunction(origin) ? 'async' : 'sync';
  113. }
  114. if (is.function(data)) {
  115. const fn = data;
  116. // if original is generator function or async function
  117. // but the mock function is normal function, need to change it return a promise
  118. if (type === 'async' &&
  119. (!is.generatorFunction(fn) && !is.asyncFunction(fn))) {
  120. mm(obj, name, function(...args) {
  121. return new Promise(resolve => {
  122. resolve(fn.apply(this, args));
  123. });
  124. });
  125. return;
  126. }
  127. mm(obj, name, fn);
  128. return;
  129. }
  130. if (type === 'async') {
  131. mm(obj, name, () => {
  132. return new Promise((resolve, reject) => {
  133. if (data instanceof Error) return reject(data);
  134. resolve(data);
  135. });
  136. });
  137. return;
  138. }
  139. mm(obj, name, () => {
  140. if (data instanceof Error) {
  141. throw data;
  142. }
  143. return data;
  144. });
  145. },
  146. /**
  147. * mock request
  148. * @function App#mockRequest
  149. * @param {Request} req - mock request
  150. * @return {Request} req
  151. */
  152. mockRequest(req) {
  153. req = Object.assign({}, req);
  154. const headers = req.headers || {};
  155. for (const key in req.headers) {
  156. headers[key.toLowerCase()] = req.headers[key];
  157. }
  158. if (!headers['x-forwarded-for']) {
  159. headers['x-forwarded-for'] = '127.0.0.1';
  160. }
  161. req.headers = headers;
  162. merge(req, {
  163. query: {},
  164. querystring: '',
  165. host: '127.0.0.1',
  166. hostname: '127.0.0.1',
  167. protocol: 'http',
  168. secure: 'false',
  169. method: 'GET',
  170. url: '/',
  171. path: '/',
  172. socket: {
  173. remoteAddress: '127.0.0.1',
  174. remotePort: 7001,
  175. },
  176. });
  177. return req;
  178. },
  179. /**
  180. * mock cookies
  181. * @function App#mockCookies
  182. * @param {Object} cookies - cookie
  183. * @return {Context} this
  184. */
  185. mockCookies(cookies) {
  186. if (!cookies) {
  187. return this;
  188. }
  189. const createContext = this.createContext;
  190. mm(this, 'createContext', function(req, res) {
  191. const ctx = createContext.call(this, req, res);
  192. const getCookie = ctx.cookies.get;
  193. mm(ctx.cookies, 'get', function(key, opts) {
  194. if (cookies[key]) {
  195. return cookies[key];
  196. }
  197. return getCookie.call(this, key, opts);
  198. });
  199. return ctx;
  200. });
  201. return this;
  202. },
  203. /**
  204. * mock header
  205. * @function App#mockHeaders
  206. * @param {Object} headers - header 对象
  207. * @return {Context} this
  208. */
  209. mockHeaders(headers) {
  210. if (!headers) {
  211. return this;
  212. }
  213. const getHeader = this.request.get;
  214. mm(this.request, 'get', function(field) {
  215. const header = findHeaders(headers, field);
  216. if (header) return header;
  217. return getHeader.call(this, field);
  218. });
  219. return this;
  220. },
  221. /**
  222. * mock csrf
  223. * @function App#mockCsrf
  224. * @return {App} this
  225. * @since 1.11
  226. */
  227. mockCsrf() {
  228. mm(this.context, 'assertCSRF', () => {});
  229. mm(this.context, 'assertCsrf', () => {});
  230. return this;
  231. },
  232. /**
  233. * mock httpclient
  234. * @function App#mockHttpclient
  235. * @param {...any} args - args
  236. * @return {Context} this
  237. */
  238. mockHttpclient(...args) {
  239. if (!this._mockHttpclient) {
  240. this._mockHttpclient = mockHttpclient(this);
  241. }
  242. return this._mockHttpclient(...args);
  243. },
  244. mockUrllib(...args) {
  245. this.deprecate('[egg-mock] Please use app.mockHttpclient instead of app.mockUrllib');
  246. return this.mockHttpclient(...args);
  247. },
  248. /**
  249. * @see mm#restore
  250. * @function App#mockRestore
  251. */
  252. mockRestore: mm.restore,
  253. /**
  254. * @see mm
  255. * @function App#mm
  256. */
  257. mm,
  258. /**
  259. * override loadAgent
  260. * @function App#loadAgent
  261. */
  262. loadAgent() {},
  263. /**
  264. * mock serverEnv
  265. * @function App#mockEnv
  266. * @param {String} env - serverEnv
  267. * @return {App} this
  268. */
  269. mockEnv(env) {
  270. mm(this.config, 'env', env);
  271. mm(this.config, 'serverEnv', env);
  272. return this;
  273. },
  274. /**
  275. * http request helper
  276. * @function App#httpRequest
  277. * @return {SupertestRequest} req - supertest request
  278. * @see https://github.com/visionmedia/supertest
  279. */
  280. httpRequest() {
  281. return supertestRequest(this);
  282. },
  283. /**
  284. * collection logger message, then can be use on `expectLog()`
  285. * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
  286. * @function App#mockLog
  287. */
  288. mockLog(logger) {
  289. logger = logger || this.logger;
  290. if (typeof logger === 'string') {
  291. logger = this.getLogger(logger);
  292. }
  293. // make sure mock once
  294. if (logger._mockLogs) return;
  295. const transport = new Transport(logger.options);
  296. // https://github.com/eggjs/egg-logger/blob/master/lib/logger.js#L64
  297. const log = logger.log;
  298. mm(logger, '_mockLogs', []);
  299. mm(logger, 'log', (level, args, meta) => {
  300. const message = transport.log(level, args, meta);
  301. logger._mockLogs.push(message);
  302. log.apply(logger, [ level, args, meta ]);
  303. });
  304. },
  305. __checkExpectLog(expectOrNot, str, logger) {
  306. logger = logger || this.logger;
  307. if (typeof logger === 'string') {
  308. logger = this.getLogger(logger);
  309. }
  310. const filepath = logger.options.file;
  311. let content;
  312. if (logger._mockLogs) {
  313. content = logger._mockLogs.join('\n');
  314. } else {
  315. content = fs.readFileSync(filepath, 'utf8');
  316. }
  317. let match;
  318. let type;
  319. if (str instanceof RegExp) {
  320. match = str.test(content);
  321. type = 'RegExp';
  322. } else {
  323. match = content.includes(String(str));
  324. type = 'String';
  325. }
  326. if (expectOrNot) {
  327. assert(match, `Can't find ${type}:"${str}" in ${filepath}, log content: ...${content.substring(content.length - 500)}`);
  328. } else {
  329. assert(!match, `Find ${type}:"${str}" in ${filepath}, log content: ...${content.substring(content.length - 500)}`);
  330. }
  331. },
  332. /**
  333. * expect str/regexp in the logger, if your server disk is slow, please call `mockLog()` first.
  334. * @param {String|RegExp} str - test str or regexp
  335. * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
  336. * @function App#expectLog
  337. */
  338. expectLog(str, logger) {
  339. this.__checkExpectLog(true, str, logger);
  340. },
  341. /**
  342. * not expect str/regexp in the logger, if your server disk is slow, please call `mockLog()` first.
  343. * @param {String|RegExp} str - test str or regexp
  344. * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
  345. * @function App#notExpectLog
  346. */
  347. notExpectLog(str, logger) {
  348. this.__checkExpectLog(false, str, logger);
  349. },
  350. // private method
  351. backgroundTasksFinished() {
  352. const tasks = this._backgroundTasks;
  353. debug('waiting %d background tasks', tasks.length);
  354. if (tasks.length === 0) return Promise.resolve();
  355. this._backgroundTasks = [];
  356. return Promise.all(tasks).then(() => {
  357. debug('finished %d background tasks', tasks.length);
  358. if (this._backgroundTasks.length) {
  359. debug('new background tasks created: %s', this._backgroundTasks.length);
  360. return this.backgroundTasksFinished();
  361. }
  362. });
  363. },
  364. get _backgroundTasks() {
  365. if (!this[BACKGROUND_TASKS]) {
  366. this[BACKGROUND_TASKS] = [];
  367. }
  368. return this[BACKGROUND_TASKS];
  369. },
  370. set _backgroundTasks(tasks) {
  371. this[BACKGROUND_TASKS] = tasks;
  372. },
  373. };
  374. function findHeaders(headers, key) {
  375. if (!headers || !key) {
  376. return null;
  377. }
  378. key = key.toLowerCase();
  379. for (const headerKey in headers) {
  380. if (key === headerKey.toLowerCase()) {
  381. return headers[headerKey];
  382. }
  383. }
  384. return null;
  385. }