mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat: reader-compat batch — JSON Feed, OPML export, conditional GET, dedup
Batch of four reader-facing improvements (TODO "Compat lecteurs + dedup"): - JSON Feed at /json/:feedId (feed lib .json1()); all formats cross-link - OPML export at /admin/opml (admin-protected; the registry lists every feed URL, so it must not be public) - Conditional GET on /rss + /atom: strong ETag + Last-Modified, 304 on If-None-Match/If-Modified-Since, validators shared via http-cache.ts - Duplicate-send dedup in ingestion: match by Message-ID, fall back to a SHA-256 of normalized subject+content; a duplicate is a no-op and bumps the new emails_deduplicated counter (status page + /api/v1/stats) 429 tests green, tsc clean, build dry-run OK. Docs (README/CLAUDE/TODO + landing cards) updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -144,4 +144,117 @@ describe("Atom Feed Route", () => {
|
||||
expect(body).toContain('xmlns="http://www.w3.org/2005/Atom"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("conditional GET (ETag + Last-Modified)", () => {
|
||||
const FEED_ID = "test-feed-atom-cget";
|
||||
const EMAIL_RECEIVED_AT = 1700000001000;
|
||||
|
||||
beforeEach(async () => {
|
||||
const emailKey = `feed:${FEED_ID}:${EMAIL_RECEIVED_AT}`;
|
||||
await mockEnv.EMAIL_STORAGE.put(
|
||||
emailKey,
|
||||
JSON.stringify({
|
||||
subject: "Atom Subject",
|
||||
from: "Sender <sender@example.com>",
|
||||
content: "<p>Body</p>",
|
||||
receivedAt: EMAIL_RECEIVED_AT,
|
||||
headers: {},
|
||||
}),
|
||||
);
|
||||
await mockEnv.EMAIL_STORAGE.put(
|
||||
`feed:${FEED_ID}:metadata`,
|
||||
JSON.stringify({
|
||||
emails: [
|
||||
{
|
||||
key: emailKey,
|
||||
subject: "Atom Subject",
|
||||
receivedAt: EMAIL_RECEIVED_AT,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
await mockEnv.EMAIL_STORAGE.put(
|
||||
`feed:${FEED_ID}:config`,
|
||||
JSON.stringify({
|
||||
title: "Atom Cget Feed",
|
||||
language: "en",
|
||||
created_at: 1700000000000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("first GET returns 200 with ETag and Last-Modified headers", async () => {
|
||||
const res = await testApp.request(`/${FEED_ID}`, {}, mockEnv);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("ETag")).toBeTruthy();
|
||||
expect(res.headers.get("Last-Modified")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("GET with matching If-None-Match returns 304 with empty body", async () => {
|
||||
const first = await testApp.request(`/${FEED_ID}`, {}, mockEnv);
|
||||
const etag = first.headers.get("ETag")!;
|
||||
|
||||
const res = await testApp.request(
|
||||
`/${FEED_ID}`,
|
||||
{ headers: { "If-None-Match": etag } },
|
||||
mockEnv,
|
||||
);
|
||||
expect(res.status).toBe(304);
|
||||
expect(await res.text()).toBe("");
|
||||
});
|
||||
|
||||
it("GET with If-Modified-Since in the future returns 304", async () => {
|
||||
const future = new Date(EMAIL_RECEIVED_AT + 1000).toUTCString();
|
||||
const res = await testApp.request(
|
||||
`/${FEED_ID}`,
|
||||
{ headers: { "If-Modified-Since": future } },
|
||||
mockEnv,
|
||||
);
|
||||
expect(res.status).toBe(304);
|
||||
});
|
||||
|
||||
it("stale If-None-Match after new email results in 200", async () => {
|
||||
const first = await testApp.request(`/${FEED_ID}`, {}, mockEnv);
|
||||
const oldEtag = first.headers.get("ETag")!;
|
||||
|
||||
const newReceivedAt = EMAIL_RECEIVED_AT + 5000;
|
||||
const newEmailKey = `feed:${FEED_ID}:${newReceivedAt}`;
|
||||
await mockEnv.EMAIL_STORAGE.put(
|
||||
newEmailKey,
|
||||
JSON.stringify({
|
||||
subject: "Newer Atom Email",
|
||||
from: "Sender <sender@example.com>",
|
||||
content: "<p>New body</p>",
|
||||
receivedAt: newReceivedAt,
|
||||
headers: {},
|
||||
}),
|
||||
);
|
||||
await mockEnv.EMAIL_STORAGE.put(
|
||||
`feed:${FEED_ID}:metadata`,
|
||||
JSON.stringify({
|
||||
emails: [
|
||||
{
|
||||
key: newEmailKey,
|
||||
subject: "Newer Atom Email",
|
||||
receivedAt: newReceivedAt,
|
||||
},
|
||||
{
|
||||
key: `feed:${FEED_ID}:${EMAIL_RECEIVED_AT}`,
|
||||
subject: "Atom Subject",
|
||||
receivedAt: EMAIL_RECEIVED_AT,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await testApp.request(
|
||||
`/${FEED_ID}`,
|
||||
{ headers: { "If-None-Match": oldEtag } },
|
||||
mockEnv,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const newEtag = res.headers.get("ETag");
|
||||
expect(newEtag).not.toBe(oldEtag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user