egg-mock
Version:
mock server for egg
417 lines (378 loc) • 10.4 kB
JavaScript
'use strict';
const mm = require('mm');
const http = require('http');
const merge = require('merge-descriptors');
const is = require('is-type-of');
const assert = require('assert');
const extend = require('extend2');
const supertestRequest = require('../../lib/supertest');
const ORIGIN_TYPES = Symbol('egg-mock:originTypes');
module.exports = {
/**
* mock Context
* @method 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
* @method 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
* @method 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
* @method 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
* @method 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
* @method 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
* @method 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
* @method App#mockCsrf
* @return {App} this
* @sice 1.11
*/
mockCsrf() {
mm(this.context, 'assertCSRF', () => {});
mm(this.context, 'assertCsrf', () => {});
return this;
},
/**
* mock httpclient
* @method App#mockHttpclient
* @param {String} mockUrl - url
* @param {String|Array} mockMethod - http method
* @param {Object} mockResult - you data
* - data - buffer / string / json
* - status - http status
* - headers - response header
* @return {Context} this
*/
mockHttpclient(mockUrl, mockMethod, mockResult) {
if (!mockResult) {
// app.mockHttpclient(mockUrl, mockResult)
mockResult = mockMethod;
mockMethod = '*';
}
if (!Array.isArray(mockMethod)) mockMethod = [ mockMethod ];
mockMethod = mockMethod.map(method => (method || 'GET').toUpperCase());
if (!mockResult.status) {
mockResult.status = 200;
}
mockResult.data = mockResult.data || '';
if (Buffer.isBuffer(mockResult.data)) {
// do nothing
} else if (typeof mockResult.data === 'object') {
// json
mockResult.data = new Buffer(JSON.stringify(mockResult.data));
} else if (typeof mockResult.data === 'string') {
// string
mockResult.data = new Buffer(mockResult.data);
} else {
throw new Error('`mockResult.data` must be buffer, string or json');
}
if (!mockResult.res) {
mockResult.res = {
status: mockResult.status,
};
}
mockResult.responseSize = mockResult.responseSize || 0;
if (mockResult.data) {
mockResult.responseSize = mockResult.data.length;
}
mockResult.headers = mockResult.headers || {};
const httpclient = this.httpclient;
const rawRequest = httpclient.request;
mm(httpclient, 'requestThunk', _request);
mm(httpclient, 'request', _request);
mm(httpclient, 'curl', _request);
return this;
function matchMethod(method) {
return mockMethod.some(m => m === '*' || m === method);
}
function matchUrl(url) {
if (url === mockUrl) return true;
if (mockUrl instanceof RegExp && url.match(mockUrl)) return true;
return false;
}
// support generator rather than callback and promise
function _request(url, opt) {
opt = opt || {};
opt.method = (opt.method || 'GET').toUpperCase();
opt.headers = opt.headers || {};
if (matchUrl(url) && matchMethod(opt.method)) {
const result = extend(true, {}, mockResult);
const response = {
status: result.status,
statusCode: result.status,
headers: result.headers,
size: result.responseSize,
aborted: false,
rt: 1,
keepAliveSocket: result.keepAliveSocket || false,
};
httpclient.emit('response', {
error: null,
ctx: opt.ctx,
req: {
url,
options: opt,
size: result.requestSize,
},
res: response,
});
if (opt.dataType === 'json') {
try {
result.data = JSON.parse(result.data);
} catch (err) {
err.name = 'JSONResponseFormatError';
throw err;
}
} else if (opt.dataType === 'text') {
result.data = result.data.toString();
}
return Promise.resolve(result);
}
return rawRequest.call(httpclient, url, opt);
}
},
mockUrllib(...args) {
this.deprecate('[egg-mock] Please use app.mockHttpclient instead of app.mockUrllib');
return this.mockHttpclient(...args);
},
/**
* @see mm#restore
* @method App#mockRestore
*/
mockRestore: mm.restore,
/**
* @see mm
* @method App#mm
*/
mm,
/**
* override loadAgent
* @method App#loadAgent
*/
loadAgent() {},
/**
* mock serverEnv
* @method 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
* @method App#httpRequest
* @return {SupertestRequest} req - supertest request
* @see https://github.com/visionmedia/supertest
*/
httpRequest() {
return supertestRequest(this);
},
};
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;
}