Files
portfolio-next/test/sources/sources.test.ts
T
Lucas WintherandClaude Fable 5.2 07854c6a2b fix: stop inlining fonts into CSS, page the Ghost fetch, audit small fixes
- vite assetsInlineLimit 0: fonts-work.css 42.8 KB -> 5.9 KB
- Ghost posts fetched with limit=100&page=N instead of limit=all
- 404 title uses site.name; drop the duplicate js-class script on /everything/
- deploy/compose.yml: LISTENBRAINZ_USER
- HANDOFF and TODO: the site is live and the sources ran

Audit findings 2, 6, 7, 18, 19, 21 (docs/audits/2026-11-11-audit.md).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 09:23:14 +01:00

281 lines
15 KiB
TypeScript

import { afterEach, beforeEach, expect, test } from 'bun:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { HARDCOVER_QUERY, loadSources, postFromGhost } from '../../src/sources/index.ts';
const FIX = join(import.meta.dir, '../../fixtures/sources');
const read = (n: string) => Bun.file(join(FIX, n)).text();
const env = {
GHOST_URL: 'https://blog.example.test', GHOST_CONTENT_KEY: 'k',
TRAKT_CLIENT_ID: 'cid', TRAKT_USER: 'lucasw89', HARDCOVER_TOKEN: 'tok', LISTENBRAINZ_USER: 'fixture-user',
};
let dir: string;
beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'sources-')); });
afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
type Opts = { traktStatus?: number; history?: string; movieRatings?: string; ratingPages?: string[]; seen?: string[]; lbPages?: unknown[][]; lbStatus?: number };
function fixtureFetch(o: Opts = {}): typeof fetch {
return (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
o.seen?.push(url);
const json = (body: string, status = 200) => new Response(body, { status });
if (url.includes('/ghost/api/content/posts/')) return json(await read('ghost-posts.json'));
if (url.includes('api.trakt.tv')) {
const h = new Headers(init?.headers);
if (h.get('trakt-api-version') !== '2' || h.get('trakt-api-key') !== 'cid') return json('', 400);
if (o.traktStatus) return json('', o.traktStatus);
if (url.endsWith('/history/movies?limit=1')) return json(o.history ?? await read('trakt-history.json'));
const rp = url.match(/\/ratings\/movies\?page=(\d+)&limit=100$/);
if (rp) {
const pages = o.ratingPages ?? [o.movieRatings ?? await read('trakt-movie-ratings.json')];
const body = pages[Number(rp[1]) - 1] ?? '[]';
return new Response(body, { status: 200, headers: { 'x-pagination-page-count': String(pages.length) } });
}
if (url.endsWith('/ratings?limit=5')) return json(await read('trakt-ratings.json'));
}
if (url.startsWith('https://api.listenbrainz.org/1/user/fixture-user/listens?count=1000')) {
if (o.lbStatus) return json('', o.lbStatus);
const pages = o.lbPages ?? [JSON.parse(await read('listenbrainz-listens.json')).payload.listens];
const maxTs = url.match(/&max_ts=(\d+)/)?.[1];
const i = maxTs ? (o.lbPages ?? []).findIndex((p) => (p as { listened_at: number }[]).every((l) => l.listened_at < Number(maxTs))) : 0;
return json(JSON.stringify({ payload: { listens: i < 0 ? [] : pages[i] } }));
}
if (url === 'https://api.hardcover.app/v1/graphql') {
if (new Headers(init?.headers).get('authorization') !== 'Bearer tok') return json('', 401);
return json(await read('hardcover.json'));
}
return json('not found', 404);
}) as typeof fetch;
}
test('ghost posts are fetched page by page until the last page', async () => {
const post = (slug: string) => ({ slug, title: slug, published_at: '2025-01-01T00:00:00.000+00:00', url: `https://blog.example.test/${slug}/` });
const pages = [
{ posts: [post('one'), post('two')], meta: { pagination: { page: 1, pages: 2, next: 2 } } },
{ posts: [post('three')], meta: { pagination: { page: 2, pages: 2, next: null } } },
];
const asked: string[] = [];
const f = (async (input: RequestInfo | URL) => {
const url = new URL(String(input));
if (!url.pathname.endsWith('/ghost/api/content/posts/')) return new Response('', { status: 500 });
asked.push(`${url.searchParams.get('limit')}/${url.searchParams.get('page')}`);
return Response.json(pages[Number(url.searchParams.get('page')) - 1] ?? { posts: [] });
}) as typeof fetch;
const s = await loadSources({ cacheDir: dir, env, fetch: f });
expect(s.status.ghost.ok).toBe(true);
expect(s.posts.map((p) => p.slug)).toEqual(['one', 'two', 'three']);
expect(asked).toEqual(['100/1', '100/2']);
});
test('ghost tags decide side, kind and ai level', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch() });
expect(s.status.ghost.ok).toBe(true);
expect(s.posts.map((p) => [p.side, p.kind, p.ai])).toEqual([
['work', 'post', 'unknown'],
['off-the-clock', 'essay', 'by-hand'],
['off-the-clock', 'life', 'some-ai'],
]);
expect(s.posts[1]!.date).toBe('2025-05-02');
expect(s.posts[1]!.tags).not.toContain('hash-internal');
expect(s.posts[0]!.url).toBe('https://blog.example.test/synthetic-work-post/');
});
test('internal tags never decide anything', () => {
const p = postFromGhost({
slug: 'x', title: 'X', published_at: '2025-01-01T00:00:00Z', url: 'u',
tags: [{ slug: 'hash-off-the-clock', name: '#off-the-clock', visibility: 'internal' }],
});
expect(p.side).toBe('work');
expect(p.tags).toEqual([]);
});
const LAST = { title: 'Synthetic Movie', year: 2019, url: 'https://trakt.tv/movies/synthetic-movie', date: '2025-06-30' };
test('trakt maps the last film with its rating, and recent ratings', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch() });
expect(s.trakt.lastFilm).toEqual({ ...LAST, rating: 9 });
expect(s.trakt.recent[0]).toEqual({ title: 'Synthetic Movie', rating: 9, url: 'https://trakt.tv/movies/synthetic-movie', date: '2025-06-30' });
});
test('a last film not yet rated has no rating', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ movieRatings: '[]' }) });
expect(s.trakt.lastFilm).toEqual(LAST);
});
test('the rating is matched by slug when the trakt id is missing', async () => {
const history = JSON.stringify([{ watched_at: '2025-06-30T20:00:00.000Z', type: 'movie', movie: { title: 'Synthetic Movie', year: 2019, ids: { slug: 'synthetic-movie' } } }]);
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ history }) });
expect(s.trakt.lastFilm?.rating).toBe(9);
});
test('the rating is found on a later page of film ratings, and paging stops there', async () => {
const other = (n: number) => JSON.stringify([{ rated_at: '2026-01-01T00:00:00.000Z', rating: 5, type: 'movie', movie: { title: `Other ${n}`, ids: { trakt: 100 + n, slug: `other-${n}` } } }]);
const seen: string[] = [];
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ ratingPages: [other(1), await read('trakt-movie-ratings.json'), other(3)], seen }) });
expect(s.trakt.lastFilm?.rating).toBe(9);
expect(seen.filter((u) => u.includes('/ratings/movies')).length).toBe(2);
});
test('a history entry without a date is skipped, not fatal', async () => {
const history = JSON.stringify([{ type: 'movie', movie: { title: 'No Date', ids: { trakt: 1 } } }]);
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ history }) });
expect(s.status.trakt.ok).toBe(true);
expect(s.trakt.lastFilm).toBeNull();
});
test('an empty history means no last film', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ history: '[]' }) });
expect(s.status.trakt.ok).toBe(true);
expect(s.trakt.lastFilm).toBeNull();
expect(s.trakt.recent).toHaveLength(2);
});
test('never asks Trakt what is playing right now', async () => {
const seen: string[] = [];
await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ seen }) });
expect(seen.some((u) => u.includes('api.trakt.tv'))).toBe(true);
expect(seen.some((u) => u.endsWith('/watching'))).toBe(false);
});
test('a cache written before the last film existed reads as no last film', async () => {
await Bun.write(join(dir, 'trakt.json'), JSON.stringify({ fetchedAt: '2026-01-01T00:00:00.000Z', data: { watching: null, recent: [] } }));
const down = (async () => { throw new Error('network down'); }) as unknown as typeof fetch;
const s = await loadSources({ cacheDir: dir, env, fetch: down });
expect(s.trakt).toEqual({ lastFilm: null, recent: [] });
});
test('hardcover maps reading and recent ratings', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch() });
expect(s.hardcover.reading).toEqual([{
title: 'A Synthetic Novel', author: 'Ada Example', progress: 25,
url: 'https://hardcover.app/books/a-synthetic-novel', cover: 'https://covers.example.test/a-synthetic-novel.jpg',
}]);
expect(s.hardcover.recent[0]).toEqual({ title: 'Another Synthetic Book', rating: 4.5, url: 'https://hardcover.app/books/another-synthetic-book', date: '2025-06-25' });
expect(HARDCOVER_QUERY).toContain('status_id');
});
test('a failing source returns cached data and reports the error', async () => {
const first = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch() });
const down = (async () => { throw new Error('network down'); }) as unknown as typeof fetch;
const second = await loadSources({ cacheDir: dir, env, fetch: down });
expect(second.trakt).toEqual(first.trakt);
expect(second.posts).toEqual(first.posts);
expect(second.status.trakt.ok).toBe(false);
expect(second.status.trakt.error).toContain('network down');
expect(second.status.trakt.fetchedAt).toBe(first.status.trakt.fetchedAt!);
});
test('a failing source with no cache returns empty data', async () => {
const down = (async () => { throw new Error('boom'); }) as unknown as typeof fetch;
const s = await loadSources({ cacheDir: dir, env, fetch: down });
expect(s.posts).toEqual([]);
expect(s.trakt).toEqual({ lastFilm: null, recent: [] });
expect(s.hardcover).toEqual({ reading: [], recent: [] });
expect(s.status.ghost.ok).toBe(false);
});
test('non-2xx and bad JSON are failures, not throws', async () => {
const bad = (async (u: RequestInfo | URL) =>
String(u).includes('ghost') ? new Response('<html>', { status: 200 }) : new Response('', { status: 500 })) as unknown as typeof fetch;
const s = await loadSources({ cacheDir: dir, env, fetch: bad });
expect(s.status.ghost.ok).toBe(false);
expect(s.status.trakt.error).toContain('500');
expect(s.status.hardcover.ok).toBe(false);
});
test('missing credentials report a clear error, not a throw', async () => {
const s = await loadSources({ cacheDir: dir, env: {}, fetch: fixtureFetch() });
expect(s.status.hardcover.error).toContain('HARDCOVER_TOKEN');
expect(s.status.trakt.error).toContain('TRAKT_CLIENT_ID');
expect(s.status.ghost.error).toContain('GHOST');
});
test('private trakt profile is reported', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ traktStatus: 403 }) });
expect(s.status.trakt.ok).toBe(false);
expect(s.status.trakt.error).toBe('Trakt profile not readable with an API key (private?)');
});
test('SOURCES_FIXTURES reads fixture files instead of fetching', async () => {
const never = (async () => { throw new Error('network must not be used'); }) as unknown as typeof fetch;
const before = Date.now();
const s = await loadSources({ cacheDir: dir, env: { SOURCES_FIXTURES: FIX }, fetch: never });
for (const k of ['ghost', 'trakt', 'hardcover'] as const) {
expect(s.status[k].ok).toBe(true);
expect(Date.parse(s.status[k].fetchedAt!)).toBeGreaterThanOrEqual(before);
}
expect(s.posts).toHaveLength(3);
expect(s.trakt.lastFilm?.title).toBe('Synthetic Movie');
expect(s.hardcover.reading[0]!.title).toBe('A Synthetic Novel');
});
test('a stalled request times out and falls back to the cache', async () => {
const first = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch() });
const stalled = ((_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<Response>((_res, rej) => {
init?.signal?.addEventListener('abort', () => rej(init.signal!.reason));
})) as typeof fetch;
const second = await loadSources({ cacheDir: dir, env, fetch: stalled, timeoutMs: 50 });
for (const k of ['ghost', 'trakt', 'hardcover'] as const) expect(second.status[k].ok).toBe(false);
expect(second.posts).toEqual(first.posts);
expect(second.trakt).toEqual(first.trakt);
expect(second.hardcover).toEqual(first.hardcover);
});
/* ---------- ListenBrainz: the week's top track (spec 004) ---------- */
const NOW = (1751371200 + 3600) * 1000; // an hour after the newest fixture listen
const lb = (ts: number, artist: string, track: string) => ({ listened_at: ts, track_metadata: { artist_name: artist, track_name: track } });
test('listening: the most played track of the last 7 days, variants counted as one', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch(), now: NOW });
expect(s.status.listenbrainz.ok).toBe(true);
expect(s.listening.top).toEqual({ artist: 'Fixture Band', title: 'Synthetic Track A', plays: 3, url: 'https://listenbrainz.org/user/fixture-user/' });
});
test('listening: a tie goes to the track played most recently', async () => {
const t = 1751371200;
const page = [lb(t, 'B', 'Two'), lb(t - 10, 'A', 'One'), lb(t - 20, 'B', 'Two'), lb(t - 30, 'A', 'One')];
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ lbPages: [page] }), now: NOW });
expect(s.listening.top?.title).toBe('Two');
});
test('listening: pages back until the window edge, and no further', async () => {
const t = 1751371200;
const page1 = Array.from({ length: 1000 }, (_, i) => lb(t - i, 'A', 'One'));
const page2 = [lb(t - 2000, 'B', 'Two'), lb(t - 9 * 86400, 'C', 'Old')];
const page3 = [lb(t - 10 * 86400, 'C', 'Old')];
const seen: string[] = [];
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ lbPages: [page1, page2, page3], seen }), now: NOW });
expect(s.listening.top).toMatchObject({ title: 'One', plays: 1000 });
expect(seen.filter((u) => u.includes('listenbrainz')).length).toBe(2);
});
test('listening: nothing in the last 7 days means no top track', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ lbPages: [[lb(1751371200 - 8 * 86400, 'C', 'Old')]] }), now: NOW });
expect(s.listening.top).toBeNull();
});
test('listening: a failing ListenBrainz falls back like the other sources', async () => {
const s = await loadSources({ cacheDir: dir, env, fetch: fixtureFetch({ lbStatus: 503 }), now: NOW });
expect(s.status.listenbrainz.ok).toBe(false);
expect(s.status.listenbrainz.error).toContain('503');
expect(s.listening).toEqual({ top: null });
});
test('listening: a cache from before ListenBrainz reads as no top track', async () => {
const down = (async () => { throw new Error('network down'); }) as unknown as typeof fetch;
const s = await loadSources({ cacheDir: dir, env, fetch: down });
expect(s.listening).toEqual({ top: null });
});
test('listening: without LISTENBRAINZ_USER nothing is fetched', async () => {
const seen: string[] = [];
const s = await loadSources({ cacheDir: dir, env: {}, fetch: fixtureFetch({ seen }) });
expect(seen.some((u) => u.includes('listenbrainz'))).toBe(false);
expect(s.status.listenbrainz.error).toContain('LISTENBRAINZ_USER is not set');
expect(s.listening).toEqual({ top: null });
});