// The Journal client against a fake fetcher: query building, include_empty, // category lookup and 404 handling. No network. import { test } from 'node:test' import assert from 'node:assert/strict' import { JOURNAL_DEFAULT_PER_PAGE, JOURNAL_MAX_PER_PAGE, buildPostListQuery, clampPerPage, createJournalClient, encodeSlugPath, flattenCategories, isNotFound, } from '../app/utils/client.ts' import type { JournalCategory } from '../app/utils/types.ts' type Call = { path: string; query?: Record } // A fetcher that records each call and answers from `routes` by path; a // function value may throw to simulate an HTTP error. function fakeFetcher(routes: Record unknown)>) { const calls: Call[] = [] const fetcher = async (path: string, query?: Record) => { calls.push(query === undefined ? { path } : { path, query }) if (!(path in routes)) throw Object.assign(new Error(`no route ${path}`), { status: 404 }) const value = routes[path] return typeof value === 'function' ? (value as () => unknown)() : value } return { fetcher, calls } } function category(id: number, slug: string, children?: JournalCategory[]): JournalCategory { const row: JournalCategory = { id, name: slug, slug, description: null, content: null, parent_id: null, nest_depth: 0, post_count: 0, } if (children) row.children = children return row } const TREE = [category(1, 'grzyby', [category(2, 'cordyceps', [category(3, 'militaris')])]), category(4, 'chaga')] test('the per_page limits match the API', () => { assert.equal(JOURNAL_MAX_PER_PAGE, 30) assert.equal(JOURNAL_DEFAULT_PER_PAGE, 9) }) test('clampPerPage clamps to 1..30 and defaults to 9', () => { assert.equal(clampPerPage(0), 1) assert.equal(clampPerPage(-5), 1) assert.equal(clampPerPage(31), 30) assert.equal(clampPerPage(12), 12) assert.equal(clampPerPage(12.7), 12) assert.equal(clampPerPage('12'), 12) assert.equal(clampPerPage('abc'), 9) assert.equal(clampPerPage(''), 9) assert.equal(clampPerPage(undefined), 9) assert.equal(clampPerPage(Number.NaN), 9) }) test('buildPostListQuery always sends per_page and the rest only when set', () => { assert.deepEqual(buildPostListQuery(), { per_page: 9 }) assert.deepEqual( buildPostListQuery({ page: 2, perPage: 12, sort: 'published_at desc', category: 'cordyceps', tag: 'zdrowie' }), { per_page: 12, page: 2, sort: 'published_at desc', category: 'cordyceps', tag: 'zdrowie' }, ) }) test('buildPostListQuery drops an invalid page and empty strings', () => { assert.deepEqual(buildPostListQuery({ page: 0, sort: '', category: '', tag: '' }), { per_page: 9 }) assert.deepEqual(buildPostListQuery({ page: 1.5 }), { per_page: 9 }) assert.deepEqual(buildPostListQuery({ perPage: 100 }), { per_page: 30 }) }) test('encodeSlugPath keeps / as a separator', () => { assert.equal(encodeSlugPath('a b/c?d'), 'a%20b/c%3Fd') }) test('flattenCategories walks parents before children', () => { assert.deepEqual(flattenCategories(TREE).map((c) => c.slug), ['grzyby', 'cordyceps', 'militaris', 'chaga']) assert.deepEqual(flattenCategories(null), []) }) test('isNotFound reads status, statusCode and response.status', () => { assert.equal(isNotFound({ status: 404 }), true) assert.equal(isNotFound({ statusCode: 404 }), true) assert.equal(isNotFound({ response: { status: 404 } }), true) assert.equal(isNotFound({ status: 500 }), false) assert.equal(isNotFound(new Error('boom')), false) assert.equal(isNotFound(null), false) assert.equal(isNotFound('404'), false) }) test('listCategories always sends include_empty=1', async () => { const { fetcher, calls } = fakeFetcher({ '/categories': { data: TREE } }) const client = createJournalClient(fetcher) assert.deepEqual(await client.listCategories(), TREE) await client.findCategory('chaga') assert.equal(calls.length, 2) for (const call of calls) { assert.equal(call.path, '/categories') assert.deepEqual(call.query, { include_empty: 1 }) } }) test('listCategories gives [] for a malformed body', async () => { const { fetcher } = fakeFetcher({ '/categories': { data: 'nope' } }) assert.deepEqual(await createJournalClient(fetcher).listCategories(), []) }) test('findCategory finds a nested child and null for an unknown slug', async () => { const { fetcher } = fakeFetcher({ '/categories': { data: TREE } }) const client = createJournalClient(fetcher) assert.equal((await client.findCategory('militaris'))?.id, 3) assert.equal(await client.findCategory('reishi'), null) }) test('listPosts sends the built query', async () => { const page = { data: [], meta: { current_page: 2, last_page: 2, per_page: 12, total: 13 } } const { fetcher, calls } = fakeFetcher({ '/posts': page }) const res = await createJournalClient(fetcher).listPosts({ page: 2, perPage: 12, category: 'cordyceps' }) assert.deepEqual(res, page) assert.deepEqual(calls, [{ path: '/posts', query: { per_page: 12, page: 2, category: 'cordyceps' } }]) }) test('getPost returns the data and encodes the slug', async () => { const { fetcher, calls } = fakeFetcher({ '/posts/a%20b': { data: { id: 1, slug: 'a b' } } }) const post = await createJournalClient(fetcher).getPost('a b') assert.equal(post?.id, 1) assert.equal(calls[0]?.path, '/posts/a%20b') }) test('getPost returns null for status 404 and statusCode 404', async () => { const byStatus = fakeFetcher({ '/posts/gone': () => { throw Object.assign(new Error('Not Found'), { status: 404 }) }, }) assert.equal(await createJournalClient(byStatus.fetcher).getPost('gone'), null) const byStatusCode = fakeFetcher({ '/posts/gone': () => { throw Object.assign(new Error('Not Found'), { statusCode: 404 }) }, }) assert.equal(await createJournalClient(byStatusCode.fetcher).getPost('gone'), null) }) test('getPost rethrows a 500', async () => { const { fetcher } = fakeFetcher({ '/posts/boom': () => { throw Object.assign(new Error('Server Error'), { status: 500 }) }, }) await assert.rejects(createJournalClient(fetcher).getPost('boom'), /Server Error/) }) test('listTags returns the data or []', async () => { const tags = [{ id: 1, name: 'zdrowie', slug: 'zdrowie' }] assert.deepEqual(await createJournalClient(fakeFetcher({ '/tags': { data: tags } }).fetcher).listTags(), tags) assert.deepEqual(await createJournalClient(fakeFetcher({ '/tags': {} }).fetcher).listTags(), []) }) test('getAuthor and listAuthorPosts encode the author and map 404 to null', async () => { const author = { id: 7, slug: 'jan kowalski', first_name: 'Jan', last_name: 'Kowalski' } const page = { data: [], meta: { current_page: 1, last_page: 1, per_page: 10, total: 0 } } const { fetcher, calls } = fakeFetcher({ '/authors/jan%20kowalski': { data: author }, '/authors/jan%20kowalski/posts': page, }) const client = createJournalClient(fetcher) assert.deepEqual(await client.getAuthor('jan kowalski'), author) assert.deepEqual(await client.listAuthorPosts('jan kowalski', { page: 2, perPage: 10, sort: 'published_at desc' }), page) assert.deepEqual(calls[1], { path: '/authors/jan%20kowalski/posts', query: { per_page: 10, page: 2, sort: 'published_at desc' }, }) assert.equal(await client.getAuthor('nobody'), null) assert.equal(await client.listAuthorPosts('nobody'), null) })