Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

165 lines
5.5 KiB
TypeScript

import type { PluginContext } from "emdash";
import type { EmailDeliverEvent } from "emdash/plugin";
import { describe, it, expect, vi } from "vitest";
import {
cloudflareEmail,
createCloudflareEmailDeliver,
createPlugin,
type CloudflareEmailConfig,
} from "../src/plugins/cloudflare-email.js";
const message = {
to: "user@example.com",
subject: "Your magic link",
text: "Click here",
};
const event: EmailDeliverEvent = { message, source: "system" };
function fakeCtx(): PluginContext {
return { log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } } as unknown as PluginContext;
}
function fakeEnv(sendImpl?: () => Promise<{ messageId?: string }>) {
const send = vi.fn(sendImpl ?? (async () => ({ messageId: "msg-1" })));
return { env: { EMAIL: { send } }, send };
}
describe("cloudflareEmail()", () => {
it("returns a descriptor with a bundlable entrypoint (not an in-process definition)", () => {
// The astro integration rejects `plugins: []` entries without an
// `entrypoint` (#1721), so the documented usage must produce a
// descriptor the integration can statically import at build time.
const descriptor = cloudflareEmail({ from: "cms@mails.example.com" });
expect(descriptor.id).toBe("cloudflare-email");
expect(descriptor.entrypoint).toBe("@emdash-cms/cloudflare/plugins/cloudflare-email");
expect(descriptor.format).toBe("native");
expect(descriptor.options).toEqual({ from: "cms@mails.example.com" });
expect(descriptor.capabilities).toEqual(["hooks.email-transport:register"]);
});
it("rejects a missing or invalid from address", () => {
expect(() => cloudflareEmail({ from: "" })).toThrow(/config\.from is required/);
expect(() =>
cloudflareEmail({ from: { email: "not-an-address" } } as CloudflareEmailConfig),
).toThrow(/config\.from is required/);
});
});
describe("createPlugin()", () => {
it("returns a resolved plugin with an exclusive email:deliver hook", () => {
const plugin = createPlugin({ from: "cms@mails.example.com" });
expect(plugin.id).toBe("cloudflare-email");
expect(plugin.capabilities).toEqual(["hooks.email-transport:register"]);
const hook = plugin.hooks["email:deliver"];
expect(hook).toBeDefined();
expect(hook).toMatchObject({ exclusive: true });
});
it("rejects a missing or invalid from address", () => {
expect(() => createPlugin({ from: "" })).toThrow(/config\.from is required/);
});
it("round-trips the descriptor options the virtual plugins module passes back", async () => {
const descriptor = cloudflareEmail({ from: "cms@mails.example.com" });
// Simulate what the generated virtual:emdash/plugins module does:
// `createPlugin(descriptor.options)`.
const plugin = createPlugin(descriptor.options as CloudflareEmailConfig);
const hook = plugin.hooks["email:deliver"];
expect(hook).toBeDefined();
// The handler resolves the env via cloudflare:workers, which is not
// available in vitest — assert it fails with the runtime-guard error
// rather than a config error, proving the wiring is intact.
await expect(hook?.handler(event, fakeCtx())).rejects.toThrow(
/cloudflare:workers is not available/,
);
});
});
describe("createCloudflareEmailDeliver()", () => {
it("sends through the binding with from/replyTo applied", async () => {
const { env, send } = fakeEnv();
const deliver = createCloudflareEmailDeliver(
{ from: { email: "cms@mails.example.com", name: "CMS" }, replyTo: "hello@example.com" },
async () => env,
);
await deliver(event, fakeCtx());
expect(send).toHaveBeenCalledWith({
from: { email: "cms@mails.example.com", name: "CMS" },
to: "user@example.com",
subject: "Your magic link",
text: "Click here",
replyTo: "hello@example.com",
});
});
it("accepts a bare string from and omits optional fields", async () => {
const { env, send } = fakeEnv();
const deliver = createCloudflareEmailDeliver(
{ from: "cms@mails.example.com" },
async () => env,
);
await deliver(event, fakeCtx());
expect(send).toHaveBeenCalledWith({
from: { email: "cms@mails.example.com" },
to: "user@example.com",
subject: "Your magic link",
text: "Click here",
});
});
it("passes html through when present", async () => {
const { env, send } = fakeEnv();
const deliver = createCloudflareEmailDeliver(
{ from: "cms@mails.example.com" },
async () => env,
);
await deliver(
{ message: { ...message, html: "<p>Click here</p>" }, source: "system" },
fakeCtx(),
);
expect(send).toHaveBeenCalledWith(expect.objectContaining({ html: "<p>Click here</p>" }));
});
it("resolves the binding by its configured name", async () => {
const send = vi.fn(async () => ({ messageId: "msg-2" }));
const deliver = createCloudflareEmailDeliver(
{ from: "cms@mails.example.com", binding: "MAILER" },
async () => ({ MAILER: { send } }),
);
await deliver(event, fakeCtx());
expect(send).toHaveBeenCalledOnce();
});
it("throws a descriptive error when the binding is missing", async () => {
const deliver = createCloudflareEmailDeliver(
{ from: "cms@mails.example.com" },
async () => ({}),
);
await expect(deliver(event, fakeCtx())).rejects.toThrow(/binding "EMAIL" not found/);
});
it("propagates provider errors so the pipeline can surface them", async () => {
const { env } = fakeEnv(async () => {
throw new Error("sender domain not verified");
});
const deliver = createCloudflareEmailDeliver(
{ from: "cms@mails.example.com" },
async () => env,
);
await expect(deliver(event, fakeCtx())).rejects.toThrow("sender domain not verified");
});
});