import { CreateArticleModel, UpdateArticleModel, } from '@/lib/feature/article/article.model'; import { deleteArticleByExternalId, getArticleByExternalId, getArticleBySlug, getArticlesByAuthorId, getArticlesPaginated, saveArticle, updateArticleByExternalId, } from '@/lib/feature/article/article.service'; import { CreateUserModel } from '@/lib/feature/user/user.model'; import { saveUser } from '@/lib/feature/user/user.service'; import { UUIDv4 } from '@/utils/types/uuid'; import { AbstractStartedContainer } from 'testcontainers'; import { startTestDB } from '~/tests/setup/setup-db'; jest.mock('@clerk/nextjs/server', () => ({ clerkClient: jest.fn(), })); describe('ArticleService', () => { let container: AbstractStartedContainer; let authorId: string; beforeAll(async () => { container = await startTestDB(); const author: CreateUserModel = { name: 'Article Author', email: 'author@email.com', role: 'admin', }; const savedAuthor = await saveUser(author); if (!savedAuthor.ok) throw savedAuthor.error; authorId = savedAuthor.value.id; }, 1_000_000); afterAll(async () => { await container.stop(); }, 1_000_000); test('can save article', async () => { const articleToSave: CreateArticleModel = { title: 'Test Article', slug: 'test-article', description: 'A test article description', coverImageUrl: 'https://example.com/cover.png', content: 'This is the article content.', authorId: authorId, }; const result = await saveArticle(articleToSave); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value.title).toBe(articleToSave.title); expect(result.value.slug).toBe(articleToSave.slug); expect(result.value.description).toBe(articleToSave.description); expect(result.value.coverImageUrl).toBe(articleToSave.coverImageUrl); expect(result.value.content).toBe(articleToSave.content); expect(result.value.authorId).toBe(authorId); expect(result.value.externalId).toBeDefined(); }); test('cannot save article with existing slug', async () => { const articleToSave: CreateArticleModel = { title: 'Duplicate Slug Article', slug: 'test-article', description: 'Another article with the same slug', coverImageUrl: 'https://example.com/cover2.png', content: 'Duplicate content.', authorId: authorId, }; const result = await saveArticle(articleToSave); expect(result.ok).toBe(false); if (result.ok) return; expect(result.error.message).toContain(articleToSave.slug); }); test('can getArticleBySlug', async () => { const result = await getArticleBySlug('test-article'); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeDefined(); expect(result.value?.slug).toBe('test-article'); expect(result.value?.title).toBe('Test Article'); expect(result.value?.authorId).toBe(authorId); expect(result.value?.externalId).toBeDefined(); }); test('cannot getArticleBySlug with non-existing slug', async () => { const result = await getArticleBySlug('non-existing-slug'); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeNull(); }); test('can getArticlesByAuthorId', async () => { const result = await getArticlesByAuthorId(authorId); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeDefined(); expect(result.value.length).toBeGreaterThanOrEqual(1); expect(result.value[0].authorId).toBe(authorId); }); test('getArticlesByAuthorId returns empty for non-existing author', async () => { const result = await getArticlesByAuthorId('9999'); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeDefined(); expect(result.value.length).toBe(0); }); test('can getArticleByExternalId', async () => { const slugResult = await getArticleBySlug('test-article'); expect(slugResult.ok).toBe(true); if (!slugResult.ok) return; const article = slugResult.value; expect(article).toBeDefined(); const result = await getArticleByExternalId( article!.externalId as UUIDv4 ); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeDefined(); expect(result.value?.title).toBe(article!.title); expect(result.value?.slug).toBe(article!.slug); expect(result.value?.externalId).toBe(article!.externalId); }); test('getArticleByExternalId returns null for non-existing id', async () => { const result = await getArticleByExternalId( '00000000-0000-4000-a000-000000000000' as UUIDv4 ); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeNull(); }); test('can getArticlesPaginated with defaults', async () => { const result = await getArticlesPaginated(); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value.data.length).toBeGreaterThanOrEqual(1); expect(result.value.page).toBe(1); expect(result.value.pageSize).toBe(10); expect(result.value.total).toBeGreaterThanOrEqual(1); expect(result.value.totalPages).toBeGreaterThanOrEqual(1); }); test('can getArticlesPaginated with custom page size', async () => { // Save extra articles to test pagination for (let i = 0; i < 3; i++) { const r = await saveArticle({ title: `Paginated Article ${i}`, slug: `paginated-article-${i}`, description: `Description ${i}`, coverImageUrl: `https://example.com/cover-${i}.png`, content: `Content ${i}`, authorId: authorId, }); if (!r.ok) throw r.error; } const firstPageResult = await getArticlesPaginated(1, 2); expect(firstPageResult.ok).toBe(true); if (!firstPageResult.ok) return; expect(firstPageResult.value.data.length).toBe(2); expect(firstPageResult.value.page).toBe(1); expect(firstPageResult.value.pageSize).toBe(2); expect(firstPageResult.value.total).toBeGreaterThanOrEqual(4); expect(firstPageResult.value.totalPages).toBeGreaterThanOrEqual(2); const secondPageResult = await getArticlesPaginated(2, 2); expect(secondPageResult.ok).toBe(true); if (!secondPageResult.ok) return; expect(secondPageResult.value.data.length).toBe(2); expect(secondPageResult.value.page).toBe(2); expect(secondPageResult.value.pageSize).toBe(2); }); test('can getArticlesPaginated returns empty on out-of-range page', async () => { const result = await getArticlesPaginated(999, 10); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value.data.length).toBe(0); expect(result.value.page).toBe(999); expect(result.value.total).toBeGreaterThanOrEqual(1); }); test('can update article', async () => { const dataToUpdate: UpdateArticleModel = { title: 'Updated Article Title', description: 'Updated description', }; const slugResult = await getArticleBySlug('test-article'); expect(slugResult.ok).toBe(true); if (!slugResult.ok) return; const article = slugResult.value; expect(article).toBeDefined(); const result = await updateArticleByExternalId( article!.externalId, dataToUpdate ); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.value).toBeDefined(); expect(result.value.title).toBe(dataToUpdate.title); expect(result.value.description).toBe(dataToUpdate.description); expect(result.value.slug).toBe(article!.slug); expect(result.value.content).toBe(article!.content); expect(result.value.externalId).toBeDefined(); }); test('cannot update non-existing article', async () => { const dataToUpdate: UpdateArticleModel = { title: 'Updated Article Title', }; const result = await updateArticleByExternalId('9999', dataToUpdate); expect(result.ok).toBe(false); if (result.ok) return; expect(result.error.message).toContain('9999'); }); test('can delete article', async () => { const articleToSave: CreateArticleModel = { title: 'Article to Delete', slug: 'article-to-delete', description: 'This article will be deleted', coverImageUrl: 'https://example.com/delete-cover.png', content: 'Content to delete.', authorId: authorId, }; const saveResult = await saveArticle(articleToSave); expect(saveResult.ok).toBe(true); if (!saveResult.ok) return; expect(saveResult.value.externalId).toBeDefined(); const deleteResult = await deleteArticleByExternalId( saveResult.value.externalId ); expect(deleteResult.ok).toBe(true); const getResult = await getArticleBySlug('article-to-delete'); expect(getResult.ok).toBe(true); if (!getResult.ok) return; expect(getResult.value).toBeNull(); }); test('cannot delete non-existing article', async () => { const result = await deleteArticleByExternalId('9999'); expect(result.ok).toBe(false); if (result.ok) return; expect(result.error.message).toContain('9999'); }); });