Files
blog/blog-admin/tools/selftest.ts
T
zqlit a74b3c7127
Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 26s
Deploy to Production / notify-failure (push) Skipped
归档 artalk-cf 评论后端 + rss-robot 到 blog-admin(含技术选型/模块分布 README)
2026-10-04 08:45:40 +08:00

118 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自检:不依赖 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();