import { getArticlesPaginated } from '@/lib/feature/article/article.external'; import { CreateArticleModel, UpdateArticleModel, } from '@/lib/feature/article/article.model'; import { deleteArticle, getArticleByExternalId, getArticleBySlug, getArticlesByAuthorId, saveArticle, updateArticle, } 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); authorId = savedAuthor.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 savedArticle = await saveArticle(articleToSave); expect(savedArticle.id).toBeDefined(); expect(savedArticle.title).toBe(articleToSave.title); expect(savedArticle.slug).toBe(articleToSave.slug); expect(savedArticle.description).toBe(articleToSave.description); expect(savedArticle.coverImageUrl).toBe(articleToSave.coverImageUrl); expect(savedArticle.content).toBe(articleToSave.content); expect(savedArticle.authorId).toBe(authorId); expect(savedArticle.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, }; await expect(saveArticle(articleToSave)).rejects.toThrow( `Article with slug ${articleToSave.slug} already exists` ); }); test('can getArticleBySlug', async () => { const article = await getArticleBySlug('test-article'); expect(article).toBeDefined(); expect(article?.slug).toBe('test-article'); expect(article?.title).toBe('Test Article'); expect(article?.authorId).toBe(authorId); expect(article?.externalId).toBeDefined(); }); test('cannot getArticleBySlug with non-existing slug', async () => { await expect(getArticleBySlug('non-existing-slug')).resolves.toBeNull(); }); test('can getArticlesByAuthorId', async () => { const articles = await getArticlesByAuthorId(authorId); expect(articles).toBeDefined(); expect(articles.length).toBeGreaterThanOrEqual(1); expect(articles[0].authorId).toBe(authorId); }); test('getArticlesByAuthorId returns empty for non-existing author', async () => { const articles = await getArticlesByAuthorId('9999'); expect(articles).toBeDefined(); expect(articles.length).toBe(0); }); test('can getArticleByExternalId', async () => { const article = await getArticleBySlug('test-article'); expect(article).toBeDefined(); const foundArticle = await getArticleByExternalId( article!.externalId as UUIDv4 ); expect(foundArticle).toBeDefined(); expect(foundArticle?.id).toBe(article!.id); expect(foundArticle?.title).toBe(article!.title); expect(foundArticle?.slug).toBe(article!.slug); expect(foundArticle?.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).toBeNull(); }); test('can getArticlesPaginated with defaults', async () => { const result = await getArticlesPaginated(); expect(result).toBeDefined(); expect(result.data.length).toBeGreaterThanOrEqual(1); expect(result.page).toBe(1); expect(result.pageSize).toBe(10); expect(result.total).toBeGreaterThanOrEqual(1); expect(result.totalPages).toBeGreaterThanOrEqual(1); }); test('can getArticlesPaginated with custom page size', async () => { // Save extra articles to test pagination for (let i = 0; i < 3; i++) { 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, }); } const firstPage = await getArticlesPaginated(1, 2); expect(firstPage.data.length).toBe(2); expect(firstPage.page).toBe(1); expect(firstPage.pageSize).toBe(2); expect(firstPage.total).toBeGreaterThanOrEqual(4); expect(firstPage.totalPages).toBeGreaterThanOrEqual(2); const secondPage = await getArticlesPaginated(2, 2); expect(secondPage.data.length).toBe(2); expect(secondPage.page).toBe(2); expect(secondPage.pageSize).toBe(2); }); test('can getArticlesPaginated returns empty on out-of-range page', async () => { const result = await getArticlesPaginated(999, 10); expect(result.data.length).toBe(0); expect(result.page).toBe(999); expect(result.total).toBeGreaterThanOrEqual(1); }); test('can update article', async () => { const dataToUpdate: UpdateArticleModel = { title: 'Updated Article Title', description: 'Updated description', }; const article = await getArticleBySlug('test-article'); expect(article).toBeDefined(); const updatedArticle = await updateArticle(article!.id, dataToUpdate); expect(updatedArticle).toBeDefined(); expect(updatedArticle.id).toBe(article!.id); expect(updatedArticle.title).toBe(dataToUpdate.title); expect(updatedArticle.description).toBe(dataToUpdate.description); expect(updatedArticle.slug).toBe(article!.slug); expect(updatedArticle.content).toBe(article!.content); expect(updatedArticle.externalId).toBeDefined(); }); test('cannot update non-existing article', async () => { const dataToUpdate: UpdateArticleModel = { title: 'Updated Article Title', }; await expect(updateArticle('9999', dataToUpdate)).rejects.toThrow( `Article with ID 9999 not found` ); }); 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 savedArticle = await saveArticle(articleToSave); expect(savedArticle.id).toBeDefined(); await deleteArticle(savedArticle.id); const deletedArticle = await getArticleBySlug('article-to-delete'); expect(deletedArticle).toBeNull(); }); test('cannot delete non-existing article', async () => { await expect(deleteArticle('9999')).rejects.toThrow( `Article with ID 9999 not found` ); }); });