From b24ee969d11d51115dfd84f5814310cb5d23e2ff Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 11:35:37 +0200 Subject: [PATCH] style: fix Prettier formatting on 11 files Co-Authored-By: Claude Sonnet 4.6 --- README.md | 29 ++++--- src/lib/email-processor.test.ts | 78 +++++++++++++++---- src/lib/email-processor.ts | 13 +++- src/lib/forwardemail.ts | 14 ++-- src/routes/admin.test.ts | 8 +- src/routes/atom.test.ts | 6 +- src/routes/entries.ts | 72 +++++++++++------ src/routes/files.test.ts | 3 +- src/routes/inbound.ts | 5 +- src/utils/email-parser.test.ts | 28 ++++--- src/utils/feed-generator.test.ts | 128 ++++++++++++++++++++++++++----- 11 files changed, 289 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index df3a5d4..063719e 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ Email-to-RSS keeps the same workflow while avoiding shared domains and shared da Two ingestion methods are supported — pick one or use both: -| Method | How it works | -| ---------------------- | ------------------------------------------------------------------ | -| **Cloudflare Email Workers** | Cloudflare Email Routing delivers the raw message directly to the Worker via the `email()` handler — no outbound webhook needed | -| **ForwardEmail webhook** | ForwardEmail parses the message and POSTs a JSON payload to `POST /api/inbound`; the Worker verifies the source IP before processing | +| Method | How it works | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| **Cloudflare Email Workers** | Cloudflare Email Routing delivers the raw message directly to the Worker via the `email()` handler — no outbound webhook needed | +| **ForwardEmail webhook** | ForwardEmail parses the message and POSTs a JSON payload to `POST /api/inbound`; the Worker verifies the source IP before processing | Common path: @@ -60,7 +60,7 @@ Main routes: ## Cloudflare setup -If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://dash.cloudflare.com/), go to *Add a site*, enter your domain, choose the Free plan, and follow the instructions to update your nameservers at your registrar. Wait for the zone to become active (usually a few minutes). +If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://dash.cloudflare.com/), go to _Add a site_, enter your domain, choose the Free plan, and follow the instructions to update your nameservers at your registrar. Wait for the zone to become active (usually a few minutes). ## Setup @@ -70,9 +70,11 @@ If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://d npx wrangler login ``` 3. Run setup: + ```bash bash setup.sh ``` + The script will prompt for an admin password and your domain, then: - install npm dependencies - verify Cloudflare auth (`wrangler whoami`) @@ -86,8 +88,8 @@ If your domain is not yet on Cloudflare: in the [Cloudflare dashboard](https://d No third-party service required. Cloudflare receives the email and hands it directly to the Worker. -1. In the Cloudflare dashboard, go to *Email → Email Routing* for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically. -2. Under *Email Routing → Routing Rules*, add a **Catch-all** rule: +1. In the Cloudflare dashboard, go to _Email → Email Routing_ for your zone and click **Enable Email Routing**. Cloudflare will prompt you to add MX and SPF records — accept and it adds them automatically. +2. Under _Email Routing → Routing Rules_, add a **Catch-all** rule: - Action: **Send to Worker** - Worker: `email-to-rss` (the name from `wrangler.toml`) @@ -97,7 +99,7 @@ That's it. No webhook configuration is needed. Use this if you prefer ForwardEmail's additional features (sender filtering, open-tracking, etc.). -Add these DNS records in Cloudflare (*DNS → Records*): +Add these DNS records in Cloudflare (_DNS → Records_): | Type | Name | Content | Notes | | ---- | ---- | ---------------------------------------------------- | ----------------------- | @@ -111,14 +113,16 @@ Replace `yourdomain.com` with your actual domain. The Worker verifies each webhook request against ForwardEmail's published MX IP list before processing it. 5. Deploy: + ```bash npm run deploy ``` + Wrangler will create the Worker and register `yourdomain.com` (and `www.yourdomain.com`) as custom domains pointing to it. Cloudflare handles TLS automatically. 6. Open `https://yourdomain.com/admin` and sign in. -> **Tip:** To verify the Worker is running, check *Workers & Pages → email-to-rss* in the Cloudflare dashboard. The *Custom Domains* tab should list your domain once the deploy succeeds. +> **Tip:** To verify the Worker is running, check _Workers & Pages → email-to-rss_ in the Cloudflare dashboard. The _Custom Domains_ tab should list your domain once the deploy succeeds. ## Development @@ -153,7 +157,7 @@ This feature is **optional**. If no R2 bucket is bound, attachments are silently **Setup:** -1. Create an R2 bucket in the Cloudflare dashboard (*R2 Object Storage → Create bucket*), or with Wrangler: +1. Create an R2 bucket in the Cloudflare dashboard (_R2 Object Storage → Create bucket_), or with Wrangler: ```bash npx wrangler r2 bucket create your-bucket-name ``` @@ -177,8 +181,8 @@ Instead of the built-in password login you can delegate admin authentication to **Required Worker secrets** (set with `wrangler secret put`, never in `[vars]`): -| Secret | Description | -|---|---| +| Secret | Description | +| ------------------- | ---------------------------------------------- | | `PROXY_AUTH_SECRET` | Shared secret between the proxy and the Worker | **Required `[vars]`** in `wrangler.toml`: @@ -188,6 +192,7 @@ PROXY_TRUSTED_IPS = "10.0.0.1" # comma-separated IPs of your reverse proxy ``` When both are configured, the Worker authenticates a request if: + 1. `CF-Connecting-IP` is in `PROXY_TRUSTED_IPS` 2. The `X-Auth-Proxy-Secret` header matches `PROXY_AUTH_SECRET` 3. `Remote-User` or `X-Forwarded-User` is non-empty diff --git a/src/lib/email-processor.test.ts b/src/lib/email-processor.test.ts index ac7163d..e7d7b58 100644 --- a/src/lib/email-processor.test.ts +++ b/src/lib/email-processor.test.ts @@ -1,7 +1,11 @@ import { describe, it, expect, beforeEach } from "vitest"; import "../test/setup"; import { createMockEnv, MockR2 } from "../test/setup"; -import { processEmail, ProcessEmailInput, RawAttachment } from "./email-processor"; +import { + processEmail, + ProcessEmailInput, + RawAttachment, +} from "./email-processor"; const VALID_FEED_ID = "apple.mountain.42"; const VALID_TO = `${VALID_FEED_ID}@test.getmynews.app`; @@ -139,25 +143,53 @@ describe("processEmail", () => { const oldKey1 = `feed:${VALID_FEED_ID}:111`; const oldKey2 = `feed:${VALID_FEED_ID}:222`; const bigContent = "x".repeat(200); - const email1 = JSON.stringify({ subject: "Old1", from: "a@b.com", content: bigContent, receivedAt: 111, headers: {} }); - const email2 = JSON.stringify({ subject: "Old2", from: "a@b.com", content: bigContent, receivedAt: 222, headers: {} }); + const email1 = JSON.stringify({ + subject: "Old1", + from: "a@b.com", + content: bigContent, + receivedAt: 111, + headers: {}, + }); + const email2 = JSON.stringify({ + subject: "Old2", + from: "a@b.com", + content: bigContent, + receivedAt: 222, + headers: {}, + }); await env.EMAIL_STORAGE.put(oldKey1, email1); await env.EMAIL_STORAGE.put(oldKey2, email2); await env.EMAIL_STORAGE.put( `feed:${VALID_FEED_ID}:metadata`, JSON.stringify({ emails: [ - { key: oldKey2, subject: "Old2", receivedAt: 222, size: email2.length }, - { key: oldKey1, subject: "Old1", receivedAt: 111, size: email1.length }, + { + key: oldKey2, + subject: "Old2", + receivedAt: 222, + size: email2.length, + }, + { + key: oldKey1, + subject: "Old1", + receivedAt: 111, + size: email1.length, + }, ], }), ); const tinyEnv = { ...env, FEED_MAX_SIZE_BYTES: "50" }; - const res = await processEmail(makeInput({ subject: "New" }), tinyEnv as any); + const res = await processEmail( + makeInput({ subject: "New" }), + tinyEnv as any, + ); expect(res.status).toBe(200); - const metadata = await env.EMAIL_STORAGE.get(`feed:${VALID_FEED_ID}:metadata`, "json"); + const metadata = await env.EMAIL_STORAGE.get( + `feed:${VALID_FEED_ID}:metadata`, + "json", + ); expect(metadata.emails).toHaveLength(1); expect(metadata.emails[0].subject).toBe("New"); @@ -175,13 +207,17 @@ describe("processEmail", () => { const bigEnv = { ...env, FEED_MAX_SIZE_BYTES: String(10 * 1024 * 1024) }; await processEmail(makeInput({ subject: "First" }), bigEnv as any); await processEmail(makeInput({ subject: "Second" }), bigEnv as any); - const metadata = await env.EMAIL_STORAGE.get(`feed:${VALID_FEED_ID}:metadata`, "json"); + const metadata = await env.EMAIL_STORAGE.get( + `feed:${VALID_FEED_ID}:metadata`, + "json", + ); expect(metadata.emails).toHaveLength(2); }); }); describe("processEmail — attachments", () => { - const pdfContent = new TextEncoder().encode("PDF bytes").buffer as ArrayBuffer; + const pdfContent = new TextEncoder().encode("PDF bytes") + .buffer as ArrayBuffer; const pdfAttachment: RawAttachment = { filename: "report.pdf", @@ -205,7 +241,10 @@ describe("processEmail — attachments", () => { `feed:${VALID_FEED_ID}:metadata`, "json", ); - const emailData = await env.EMAIL_STORAGE.get(metadata.emails[0].key, "json"); + const emailData = await env.EMAIL_STORAGE.get( + metadata.emails[0].key, + "json", + ); expect(emailData.attachments).toBeUndefined(); }); @@ -226,7 +265,10 @@ describe("processEmail — attachments", () => { `feed:${VALID_FEED_ID}:metadata`, "json", ); - const emailData = await env.EMAIL_STORAGE.get(metadata.emails[0].key, "json"); + const emailData = await env.EMAIL_STORAGE.get( + metadata.emails[0].key, + "json", + ); expect(emailData.attachments).toHaveLength(1); expect(emailData.attachments[0].filename).toBe("report.pdf"); @@ -271,7 +313,14 @@ describe("processEmail — attachments", () => { content: bigContent, receivedAt: 111, headers: {}, - attachments: [{ id: oldAttachmentId, filename: "old.pdf", contentType: "application/pdf", size: 100 }], + attachments: [ + { + id: oldAttachmentId, + filename: "old.pdf", + contentType: "application/pdf", + size: 100, + }, + ], }); await env.EMAIL_STORAGE.put(oldKey, oldEmail); @@ -295,7 +344,10 @@ describe("processEmail — attachments", () => { // Process with tight size budget to force trimming const tinyEnv = { ...env, FEED_MAX_SIZE_BYTES: "50" }; - const res = await processEmail(makeInput({ subject: "New" }), tinyEnv as any); + const res = await processEmail( + makeInput({ subject: "New" }), + tinyEnv as any, + ); expect(res.status).toBe(200); // Old attachment should be deleted from R2 diff --git a/src/lib/email-processor.ts b/src/lib/email-processor.ts index d94f281..2a2ae97 100644 --- a/src/lib/email-processor.ts +++ b/src/lib/email-processor.ts @@ -1,5 +1,11 @@ import { EmailParser } from "../utils/email-parser"; -import { AttachmentData, EmailMetadata, Env, FeedConfig, FeedMetadata } from "../types"; +import { + AttachmentData, + EmailMetadata, + Env, + FeedConfig, + FeedMetadata, +} from "../types"; export interface RawAttachment { filename: string; @@ -151,7 +157,10 @@ export async function processEmail( }; feedMetadata.emails.unshift(newEntry); - let totalSize = feedMetadata.emails.reduce((sum, e) => sum + (e.size ?? 0), 0); + let totalSize = feedMetadata.emails.reduce( + (sum, e) => sum + (e.size ?? 0), + 0, + ); const toDelete: EmailMetadata[] = []; while (totalSize > maxBytes && feedMetadata.emails.length > 1) { const dropped = feedMetadata.emails.pop()!; diff --git a/src/lib/forwardemail.ts b/src/lib/forwardemail.ts index 5488938..2036f33 100644 --- a/src/lib/forwardemail.ts +++ b/src/lib/forwardemail.ts @@ -6,10 +6,7 @@ export interface ForwardEmailAttachment { filename?: string; contentType?: string; size?: number; - content?: - | { type: "Buffer"; data: number[] } - | ArrayBuffer - | ArrayBufferView; + content?: { type: "Buffer"; data: number[] } | ArrayBuffer | ArrayBufferView; } export interface ForwardEmailPayload { @@ -56,8 +53,13 @@ function toArrayBuffer( ): ArrayBuffer | null { if (!content) return null; if (content instanceof ArrayBuffer) return content; - if (ArrayBuffer.isView(content)) return (content as ArrayBufferView).buffer as ArrayBuffer; - if (typeof content === "object" && content.type === "Buffer" && Array.isArray(content.data)) { + if (ArrayBuffer.isView(content)) + return (content as ArrayBufferView).buffer as ArrayBuffer; + if ( + typeof content === "object" && + content.type === "Buffer" && + Array.isArray(content.data) + ) { return Uint8Array.from(content.data).buffer as ArrayBuffer; } return null; diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 0a31e3a..a3f3289 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -14,7 +14,8 @@ describe("Admin Routes", () => { mockEnv = createMockEnv() as unknown as Env; testApp = new Hono(); testApp.route("/admin", app); - request = (path, init = {}) => Promise.resolve(testApp.request(path, init, mockEnv)); + request = (path, init = {}) => + Promise.resolve(testApp.request(path, init, mockEnv)); loginAndGetCookie = async () => { const formData = new FormData(); formData.append("password", "test-password"); @@ -366,7 +367,10 @@ describe("Admin Routes", () => { } as unknown as Env; } - function makeProxyRequest(path: string, headers: Record = {}) { + function makeProxyRequest( + path: string, + headers: Record = {}, + ) { const proxyApp = new Hono(); proxyApp.route("/admin", app); return proxyApp.request(path, { headers }, proxyEnv()); diff --git a/src/routes/atom.test.ts b/src/routes/atom.test.ts index d587a11..959cb43 100644 --- a/src/routes/atom.test.ts +++ b/src/routes/atom.test.ts @@ -68,7 +68,11 @@ describe("Atom Feed Route", () => { `feed:${FEED_ID}:metadata`, JSON.stringify({ emails: [ - { key: emailKey, subject: "Atom Entry Subject", receivedAt: 1700000001000 }, + { + key: emailKey, + subject: "Atom Entry Subject", + receivedAt: 1700000001000, + }, ], }), ); diff --git a/src/routes/entries.ts b/src/routes/entries.ts index a2c969b..438da6c 100644 --- a/src/routes/entries.ts +++ b/src/routes/entries.ts @@ -21,7 +21,9 @@ export async function handle(c: Context): Promise { return new Response("Feed not found", { status: 404 }); } - const metaEntry = feedMetadata.emails.find((e) => e.receivedAt === receivedAt); + const metaEntry = feedMetadata.emails.find( + (e) => e.receivedAt === receivedAt, + ); if (!metaEntry) { return new Response("Entry not found", { status: 404 }); } @@ -39,26 +41,50 @@ export async function handle(c: Context): Promise { "default-src 'none'; style-src 'unsafe-inline'; img-src *; frame-src 'none'", ); - return c.html(html` - - - - - ${emailData.subject} - - - -

${emailData.subject}

-
-
From:
${emailData.from}
-
Date:
${new Date(emailData.receivedAt).toUTCString()}
-
-
${raw(emailData.content)}
- -`); + return c.html( + html` + + + + + ${emailData.subject} + + + +

${emailData.subject}

+
+
From:
+
${emailData.from}
+
Date:
+
${new Date(emailData.receivedAt).toUTCString()}
+
+
${raw(emailData.content)}
+ + `, + ); } diff --git a/src/routes/files.test.ts b/src/routes/files.test.ts index 0480362..dd20616 100644 --- a/src/routes/files.test.ts +++ b/src/routes/files.test.ts @@ -39,7 +39,8 @@ describe("GET /files/:attachmentId/:filename", () => { }); it("returns 200 with stored content when attachment exists", async () => { - const content = new TextEncoder().encode("PDF content").buffer as ArrayBuffer; + const content = new TextEncoder().encode("PDF content") + .buffer as ArrayBuffer; await mockR2.put("test-uuid", content, { httpMetadata: { contentType: "application/pdf" }, }); diff --git a/src/routes/inbound.ts b/src/routes/inbound.ts index e62d7ea..cc0d69b 100644 --- a/src/routes/inbound.ts +++ b/src/routes/inbound.ts @@ -1,9 +1,6 @@ import { Context } from "hono"; import { Env } from "../types"; -import { - ForwardEmailPayload, - handleForwardEmail, -} from "../lib/forwardemail"; +import { ForwardEmailPayload, handleForwardEmail } from "../lib/forwardemail"; export async function handle(c: Context): Promise { try { diff --git a/src/utils/email-parser.test.ts b/src/utils/email-parser.test.ts index d250586..2b76981 100644 --- a/src/utils/email-parser.test.ts +++ b/src/utils/email-parser.test.ts @@ -53,15 +53,15 @@ describe("EmailParser.decodeEncodedWords", () => { // =?UTF-8?Q?caf=C3=A9?= → "café" (but decodeQuotedPrintable works byte-by-byte) // Use a simple ASCII QP sequence to stay charset-agnostic in tests // =?US-ASCII?Q?Hello=20World?= → "Hello World" (=20 → space, _ → space) - expect( - EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello=20World?="), - ).toBe("Hello World"); + expect(EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello=20World?=")).toBe( + "Hello World", + ); }); it("decodes underscores as spaces in QP encoding", () => { - expect( - EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello_World?="), - ).toBe("Hello World"); + expect(EmailParser.decodeEncodedWords("=?US-ASCII?Q?Hello_World?=")).toBe( + "Hello World", + ); }); it("leaves unrecognised encoded-word syntax unchanged", () => { @@ -79,9 +79,7 @@ describe("EmailParser.parseForwardEmailPayload", () => { }); it("throws on undefined payload", () => { - expect(() => - EmailParser.parseForwardEmailPayload(undefined), - ).toThrow(); + expect(() => EmailParser.parseForwardEmailPayload(undefined)).toThrow(); }); it("parses subject, from, and HTML content", () => { @@ -95,7 +93,9 @@ describe("EmailParser.parseForwardEmailPayload", () => { expect(result.subject).toBe("Test Subject"); expect(result.from).toBe("sender@example.com"); expect(result.content).toBe("

Hello

"); - expect(result.receivedAt).toBe(new Date("2024-01-15T10:00:00.000Z").getTime()); + expect(result.receivedAt).toBe( + new Date("2024-01-15T10:00:00.000Z").getTime(), + ); }); it("prefers HTML content over plain text", () => { @@ -137,14 +137,18 @@ describe("EmailParser.parseForwardEmailPayload", () => { it("uses Date.now() when date field is absent", () => { const before = Date.now(); - const result = EmailParser.parseForwardEmailPayload({ from: { text: "x@y.com" } }); + const result = EmailParser.parseForwardEmailPayload({ + from: { text: "x@y.com" }, + }); const after = Date.now(); expect(result.receivedAt).toBeGreaterThanOrEqual(before); expect(result.receivedAt).toBeLessThanOrEqual(after); }); it("defaults subject to 'No Subject' when absent", () => { - const result = EmailParser.parseForwardEmailPayload({ from: { text: "x@y.com" } }); + const result = EmailParser.parseForwardEmailPayload({ + from: { text: "x@y.com" }, + }); expect(result.subject).toBe("No Subject"); }); diff --git a/src/utils/feed-generator.test.ts b/src/utils/feed-generator.test.ts index 8b80e00..3527492 100644 --- a/src/utils/feed-generator.test.ts +++ b/src/utils/feed-generator.test.ts @@ -38,13 +38,23 @@ const FEED_ID = "abc123"; describe("generateRssFeed", () => { it("returns RSS 2.0 with channel element", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Test Newsletter"); }); it("includes element for email with attachment", () => { - const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); expect(result).toContain(" { }); it("does not include for email without attachments", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).not.toContain(" { - const result = generateRssFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); - expect(result).toContain(`${BASE_URL}/files/550e8400-e29b-41d4-a716-446655440000/report.pdf`); + const result = generateRssFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); + expect(result).toContain( + `${BASE_URL}/files/550e8400-e29b-41d4-a716-446655440000/report.pdf`, + ); }); it("includes rss self-link in RSS output", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`); }); it("includes email entries as elements", () => { - const result = generateRssFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateRssFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Hello World"); }); @@ -82,39 +114,74 @@ describe("generateRssFeed", () => { describe("generateAtomFeed", () => { it("returns Atom 1.0 namespace", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain('xmlns="http://www.w3.org/2005/Atom"'); }); it("contains root element", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); }); it("includes feed title", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Test Newsletter"); }); it("includes elements for each email", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(""); expect(result).toContain("Hello World"); }); it("includes author information", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Alice"); }); it("self-link points to atom URL", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/atom/${FEED_ID}`); }); it("includes rss alternate link", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`); }); @@ -125,26 +192,49 @@ describe("generateAtomFeed", () => { }); it("handles config without description", () => { - const configNoDesc: FeedConfig = { ...mockFeedConfig, description: undefined }; - const result = generateAtomFeed(configNoDesc, mockEmails, BASE_URL, FEED_ID); + const configNoDesc: FeedConfig = { + ...mockFeedConfig, + description: undefined, + }; + const result = generateAtomFeed( + configNoDesc, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain('xmlns="http://www.w3.org/2005/Atom"'); }); it("handles config with author field", () => { const configWithAuthor: FeedConfig = { ...mockFeedConfig, author: "Bob" }; - const result = generateAtomFeed(configWithAuthor, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + configWithAuthor, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).toContain("Bob"); }); it("includes enclosure link for email with attachment in Atom feed", () => { - const result = generateAtomFeed(mockFeedConfig, [mockEmailWithAttachment], BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + [mockEmailWithAttachment], + BASE_URL, + FEED_ID, + ); expect(result).toContain('rel="enclosure"'); expect(result).toContain("550e8400-e29b-41d4-a716-446655440000"); expect(result).toContain("report.pdf"); }); it("does not include enclosure link for email without attachments in Atom feed", () => { - const result = generateAtomFeed(mockFeedConfig, mockEmails, BASE_URL, FEED_ID); + const result = generateAtomFeed( + mockFeedConfig, + mockEmails, + BASE_URL, + FEED_ID, + ); expect(result).not.toContain('rel="enclosure"'); }); });