Files
vue-journal-layer/tests/client.test.ts
Jakub Zych a4dca42d77 test(16-05): Journal layer util tests
- 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
2026-10-09 20:03:46 +02:00

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)
})