128 lines
4.3 KiB
JavaScript
128 lines
4.3 KiB
JavaScript
/**
|
||
* 校验规则的单元自测(不依赖服务器与数据库)。
|
||
*
|
||
* 重点验证容易出错的地方:
|
||
* - 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;
|