From e0143fa5404db15aca73d54e32a23b758c8c5a9d Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 5 May 2026 18:44:43 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=A1=A5=E5=85=85=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/basic/src/utils/util.env.test.ts | 42 +++++++++ .../core/basic/src/utils/util.hash.test.ts | 45 ++++++++++ packages/core/basic/src/utils/util.id.test.ts | 18 ++++ .../core/basic/src/utils/util.merge.test.ts | 78 ++++++++++++++++ .../core/basic/src/utils/util.promise.test.ts | 90 +++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 packages/core/basic/src/utils/util.env.test.ts create mode 100644 packages/core/basic/src/utils/util.hash.test.ts create mode 100644 packages/core/basic/src/utils/util.id.test.ts create mode 100644 packages/core/basic/src/utils/util.merge.test.ts create mode 100644 packages/core/basic/src/utils/util.promise.test.ts diff --git a/packages/core/basic/src/utils/util.env.test.ts b/packages/core/basic/src/utils/util.env.test.ts new file mode 100644 index 000000000..7649f3242 --- /dev/null +++ b/packages/core/basic/src/utils/util.env.test.ts @@ -0,0 +1,42 @@ +/// + +import { expect } from "chai"; + +import { isDev } from "./util.env.js"; + +describe("isDev", () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + if (originalNodeEnv == null) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = originalNodeEnv; + } + }); + + it("treats missing NODE_ENV as development", () => { + delete process.env.NODE_ENV; + + expect(isDev()).to.equal(true); + }); + + it("detects development-like NODE_ENV values", () => { + process.env.NODE_ENV = "development"; + expect(isDev()).to.equal(true); + + process.env.NODE_ENV = "local"; + expect(isDev()).to.equal(true); + + process.env.NODE_ENV = "dev-test"; + expect(isDev()).to.equal(true); + }); + + it("rejects production-like NODE_ENV values", () => { + process.env.NODE_ENV = "production"; + expect(isDev()).to.equal(false); + + process.env.NODE_ENV = "test"; + expect(isDev()).to.equal(false); + }); +}); diff --git a/packages/core/basic/src/utils/util.hash.test.ts b/packages/core/basic/src/utils/util.hash.test.ts new file mode 100644 index 000000000..52119dee0 --- /dev/null +++ b/packages/core/basic/src/utils/util.hash.test.ts @@ -0,0 +1,45 @@ +/// + +import { expect } from "chai"; + +import { hashUtils } from "./util.hash.js"; + +describe("hashUtils", () => { + describe("digest helpers", () => { + it("generates md5 and sha256 hex digests by default", () => { + expect(hashUtils.md5("certd")).to.equal("3f3d9f715fcc63d54a4a224e0939a233"); + expect(hashUtils.sha256("certd")).to.equal("26a6366060d2a6477185c05075155769cb438c6c71f61f509535b8516594ad92"); + }); + + it("supports alternate digest encodings", () => { + expect(hashUtils.md5("certd", "base64")).to.equal("Pz2fcV/MY9VKSiJOCTmiMw=="); + expect(hashUtils.sha256("certd", "base64")).to.equal("JqY2YGDSpkdxhcBQdRVXactDjGxx9h9QlTW4UWWUrZI="); + }); + }); + + describe("hmac helpers", () => { + it("signs data with a provided key", () => { + expect(hashUtils.hmacSha256WithKey("secret", "certd")).to.equal("kh/kUD/Ji8FHfpt4vYUHZx+1BZvKSyyklZIiuS+Rzlg="); + }); + + it("uses an empty payload when only the key is provided", () => { + expect(hashUtils.hmacSha256("secret")).to.equal("+eZuF5tnR65UEI+C+K3os8Jddv0wr95sOVgixTAZYWk="); + }); + }); + + describe("encoding helpers", () => { + it("round trips base64 values", () => { + const encoded = hashUtils.base64("证书-certd"); + + expect(encoded).to.equal("6K+B5LmmLWNlcnRk"); + expect(hashUtils.base64Decode(encoded)).to.equal("证书-certd"); + }); + + it("converts strings and numbers to hex", () => { + expect(hashUtils.toHex("certd")).to.equal("6365727464"); + expect(hashUtils.hexToStr("6365727464")).to.equal("certd"); + expect(hashUtils.toHex(255)).to.equal("ff"); + expect(hashUtils.hexToNumber("ff")).to.equal(255); + }); + }); +}); diff --git a/packages/core/basic/src/utils/util.id.test.ts b/packages/core/basic/src/utils/util.id.test.ts new file mode 100644 index 000000000..249e988d4 --- /dev/null +++ b/packages/core/basic/src/utils/util.id.test.ts @@ -0,0 +1,18 @@ +/// + +import { expect } from "chai"; + +import { randomNumber, simpleNanoId } from "./util.id.js"; + +describe("id utils", () => { + it("generates a four digit random number string", () => { + expect(randomNumber()).to.match(/^\d{4}$/); + }); + + it("generates a twelve character simple nano id", () => { + const id = simpleNanoId(); + + expect(id).to.have.length(12); + expect(id).to.match(/^[0-9a-zA-Z]+$/); + }); +}); diff --git a/packages/core/basic/src/utils/util.merge.test.ts b/packages/core/basic/src/utils/util.merge.test.ts new file mode 100644 index 000000000..c5aadd369 --- /dev/null +++ b/packages/core/basic/src/utils/util.merge.test.ts @@ -0,0 +1,78 @@ +/// + +import { expect } from "chai"; + +import { mergeUtils, UnMergeable } from "./util.merge.js"; + +describe("mergeUtils", () => { + describe("merge", () => { + it("deep merges plain objects", () => { + const target = { acme: { email: "admin@example.com" }, deploy: { retries: 1 } }; + + const result = mergeUtils.merge(target, { acme: { dnsProvider: "cloudflare" } }); + + expect(result).to.equal(target); + expect(result).to.deep.equal({ + acme: { email: "admin@example.com", dnsProvider: "cloudflare" }, + deploy: { retries: 1 }, + }); + }); + + it("replaces arrays instead of merging them by index", () => { + const result = mergeUtils.merge({ domains: ["old.example.com", "legacy.example.com"] }, { domains: ["new.example.com"] }); + + expect(result).to.deep.equal({ domains: ["new.example.com"] }); + }); + + it("allows null to clear nested values", () => { + const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: null }); + + expect(result).to.deep.equal({ cert: null }); + }); + + it("keeps undefined sources from overwriting existing nested values", () => { + const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: undefined }); + + expect(result).to.deep.equal({ cert: { name: "certd" } }); + }); + + it("returns an UnMergeable source directly when it is merged at the top level", () => { + const source = new UnMergeable(); + + const result = mergeUtils.merge({ enabled: true }, source); + + expect(result).to.equal(source); + }); + + it("replaces nested values marked as UnMergeable", () => { + const source = new UnMergeable(); + + const result = mergeUtils.merge({ plugin: { enabled: true } }, { plugin: source }); + + expect(result.plugin).to.equal(source); + }); + }); + + describe("cloneDeep", () => { + it("deep clones plain values", () => { + const source = { acme: { email: "admin@example.com" }, domains: ["example.com"] }; + + const result = mergeUtils.cloneDeep(source); + + expect(result).to.deep.equal(source); + expect(result).not.to.equal(source); + expect(result.acme).not.to.equal(source.acme); + expect(result.domains).not.to.equal(source.domains); + }); + + it("preserves references marked as not cloneable", () => { + const uncloneable = new UnMergeable(); + const source = { plugin: uncloneable }; + + const result = mergeUtils.cloneDeep(source); + + expect(result).not.to.equal(source); + expect(result.plugin).to.equal(uncloneable); + }); + }); +}); diff --git a/packages/core/basic/src/utils/util.promise.test.ts b/packages/core/basic/src/utils/util.promise.test.ts new file mode 100644 index 000000000..3478f3bc2 --- /dev/null +++ b/packages/core/basic/src/utils/util.promise.test.ts @@ -0,0 +1,90 @@ +/// + +import { expect } from "chai"; + +import { logger } from "./util.log.js"; +import { promises } from "./util.promise.js"; + +describe("promises", () => { + describe("TimeoutPromise", () => { + it("resolves when the callback finishes before the timeout", async () => { + let completed = false; + + await promises.TimeoutPromise(async () => { + completed = true; + }, 50); + + expect(completed).to.equal(true); + }); + + it("rejects when the callback exceeds the timeout", async () => { + try { + await promises.TimeoutPromise(() => new Promise(resolve => setTimeout(resolve, 30)), 5); + expect.fail("expected TimeoutPromise to reject"); + } catch (e: any) { + expect(e.message).to.equal("Task timeout in 5 ms"); + } + }); + }); + + describe("safePromise", () => { + it("resolves values provided by the callback", async () => { + const result = await promises.safePromise(resolve => { + resolve("ok"); + }); + + expect(result).to.equal("ok"); + }); + + it("rejects synchronous errors thrown by the callback", async () => { + const oldLevel = logger.level; + logger.level = "fatal"; + try { + await promises.safePromise(() => { + throw new Error("boom"); + }); + expect.fail("expected safePromise to reject"); + } catch (e: any) { + expect(e.message).to.equal("boom"); + } finally { + logger.level = oldLevel; + } + }); + }); + + describe("promisify", () => { + it("resolves callback data", async () => { + const readValue = promises.promisify((prefix: string, callback: (err: Error | null, data?: string) => void) => { + callback(null, `${prefix}-value`); + }); + + expect(await readValue("certd")).to.equal("certd-value"); + }); + + it("rejects callback errors", async () => { + const failing = promises.promisify((callback: (err: Error) => void) => { + callback(new Error("callback failed")); + }); + + try { + await failing(); + expect.fail("expected promisified function to reject"); + } catch (e: any) { + expect(e.message).to.equal("callback failed"); + } + }); + + it("rejects synchronous errors", async () => { + const failing = promises.promisify(() => { + throw new Error("sync failed"); + }); + + try { + await failing(); + expect.fail("expected promisified function to reject"); + } catch (e: any) { + expect(e.message).to.equal("sync failed"); + } + }); + }); +});