117 lines
5.3 KiB
TypeScript
117 lines
5.3 KiB
TypeScript
/**
|
||||
|
|
* 自检:不依赖 wrangler / 网络,直接验证最容易出错的那几块纯逻辑。
|
|||
|
|
* node node_modules/.bin/esbuild tools/selftest.ts --bundle --platform=node --format=esm --outfile=tools/.selftest.mjs && node tools/.selftest.mjs
|
|||
|
|
*/
|
|||
|
|
import { md5, md5Lower } from '../src/lib/md5';
|
|||
|
|
import { formatDateCN } from '../src/lib/util';
|
|||
|
|
import { renderMarkdown } from '../src/lib/md';
|
|||
|
|
import { hashPassword, verifyPassword, signToken, verifyToken } from '../src/lib/session';
|
|||
|
|
import { parseSimpleYaml } from '../src/routes/admin';
|
|||
|
|
import type { Env } from '../src/types';
|
|||
|
|
|
|||
|
|
let pass = 0;
|
|||
|
|
let fail = 0;
|
|||
|
|
|
|||
|
|
function eq(name: string, actual: unknown, expected: unknown) {
|
|||
|
|
const a = JSON.stringify(actual);
|
|||
|
|
const e = JSON.stringify(expected);
|
|||
|
|
if (a === e) {
|
|||
|
|
pass++;
|
|||
|
|
console.log(` PASS ${name}`);
|
|||
|
|
} else {
|
|||
|
|
fail++;
|
|||
|
|
console.log(` FAIL ${name}\n 实际: ${a}\n 期望: ${e}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ok(name: string, cond: boolean) {
|
|||
|
|
eq(name, cond, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ MD5
|
|||
|
|
console.log('== MD5(Gravatar hash,必须与官方一致) ==');
|
|||
|
|
eq('md5("")', md5(''), 'd41d8cd98f00b204e9800998ecf8427e');
|
|||
|
|
eq('md5("abc")', md5('abc'), '900150983cd24fb0d6963f7d28e17f72');
|
|||
|
|
eq('md5("test@example.com")', md5('test@example.com'), '55502f40dc8b7c769880b10874abc9d0');
|
|||
|
|
eq('md5("z@usj.cc")', md5('z@usj.cc'), '1cce0a22c2c7648eab76ec876c6a54d9');
|
|||
|
|
eq('md5("优世界") 多字节', md5('优世界'), '7fc1dce97eead18fa9e9ae4fbb8044cb');
|
|||
|
|
eq('md5 长输入(跨 block)', md5('a'.repeat(200)), '887f30b43b2867f4a9accceee7d16e6c');
|
|||
|
|
eq('md5Lower 去空格+小写', md5Lower(' Z@USJ.CC '), '1cce0a22c2c7648eab76ec876c6a54d9');
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
// ------------------------------------------------------------------ 时间
|
|||
|
|
console.log('== 时间格式(Asia/Shanghai) ==');
|
|||
|
|
eq('1631363636000', formatDateCN(1631363636000), '2021-09-11 20:33:56');
|
|||
|
|
eq('0', formatDateCN(0), '1970-01-01 08:00:00');
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ Markdown
|
|||
|
|
console.log('== Markdown 渲染 ==');
|
|||
|
|
ok('<script> 被转义', !renderMarkdown('<script>alert(1)</script>').includes('<script>'));
|
|||
|
|
ok('img onerror 被转义', !renderMarkdown('<img src=x onerror=alert(1)>').includes('onerror='));
|
|||
|
|
ok('javascript: 链接被剔除', !renderMarkdown('[x](javascript:alert(1))').includes('javascript:'));
|
|||
|
|
ok('**粗体**', renderMarkdown('**hi**').includes('<strong>hi</strong>'));
|
|||
|
|
ok('`行内代码`', renderMarkdown('`code`').includes('<code>code</code>'));
|
|||
|
|
ok('裸链接可点', renderMarkdown('见 https://usj.cc 一游').includes('<a href="https://usj.cc"'));
|
|||
|
|
ok('单换行 → <br>', renderMarkdown('a\nb').includes('a<br>b'));
|
|||
|
|
ok('``` 代码块', renderMarkdown('```js\nlet a=1;\n```').includes('<pre><code class="language-js">'));
|
|||
|
|
ok('& 被转义', renderMarkdown('a & b').includes('a & b'));
|
|||
|
|
ok('含单引号的正文不炸', renderMarkdown("it's fine").includes('it's fine'));
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ 密码
|
|||
|
|
console.log('== 密码(PBKDF2 / md5 / 明文兼容) ==');
|
|||
|
|
const hashed = await hashPassword('s3cret-密码');
|
|||
|
|
ok('hash 前缀是 (pbkdf2)', hashed.startsWith('(pbkdf2)'));
|
|||
|
|
ok('正确密码通过', await verifyPassword(hashed, 's3cret-密码'));
|
|||
|
|
ok('错误密码拒绝', !(await verifyPassword(hashed, 'wrong')));
|
|||
|
|
ok('空密码拒绝', !(await verifyPassword('', '')));
|
|||
|
|
ok('兼容 (md5) 旧密码', await verifyPassword('(md5)1cce0a22c2c7648eab76ec876c6a54d9', 'z@usj.cc'));
|
|||
|
|
ok('兼容明文旧密码', await verifyPassword('plain', 'plain'));
|
|||
|
|
ok('(bcrypt) 明确不支持', !(await verifyPassword('(bcrypt)$2a$10$xxx', 'whatever')));
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ Token
|
|||
|
|
console.log('== 登录 token(HMAC 无状态) ==');
|
|||
|
|
const env = { TOKEN_SECRET: 'test-secret-please-change' } as Env;
|
|||
|
|
const token = await signToken(env, 42);
|
|||
|
|
ok('token 是两段式', token.split('.').length === 2);
|
|||
|
|
const payload = await verifyToken(env, token);
|
|||
|
|
eq('解出的 uid', payload?.uid, 42);
|
|||
|
|
ok(
|
|||
|
|
'改一位就失效',
|
|||
|
|
!(await verifyToken(env, token.slice(0, -2) + (token.endsWith('AA') ? 'BB' : 'AA'))),
|
|||
|
|
);
|
|||
|
|
ok('换密钥就失效', !(await verifyToken({ TOKEN_SECRET: 'other' } as Env, token)));
|
|||
|
|
ok('乱串被拒', !(await verifyToken(env, 'not-a-token')));
|
|||
|
|
ok('空串被拒', !(await verifyToken(env, '')));
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ YAML
|
|||
|
|
console.log('== 设置模板 YAML 解析 ==');
|
|||
|
|
const tpl = `host: "0.0.0.0"
|
|||
|
|
port: 23366
|
|||
|
|
captcha:
|
|||
|
|
enabled: false
|
|||
|
|
mode: off # 关闭验证码
|
|||
|
|
moderator:
|
|||
|
|
pending_default: true
|
|||
|
|
ip_region:
|
|||
|
|
enabled: false
|
|||
|
|
frontend:
|
|||
|
|
pvAdd: true
|
|||
|
|
locale: zh-CN
|
|||
|
|
pluginURLs: []
|
|||
|
|
site_default: 优世界
|
|||
|
|
`;
|
|||
|
|
const parsed = parseSimpleYaml(tpl) as any;
|
|||
|
|
eq('顶层字符串', parsed.host, '0.0.0.0');
|
|||
|
|
eq('顶层数字', parsed.port, 23366);
|
|||
|
|
eq('嵌套布尔', parsed.captcha.enabled, false);
|
|||
|
|
eq('行内注释被剔除', parsed.captcha.mode, 'off');
|
|||
|
|
eq('嵌套布尔2', parsed.moderator.pending_default, true);
|
|||
|
|
eq('三层取值', parsed.frontend.pvAdd, true);
|
|||
|
|
eq('中文值', parsed.site_default, '优世界');
|
|||
|
|
eq('空数组', parsed.frontend.pluginURLs, []);
|
|||
|
|
|
|||
|
|
console.log(`\n通过 ${pass} / 失败 ${fail}`);
|
|||
|
|
process.exit(fail === 0 ? 0 : 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void main();
|