281 lines
9.9 KiB
TypeScript
281 lines
9.9 KiB
TypeScript
import {
|
|
CreateArticleModel,
|
|
UpdateArticleModel,
|
|
} from '@/lib/feature/article/article.model';
|
|
import {
|
|
deleteArticleByExternalId,
|
|
getArticleByExternalId,
|
|
getArticleBySlug,
|
|
getArticlesByAuthorId,
|
|
getArticlesPaginated,
|
|
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);
|
|
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.id).toBeDefined();
|
|
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?.id).toBe(article!.id);
|
|
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 updateArticle(article!.id, dataToUpdate);
|
|
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(result.value).toBeDefined();
|
|
expect(result.value.id).toBe(article!.id);
|
|
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 updateArticle('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.id).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');
|
|
});
|
|
});
|