Files
PodTui/tests/source-credentials.test.ts
Michael Freno ef9fc13aaa feat(settings): add Podcast Index fallback source with credential storage
Podcast Index (api.podcastindex.org) ships as a disabled, key-less source
and is only consulted as a fallback when primary search results are fewer
than 3 — never on the hot path, never when disabled or credential-less.
A failed fallback leaves primary results intact.

Credentials are user-supplied: enabling the source pops a dialog that
asks for the free key+secret, prefilled masked (first 3 chars + "...")
when already stored; toggling off never clears them. Secrets prefer the
macOS keychain (security CLI, encrypted at rest) with a plaintext
config.json fallback when the keychain is unavailable; sources carry only
a hasCredentials/credentialStorage marker, and legacy plaintext keys in
existing configs are migrated on load.

Auth follows the documented scheme: X-Auth-Key, X-Auth-Date (epoch) and
Authorization = sha1(key + secret + date). Dead feeds are filtered, feed
URLs are used directly, and episode-scope search is a no-op (no endpoint).
2026-08-11 00:37:56 -04:00

47 lines
1.5 KiB
TypeScript

/**
* Credential resolution tests against the real module (no mocks).
*
* Only the plaintext branch is exercised: the keychain branch spawns the
* `security` CLI and would depend on the host machine's keychain state
* (the keychain-backed pipeline is covered in podcastindex-fallback.test.ts
* with a stubbed module). The plaintext branch must never touch the
* keychain — it is the fallback that keeps the source working on machines
* without a usable macOS keychain.
*/
import { test, expect } from "bun:test";
import { resolveSourceCredentials } from "../src/utils/source-credentials";
import { SourceType } from "../src/types/source";
import type { PodcastSource } from "../src/types/source";
const base: PodcastSource = {
id: "podcastindex",
name: "Podcast Index",
type: SourceType.API,
baseUrl: "https://api.podcastindex.org/api/1.0/search/byterm",
enabled: true,
hasCredentials: true,
};
test("plaintext-storage sources resolve their own fields, no keychain call", async () => {
const source: PodcastSource = {
...base,
credentialStorage: "plaintext",
apiKey: "PLAINTEXTKEY",
apiSecret: "PLAINTEXTSECRET",
};
expect(await resolveSourceCredentials(source)).toEqual({
apiKey: "PLAINTEXTKEY",
apiSecret: "PLAINTEXTSECRET",
});
});
test("plaintext-storage sources with empty fields resolve to null", async () => {
const source: PodcastSource = {
...base,
credentialStorage: "plaintext",
apiKey: undefined,
apiSecret: undefined,
};
expect(await resolveSourceCredentials(source)).toBeNull();
});