This commit is contained in:
z.ai committed 2026-10-08 11:09:37 +08:00
1 parent c1541b80df
commit b4c7908029
97 files changed
+12403 -154

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
/**
* 校验规则的单元自测(不依赖服务器与数据库)。
*
* 重点验证容易出错的地方:
* - PATCH 空对象必须被拒绝(`.partial()` 不会移除 `.default()`)
* - slug 格式、标签去重、日期转换
* - 列表查询参数的类型强制与边界
*
* 用法:node scripts/test-validation.mjs
*/
import {
createPostSchema,
updatePostSchema,
listPostsQuerySchema,
estimateReadingMinutes,
} from "../lib/validation/post.ts";
let pass = 0;
let fail = 0;
function check(name, condition, detail = "") {
if (condition) {
pass++;
console.log(` [PASS] ${name}`);
} else {
fail++;
console.log(` [FAIL] ${name} ${detail}`);
}
}
console.log("── createPostSchema ─────────────────");
const validCreate = createPostSchema.safeParse({
slug: "hello-world",
title: "标题",
summary: "摘要",
content: "正文",
});
check("合法输入通过", validCreate.success);
check("status 默认为 draft", validCreate.data?.status === "draft");
check("tags 默认为空数组", Array.isArray(validCreate.data?.tags) && validCreate.data.tags.length === 0);
const badSlug = createPostSchema.safeParse({
slug: "Hello_World",
title: "t",
summary: "s",
content: "c",
});
check("非法 slug 被拒绝", !badSlug.success);
const emptyTitle = createPostSchema.safeParse({
slug: "ok",
title: " ",
summary: "s",
content: "c",
});
check("纯空白标题被拒绝", !emptyTitle.success);
const dupTags = createPostSchema.safeParse({
slug: "ok",
title: "t",
summary: "s",
content: "c",
tags: ["a", "a", "b"],
});
check("标签自动去重", dupTags.success && dupTags.data.tags.length === 2, JSON.stringify(dupTags.data?.tags));
console.log("\n── updatePostSchema(关键回归点)──");
const emptyUpdate = updatePostSchema.safeParse({});
check("空对象被拒绝(回归:不得因 default 而通过)", !emptyUpdate.success, JSON.stringify(emptyUpdate.data));
const titleOnly = updatePostSchema.safeParse({ title: "新标题" });
check("只传 title 通过", titleOnly.success);
check("未传的字段不出现在结果中", !("summary" in (titleOnly.data ?? {})), JSON.stringify(titleOnly.data));
const tagsOnly = updatePostSchema.safeParse({ tags: ["x"] });
check("只传 tags 通过", tagsOnly.success);
const invalidStatus = updatePostSchema.safeParse({ status: "archived" });
check("非法 status 被拒绝", !invalidStatus.success);
const slugUpdate = updatePostSchema.safeParse({ slug: "new-slug" });
check("可单独更新 slug", slugUpdate.success);
console.log("\n── listPostsQuerySchema ─────────────");
const defaults = listPostsQuerySchema.safeParse({});
check("默认 limit=20", defaults.data?.limit === 20);
check("默认 offset=0", defaults.data?.offset === 0);
check("默认 includeDrafts=false", defaults.data?.includeDrafts === false);
const coerced = listPostsQuerySchema.safeParse({ limit: "5", offset: "10" });
check("字符串数字被强制转换", coerced.data?.limit === 5 && coerced.data?.offset === 10);
const tooBig = listPostsQuerySchema.safeParse({ limit: "101" });
check("limit 超上限被拒绝", !tooBig.success);
const zero = listPostsQuerySchema.safeParse({ limit: "0" });
check("limit=0 被拒绝", !zero.success);
const negative = listPostsQuerySchema.safeParse({ offset: "-5" });
check("负 offset 被拒绝", !negative.success);
const drafts = listPostsQuerySchema.safeParse({ includeDrafts: "true" });
check("includeDrafts 字符串转布尔", drafts.data?.includeDrafts === true);
const badStatus = listPostsQuerySchema.safeParse({ status: "nope" });
check("非法 status 被拒绝", !badStatus.success);
console.log("\n── estimateReadingMinutes ───────────");
check("空内容至少 1 分钟", estimateReadingMinutes("") === 1);
check("短内容为 1 分钟", estimateReadingMinutes("hello world") === 1);
check(
"长中文内容时长增加",
estimateReadingMinutes("中".repeat(1400)) >= 3,
`got ${estimateReadingMinutes("中".repeat(1400))}`,
);
check(
"长英文内容时长增加",
estimateReadingMinutes("word ".repeat(1000)) >= 4,
`got ${estimateReadingMinutes("word ".repeat(1000))}`,
);
console.log("\n================================");
console.log(` 通过 ${pass} / ${pass + fail}`);
console.log("================================");
process.exitCode = fail > 0 ? 1 : 0;