- node --test over routes, client (fake fetcher, include_empty=1), pagination, htmlGate (Go FormatHTML parity cases), sitemap, seo and pl/en i18n parity - package script test and a Testing line in the README
186 lines
7.3 KiB
TypeScript
186 lines
7.3 KiB
TypeScript
// 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<string, string | number> }
|
|
|
|
// 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<string, unknown | (() => unknown)>) {
|
|
const calls: Call[] = []
|
|
const fetcher = async (path: string, query?: Record<string, string | number>) => {
|
|
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)
|
|
})
|