/**
* End-to-end subscribe test for feedless directory stubs.
*
* A delisted show (feedUrl "" + directoryUrl) must resolve its real feed from
* the directory page inside addFeed — so subscribing just works. When the
* page can't be resolved, addFeed must refuse (return null) instead of adding
* a broken feed. Served over a real local HTTP server, mirroring how the
* app's other store tests exercise the network path.
*/
import { test, expect, beforeAll, afterAll } from "bun:test";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
// Point the config dir at a throwaway directory BEFORE importing the stores
// (their module-level init reads it).
const configHome = mkdtempSync(join(tmpdir(), "podtui-feedless-"));
process.env.XDG_CONFIG_HOME = configHome;
import { useFeedStore } from "../src/stores/feed";
import type { Podcast } from "../src/types/podcast";
const FEED_ID = "12345";
const EPISODE_TITLES = ["Ep 2", "Ep 1"];
function pageHtml(feedUrl: string): string {
return `
`;
}
function feedXml(origin: string): string {
const items = EPISODE_TITLES.map(
(title, i) => `-
${title}
2026-08-0${2 - i}T00:00:00Z
`,
).join("\n");
return `
Delisted Show
Feedless stub test
${items}
`;
}
let server: ReturnType | null = null;
let feedUrl = "";
let pageUrl = "";
beforeAll(() => {
server = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url);
if (url.pathname.endsWith(".rss")) {
return new Response(feedXml(url.origin), {
headers: { "Content-Type": "application/rss+xml" },
});
}
if (url.pathname.startsWith("/show")) {
return new Response(pageHtml(feedUrl), {
headers: { "Content-Type": "text/html" },
});
}
return new Response("not found", { status: 404 });
},
});
feedUrl = `http://127.0.0.1:${server!.port}/feed.rss`;
// The resolver anchors on `/id` in the directory URL.
pageUrl = `http://127.0.0.1:${server!.port}/show/id${FEED_ID}`;
});
afterAll(() => {
server?.stop(true);
rmSync(configHome, { recursive: true, force: true });
});
const makeStub = (feedUrl: string, directoryUrl?: string): Podcast => ({
id: "itunes-12345",
title: "Delisted Show",
description: "Show that left the directory",
author: "Some Network",
feedUrl,
directoryUrl,
lastUpdated: new Date(),
isSubscribed: false,
});
test("addFeed resolves a feedless stub's feed from its directory page", async () => {
const store = useFeedStore();
const feed = await store.addFeed(makeStub("", pageUrl), "itunes");
expect(feed).not.toBeNull();
expect(feed!.podcast.feedUrl).toBe(feedUrl);
// Resolution metadata is dropped from the persisted feed record.
expect(feed!.podcast.directoryUrl).toBeUndefined();
expect(feed!.episodes.map((e) => e.title)).toEqual(EPISODE_TITLES);
// Remove the feed: bun test shares the store singleton across files, and a
// leftover feed (whose server dies in afterAll) would reorder other files'
// refresh assertions.
store.removeFeed(feed!.id);
});
test("addFeed refuses a stub whose directory page cannot be resolved", async () => {
const store = useFeedStore();
const unreachable = makeStub("", "http://127.0.0.1:1/nope/id999");
const feed = await store.addFeed(unreachable, "itunes");
expect(feed).toBeNull();
});
test("addFeed refuses a stub with no directory page at all", async () => {
const store = useFeedStore();
const bare = makeStub("", undefined);
const feed = await store.addFeed(bare, "itunes");
expect(feed).toBeNull();
});