123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- 'use strict';
- const debug = require('debug')('egg-mock:application');
- const mm = require('mm');
- const http = require('http');
- const fs = require('fs');
- const merge = require('merge-descriptors');
- const is = require('is-type-of');
- const assert = require('assert');
- const Transport = require('egg-logger').Transport;
- const mockHttpclient = require('../../lib/mock_httpclient');
- const supertestRequest = require('../../lib/supertest');
- const ORIGIN_TYPES = Symbol('egg-mock:originTypes');
- const BACKGROUND_TASKS = Symbol('Application#backgroundTasks');
- module.exports = {
- /**
- * mock Context
- * @function App#mockContext
- * @param {Object} data - ctx data
- * @return {Context} ctx
- * @example
- * ```js
- * const ctx = app.mockContext({
- * user: {
- * name: 'Jason'
- * }
- * });
- * console.log(ctx.user.name); // Jason
- *
- * // controller
- * module.exports = function*() {
- * this.body = this.user.name;
- * };
- * ```
- */
- mockContext(data) {
- data = data || {};
- if (this._customMockContext) {
- this._customMockContext(data);
- }
- // 使用者自定义mock,可以覆盖上面的 mock
- for (const key in data) {
- mm(this.context, key, data[key]);
- }
- const req = this.mockRequest(data);
- const res = new http.ServerResponse(req);
- return this.createContext(req, res);
- },
- /**
- * mock cookie session
- * @function App#mockSession
- * @param {Object} data - session object
- * @return {App} this
- */
- mockSession(data) {
- if (!data) {
- return this;
- }
- if (is.object(data) && !data.save) {
- Object.defineProperty(data, 'save', {
- value: () => {},
- enumerable: false,
- });
- }
- mm(this.context, 'session', data);
- return this;
- },
- /**
- * Mock service
- * @function App#mockService
- * @param {String} service - name
- * @param {String} methodName - method
- * @param {Object/Function/Error} fn - mock you data
- * @return {App} this
- */
- mockService(service, methodName, fn) {
- if (typeof service === 'string') {
- const arr = service.split('.');
- service = this.serviceClasses;
- for (const key of arr) {
- service = service[key];
- }
- service = service.prototype || service;
- }
- this._mockFn(service, methodName, fn);
- return this;
- },
- /**
- * mock service that return error
- * @function App#mockServiceError
- * @param {String} service - name
- * @param {String} methodName - method
- * @param {Error} [err] - error infomation
- * @return {App} this
- */
- mockServiceError(service, methodName, err) {
- if (typeof err === 'string') {
- err = new Error(err);
- } else if (!err) {
- // mockServiceError(service, methodName)
- err = new Error('mock ' + methodName + ' error');
- }
- this.mockService(service, methodName, err);
- return this;
- },
- _mockFn(obj, name, data) {
- const origin = obj[name];
- assert(is.function(origin), `property ${name} in original object must be function`);
- // keep origin properties' type to support mock multitimes
- if (!obj[ORIGIN_TYPES]) obj[ORIGIN_TYPES] = {};
- let type = obj[ORIGIN_TYPES][name];
- if (!type) {
- type = obj[ORIGIN_TYPES][name] = is.generatorFunction(origin) || is.asyncFunction(origin) ? 'async' : 'sync';
- }
- if (is.function(data)) {
- const fn = data;
- // if original is generator function or async function
- // but the mock function is normal function, need to change it return a promise
- if (type === 'async' &&
- (!is.generatorFunction(fn) && !is.asyncFunction(fn))) {
- mm(obj, name, function(...args) {
- return new Promise(resolve => {
- resolve(fn.apply(this, args));
- });
- });
- return;
- }
- mm(obj, name, fn);
- return;
- }
- if (type === 'async') {
- mm(obj, name, () => {
- return new Promise((resolve, reject) => {
- if (data instanceof Error) return reject(data);
- resolve(data);
- });
- });
- return;
- }
- mm(obj, name, () => {
- if (data instanceof Error) {
- throw data;
- }
- return data;
- });
- },
- /**
- * mock request
- * @function App#mockRequest
- * @param {Request} req - mock request
- * @return {Request} req
- */
- mockRequest(req) {
- req = Object.assign({}, req);
- const headers = req.headers || {};
- for (const key in req.headers) {
- headers[key.toLowerCase()] = req.headers[key];
- }
- if (!headers['x-forwarded-for']) {
- headers['x-forwarded-for'] = '127.0.0.1';
- }
- req.headers = headers;
- merge(req, {
- query: {},
- querystring: '',
- host: '127.0.0.1',
- hostname: '127.0.0.1',
- protocol: 'http',
- secure: 'false',
- method: 'GET',
- url: '/',
- path: '/',
- socket: {
- remoteAddress: '127.0.0.1',
- remotePort: 7001,
- },
- });
- return req;
- },
- /**
- * mock cookies
- * @function App#mockCookies
- * @param {Object} cookies - cookie
- * @return {Context} this
- */
- mockCookies(cookies) {
- if (!cookies) {
- return this;
- }
- const createContext = this.createContext;
- mm(this, 'createContext', function(req, res) {
- const ctx = createContext.call(this, req, res);
- const getCookie = ctx.cookies.get;
- mm(ctx.cookies, 'get', function(key, opts) {
- if (cookies[key]) {
- return cookies[key];
- }
- return getCookie.call(this, key, opts);
- });
- return ctx;
- });
- return this;
- },
- /**
- * mock header
- * @function App#mockHeaders
- * @param {Object} headers - header 对象
- * @return {Context} this
- */
- mockHeaders(headers) {
- if (!headers) {
- return this;
- }
- const getHeader = this.request.get;
- mm(this.request, 'get', function(field) {
- const header = findHeaders(headers, field);
- if (header) return header;
- return getHeader.call(this, field);
- });
- return this;
- },
- /**
- * mock csrf
- * @function App#mockCsrf
- * @return {App} this
- * @since 1.11
- */
- mockCsrf() {
- mm(this.context, 'assertCSRF', () => {});
- mm(this.context, 'assertCsrf', () => {});
- return this;
- },
- /**
- * mock httpclient
- * @function App#mockHttpclient
- * @param {...any} args - args
- * @return {Context} this
- */
- mockHttpclient(...args) {
- if (!this._mockHttpclient) {
- this._mockHttpclient = mockHttpclient(this);
- }
- return this._mockHttpclient(...args);
- },
- mockUrllib(...args) {
- this.deprecate('[egg-mock] Please use app.mockHttpclient instead of app.mockUrllib');
- return this.mockHttpclient(...args);
- },
- /**
- * @see mm#restore
- * @function App#mockRestore
- */
- mockRestore: mm.restore,
- /**
- * @see mm
- * @function App#mm
- */
- mm,
- /**
- * override loadAgent
- * @function App#loadAgent
- */
- loadAgent() {},
- /**
- * mock serverEnv
- * @function App#mockEnv
- * @param {String} env - serverEnv
- * @return {App} this
- */
- mockEnv(env) {
- mm(this.config, 'env', env);
- mm(this.config, 'serverEnv', env);
- return this;
- },
- /**
- * http request helper
- * @function App#httpRequest
- * @return {SupertestRequest} req - supertest request
- * @see https://github.com/visionmedia/supertest
- */
- httpRequest() {
- return supertestRequest(this);
- },
- /**
- * collection logger message, then can be use on `expectLog()`
- * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
- * @function App#mockLog
- */
- mockLog(logger) {
- logger = logger || this.logger;
- if (typeof logger === 'string') {
- logger = this.getLogger(logger);
- }
- // make sure mock once
- if (logger._mockLogs) return;
- const transport = new Transport(logger.options);
- // https://github.com/eggjs/egg-logger/blob/master/lib/logger.js#L64
- const log = logger.log;
- mm(logger, '_mockLogs', []);
- mm(logger, 'log', (level, args, meta) => {
- const message = transport.log(level, args, meta);
- logger._mockLogs.push(message);
- log.apply(logger, [ level, args, meta ]);
- });
- },
- __checkExpectLog(expectOrNot, str, logger) {
- logger = logger || this.logger;
- if (typeof logger === 'string') {
- logger = this.getLogger(logger);
- }
- const filepath = logger.options.file;
- let content;
- if (logger._mockLogs) {
- content = logger._mockLogs.join('\n');
- } else {
- content = fs.readFileSync(filepath, 'utf8');
- }
- let match;
- let type;
- if (str instanceof RegExp) {
- match = str.test(content);
- type = 'RegExp';
- } else {
- match = content.includes(String(str));
- type = 'String';
- }
- if (expectOrNot) {
- assert(match, `Can't find ${type}:"${str}" in ${filepath}, log content: ...${content.substring(content.length - 500)}`);
- } else {
- assert(!match, `Find ${type}:"${str}" in ${filepath}, log content: ...${content.substring(content.length - 500)}`);
- }
- },
- /**
- * expect str/regexp in the logger, if your server disk is slow, please call `mockLog()` first.
- * @param {String|RegExp} str - test str or regexp
- * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
- * @function App#expectLog
- */
- expectLog(str, logger) {
- this.__checkExpectLog(true, str, logger);
- },
- /**
- * not expect str/regexp in the logger, if your server disk is slow, please call `mockLog()` first.
- * @param {String|RegExp} str - test str or regexp
- * @param {String|Logger} [logger] - logger instance, default is `ctx.logger`
- * @function App#notExpectLog
- */
- notExpectLog(str, logger) {
- this.__checkExpectLog(false, str, logger);
- },
- // private method
- backgroundTasksFinished() {
- const tasks = this._backgroundTasks;
- debug('waiting %d background tasks', tasks.length);
- if (tasks.length === 0) return Promise.resolve();
- this._backgroundTasks = [];
- return Promise.all(tasks).then(() => {
- debug('finished %d background tasks', tasks.length);
- if (this._backgroundTasks.length) {
- debug('new background tasks created: %s', this._backgroundTasks.length);
- return this.backgroundTasksFinished();
- }
- });
- },
- get _backgroundTasks() {
- if (!this[BACKGROUND_TASKS]) {
- this[BACKGROUND_TASKS] = [];
- }
- return this[BACKGROUND_TASKS];
- },
- set _backgroundTasks(tasks) {
- this[BACKGROUND_TASKS] = tasks;
- },
- };
- function findHeaders(headers, key) {
- if (!headers || !key) {
- return null;
- }
- key = key.toLowerCase();
- for (const headerKey in headers) {
- if (key === headerKey.toLowerCase()) {
- return headers[headerKey];
- }
- }
- return null;
- }
|