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).
This commit is contained in:
46
tests/source-credentials.test.ts
Normal file
46
tests/source-credentials.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
Reference in New Issue
Block a user