Files
kill-the-news/src/infrastructure/cloudflare-email.test.ts
T
Julien HerrandClaude Opus 4.7 1a4a479190 feat: decouple read FeedId from inbound MailboxId
Separate the two feed identities so the public read URL never reveals the
inbound address and vice-versa:

- FeedId becomes an opaque high-entropy token (read id + KV key); MailboxId
  (noun.noun.NN) owns the inbound address and the untrusted-input boundary
  via MailboxId.parse. They map only through the inbound:<mailbox> secondary
  index, resolved solely at reception.
- inbound index lifecycle is owned by FeedRepository: written by save/saveConfig,
  dropped by removeFromList(Bulk) — symmetric, never mirrored by hand (removes the
  manual delete in feed-service + the cron loop, and a silent empty-catch).
- Feed.mailboxId exposes a MailboxId VO (symmetry with Feed.id); the
  mailbox@domain shape lives on MailboxId.emailAddress(domain).
- Distinguish mailbox_unknown (no feed claims the address) from feed_not_found
  (dangling index) for observability; both forwardable, both 404.
- Drop the redundant EmailParser.extractMailbox pass-through so MailboxId.parse
  is the single parse boundary.

Docs (README/INSTALL/CLAUDE.md/landing) and tests updated; 439 tests green,
tsc clean, build dry-run OK.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 22:46:37 +02:00

300 lines
8.4 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import "../test/setup";
import { createMockEnv, seedInboundIndex } from "../test/setup";
import { handleCloudflareEmail } from "./cloudflare-email";
import { getCounters } from "../application/stats";
const VALID_FEED_ID = "apple.mountain.42";
const DOMAIN = "test.getmynews.app";
const RAW_EMAIL = [
"From: Sender Name <[email protected]>",
`To: ${VALID_FEED_ID}@${DOMAIN}`,
"Subject: Hello World",
"Date: Thu, 01 Jan 2026 12:00:00 +0000",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8",
"",
"This is the email body.",
].join("\r\n");
function makeMessage(
overrides: Partial<{
from: string;
to: string;
rawText: string;
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
}> = {},
): ForwardableEmailMessage {
const rawText = overrides.rawText ?? RAW_EMAIL;
const encoder = new TextEncoder();
const bytes = encoder.encode(rawText);
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(bytes);
controller.close();
},
});
return {
from: overrides.from ?? "[email protected]",
to: overrides.to ?? `${VALID_FEED_ID}@${DOMAIN}`,
headers: new Headers(),
raw: stream,
rawSize: bytes.length,
forward: overrides.forward ?? (async () => {}),
reply: async () => {},
setReject: () => {},
} as unknown as ForwardableEmailMessage;
}
/** Records every message.forward() call so tests can assert on routing. */
function spyForward() {
const calls: string[] = [];
const forward = async (rcptTo: string) => {
calls.push(rcptTo);
};
return { calls, forward };
}
const FALLBACK = "[email protected]";
describe("handleCloudflareEmail", () => {
let env: ReturnType<typeof createMockEnv>;
beforeEach(async () => {
env = createMockEnv();
await seedInboundIndex(env, VALID_FEED_ID);
});
it("stores email in KV when feed exists", async () => {
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({}),
);
await handleCloudflareEmail(
makeMessage(),
env as any,
{ waitUntil: () => {} } as any,
);
const metadata = await env.EMAIL_STORAGE.get(
`feed:${VALID_FEED_ID}:metadata`,
"json",
);
expect(metadata.emails).toHaveLength(1);
expect(metadata.emails[0].subject).toBe("Hello World");
});
it("does not throw when feed does not exist", async () => {
await expect(
handleCloudflareEmail(
makeMessage(),
env as any,
{ waitUntil: () => {} } as any,
),
).resolves.toBeUndefined();
});
it("does not throw when email is malformed", async () => {
const msg = makeMessage({ rawText: "not a valid email" });
await expect(
handleCloudflareEmail(msg, env as any, { waitUntil: () => {} } as any),
).resolves.toBeUndefined();
});
it("uses sender from message.from for allowlist check", async () => {
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({ allowed_senders: ["[email protected]"] }),
);
await handleCloudflareEmail(
makeMessage(),
env as any,
{ waitUntil: () => {} } as any,
);
const metadata = await env.EMAIL_STORAGE.get(
`feed:${VALID_FEED_ID}:metadata`,
"json",
);
expect(metadata.emails).toHaveLength(1);
});
it("rejects email when sender is not in allowlist (stored nothing)", async () => {
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({ allowed_senders: ["[email protected]"] }),
);
await handleCloudflareEmail(
makeMessage(),
env as any,
{ waitUntil: () => {} } as any,
);
const metadata = await env.EMAIL_STORAGE.get(
`feed:${VALID_FEED_ID}:metadata`,
"json",
);
expect(metadata).toBeNull();
});
describe("FALLBACK_FORWARD_ADDRESS catch-all fallback", () => {
it("forwards to the fallback when the feed does not exist", async () => {
const { calls, forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([FALLBACK]);
});
it("forwards to the fallback when the address is not a feed", async () => {
const { calls, forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await handleCloudflareEmail(
makeMessage({ to: `not-a-feed@${DOMAIN}`, forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([FALLBACK]);
});
it("does NOT forward an expired feed's mail (no newsletter leak)", async () => {
const { calls, forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({ expires_at: Date.now() - 1000 }),
);
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([]);
});
it("does NOT forward when the sender is blocked", async () => {
const { calls, forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({ allowed_senders: ["[email protected]"] }),
);
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([]);
});
it("does NOT forward when the email was ingested", async () => {
const { calls, forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({}),
);
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([]);
});
it("does NOT forward when the env var is unset (current drop behavior)", async () => {
const { calls, forward } = spyForward();
// env.FALLBACK_FORWARD_ADDRESS intentionally left unset.
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
expect(calls).toEqual([]);
});
it("does not throw when the fallback forward fails (unverified address)", async () => {
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
const forward = async () => {
throw new Error("destination address not verified");
};
await expect(
handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
),
).resolves.toBeUndefined();
});
it("increments the emails_forwarded counter on a successful forward", async () => {
const { forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
const counters = await getCounters(env.EMAIL_STORAGE as any);
expect(counters.emails_forwarded).toBe(1);
});
it("does not increment emails_forwarded when the forward fails", async () => {
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
const forward = async () => {
throw new Error("destination address not verified");
};
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
const counters = await getCounters(env.EMAIL_STORAGE as any);
expect(counters.emails_forwarded).toBe(0);
});
it("does not increment emails_forwarded for dropped reasons", async () => {
const { forward } = spyForward();
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
await env.EMAIL_STORAGE.put(
`feed:${VALID_FEED_ID}:config`,
JSON.stringify({ expires_at: Date.now() - 1000 }),
);
await handleCloudflareEmail(
makeMessage({ forward }),
env as any,
{ waitUntil: () => {} } as any,
);
const counters = await getCounters(env.EMAIL_STORAGE as any);
expect(counters.emails_forwarded).toBe(0);
});
});
});