workers/participants.test.ts
Rama main · réplica de lectura del repositorio original.
// Copyright (c) 2026 Doforu
// Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only).
// See the LICENSE file or https://www.gnu.org/licenses/agpl-3.0.txt
/**
* Tests for the email-list sender / participant label helpers.
*
* To run: npm test (npx tsx --test 'workers/**\/*.test.ts')
*
* Regression guard for the "everything looks like noreply" bug: the list must
* prefer the From display name and only fall back to the address local-part.
*/
import assert from "node:assert";
import { describe, it } from "node:test";
import {
PARTICIPANT_ENTRY_SEP,
PARTICIPANT_NAME_SEP,
decodeMimeWords,
formatParticipantLabel,
formatSenderFull,
formatSenderLabel,
formatSenderWithAddress,
parseParticipantLabels,
sanitizeSenderName,
stripHeaderChars,
} from "../shared/participants";
/** Build a participants_meta string the same way the SQL aggregate does. */
function meta(...pairs: Array<[string | null, string]>): string {
return pairs
.map(([name, address]) => `${name ?? ""}${PARTICIPANT_NAME_SEP}${address}`)
.join(PARTICIPANT_ENTRY_SEP);
}
/** Base64-encode UTF-8 text without relying on Node's Buffer. */
function toBase64(text: string): string {
let binary = "";
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
return btoa(binary);
}
/** Build an RFC 2047 encoded-word the way a mail client would. */
function encodedWord(text: string, charset = "utf-8"): string {
return `=?${charset}?B?${toBase64(text)}?=`;
}
describe("formatSenderLabel", () => {
it("prefers the display name over the address", () => {
assert.strictEqual(
formatSenderLabel("GitHub", "noreply@github.com"),
"GitHub",
);
});
it("falls back to the local-part when there is no display name", () => {
assert.strictEqual(formatSenderLabel(null, "noreply@github.com"), "noreply");
assert.strictEqual(formatSenderLabel("", "jane@example.com"), "jane");
assert.strictEqual(formatSenderLabel(" ", "jane@example.com"), "jane");
});
it("returns an empty string when neither is present", () => {
assert.strictEqual(formatSenderLabel(null, null), "");
assert.strictEqual(formatSenderLabel(undefined, undefined), "");
});
it("treats a name that is itself an address as absent", () => {
// Shape produced by `"noreply@github.com" <noreply@github.com>`.
assert.strictEqual(formatSenderLabel("noreply@github.com", "noreply@github.com"), "noreply");
assert.strictEqual(formatSenderLabel("jane@example.com", "jane@example.com"), "jane");
// A bare "@" that is not an address is NOT enough to discard the name.
assert.strictEqual(formatSenderLabel("Support via @help", "a@b.com"), "Support via @help");
assert.strictEqual(formatSenderLabel("Acme@Home", "a@b.com"), "Acme@Home");
});
it("keeps a legitimate display name that contains an '@'", () => {
// Regression: the backfill used to drop any name containing "@", so these
// rendered as the local-part on old rows but as the name on new ones.
assert.strictEqual(formatSenderLabel("Acme @ Home", "noreply@acme.com"), "Acme @ Home");
assert.strictEqual(formatSenderLabel("Alice <@alice>", "alice@x.com"), "Alice <@alice>");
assert.strictEqual(formatSenderLabel("Ann Smith, @ACME", "ann@acme.com"), "Ann Smith, @ACME");
});
});
describe("formatSenderWithAddress", () => {
it("keeps the name and the address visible to machine consumers", () => {
assert.strictEqual(
formatSenderWithAddress("GitHub", "noreply@github.com"),
"GitHub <noreply@github.com>",
);
});
it("falls back to the bare address when there is no name", () => {
assert.strictEqual(formatSenderWithAddress(null, "noreply@github.com"), "noreply@github.com");
assert.strictEqual(formatSenderWithAddress("noreply@github.com", "noreply@github.com"), "noreply@github.com");
});
it("returns an empty string when neither is present", () => {
assert.strictEqual(formatSenderWithAddress(null, undefined), "");
});
});
describe("parseParticipantLabels", () => {
it("keeps display names paired with their address", () => {
assert.deepStrictEqual(
parseParticipantLabels(meta(["GitHub", "noreply@github.com"], ["Jane Doe", "jane@example.com"])),
["GitHub", "Jane Doe"],
);
});
it("preserves a comma inside a display name", () => {
// A "," separator would have split this into two participants.
assert.deepStrictEqual(
parseParticipantLabels(meta(["Smith, John", "comma@example.com"])),
["Smith, John"],
);
});
it("merges duplicate addresses, preferring the named entry", () => {
// Same address appears once with a name and once without (older message).
assert.deepStrictEqual(
parseParticipantLabels(meta([null, "noreply@github.com"], ["GitHub", "noreply@github.com"])),
["GitHub"],
);
// ...and the other way round.
assert.deepStrictEqual(
parseParticipantLabels(meta(["GitHub", "noreply@github.com"], [null, "noreply@github.com"])),
["GitHub"],
);
});
it("falls back to the local-part for name-less entries", () => {
assert.deepStrictEqual(
parseParticipantLabels(meta([null, "plain@example.com"])),
["plain"],
);
});
it("returns an empty array for missing or empty input", () => {
assert.deepStrictEqual(parseParticipantLabels(null), []);
assert.deepStrictEqual(parseParticipantLabels(undefined), []);
assert.deepStrictEqual(parseParticipantLabels(""), []);
});
});
describe("formatParticipantLabel", () => {
it("shows the display name for a single-sender conversation", () => {
assert.strictEqual(
formatParticipantLabel(meta(["GitHub", "noreply@github.com"])),
"GitHub",
);
});
it("joins up to three participants", () => {
assert.strictEqual(
formatParticipantLabel(
meta(
["GitHub", "noreply@github.com"],
["Jane Doe", "jane@example.com"],
["Bob", "bob@example.com"],
),
),
"GitHub, Jane Doe, Bob",
);
});
it("collapses busy threads to 'first two +N'", () => {
assert.strictEqual(
formatParticipantLabel(
meta(
["GitHub", "noreply@github.com"],
["Jane Doe", "jane@example.com"],
["Bob", "bob@example.com"],
["Amy", "amy@example.com"],
),
),
"GitHub, Jane Doe +2",
);
});
it("falls back to the single sender when there is no aggregate", () => {
// Non-threaded requests (no folder) return no participants_meta.
assert.strictEqual(
formatParticipantLabel(null, {
name: "GitHub",
address: "noreply@github.com",
}),
"GitHub",
);
assert.strictEqual(
formatParticipantLabel(undefined, {
name: null,
address: "noreply@github.com",
}),
"noreply",
);
});
it("regression: never renders a bare 'noreply' when a name exists", () => {
const label = formatParticipantLabel(meta(["GitHub", "noreply@github.com"]), {
name: null,
address: "noreply@github.com",
});
assert.notStrictEqual(label, "noreply");
assert.strictEqual(label, "GitHub");
});
});
describe("decodeMimeWords (RFC 2047)", () => {
it("decodes a base64 encoded-word", () => {
assert.strictEqual(decodeMimeWords(encodedWord("Announcements")), "Announcements");
assert.strictEqual(decodeMimeWords(encodedWord("会议通知")), "会议通知");
});
it("decodes a quoted-printable encoded-word", () => {
assert.strictEqual(decodeMimeWords("=?utf-8?Q?M=C3=BCller?="), "Müller");
// "_" stands for a space inside an encoded-word.
assert.strictEqual(decodeMimeWords("=?utf-8?Q?Hello_World?="), "Hello World");
});
it("decodes encoded-words mixed with plain text", () => {
assert.strictEqual(
decodeMimeWords(`${encodedWord("Announcements")} Team`),
"Announcements Team",
);
});
it("passes plain values through untouched", () => {
assert.strictEqual(decodeMimeWords("GitHub"), "GitHub");
assert.strictEqual(decodeMimeWords("Smith, John"), "Smith, John");
assert.strictEqual(decodeMimeWords(null), "");
assert.strictEqual(decodeMimeWords(undefined), "");
});
it("leaves a malformed encoded-word intact instead of throwing", () => {
assert.strictEqual(
decodeMimeWords("=?utf-8?B?!!!not-base64!!!?="),
"=?utf-8?B?!!!not-base64!!!?=",
);
assert.strictEqual(decodeMimeWords("=?broken"), "=?broken");
});
it("falls back to utf-8 for an unknown charset", () => {
assert.strictEqual(decodeMimeWords(encodedWord("Test", "x-unknown-charset")), "Test");
});
});
describe("sanitizeSenderName", () => {
it("strips control characters that would corrupt participants_meta", () => {
assert.strictEqual(sanitizeSenderName(`Bad${PARTICIPANT_ENTRY_SEP}Name`), "Bad Name");
assert.strictEqual(sanitizeSenderName(`Bad${PARTICIPANT_NAME_SEP}Name`), "Bad Name");
assert.strictEqual(sanitizeSenderName("a\u0000b\u007fc"), "a b c");
});
it("collapses whitespace and trims", () => {
assert.strictEqual(sanitizeSenderName(" Jane Doe "), "Jane Doe");
});
it("returns null for empty or whitespace-only values", () => {
assert.strictEqual(sanitizeSenderName(null), null);
assert.strictEqual(sanitizeSenderName(undefined), null);
assert.strictEqual(sanitizeSenderName(" "), null);
});
it("caps the stored length", () => {
assert.strictEqual(sanitizeSenderName("x".repeat(500))?.length, 400);
});
});
describe("display labels with messy input", () => {
it("decodes encoded-words when rendering the label", () => {
assert.strictEqual(
formatSenderLabel(encodedWord("会议通知"), "noreply@example.com"),
"会议通知",
);
});
it("never leaks control characters into the label", () => {
const label = formatSenderLabel(`Evil${PARTICIPANT_ENTRY_SEP}Corp`, "e@example.com");
assert.ok(!label.includes(PARTICIPANT_ENTRY_SEP));
assert.strictEqual(label, "Evil Corp");
});
it("formatSenderFull prefers the name and falls back to the full address", () => {
assert.strictEqual(formatSenderFull("GitHub", "noreply@github.com"), "GitHub");
assert.strictEqual(formatSenderFull(null, "noreply@github.com"), "noreply@github.com");
});
});
describe("display spoofing defences", () => {
it("strips bidi overrides that could reorder the rendered label", () => {
assert.strictEqual(formatSenderLabel("Acme\u202Egnixalgm", "a@b.com"), "Acmegnixalgm");
assert.strictEqual(formatSenderLabel("\u202Bevil\u202C", "a@b.com"), "evil");
assert.strictEqual(formatSenderLabel("a\u2066b\u2069", "a@b.com"), "ab");
});
it("strips zero-width characters", () => {
assert.strictEqual(formatSenderLabel("Ad\u200Bmin", "a@b.com"), "Admin");
assert.strictEqual(formatSenderLabel("\uFEFFGitHub", "a@b.com"), "GitHub");
assert.strictEqual(formatSenderLabel("so\u00ADft", "a@b.com"), "soft");
});
it("treats an all-invisible name as absent", () => {
assert.strictEqual(sanitizeSenderName("\u200B"), null);
assert.strictEqual(sanitizeSenderName("\u200B\u202E\uFEFF"), null);
assert.strictEqual(formatSenderLabel("\u200B", "noreply@github.com"), "noreply");
});
it("strips NUL bytes smuggled through base64", () => {
// "AB" is not valid padded base64; atob() silently yields a NUL.
assert.ok(!decodeMimeWords("=?utf-8?B?AB?=").includes("\u0000"));
assert.ok(!decodeMimeWords("=?utf-8?Q?A=00B?=").includes("\u0000"));
});
});
describe("sanitizeSenderName truncation", () => {
it("never splits a surrogate pair", () => {
const capped = sanitizeSenderName("b".repeat(399) + "😀😀");
assert.ok(capped, "a 401-code-point name must survive the 400 cap");
if (!capped) throw new Error("unreachable: assert.ok above guarantees non-null");
const last = capped.charCodeAt(capped.length - 1);
assert.ok(
!(last >= 0xd800 && last <= 0xdbff),
"must not end with a lone high surrogate",
);
assert.strictEqual(Array.from(capped).length, 399 + 1); // 399 b's + one emoji
});
});
describe("participants_meta parsing edge cases", () => {
it("treats a separator-less entry as a name, not a lower-cased address", () => {
assert.deepStrictEqual(parseParticipantLabels("GitHub"), ["GitHub"]);
assert.deepStrictEqual(parseParticipantLabels("Jane Doe"), ["Jane Doe"]);
});
it("handles entries with an empty name", () => {
assert.deepStrictEqual(
parseParticipantLabels(`${PARTICIPANT_NAME_SEP}noreply@github.com`),
["noreply"],
);
});
});
describe("stripHeaderChars", () => {
it("removes the separator characters without lower-casing or trimming", () => {
assert.strictEqual(
stripHeaderChars(`evil${PARTICIPANT_ENTRY_SEP}injected@example.com`),
"evil injected@example.com",
);
assert.strictEqual(
stripHeaderChars(`Bad${PARTICIPANT_NAME_SEP}Name`),
"Bad Name",
);
assert.strictEqual(stripHeaderChars("User@Example.com"), "User@Example.com");
});
});
describe("adjacent encoded-words", () => {
it("ignores the whitespace between two encoded-words (RFC 2047 6.2)", () => {
assert.strictEqual(
decodeMimeWords("=?utf-8?B?SGVs?= =?utf-8?B?bG8=?="),
"Hello",
);
});
});