UNPKG

tomjs

Version:
253 lines (218 loc) 7.46 kB
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; });