diff --git a/server/service/config/index.ts b/server/service/config/index.ts index 74a5c65..1673ac0 100644 --- a/server/service/config/index.ts +++ b/server/service/config/index.ts @@ -13,6 +13,7 @@ import { serializeConfigValue, validateConfigValue, } from "./registry"; +import { normalizeGlobalConfigValue } from "./normalization"; export class ConfigKeyNotFoundError extends Error { code = "CONFIG_KEY_NOT_FOUND"; @@ -108,9 +109,7 @@ export async function setGlobalConfigValue(key: K, val if (scope === "user") { throw new ConfigScopeInvalidError(`配置项 ${key} 不支持全局写入`); } - if (key === "siteName" && typeof value === "string") { - value = value.trim(); - } + value = normalizeGlobalConfigValue(key, value); if (!validateConfigValue(key, value)) { throw new ConfigValueInvalidError(`配置项 ${key} 的值不合法`); } diff --git a/server/service/config/normalization.ts b/server/service/config/normalization.ts new file mode 100644 index 0000000..61b2337 --- /dev/null +++ b/server/service/config/normalization.ts @@ -0,0 +1,18 @@ +import { KnownConfigKey } from "./registry"; + +const TRIMMABLE_GLOBAL_CONFIG_KEYS = new Set([ + "commentMailFromEmail", + "commentSmtpHost", + "commentSmtpUser", + "commentSmtpPass", +]); + +export function normalizeGlobalConfigValue(key: K, value: unknown): unknown { + if (typeof value !== "string") { + return value; + } + if (key === "siteName" || TRIMMABLE_GLOBAL_CONFIG_KEYS.has(key)) { + return value.trim(); + } + return value; +} diff --git a/server/service/config/registry.test.ts b/server/service/config/registry.test.ts index 0abe72a..c71c16d 100644 --- a/server/service/config/registry.test.ts +++ b/server/service/config/registry.test.ts @@ -1,84 +1,69 @@ import { describe, expect, test } from "bun:test"; -import { getConfigDefinition, validateConfigValue } from "./registry"; +import { getConfigDefinition, KnownConfigKey, validateConfigValue } from "./registry"; +import { normalizeGlobalConfigValue } from "./normalization"; describe("comment email config validation", () => { test("accepts an empty commentMailFromEmail", () => { - expect(validateConfigValue("commentMailFromEmail" as never, "")).toBe(true); + expect(validateConfigValue("commentMailFromEmail", "")).toBe(true); }); test("accepts a valid commentMailFromEmail", () => { - expect(validateConfigValue("commentMailFromEmail" as never, "noreply@example.com")).toBe(true); + expect(validateConfigValue("commentMailFromEmail", "noreply@example.com")).toBe(true); }); test("rejects an invalid commentMailFromEmail", () => { - expect(validateConfigValue("commentMailFromEmail" as never, "invalid-email")).toBe(false); + expect(validateConfigValue("commentMailFromEmail", "invalid-email")).toBe(false); }); test("validates commentNotifyEnabled as boolean", () => { - expect(validateConfigValue("commentNotifyEnabled" as never, true)).toBe(true); - expect(validateConfigValue("commentNotifyEnabled" as never, false)).toBe(true); - expect(validateConfigValue("commentNotifyEnabled" as never, "true")).toBe(false); + expect(validateConfigValue("commentNotifyEnabled", true)).toBe(true); + expect(validateConfigValue("commentNotifyEnabled", false)).toBe(true); + expect(validateConfigValue("commentNotifyEnabled", "true")).toBe(false); }); test("enforces commentSmtpPort boundaries", () => { - expect(validateConfigValue("commentSmtpPort" as never, 1)).toBe(true); - expect(validateConfigValue("commentSmtpPort" as never, 65535)).toBe(true); - expect(validateConfigValue("commentSmtpPort" as never, 0)).toBe(false); - expect(validateConfigValue("commentSmtpPort" as never, 65536)).toBe(false); - expect(validateConfigValue("commentSmtpPort" as never, 465)).toBe(true); + expect(validateConfigValue("commentSmtpPort", 1)).toBe(true); + expect(validateConfigValue("commentSmtpPort", 65535)).toBe(true); + expect(validateConfigValue("commentSmtpPort", 0)).toBe(false); + expect(validateConfigValue("commentSmtpPort", 65536)).toBe(false); + expect(validateConfigValue("commentSmtpPort", 465)).toBe(true); }); test("keeps metadata consistent for new global comment config keys", () => { - expect(getConfigDefinition("commentEmailNotifyEnabled" as never)).toMatchObject({ - scope: "global", - valueType: "boolean", - defaultValue: false, - userOverridable: false, - }); - expect(getConfigDefinition("commentMailFromEmail" as never)).toMatchObject({ - scope: "global", - valueType: "string", - defaultValue: "", - userOverridable: false, - }); - expect(getConfigDefinition("commentSmtpHost" as never)).toMatchObject({ - scope: "global", - valueType: "string", - defaultValue: "", - userOverridable: false, - }); - expect(getConfigDefinition("commentSmtpPort" as never)).toMatchObject({ - scope: "global", - valueType: "number", - defaultValue: 465, - userOverridable: false, - }); - expect(getConfigDefinition("commentSmtpSecure" as never)).toMatchObject({ - scope: "global", - valueType: "boolean", - defaultValue: true, - userOverridable: false, - }); - expect(getConfigDefinition("commentSmtpUser" as never)).toMatchObject({ - scope: "global", - valueType: "string", - defaultValue: "", - userOverridable: false, - }); - expect(getConfigDefinition("commentSmtpPass" as never)).toMatchObject({ - scope: "global", - valueType: "string", - defaultValue: "", - userOverridable: false, - }); + const expected = [ + ["commentEmailNotifyEnabled", "global", "boolean", false, false], + ["commentMailFromEmail", "global", "string", "", false], + ["commentSmtpHost", "global", "string", "", false], + ["commentSmtpPort", "global", "number", 465, false], + ["commentSmtpSecure", "global", "boolean", true, false], + ["commentSmtpUser", "global", "string", "", false], + ["commentSmtpPass", "global", "string", "", false], + ] as const satisfies readonly [KnownConfigKey, "global", "string" | "number" | "boolean", string | number | boolean, boolean][]; + + for (const [key, scope, valueType, defaultValue, userOverridable] of expected) { + expect(getConfigDefinition(key)).toMatchObject({ + scope, + valueType, + defaultValue, + userOverridable, + }); + } }); test("keeps metadata consistent for commentNotifyEnabled", () => { - expect(getConfigDefinition("commentNotifyEnabled" as never)).toMatchObject({ + expect(getConfigDefinition("commentNotifyEnabled")).toMatchObject({ scope: "both", valueType: "boolean", defaultValue: true, userOverridable: true, }); }); + + test("normalizes comment email and smtp strings by trimming", () => { + expect(normalizeGlobalConfigValue("commentMailFromEmail", " noreply@example.com ")).toBe("noreply@example.com"); + expect(normalizeGlobalConfigValue("commentMailFromEmail", " ")).toBe(""); + expect(normalizeGlobalConfigValue("commentSmtpHost", " smtp.example.com ")).toBe("smtp.example.com"); + expect(normalizeGlobalConfigValue("commentSmtpUser", " username ")).toBe("username"); + expect(normalizeGlobalConfigValue("commentSmtpPass", " password ")).toBe("password"); + }); });