Browse Source

fix(config): normalize comment mail smtp global values before validation

Apply trim/blank-to-empty normalization for comment email/smtp global keys in setGlobalConfigValue, and replace registry test casts with KnownConfigKey-safe assertions plus normalization coverage.

Made-with: Cursor
main
npmrun 3 weeks ago
parent
commit
c6349c65f5
  1. 5
      server/service/config/index.ts
  2. 18
      server/service/config/normalization.ts
  3. 95
      server/service/config/registry.test.ts

5
server/service/config/index.ts

@ -13,6 +13,7 @@ import {
serializeConfigValue, serializeConfigValue,
validateConfigValue, validateConfigValue,
} from "./registry"; } from "./registry";
import { normalizeGlobalConfigValue } from "./normalization";
export class ConfigKeyNotFoundError extends Error { export class ConfigKeyNotFoundError extends Error {
code = "CONFIG_KEY_NOT_FOUND"; code = "CONFIG_KEY_NOT_FOUND";
@ -108,9 +109,7 @@ export async function setGlobalConfigValue<K extends KnownConfigKey>(key: K, val
if (scope === "user") { if (scope === "user") {
throw new ConfigScopeInvalidError(`配置项 ${key} 不支持全局写入`); throw new ConfigScopeInvalidError(`配置项 ${key} 不支持全局写入`);
} }
if (key === "siteName" && typeof value === "string") { value = normalizeGlobalConfigValue(key, value);
value = value.trim();
}
if (!validateConfigValue(key, value)) { if (!validateConfigValue(key, value)) {
throw new ConfigValueInvalidError(`配置项 ${key} 的值不合法`); throw new ConfigValueInvalidError(`配置项 ${key} 的值不合法`);
} }

18
server/service/config/normalization.ts

@ -0,0 +1,18 @@
import { KnownConfigKey } from "./registry";
const TRIMMABLE_GLOBAL_CONFIG_KEYS = new Set<KnownConfigKey>([
"commentMailFromEmail",
"commentSmtpHost",
"commentSmtpUser",
"commentSmtpPass",
]);
export function normalizeGlobalConfigValue<K extends KnownConfigKey>(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;
}

95
server/service/config/registry.test.ts

@ -1,84 +1,69 @@
import { describe, expect, test } from "bun:test"; 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", () => { describe("comment email config validation", () => {
test("accepts an empty commentMailFromEmail", () => { test("accepts an empty commentMailFromEmail", () => {
expect(validateConfigValue("commentMailFromEmail" as never, "")).toBe(true); expect(validateConfigValue("commentMailFromEmail", "")).toBe(true);
}); });
test("accepts a valid commentMailFromEmail", () => { 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", () => { 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", () => { test("validates commentNotifyEnabled as boolean", () => {
expect(validateConfigValue("commentNotifyEnabled" as never, true)).toBe(true); expect(validateConfigValue("commentNotifyEnabled", true)).toBe(true);
expect(validateConfigValue("commentNotifyEnabled" as never, false)).toBe(true); expect(validateConfigValue("commentNotifyEnabled", false)).toBe(true);
expect(validateConfigValue("commentNotifyEnabled" as never, "true")).toBe(false); expect(validateConfigValue("commentNotifyEnabled", "true")).toBe(false);
}); });
test("enforces commentSmtpPort boundaries", () => { test("enforces commentSmtpPort boundaries", () => {
expect(validateConfigValue("commentSmtpPort" as never, 1)).toBe(true); expect(validateConfigValue("commentSmtpPort", 1)).toBe(true);
expect(validateConfigValue("commentSmtpPort" as never, 65535)).toBe(true); expect(validateConfigValue("commentSmtpPort", 65535)).toBe(true);
expect(validateConfigValue("commentSmtpPort" as never, 0)).toBe(false); expect(validateConfigValue("commentSmtpPort", 0)).toBe(false);
expect(validateConfigValue("commentSmtpPort" as never, 65536)).toBe(false); expect(validateConfigValue("commentSmtpPort", 65536)).toBe(false);
expect(validateConfigValue("commentSmtpPort" as never, 465)).toBe(true); expect(validateConfigValue("commentSmtpPort", 465)).toBe(true);
}); });
test("keeps metadata consistent for new global comment config keys", () => { test("keeps metadata consistent for new global comment config keys", () => {
expect(getConfigDefinition("commentEmailNotifyEnabled" as never)).toMatchObject({ const expected = [
scope: "global", ["commentEmailNotifyEnabled", "global", "boolean", false, false],
valueType: "boolean", ["commentMailFromEmail", "global", "string", "", false],
defaultValue: false, ["commentSmtpHost", "global", "string", "", false],
userOverridable: false, ["commentSmtpPort", "global", "number", 465, false],
}); ["commentSmtpSecure", "global", "boolean", true, false],
expect(getConfigDefinition("commentMailFromEmail" as never)).toMatchObject({ ["commentSmtpUser", "global", "string", "", false],
scope: "global", ["commentSmtpPass", "global", "string", "", false],
valueType: "string", ] as const satisfies readonly [KnownConfigKey, "global", "string" | "number" | "boolean", string | number | boolean, boolean][];
defaultValue: "",
userOverridable: false, for (const [key, scope, valueType, defaultValue, userOverridable] of expected) {
}); expect(getConfigDefinition(key)).toMatchObject({
expect(getConfigDefinition("commentSmtpHost" as never)).toMatchObject({ scope,
scope: "global", valueType,
valueType: "string", defaultValue,
defaultValue: "", userOverridable,
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,
});
}); });
test("keeps metadata consistent for commentNotifyEnabled", () => { test("keeps metadata consistent for commentNotifyEnabled", () => {
expect(getConfigDefinition("commentNotifyEnabled" as never)).toMatchObject({ expect(getConfigDefinition("commentNotifyEnabled")).toMatchObject({
scope: "both", scope: "both",
valueType: "boolean", valueType: "boolean",
defaultValue: true, defaultValue: true,
userOverridable: 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");
});
}); });

Loading…
Cancel
Save