mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
06c436c36a
Move four DDD tensions on the Feed aggregate to ground: - #1 The aggregate now holds a domain FeedState (camelCase) instead of the snake_case FeedConfig DTO; infrastructure/feed-mapper.ts owns the FeedState<->FeedConfig/FeedListItem translation as the sole snake_case site outside the HTTP edge. - #3 Replace the edit() recomputeExpiry control flag with a Lifetime VO: passing a lifetime recomputes expiry, omitting it preserves the current one (the dashboard quick-edit path). - #4 Domain events carry their own feedId; dispatchFeedEvents centralizes the drain+dispatch in the application layer (no more manual pullEvents at call sites), keeping infra->application dependency direction intact. - #6 Rename FeedId.fromTrusted to FeedId.unchecked to make the absence of revalidation explicit. Adds Lifetime + feed-mapper round-trip tests. 353 tests green, tsc clean, wrangler dry-run OK. Docs (CLAUDE.md) synced. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { createMockEnv } from "../test/setup";
|
|
import { createFeedRecord, editFeed } from "./feed-service";
|
|
import { getCounters } from "./stats";
|
|
import { FeedId } from "../domain/value-objects/feed-id";
|
|
import type { Env } from "../types";
|
|
|
|
const mkEnv = (overrides: Partial<Env> = {}) =>
|
|
({ ...createMockEnv(), ...overrides }) as unknown as Env;
|
|
|
|
const baseInput = {
|
|
title: "N",
|
|
language: "en",
|
|
allowedSenders: [],
|
|
blockedSenders: [],
|
|
};
|
|
|
|
const TWO_HOURS = 2 * 3_600_000;
|
|
|
|
// The lifetime policy (parse env, apply the server-side FEED_TTL_HOURS override)
|
|
// lives here in the application layer; the domain only receives a resolved
|
|
// ttlHours. These tests pin that policy at the public service boundary.
|
|
describe("createFeedRecord — TTL policy", () => {
|
|
it("never expires when neither server nor client lifetime is set", async () => {
|
|
const { config } = await createFeedRecord(mkEnv(), { ...baseInput });
|
|
expect(config.expires_at).toBeUndefined();
|
|
});
|
|
|
|
it("uses the client lifetimeHours when there is no server override", async () => {
|
|
const before = Date.now();
|
|
const { config } = await createFeedRecord(mkEnv(), {
|
|
...baseInput,
|
|
lifetimeHours: 2,
|
|
});
|
|
expect(config.expires_at!).toBeGreaterThanOrEqual(before + TWO_HOURS);
|
|
});
|
|
|
|
it("lets a server FEED_TTL_HOURS override a larger client lifetime", async () => {
|
|
const before = Date.now();
|
|
const { config } = await createFeedRecord(mkEnv({ FEED_TTL_HOURS: "1" }), {
|
|
...baseInput,
|
|
lifetimeHours: 9999,
|
|
});
|
|
// 1h (server) wins over 9999h (client).
|
|
expect(config.expires_at!).toBeLessThan(before + TWO_HOURS);
|
|
});
|
|
|
|
it("bumps the feeds_created counter via the FeedCreated domain event", async () => {
|
|
const env = mkEnv();
|
|
await createFeedRecord(env, { ...baseInput });
|
|
const counters = await getCounters(env.EMAIL_STORAGE);
|
|
expect(counters.feeds_created).toBe(1);
|
|
expect(counters.last_feed_created_at).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("editFeed — TTL policy", () => {
|
|
it("recomputes expiry from the server override on edit", async () => {
|
|
const env = mkEnv({ FEED_TTL_HOURS: "1" });
|
|
const { feedId } = await createFeedRecord(env, { ...baseInput });
|
|
|
|
const before = Date.now();
|
|
const result = await editFeed(env, FeedId.unchecked(feedId), {
|
|
title: "renamed",
|
|
});
|
|
|
|
expect(result.status).toBe("ok");
|
|
if (result.status === "ok") {
|
|
expect(result.config.title).toBe("renamed");
|
|
expect(result.config.expires_at!).toBeLessThan(before + TWO_HOURS);
|
|
}
|
|
});
|
|
|
|
it("preserves expiry when neither server TTL nor client lifetime is given", async () => {
|
|
const env = mkEnv();
|
|
const { feedId, config } = await createFeedRecord(env, {
|
|
...baseInput,
|
|
lifetimeHours: 5,
|
|
});
|
|
|
|
const result = await editFeed(env, FeedId.unchecked(feedId), {
|
|
title: "x",
|
|
});
|
|
|
|
expect(result.status).toBe("ok");
|
|
if (result.status === "ok") {
|
|
expect(result.config.expires_at).toBe(config.expires_at);
|
|
}
|
|
});
|
|
});
|