egg-mock
Version:
mock server for egg
466 lines (423 loc) • 12 kB
JavaScript
const debug = require('util').debuglog('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 mockAgent = require('../../lib/mock_agent');
const mockHttpclient = require('../../lib/mock_httpclient');
const supertestRequest = require('../../lib/supertest');
const ORIGIN_TYPES = Symbol('egg-mock:originTypes');
const BACKGROUND_TASKS = Symbol('Application#backgroundTasks');
const REUSED_CTX = Symbol('Context#reusedInSuite');
module.exports = {
/**
* mock Context
* @function App#mockContext
* @param {Object} data - ctx data
* @param {Object} [options] - mock ctx options
* @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, options) {
function mockRequest(req) {
for (const key in (data.headers) || {}) {
mm(req.headers, key, data.headers[key]);
mm(req.headers, key.toLowerCase(), data.headers[key]);
}
}
options = Object.assign({ mockCtxStorage: true }, options);
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);
if (options.reuseCtxStorage !== false) {
if (this.currentContext && !this.currentContext[REUSED_CTX]) {
mockRequest(this.currentContext.request.req);
this.currentContext[REUSED_CTX] = true;
return this.currentContext;
}
}
const ctx = this.createContext(req, res);
if (options.mockCtxStorage) {
mm(this.ctxStorage, 'getStore', () => ctx);
}
return ctx;
},
async mockContextScope(fn, data) {
const ctx = this.mockContext(data, {
mockCtxStorage: false,
reuseCtxStorage: false,
});
return await this.ctxStorage.run(ctx, fn, ctx);
},
/**
* 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);
},
/**
* get mock httpclient agent
* @function App#mockHttpclientAgent
* @return {MockAgent} agent
*/
mockAgent() {
return mockAgent.getAgent();
},
mockAgentRestore() {
return mockAgent.restore();
},
/**
* @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;
}