Files
blog/blog-admin/tools/selftest.ts
T

117 lines
5.3 KiB
TypeScript
Raw Normal View History

/**
* 自检:不依赖 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 &amp; b'));
ok('含单引号的正文不炸', renderMarkdown("it's fine").includes('it&#39;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();