tomjs
Version:
tomjs framework
253 lines (218 loc) • 7.46 kB
JavaScript
const assert = require('assert');
const Module = require('module');
const auth_cfg = {
captcha_key_field: 'captcha_key',
login_captcha_field: 'login_captcha',
login_username_fields: 'name',
register_name_email_mobile_lower_case: true,
expiresin_long: 'expiresin_long',
jwt_tokenkey: 'tokenkey',
jwt_key_status: 'status',
};
let password_compare_result = true;
class BaseApiError extends Error {
constructor(code, info) {
super(info && info.message);
this.code = code;
}
}
BaseApiError.LOGIN_ERROR = 100;
BaseApiError.DB_ERROR = 101;
class BaseUser {
decodeToken() {
return { exp: 123, exp_is_long: false };
}
}
const require2_modules = {
'tomjs/error/base_api_error': BaseApiError,
'tomjs/controllers/base_user': BaseUser,
'tomjs/handlers/tools': {
isObject(value) {
return typeof value == 'object' && value !== null && !Array.isArray(value);
},
},
'tomjs/password': {
async compare() {
return password_compare_result;
},
},
'tomjs/configs': () => ({ auth: auth_cfg }),
};
/**
* 在不启动完整 Koa 应用和数据库的情况下加载登录基类。
*
* tomjs 的模块通过 require2 读取应用配置,直接加载登录基类会要求存在完整项目环境。
* 此处只替换登录基类实际依赖的边界模块,使测试能够专注验证查询构造、密码剥离和事件数据,
* 不会把数据库、JWT 和应用配置的行为混入本次安全测试。
*
* @returns {Object} 登录控制器基类和登录验证规则基类。
*/
function loadBaseLogin() {
const original_load = Module._load;
Module._load = function(request, parent, is_main) {
if (request == 'tomjs/handlers/require2') {
return function(module_name) {
return require2_modules[module_name];
};
}
return original_load.call(this, request, parent, is_main);
};
try {
return {
BaseLogin: require('../controllers/auth/base_login'),
BaseLoginRules: require('../rules/auth/base_login'),
};
}
finally {
Module._load = original_load;
}
}
const { BaseLogin, BaseLoginRules } = loadBaseLogin();
/**
* 创建只包含登录测试所需依赖的控制器实例。
*
* @param {Function} find_one 模拟用户查询的方法。
* @param {Function} emit 模拟登录事件的方法。
* @returns {BaseLogin} 可执行登录逻辑的控制器实例。
*/
function makeController(find_one, emit = () => {}) {
const controller = Object.create(BaseLogin.prototype);
controller.users = { findOne: find_one };
controller.emitter = { emit };
controller.BuildToken = async ctx => {
ctx.state[auth_cfg.jwt_tokenkey] = 'test-token';
return 'test-token';
};
return controller;
}
/**
* 创建登录测试使用的最小 Koa 上下文。
*
* @param {Object} body 登录请求数据。
* @returns {Object} 最小 Koa 上下文。
*/
function makeContext(body) {
return {
request: { body },
state: { __: value => value },
};
}
const tests = [];
/**
* 登记一个按顺序执行的登录安全测试。
*
* @param {String} name 测试名称。
* @param {Function} callback 测试逻辑。
* @returns {void}
*/
function test(name, callback) {
tests.push({ name, callback });
}
test('HTTP 登录只把服务端允许的字段交给用户查询', async () => {
let received_where;
const user = { id: '1', name: 'tom', password: 'hash', status: 1 };
const controller = makeController(async where => {
received_where = where;
return user;
});
const ctx = makeContext({
name: 'Tom',
password: 'Ab#123456',
expiresin_long: 1,
$where: 'sleep(1000)',
role: 'admin',
});
await controller.login(ctx);
assert.deepStrictEqual(received_where, { name: 'tom' });
assert.strictEqual(ctx.body.userid, '1');
});
test('多个登录字段仍由框架按配置生成 $or 查询', () => {
const controller = makeController(async () => null);
const where = controller.buildLoginWhere({ name: 'Tom@Example.com' }, 'name,email,mobile');
assert.deepStrictEqual(where, {
$or: [
{ name: 'tom@example.com' },
{ email: 'tom@example.com' },
{ mobile: 'tom@example.com' },
],
});
});
test('合法的嵌套字段可用,非法字段路径会让查询构造失败', () => {
const controller = makeController(async () => null);
assert.deepStrictEqual(
controller.buildLoginWhere({ name: 'tom' }, 'profile.login_name'),
{ 'profile.login_name': 'tom' }
);
assert.strictEqual(controller.buildLoginWhere({ name: 'tom' }, 'name..value'), false);
assert.strictEqual(controller.buildLoginWhere({ name: 'tom' }, 'name,$where'), false);
assert.strictEqual(controller.buildLoginWhere({ name: 'tom' }, '__proto__'), false);
});
test('对象型登录名不会进入数据库查询', () => {
const controller = makeController(async () => null);
assert.strictEqual(controller.buildLoginWhere({ name: { $ne: null } }, 'name'), false);
});
test('loginByWhere 保留可信动态查询并且不修改调用方对象', async () => {
let received_where;
const controller = makeController(async where => {
received_where = where;
return { id: '1', password: 'hash' };
});
const ctx = makeContext({});
const input_where = {
tenant_id: 'tenant-1',
status: { $in: [1, 2] },
password: 'Ab#123456',
expiresin_long: 1,
};
const old_input_where = JSON.parse(JSON.stringify(input_where));
await controller.loginByWhere(ctx, input_where, true);
assert.deepStrictEqual(received_where, {
tenant_id: 'tenant-1',
status: { $in: [1, 2] },
});
assert.deepStrictEqual(input_where, old_input_where);
});
test('登录失败事件不包含密码和长时登录控制字段', async () => {
let event_data;
const controller = makeController(
async () => null,
(event_name, data) => {
if (event_name == 'login_error') {
event_data = data;
}
}
);
const input_where = {
name: 'tom',
password: 'Ab#123456',
expiresin_long: 1,
};
await controller.loginByWhere(makeContext({}), input_where, true);
assert.deepStrictEqual(event_data.where, { name: 'tom' });
});
test('登录验证规则要求密码必须是字符串', async () => {
const rules = await new BaseLoginRules().login(makeContext({}));
assert.strictEqual(rules.rules.password, 'required|string|min:6|max:70');
});
/**
* 按声明顺序执行测试并输出明确结果,任何失败都会让 npm test 返回非零状态。
*
* @returns {Promise<void>} 所有测试执行完成后结束。
*/
async function run() {
for (const current_test of tests) {
try {
await current_test.callback();
console.log(`通过: ${current_test.name}`);
}
catch (error) {
console.error(`失败: ${current_test.name}`);
throw error;
}
}
console.log(`共通过 ${tests.length} 项登录安全测试`);
}
run().catch(error => {
console.error(error);
process.exitCode = 1;
});