260 lines
9.3 KiB
TypeScript
260 lines
9.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const mockRequest = vi.fn()
|
|
const mockWith = vi.fn()
|
|
|
|
vi.mock('@directus/sdk', () => ({
|
|
createDirectus: vi.fn(() => ({ with: mockWith })),
|
|
rest: vi.fn(() => 'rest-plugin'),
|
|
staticToken: vi.fn(() => 'token-plugin'),
|
|
readItems: vi.fn((collection: string) => ({ _op: 'readItems', collection })),
|
|
readSingleton: vi.fn((collection: string) => ({ _op: 'readSingleton', collection })),
|
|
readItem: vi.fn((collection: string, id: string) => ({ _op: 'readItem', collection, id })),
|
|
}))
|
|
|
|
mockWith.mockReturnValue({ with: mockWith, request: mockRequest })
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
mockRequest.mockReset()
|
|
process.env.DIRECTUS_URL = 'https://cms.achmad.dev'
|
|
process.env.DIRECTUS_TOKEN = 'test-token'
|
|
})
|
|
|
|
describe('getAllCategories', () => {
|
|
it('returns array of categories from Directus', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{ id: '1', name: 'Anime', slug: 'anime', description: null },
|
|
{ id: '2', name: 'VTubers', slug: 'vtubers', description: null },
|
|
])
|
|
const { getAllCategories } = await import('@/lib/directus')
|
|
const result = await getAllCategories()
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].slug).toBe('anime')
|
|
})
|
|
})
|
|
|
|
describe('getArticleBySlug', () => {
|
|
it('returns null when no article matches', async () => {
|
|
mockRequest.mockResolvedValue([])
|
|
const { getArticleBySlug } = await import('@/lib/directus')
|
|
const result = await getArticleBySlug('nonexistent')
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns the first matching article', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{
|
|
id: '1', title: 'Frieren S2', slug: 'frieren-s2', status: 'published',
|
|
content: '<p>Body</p>', excerpt: 'Short', featured_image: null,
|
|
published_at: '2026-05-28T00:00:00Z', date_created: '2026-05-28T00:00:00Z', is_featured: false,
|
|
seo_title: null, seo_description: null,
|
|
category: { id: '1', name: 'Anime', slug: 'anime', description: null },
|
|
tags: [],
|
|
},
|
|
])
|
|
const { getArticleBySlug } = await import('@/lib/directus')
|
|
const result = await getArticleBySlug('frieren-s2')
|
|
expect(result?.title).toBe('Frieren S2')
|
|
})
|
|
})
|
|
|
|
describe('getAssetUrl', () => {
|
|
it('constructs a proxied asset URL from a file UUID', async () => {
|
|
const { getAssetUrl } = await import('@/lib/directus')
|
|
const url = getAssetUrl('abc-123-uuid')
|
|
expect(url).toBe('/api/image/abc-123-uuid')
|
|
})
|
|
|
|
it('appends width and quality query params', async () => {
|
|
const { getAssetUrl } = await import('@/lib/directus')
|
|
const url = getAssetUrl('abc-123-uuid', { width: 800, quality: 80 })
|
|
expect(url).toContain('width=800')
|
|
expect(url).toContain('quality=80')
|
|
expect(url).toContain('/api/image/')
|
|
})
|
|
|
|
it('handles height param', async () => {
|
|
const { getAssetUrl } = await import('@/lib/directus')
|
|
const url = getAssetUrl('abc-123-uuid', { height: 600 })
|
|
expect(url).toContain('height=600')
|
|
})
|
|
|
|
it('omits params when none are passed', async () => {
|
|
const { getAssetUrl } = await import('@/lib/directus')
|
|
const url = getAssetUrl('abc-123-uuid')
|
|
expect(url).toBe('/api/image/abc-123-uuid')
|
|
})
|
|
})
|
|
|
|
describe('getCategoryBySlug', () => {
|
|
it('returns the matching category when found', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{ id: '1', name: 'Anime', slug: 'anime', description: null },
|
|
])
|
|
const { getCategoryBySlug } = await import('@/lib/directus')
|
|
const result = await getCategoryBySlug('anime')
|
|
expect(result).not.toBeNull()
|
|
expect(result?.name).toBe('Anime')
|
|
})
|
|
|
|
it('returns null when no category matches', async () => {
|
|
mockRequest.mockResolvedValue([])
|
|
const { getCategoryBySlug } = await import('@/lib/directus')
|
|
const result = await getCategoryBySlug('nonexistent')
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('getArticles', () => {
|
|
it('returns articles with default limit and sort', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{
|
|
id: '1', title: 'Article 1', slug: 'article-1', status: 'published',
|
|
excerpt: null, featured_image: null,
|
|
published_at: '2026-05-30T00:00:00Z', date_created: '2026-05-30T00:00:00Z',
|
|
is_featured: false,
|
|
category: { id: '1', name: 'Anime', slug: 'anime' },
|
|
},
|
|
])
|
|
const { getArticles } = await import('@/lib/directus')
|
|
const result = await getArticles()
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].title).toBe('Article 1')
|
|
})
|
|
|
|
it('passes categorySlug filter when provided', async () => {
|
|
mockRequest
|
|
.mockResolvedValueOnce([{ id: '1', name: 'Games', slug: 'games', description: null }])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
id: '1', title: 'Game Article', slug: 'game-1', status: 'published',
|
|
excerpt: null, featured_image: null,
|
|
published_at: '2026-05-30T00:00:00Z', date_created: '2026-05-30T00:00:00Z',
|
|
is_featured: false,
|
|
category: { id: '1', name: 'Games', slug: 'games' },
|
|
},
|
|
])
|
|
const { getArticles } = await import('@/lib/directus')
|
|
const result = await getArticles({ categorySlug: 'games' })
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].title).toBe('Game Article')
|
|
})
|
|
|
|
it('passes categoryId filter when provided', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{
|
|
id: '1', title: 'Music Article', slug: 'music-1', status: 'published',
|
|
excerpt: null, featured_image: null,
|
|
published_at: '2026-05-30T00:00:00Z', date_created: '2026-05-30T00:00:00Z',
|
|
is_featured: false,
|
|
category: { id: '2', name: 'Music', slug: 'music' },
|
|
},
|
|
])
|
|
const { getArticles } = await import('@/lib/directus')
|
|
const result = await getArticles({ categoryId: '2' })
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].title).toBe('Music Article')
|
|
})
|
|
|
|
it('applies limit and offset', async () => {
|
|
const { getArticles } = await import('@/lib/directus')
|
|
const { readItems } = await import('@directus/sdk')
|
|
await getArticles({ limit: 5, offset: 10 })
|
|
expect(readItems).toHaveBeenCalledWith(
|
|
'articles',
|
|
expect.objectContaining({ limit: 5, offset: 10 }),
|
|
)
|
|
})
|
|
|
|
it('returns empty array when no articles match categorySlug and category does not exist', async () => {
|
|
mockRequest.mockResolvedValue([])
|
|
const { getArticles } = await import('@/lib/directus')
|
|
const result = await getArticles({ categorySlug: 'unknown' })
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('getRelatedArticles', () => {
|
|
it('returns related articles in the same category excluding the current article', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{
|
|
id: '2', title: 'Related', slug: 'related', status: 'published',
|
|
excerpt: null, featured_image: null,
|
|
published_at: '2026-05-29T00:00:00Z', date_created: '2026-05-29T00:00:00Z',
|
|
category: { id: '1', name: 'Games', slug: 'games' },
|
|
},
|
|
])
|
|
const { getRelatedArticles } = await import('@/lib/directus')
|
|
const result = await getRelatedArticles('games', 'current-article')
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].title).toBe('Related')
|
|
})
|
|
})
|
|
|
|
describe('getSiteSettings', () => {
|
|
it('returns site settings with hero article', async () => {
|
|
mockRequest.mockResolvedValue({
|
|
id: '1',
|
|
site_name: 'Kotobane',
|
|
hero_article: { id: '1', title: 'Hero', slug: 'hero' },
|
|
})
|
|
const { getSiteSettings } = await import('@/lib/directus')
|
|
const result = await getSiteSettings()
|
|
expect(result.site_name).toBe('Kotobane')
|
|
expect(result.hero_article?.title).toBe('Hero')
|
|
})
|
|
|
|
it('returns settings without hero when none is set', async () => {
|
|
mockRequest.mockResolvedValue({
|
|
id: '1',
|
|
site_name: 'Kotobane',
|
|
hero_article: null,
|
|
})
|
|
const { getSiteSettings } = await import('@/lib/directus')
|
|
const result = await getSiteSettings()
|
|
expect(result.hero_article).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('searchArticles', () => {
|
|
it('searches articles by title or excerpt', async () => {
|
|
mockRequest.mockResolvedValue([
|
|
{
|
|
id: '1', title: 'Frieren News', slug: 'frieren-news',
|
|
category: { slug: 'anime', name: 'Anime' },
|
|
},
|
|
])
|
|
const { searchArticles } = await import('@/lib/directus')
|
|
const result = await searchArticles('Frieren')
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].title).toBe('Frieren News')
|
|
})
|
|
|
|
it('returns empty array when no matches', async () => {
|
|
mockRequest.mockResolvedValue([])
|
|
const { searchArticles } = await import('@/lib/directus')
|
|
const result = await searchArticles('zzzzzz')
|
|
expect(result).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('getArticlePathById', () => {
|
|
it('returns slug and category slug', async () => {
|
|
mockRequest.mockResolvedValue({
|
|
slug: 'my-article',
|
|
category: { slug: 'anime' },
|
|
})
|
|
const { getArticlePathById } = await import('@/lib/directus')
|
|
const result = await getArticlePathById('uuid-123')
|
|
expect(result).toEqual({ slug: 'my-article', category: { slug: 'anime' } })
|
|
})
|
|
|
|
it('returns null when article is not found', async () => {
|
|
mockRequest.mockRejectedValue(new Error('Not found'))
|
|
const { getArticlePathById } = await import('@/lib/directus')
|
|
const result = await getArticlePathById('nonexistent')
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|