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: '

Body

', 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() }) })