v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/** # 测试 i18n 词典完整性:全部受支持语言必须覆盖全部英文键(不允许缺词回退上线) */
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { en } from "../src/i18n/locales/en";
|
||||
import { LOCALES } from "../src/i18n/shared";
|
||||
import "../src/i18n/locales";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import { getDict } from "../src/i18n/index";
|
||||
import { createT } from "../src/i18n/index";
|
||||
|
||||
const enKeys = Object.keys(en) as (keyof typeof en)[];
|
||||
|
||||
describe("i18n 词典", () => {
|
||||
test("英文词典键数合理(防止误清空)", () => {
|
||||
expect(enKeys.length).toBeGreaterThan(60);
|
||||
});
|
||||
|
||||
test("每个受支持语言都有词典注册且覆盖全部键", () => {
|
||||
for (const l of LOCALES) {
|
||||
if (l.code === "en") continue;
|
||||
const d = getDict(l.code);
|
||||
expect(Object.keys(d).length).toBeGreaterThan(0);
|
||||
const missing = enKeys.filter((k) => !(k in d));
|
||||
expect(missing).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test("插值占位符在各语言中保持一致", () => {
|
||||
for (const l of LOCALES) {
|
||||
if (l.code === "en") continue;
|
||||
const d = getDict(l.code) as Record<string, string>;
|
||||
for (const [k, v] of Object.entries(d)) {
|
||||
const vars = (en[k as keyof typeof en] as string).match(/\{(\w+)\}/g) ?? [];
|
||||
for (const vName of vars) {
|
||||
expect(v.includes(vName)).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("createT 回退与插值", () => {
|
||||
const t = createT("zh-CN");
|
||||
expect(t("nav_home")).toBe("首页");
|
||||
expect(t("shop_showing", { from: 1, to: 24, total: 100 })).toContain("100");
|
||||
// 未注册语言回退英文
|
||||
const t2 = createT("xx-unknown");
|
||||
expect(t2("nav_home")).toBe("Home");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* # 测试 商城数据层集成测试(真实 PostgreSQL)
|
||||
* 覆盖:加购物车 → 建订单 → 库存扣减 → 订单号唯一 + 购物车清理。
|
||||
* 直接调用 src/server/commerce.ts 的核心函数(ensureCart / addProductToCart / placeOrderCore),
|
||||
* 不经过 Cookie/会话层。测试数据用唯一标记,结束时全部清理。
|
||||
*/
|
||||
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
|
||||
import { SQL } from "bun";
|
||||
import { drizzle } from "drizzle-orm/bun-sql";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import * as schema from "~/db/schema";
|
||||
import { ensureCart, addProductToCart, placeOrderCore } from "~/server/commerce";
|
||||
|
||||
/** # 成员变量 测试专用数据库连接(结束后 end() 释放连接池) **/
|
||||
import { initDb } from "../../src/db";
|
||||
|
||||
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
||||
const db = drizzle(sql, { schema });
|
||||
|
||||
/** # 成员变量 测试数据标记(避免与真实数据冲突) **/
|
||||
const TAG = `itest_${Date.now().toString(36)}`;
|
||||
|
||||
beforeAll(async () => { await initDb(); });
|
||||
/** # 成员变量 夹具序号(同一运行内多次建夹具时保证邮箱/slug 唯一) **/
|
||||
let fixtureSeq = 0;
|
||||
/** # 成员变量 清理目标 ID 集合 **/
|
||||
const cleanupIds = { userIds: [] as number[], productIds: [] as number[], cartIds: [] as number[], orderIds: [] as number[] };
|
||||
|
||||
afterAll(async () => {
|
||||
// 清理测试数据:订单明细 → 订单 → 购物车明细 → 购物车 → 产品 → 用户
|
||||
if (cleanupIds.orderIds.length) {
|
||||
await db.delete(schema.orderItems).where(inArray(schema.orderItems.orderId, cleanupIds.orderIds));
|
||||
await db.delete(schema.orders).where(inArray(schema.orders.id, cleanupIds.orderIds));
|
||||
}
|
||||
if (cleanupIds.cartIds.length) {
|
||||
await db.delete(schema.cartItems).where(inArray(schema.cartItems.cartId, cleanupIds.cartIds));
|
||||
await db.delete(schema.carts).where(inArray(schema.carts.id, cleanupIds.cartIds));
|
||||
}
|
||||
if (cleanupIds.productIds.length) {
|
||||
await db.delete(schema.products).where(inArray(schema.products.id, cleanupIds.productIds));
|
||||
}
|
||||
if (cleanupIds.userIds.length) {
|
||||
await db.delete(schema.users).where(inArray(schema.users.id, cleanupIds.userIds));
|
||||
}
|
||||
await sql.end();
|
||||
});
|
||||
|
||||
describe("commerce 数据层", () => {
|
||||
/** # 函数 测试夹具:建测试用户 + 两个测试产品(一个带库存一个无 stock 字段) */
|
||||
async function setupFixture() {
|
||||
const tag = `${TAG}_${++fixtureSeq}`;
|
||||
const [user] = await db
|
||||
.insert(schema.users)
|
||||
.values({ email: `${tag}@example.com`, displayName: tag, passwordHash: "x", role: "customer" })
|
||||
.returning();
|
||||
cleanupIds.userIds.push(user!.id);
|
||||
const [p1] = await db
|
||||
.insert(schema.products)
|
||||
.values({ slug: `${tag}-p1`, title: `Test P1 ${tag}`, price: "19.90", stock: 10, stockStatus: "instock", status: "published" })
|
||||
.returning();
|
||||
const [p2] = await db
|
||||
.insert(schema.products)
|
||||
.values({ slug: `${tag}-p2`, title: `Test P2 ${tag}`, price: "5.00", stock: null, stockStatus: "onbackorder", status: "published" })
|
||||
.returning();
|
||||
const [p3] = await db
|
||||
.insert(schema.products)
|
||||
.values({ slug: `${tag}-p3`, title: `Test P3 ${tag}`, price: "1.00", stock: 3, stockStatus: "outofstock", status: "published" })
|
||||
.returning();
|
||||
cleanupIds.productIds.push(p1!.id, p2!.id, p3!.id);
|
||||
return { userId: user!.id, p1: p1!, p2: p2!, p3: p3! };
|
||||
}
|
||||
|
||||
test("加购物车 → 建订单 → 库存扣减 → 订单号唯一", async () => {
|
||||
const fx = await setupFixture();
|
||||
|
||||
// 1. 建购物车并加商品
|
||||
const cart1 = await ensureCart({ userId: fx.userId });
|
||||
cleanupIds.cartIds.push(cart1.cartId);
|
||||
const add1 = await addProductToCart(cart1.cartId, fx.p1.id, 2);
|
||||
expect(add1.ok).toBe(true);
|
||||
// 同产品重复加 → 数量累加
|
||||
const add2 = await addProductToCart(cart1.cartId, fx.p1.id, 1);
|
||||
expect(add2.ok).toBe(true);
|
||||
expect((add2 as { count: number }).count).toBe(3);
|
||||
// 售罄产品不可加
|
||||
const add3 = await addProductToCart(cart1.cartId, fx.p3.id, 1);
|
||||
expect(add3.ok).toBe(false);
|
||||
|
||||
// 2. 下单(带账单信息)
|
||||
const orderNumber = await placeOrderCore(fx.userId, cart1.cartId, {
|
||||
billing: { firstName: "Test", lastName: "User", email: `${TAG}@example.com`, address: "1 Test St", city: "Shenzhen", country: "CN", postcode: "518000" },
|
||||
shippingSame: true,
|
||||
note: "integration test",
|
||||
});
|
||||
expect(orderNumber).toMatch(/^DS/);
|
||||
const [order] = await db.select().from(schema.orders).where(eq(schema.orders.orderNumber, orderNumber));
|
||||
cleanupIds.orderIds.push(order!.id);
|
||||
expect(order!.userId).toBe(fx.userId);
|
||||
expect(order!.paymentMethod).toBe("bank_transfer");
|
||||
expect(order!.paymentStatus).toBe("unpaid");
|
||||
// 小计 = 19.90 * 3 = 59.70
|
||||
expect(Number(order!.subtotal)).toBeCloseTo(59.7, 2);
|
||||
|
||||
// 3. 明细快照
|
||||
const items = await db.select().from(schema.orderItems).where(eq(schema.orderItems.orderId, order!.id));
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0]!.qty).toBe(3);
|
||||
expect(Number(items[0]!.price)).toBeCloseTo(19.9, 2);
|
||||
|
||||
// 4. 库存扣减(10 - 3 = 7)
|
||||
const [p1After] = await db.select().from(schema.products).where(eq(schema.products.id, fx.p1.id));
|
||||
expect(p1After!.stock).toBe(7);
|
||||
|
||||
// 5. 购物车已清空
|
||||
const lines = await db.select().from(schema.cartItems).where(eq(schema.cartItems.cartId, cart1.cartId));
|
||||
expect(lines.length).toBe(0);
|
||||
|
||||
// 6. 订单号唯一:连续下两单(新购物车加 p2),订单号必须不同
|
||||
const cart2 = await ensureCart({ userId: fx.userId });
|
||||
cleanupIds.cartIds.push(cart2.cartId);
|
||||
await addProductToCart(cart2.cartId, fx.p2.id, 1);
|
||||
const orderNumber2 = await placeOrderCore(fx.userId, cart2.cartId, {
|
||||
billing: { firstName: "Test", lastName: "User", email: `${TAG}@example.com`, address: "1 Test St", city: "Shenzhen", country: "CN" },
|
||||
shippingSame: true,
|
||||
});
|
||||
expect(orderNumber2).not.toBe(orderNumber);
|
||||
const [order2] = await db.select().from(schema.orders).where(eq(schema.orders.orderNumber, orderNumber2));
|
||||
cleanupIds.orderIds.push(order2!.id);
|
||||
// p2 无 stock 字段 → 扣库存逻辑跳过,stock 仍为 NULL
|
||||
const [p2After] = await db.select().from(schema.products).where(eq(schema.products.id, fx.p2.id));
|
||||
expect(p2After!.stock).toBeNull();
|
||||
});
|
||||
|
||||
test("ensureCart:匿名车过户 + 合并", async () => {
|
||||
const fx = await setupFixture();
|
||||
// 匿名车加商品
|
||||
const anonCart = await ensureCart({ sessionKey: `${TAG}-anon` });
|
||||
cleanupIds.cartIds.push(anonCart.cartId);
|
||||
await addProductToCart(anonCart.cartId, fx.p1.id, 1);
|
||||
// 用户无购物车 → 匿名车过户给用户(同一条记录)
|
||||
const merged = await ensureCart({ userId: fx.userId, sessionKey: `${TAG}-anon` });
|
||||
const [transferred] = await db.select().from(schema.carts).where(eq(schema.carts.id, anonCart.cartId));
|
||||
expect(merged.cartId).toBe(anonCart.cartId);
|
||||
expect(transferred!.userId).toBe(fx.userId);
|
||||
expect(transferred!.sessionKey).toBeNull();
|
||||
// 用户已有购物车 + 第二个匿名车 → 合并后匿名车删除
|
||||
const anon2 = await ensureCart({ sessionKey: `${TAG}-anon2` });
|
||||
cleanupIds.cartIds.push(anon2.cartId);
|
||||
await addProductToCart(anon2.cartId, fx.p1.id, 2);
|
||||
const merged2 = await ensureCart({ userId: fx.userId, sessionKey: `${TAG}-anon2` });
|
||||
expect(merged2.cartId).toBe(anonCart.cartId);
|
||||
const [gone] = await db.select().from(schema.carts).where(eq(schema.carts.id, anon2.cartId));
|
||||
expect(gone).toBeUndefined();
|
||||
const lines = await db.select().from(schema.cartItems).where(eq(schema.cartItems.cartId, anonCart.cartId));
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]!.qty).toBe(3); // 1 + 2 累加
|
||||
});
|
||||
|
||||
test("售罄商品下单被事务拒绝", async () => {
|
||||
const fx = await setupFixture();
|
||||
const cart = await ensureCart({ userId: fx.userId });
|
||||
cleanupIds.cartIds.push(cart.cartId);
|
||||
// 直接插入售罄商品行绕过加车校验,验证下单事务兜底
|
||||
await db.insert(schema.cartItems).values({ cartId: cart.cartId, productId: fx.p3.id, qty: 1 });
|
||||
await expect(
|
||||
placeOrderCore(fx.userId, cart.cartId, {
|
||||
billing: { firstName: "T", lastName: "U", email: `${TAG}@example.com`, address: "a", city: "b", country: "c" },
|
||||
shippingSame: true,
|
||||
}),
|
||||
).rejects.toThrow("已售罄");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { SQL } from "bun";
|
||||
|
||||
/** 后台任务队列:入队 → worker 处理 → done 状态 */
|
||||
describe("jobs 队列", () => {
|
||||
test("email.send 任务被 worker 消费并标记 done", async () => {
|
||||
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
|
||||
const [row]: any = await sql`INSERT INTO jobs (type, payload) VALUES ('email.send', ${JSON.stringify({ to: "t@t.io", subject: "s", body: "b" })}::jsonb) RETURNING id`.values();
|
||||
const id = row[0];
|
||||
// 跑一次 worker 主循环(10 秒后自动退出)
|
||||
const { runWorkerLoop } = await import("../src/server/jobs");
|
||||
const controller = new AbortController();
|
||||
// 用短超时跑一轮:直接调用 claimJob 的逻辑由 loop 内部处理,这里以 1 个任务处理完为标准
|
||||
await Promise.race([runWorkerLoop(1500), Bun.sleep(4000)]);
|
||||
const [status]: any = await sql`SELECT status FROM jobs WHERE id=${id}`.values();
|
||||
expect(status[0]).toBe("done");
|
||||
await sql`DELETE FROM jobs WHERE id=${id}`;
|
||||
sql.end();
|
||||
}, 10000);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { phpassVerify } from "../src/server/phpass";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
/** WordPress phpass 兼容:算法自洽 + 格式防御 */
|
||||
describe("phpass 密码校验", () => {
|
||||
test("非 $P$ 前缀直接拒绝", async () => {
|
||||
expect(await phpassVerify("x", "$2y$10$abcdefghijklmnopqrstuv")).toBe(false);
|
||||
expect(await phpassVerify("x", "")).toBe(false);
|
||||
});
|
||||
test("非法迭代字符拒绝", async () => {
|
||||
expect(await phpassVerify("x", "$P$!aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")).toBe(false);
|
||||
});
|
||||
test("标准向量:phpass 官方测试用例(password / P$ hash)", async () => {
|
||||
// phpBB phpass 单元测试中的公开向量:明文 'password',salt 'k1HAndRk',countLog2=7
|
||||
// 由 node:crypto 按算法重建期望值,验证迭代与编码逻辑一致
|
||||
const ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const salt = "k1HAndRk";
|
||||
let h = createHash("md5").update(salt + "test12345").digest();
|
||||
const count = 1 << 7;
|
||||
for (let i = 0; i < count; i++) h = createHash("md5").update(h).update("test12345").digest();
|
||||
let out = "";
|
||||
let p = 0;
|
||||
do {
|
||||
const value = h[p]!;
|
||||
out += ITOA64[value & 0x3f];
|
||||
p++;
|
||||
if (p < h.length) out += ITOA64[((h[p - 1]! << 8) | h[p]!) & 0x3f];
|
||||
p++;
|
||||
if (p < h.length) out += ITOA64[(h[p - 1]! >> 4) & 0x3f];
|
||||
p++;
|
||||
if (p < h.length) out += ITOA64[((h[p - 1]! << 4) | h[p]!) & 0x3f];
|
||||
p++;
|
||||
} while (p < h.length);
|
||||
const hash = "$P$" + ITOA64[7] + salt + out;
|
||||
expect(await phpassVerify("test12345", hash)).toBe(true);
|
||||
expect(await phpassVerify("wrong", hash)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user