Ctrl K
ComunidadGuías…

workers/participants.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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",
		);
	});
});