diff --git a/src/app/(pages)/article/[slug]/page.tsx b/src/app/(pages)/article/[slug]/page.tsx index dce4bac..d5867dd 100644 --- a/src/app/(pages)/article/[slug]/page.tsx +++ b/src/app/(pages)/article/[slug]/page.tsx @@ -70,7 +70,9 @@ const ArticleContentSkeleton = () => ( const ArticleContent = async ({ params }: ArticleContentProps) => { const { slug } = await params; - const article = await getArticleBySlug(slug); + const articleResult = await getArticleBySlug(slug); + if (!articleResult.ok) throw articleResult.error; + const article = articleResult.value; if (!article) notFound(); diff --git a/src/app/(pages)/home/page.tsx b/src/app/(pages)/home/page.tsx index 4c5a223..39556c8 100644 --- a/src/app/(pages)/home/page.tsx +++ b/src/app/(pages)/home/page.tsx @@ -45,11 +45,9 @@ const ArticleList = async ({ searchParams }: ArticleListProps) => { const page = Math.max(1, Number(pageParam) || 1); const pageSize = Number(pageSizeParam) || PAGE_SIZE; - const { - data: articles, - totalPages, - total, - } = await getArticlesPaginated(page, pageSize); + const paginationResult = await getArticlesPaginated(page, pageSize); + if (!paginationResult.ok) throw paginationResult.error; + const { data: articles, totalPages, total } = paginationResult.value; return ( <> diff --git a/src/app/api/user/sync/route.ts b/src/app/api/user/sync/route.ts index 560c05e..5fbdee5 100644 --- a/src/app/api/user/sync/route.ts +++ b/src/app/api/user/sync/route.ts @@ -15,8 +15,12 @@ export async function GET() { redirect('/'); } - const syncedUser = await syncUser(parsedClaims); + const syncResult = await syncUser(parsedClaims); + if (!syncResult.ok) { + console.error(syncResult.error); + redirect('/'); + } - await setSessionData('user', syncedUser); + await setSessionData('user', syncResult.value); redirect('/'); } diff --git a/src/lib/feature/article/article.external.ts b/src/lib/feature/article/article.external.ts index 26b97bb..12f02f7 100644 --- a/src/lib/feature/article/article.external.ts +++ b/src/lib/feature/article/article.external.ts @@ -8,28 +8,35 @@ import { } from '@/lib/feature/article/article.model'; import * as service from '@/lib/feature/article/article.service'; import { getSessionData } from '@/lib/session/session-storage'; +import { TypedResult, wrap } from '@/utils/types/results'; import { UUIDv4 } from '@/utils/types/uuid'; -export const getArticleByExternalId = async ( +const _getArticleByExternalId = async ( externalId: UUIDv4 ): Promise => { - return await service.getArticleByExternalId(externalId); + const result = await service.getArticleByExternalId(externalId); + if (!result.ok) throw result.error; + return result.value; }; -export const getArticleBySlug = async ( +const _getArticleBySlug = async ( slug: string ): Promise => { - return await service.getArticleBySlug(slug); + const result = await service.getArticleBySlug(slug); + if (!result.ok) throw result.error; + return result.value; }; -export const getArticlesPaginated = async ( +const _getArticlesPaginated = async ( page: number = 1, pageSize: number = 10 ): Promise => { - return await service.getArticlesPaginated(page, pageSize); + const result = await service.getArticlesPaginated(page, pageSize); + if (!result.ok) throw result.error; + return result.value; }; -export const saveArticle = async ( +const _saveArticle = async ( article: CreateArticleModel ): Promise => { const session = await getSessionData(); @@ -38,10 +45,12 @@ export const saveArticle = async ( } article.authorId = session.user.id; - return await service.saveArticle(article); + const result = await service.saveArticle(article); + if (!result.ok) throw result.error; + return result.value; }; -export const updateArticle = async ( +const _updateArticle = async ( articleId: string, article: UpdateArticleModel ): Promise => { @@ -49,13 +58,45 @@ export const updateArticle = async ( if (!session || !session?.user || session?.user.role !== 'admin') { throw new Error('Unauthorized: Only admin users can save articles.'); } - return await service.updateArticle(articleId, article); + + const result = await service.updateArticle(articleId, article); + if (!result.ok) throw result.error; + return result.value; }; -export const deleteArticle = async (articleId: string): Promise => { +const _deleteArticle = async (articleId: string): Promise => { const session = await getSessionData(); if (!session || !session?.user || session?.user.role !== 'admin') { throw new Error('Unauthorized: Only admin users can delete articles.'); } - await service.deleteArticle(articleId); + + const result = await service.deleteArticle(articleId); + if (!result.ok) throw result.error; }; + +export const getArticleByExternalId: ( + externalId: UUIDv4 +) => Promise> = wrap(_getArticleByExternalId); + +export const getArticleBySlug: ( + slug: string +) => Promise> = wrap(_getArticleBySlug); + +export const getArticlesPaginated: ( + page?: number, + pageSize?: number +) => Promise> = wrap( + _getArticlesPaginated +); + +export const saveArticle: ( + article: CreateArticleModel +) => Promise> = wrap(_saveArticle); + +export const updateArticle: ( + articleId: string, + article: UpdateArticleModel +) => Promise> = wrap(_updateArticle); + +export const deleteArticle: (articleId: string) => Promise> = + wrap(_deleteArticle); diff --git a/src/lib/feature/article/article.service.ts b/src/lib/feature/article/article.service.ts index 008138d..4b26b21 100644 --- a/src/lib/feature/article/article.service.ts +++ b/src/lib/feature/article/article.service.ts @@ -6,6 +6,7 @@ import { PaginatedArticlesResult, UpdateArticleModel, } from '@/lib/feature/article/article.model'; +import { TypedResult, wrap } from '@/utils/types/results'; import { UUIDv4 } from '@/utils/types/uuid'; export const articleEntityToModel = ( @@ -25,11 +26,7 @@ export const articleEntityToModel = ( }; }; -/** Retrieves an artible by its external ID. - * @param externalId - The external ID of the article to retrieve. - * @returns {Promise} The article model if found, otherwise null. - * */ -export const getArticleByExternalId = async ( +const _getArticleByExternalId = async ( externalId: UUIDv4 ): Promise => { const articleRepository = await getRepository(ArticleEntity); @@ -45,12 +42,7 @@ export const getArticleByExternalId = async ( return articleEntityToModel(articleEntity); }; -/** - * Retrieves an article by its slug. - * @param slug - The slug of the article to retrieve. - * @returns {Promise} The article model if found, otherwise null. - */ -export const getArticleBySlug = async ( +const _getArticleBySlug = async ( slug: string ): Promise => { const articleRepository = await getRepository(ArticleEntity); @@ -64,12 +56,7 @@ export const getArticleBySlug = async ( return articleEntityToModel(articleEntity); }; -/** - * Retrieves all articles by a given author ID. - * @param authorId - The ID of the author. - * @returns {Promise} A list of article models. - */ -export const getArticlesByAuthorId = async ( +const _getArticlesByAuthorId = async ( authorId: string ): Promise => { const articleRepository = await getRepository(ArticleEntity); @@ -79,13 +66,7 @@ export const getArticlesByAuthorId = async ( return articleEntities.map(articleEntityToModel); }; -/** - * Retrieves a paginated list of articles ordered by creation date descending. - * @param page - The page number (1-based). - * @param pageSize - The number of articles per page. - * @returns {Promise} The paginated result. - */ -export const getArticlesPaginated = async ( +const _getArticlesPaginated = async ( page: number = 1, pageSize: number = 10 ): Promise => { @@ -106,13 +87,7 @@ export const getArticlesPaginated = async ( }; }; -/** - * Saves a new article to the database. - * @param article - The article data to save. - * @returns {Promise} The saved article model. - * @throws {Error} If an article with the same slug already exists. - */ -export const saveArticle = async ( +const _saveArticle = async ( article: CreateArticleModel ): Promise => { const articleRepository = await getRepository(ArticleEntity); @@ -129,14 +104,7 @@ export const saveArticle = async ( return articleEntityToModel(await articleRepository.save(newArticle)); }; -/** - * Updates an existing article in the database. - * @param articleId - The ID of the article to update. - * @param article - The new article data. - * @returns {Promise} The updated article model. - * @throws {Error} If the article with the given ID does not exist. - */ -export const updateArticle = async ( +const _updateArticle = async ( articleId: string, article: UpdateArticleModel ): Promise => { @@ -160,12 +128,7 @@ export const updateArticle = async ( return articleEntityToModel(await articleRepository.save(existingArticle)); }; -/** - * Deletes an article from the database. - * @param articleId - The ID of the article to delete. - * @throws {Error} If the article with the given ID does not exist. - */ -export const deleteArticle = async (articleId: string): Promise => { +const _deleteArticle = async (articleId: string): Promise => { const articleRepository = await getRepository(ArticleEntity); const existingArticle = await articleRepository.findOneBy({ @@ -177,3 +140,41 @@ export const deleteArticle = async (articleId: string): Promise => { await articleRepository.remove(existingArticle); }; + +/** Retrieves an article by its external ID. */ +export const getArticleByExternalId: ( + externalId: UUIDv4 +) => Promise> = wrap(_getArticleByExternalId); + +/** Retrieves an article by its slug. */ +export const getArticleBySlug: ( + slug: string +) => Promise> = wrap(_getArticleBySlug); + +/** Retrieves all articles by a given author ID. */ +export const getArticlesByAuthorId: ( + authorId: string +) => Promise> = wrap(_getArticlesByAuthorId); + +/** Retrieves a paginated list of articles ordered by creation date descending. */ +export const getArticlesPaginated: ( + page?: number, + pageSize?: number +) => Promise> = wrap( + _getArticlesPaginated +); + +/** Saves a new article to the database. */ +export const saveArticle: ( + article: CreateArticleModel +) => Promise> = wrap(_saveArticle); + +/** Updates an existing article in the database. */ +export const updateArticle: ( + articleId: string, + article: UpdateArticleModel +) => Promise> = wrap(_updateArticle); + +/** Deletes an article from the database. */ +export const deleteArticle: (articleId: string) => Promise> = + wrap(_deleteArticle); diff --git a/src/lib/feature/user/user.external.ts b/src/lib/feature/user/user.external.ts index 0538019..c20b606 100644 --- a/src/lib/feature/user/user.external.ts +++ b/src/lib/feature/user/user.external.ts @@ -2,11 +2,15 @@ import { getRepository } from '@/lib/db/client'; import { UserEntity } from '@/lib/db/entities'; +import { UserModel } from '@/lib/feature/user/user.model'; import { userEntityToModel } from '@/lib/feature/user/user.service'; import { getSessionData } from '@/lib/session/session-storage'; +import { TypedResult, wrap } from '@/utils/types/results'; import { UUIDv4 } from '@/utils/types/uuid'; -export const getUserByExternalId = async (externalId: UUIDv4) => { +const _getUserByExternalId = async ( + externalId: UUIDv4 +): Promise => { const sessionData = await getSessionData(); if ( !sessionData || @@ -28,3 +32,7 @@ export const getUserByExternalId = async (externalId: UUIDv4) => { return userEntityToModel(userEntity); }; + +export const getUserByExternalId: ( + externalId: UUIDv4 +) => Promise> = wrap(_getUserByExternalId); diff --git a/src/lib/feature/user/user.service.ts b/src/lib/feature/user/user.service.ts index e555122..6e0fc73 100644 --- a/src/lib/feature/user/user.service.ts +++ b/src/lib/feature/user/user.service.ts @@ -6,6 +6,7 @@ import { UpdateUserModel, UserModel, } from '@/lib/feature/user/user.model'; +import { TypedResult, wrap } from '@/utils/types/results'; export const userEntityToModel = (userEntity: UserEntity): UserModel => { return { @@ -17,14 +18,7 @@ export const userEntityToModel = (userEntity: UserEntity): UserModel => { }; }; -/** - * Retrieves a user by their email address. - * @param email - The email address of the user to retrieve. - * @returns {Promise} The user model if found, otherwise null. - */ -export const getUserByEmail = async ( - email: string -): Promise => { +const _getUserByEmail = async (email: string): Promise => { const userRepository = await getRepository(UserEntity); const userEntity = await userRepository.findOneBy({ email }); @@ -36,13 +30,7 @@ export const getUserByEmail = async ( return userEntityToModel(userEntity); }; -/** - * Saves a new user to the database. - * @param user - The user data to save. - * @returns {Promise} The saved user model. - * @throws {Error} If a user with the same email already exists. - */ -export const saveUser = async (user: CreateUserModel): Promise => { +const _saveUser = async (user: CreateUserModel): Promise => { const userRepository = await getRepository(UserEntity); if (!!(await userRepository.findOneBy({ email: user.email }))) { @@ -53,14 +41,7 @@ export const saveUser = async (user: CreateUserModel): Promise => { return userEntityToModel(await userRepository.save(newUser)); }; -/** - * Updates an existing user in the database. - * @param userId - The ID of the user to update. - * @param user - The new user data. - * @returns {Promise} The updated user model. - * @throws {Error} If the user with the given ID does not exist. - */ -export const updateUser = async ( +const _updateUser = async ( userId: string, user: UpdateUserModel ): Promise => { @@ -78,33 +59,55 @@ export const updateUser = async ( return userEntityToModel(await userRepository.save(existingUser)); }; -/** - * Synchronizes a user with the database. - * If the user already exists, it skips saving and returns the existing user. - * If the user does not exist, it creates a new user record. - * @returns {Promise} The synchronized user model. - * @throws {Error} If the user email is not provided or if there is an issue - * saving the user. - * @param sessionClaims Session Claims from the Auth Provider - */ -export const syncUser = async ( - sessionClaims: SessionClaims -): Promise => { +const _syncUser = async (sessionClaims: SessionClaims): Promise => { const { full_name, email } = sessionClaims.user; const role = sessionClaims.user.public_metadata.role; - const existingUser = await getUserByEmail(email); + const existingUser = await _getUserByEmail(email); if (!existingUser) { - return await saveUser({ + return await _saveUser({ name: full_name, email: email, role: role, }); } - return await updateUser(existingUser.id, { + return await _updateUser(existingUser.id, { name: full_name, email: existingUser.email, role: role, }); }; + +/** + * Retrieves a user by their email address. + * @param email - The email address of the user to retrieve. + * @returns {Promise>} The user model if found, otherwise null. + */ +export const getUserByEmail = wrap(_getUserByEmail); + +/** + * Saves a new user to the database. + * @param user - The user data to save. + * @returns {Promise>} The saved user model. + */ +export const saveUser = wrap(_saveUser); + +/** + * Updates an existing user in the database. + * @param userId - The ID of the user to update. + * @param user - The new user data. + * @returns {Promise>} The updated user model. + */ +export const updateUser = wrap(_updateUser); + +/** + * Synchronizes a user with the database. + * If the user already exists, updates it; if not, creates a new record. + * @param sessionClaims Session Claims from the Auth Provider + * @returns {Promise>} The synchronized user model. + */ +export const syncUser = wrap(_syncUser); + +// Explicit re-export for TypeScript consumers who need the result type +export type { TypedResult }; diff --git a/src/ui/components/internal/create-article-form.tsx b/src/ui/components/internal/create-article-form.tsx index 0a94e15..d4affb5 100644 --- a/src/ui/components/internal/create-article-form.tsx +++ b/src/ui/components/internal/create-article-form.tsx @@ -101,23 +101,20 @@ export const CreateArticleForm = () => { const handleFormSubmit = useCallback( async (data: z.infer) => { - try { - const result = await saveArticle({ ...data }); - toast.success('Article created successfully!', { - description: `Article "${result.title}" has been created.`, - position: 'bottom-right', - }); - form.reset(); - resetFiles(); - } catch (error) { + const result = await saveArticle({ ...data }); + if (!result.ok) { toast.error('Failed to create article', { - description: - error instanceof Error - ? error.message - : 'An error occurred', + description: result.error.message, position: 'bottom-right', }); + return; } + toast.success('Article created successfully!', { + description: `Article "${result.value.title}" has been created.`, + position: 'bottom-right', + }); + form.reset(); + resetFiles(); }, [form, resetFiles] ); diff --git a/tests/lib/feature/article/article.service.test.ts b/tests/lib/feature/article/article.service.test.ts index fb282ba..8d5ea16 100644 --- a/tests/lib/feature/article/article.service.test.ts +++ b/tests/lib/feature/article/article.service.test.ts @@ -34,7 +34,8 @@ describe('ArticleService', () => { role: 'admin', }; const savedAuthor = await saveUser(author); - authorId = savedAuthor.id; + if (!savedAuthor.ok) throw savedAuthor.error; + authorId = savedAuthor.value.id; }, 1_000_000); afterAll(async () => { @@ -51,16 +52,18 @@ describe('ArticleService', () => { authorId: authorId, }; - const savedArticle = await saveArticle(articleToSave); + const result = 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(); + 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 () => { @@ -72,77 +75,99 @@ describe('ArticleService', () => { content: 'Duplicate content.', authorId: authorId, }; - await expect(saveArticle(articleToSave)).rejects.toThrow( - `Article with slug ${articleToSave.slug} already exists` - ); + + 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 article = await getArticleBySlug('test-article'); + const result = 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(); + 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 () => { - await expect(getArticleBySlug('non-existing-slug')).resolves.toBeNull(); + 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 articles = await getArticlesByAuthorId(authorId); + const result = await getArticlesByAuthorId(authorId); - expect(articles).toBeDefined(); - expect(articles.length).toBeGreaterThanOrEqual(1); - expect(articles[0].authorId).toBe(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 articles = await getArticlesByAuthorId('9999'); + const result = await getArticlesByAuthorId('9999'); - expect(articles).toBeDefined(); - expect(articles.length).toBe(0); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value).toBeDefined(); + expect(result.value.length).toBe(0); }); test('can getArticleByExternalId', async () => { - const article = await getArticleBySlug('test-article'); + const slugResult = await getArticleBySlug('test-article'); + expect(slugResult.ok).toBe(true); + if (!slugResult.ok) return; + const article = slugResult.value; expect(article).toBeDefined(); - const foundArticle = await getArticleByExternalId( + const result = 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); + 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).toBeNull(); + + 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).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); + 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++) { - await saveArticle({ + const r = await saveArticle({ title: `Paginated Article ${i}`, slug: `paginated-article-${i}`, description: `Description ${i}`, @@ -150,29 +175,36 @@ describe('ArticleService', () => { content: `Content ${i}`, authorId: authorId, }); + if (!r.ok) throw r.error; } - const firstPage = await getArticlesPaginated(1, 2); + const firstPageResult = 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); + 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 secondPage = await getArticlesPaginated(2, 2); + const secondPageResult = await getArticlesPaginated(2, 2); - expect(secondPage.data.length).toBe(2); - expect(secondPage.page).toBe(2); - expect(secondPage.pageSize).toBe(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.data.length).toBe(0); - expect(result.page).toBe(999); - expect(result.total).toBeGreaterThanOrEqual(1); + 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 () => { @@ -181,18 +213,23 @@ describe('ArticleService', () => { description: 'Updated description', }; - const article = await getArticleBySlug('test-article'); + const slugResult = await getArticleBySlug('test-article'); + expect(slugResult.ok).toBe(true); + if (!slugResult.ok) return; + const article = slugResult.value; expect(article).toBeDefined(); - const updatedArticle = await updateArticle(article!.id, dataToUpdate); + const result = 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(); + 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 () => { @@ -200,9 +237,11 @@ describe('ArticleService', () => { title: 'Updated Article Title', }; - await expect(updateArticle('9999', dataToUpdate)).rejects.toThrow( - `Article with ID 9999 not found` - ); + 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 () => { @@ -215,18 +254,25 @@ describe('ArticleService', () => { authorId: authorId, }; - const savedArticle = await saveArticle(articleToSave); - expect(savedArticle.id).toBeDefined(); + const saveResult = await saveArticle(articleToSave); + expect(saveResult.ok).toBe(true); + if (!saveResult.ok) return; + expect(saveResult.value.id).toBeDefined(); - await deleteArticle(savedArticle.id); + const deleteResult = await deleteArticle(saveResult.value.id); + expect(deleteResult.ok).toBe(true); - const deletedArticle = await getArticleBySlug('article-to-delete'); - expect(deletedArticle).toBeNull(); + 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 () => { - await expect(deleteArticle('9999')).rejects.toThrow( - `Article with ID 9999 not found` - ); + const result = await deleteArticle('9999'); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error.message).toContain('9999'); }); }); diff --git a/tests/lib/feature/user/user.service.test.ts b/tests/lib/feature/user/user.service.test.ts index c2843cc..056f811 100644 --- a/tests/lib/feature/user/user.service.test.ts +++ b/tests/lib/feature/user/user.service.test.ts @@ -35,14 +35,15 @@ describe('UserService', () => { role: 'user', }; - const savedUser = await saveUser(userToSave); + const result = await saveUser(userToSave); - expect(savedUser.id).toBeDefined(); - expect(savedUser.name).toBe(userToSave.name); - expect(savedUser.email).toBe(userToSave.email); - expect(savedUser.role).toBe(userToSave.role); - expect(savedUser.role).toBe(userToSave.role); - expect(savedUser.externalId).toBeDefined(); // Default to true if not set + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.id).toBeDefined(); + expect(result.value.name).toBe(userToSave.name); + expect(result.value.email).toBe(userToSave.email); + expect(result.value.role).toBe(userToSave.role); + expect(result.value.externalId).toBeDefined(); }); test('cannot save user with existing email', async () => { @@ -51,23 +52,31 @@ describe('UserService', () => { email: 'test@email.com', role: 'user', }; - await expect(saveUser(userToSave)).rejects.toThrow( - `User with email ${userToSave.email} already exists` - ); + + const result = await saveUser(userToSave); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error.message).toContain(userToSave.email); }); test('can getUserByEmail', async () => { - const user = await getUserByEmail('test@email.com'); + const result = await getUserByEmail('test@email.com'); - expect(user).toBeDefined(); - expect(user?.email).toBe('test@email.com'); - expect(user?.name).toBe('Test User'); - expect(user?.role).toBe('user'); - expect(user?.externalId).toBeDefined(); // Default to true if not set + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value?.email).toBe('test@email.com'); + expect(result.value?.name).toBe('Test User'); + expect(result.value?.role).toBe('user'); + expect(result.value?.externalId).toBeDefined(); }); test('cannot getUserByEmail with non-existing email', async () => { - await expect(getUserByEmail('missing@email.com')).resolves.toBeNull(); + const result = await getUserByEmail('missing@email.com'); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value).toBeNull(); }); test('can update user', async () => { @@ -76,17 +85,21 @@ describe('UserService', () => { role: 'admin', }; - const user = await getUserByEmail('test@email.com'); + const userResult = await getUserByEmail('test@email.com'); + expect(userResult.ok).toBe(true); + if (!userResult.ok) return; + const user = userResult.value; expect(user).toBeDefined(); - const updatedUser = await updateUser(user!.id, dataToUpdate); + const result = await updateUser(user!.id, dataToUpdate); - expect(updatedUser).toBeDefined(); - expect(updatedUser.id).toBe(user!.id); - expect(updatedUser.name).toBe(dataToUpdate.name); - expect(updatedUser.role).toBe(dataToUpdate.role); - expect(updatedUser.email).toBe(user!.email); - expect(updatedUser.externalId).toBeDefined(); // Default to true if not set + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.id).toBe(user!.id); + expect(result.value.name).toBe(dataToUpdate.name); + expect(result.value.role).toBe(dataToUpdate.role); + expect(result.value.email).toBe(user!.email); + expect(result.value.externalId).toBeDefined(); }); test('cannot update non-existing user', async () => { @@ -95,9 +108,11 @@ describe('UserService', () => { role: 'admin', }; - await expect(updateUser('9999', dataToUpdate)).rejects.toThrow( - `User with ID 9999 not found` - ); + const result = await updateUser('9999', dataToUpdate); + + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error.message).toContain('9999'); }); test('can sync admin user', async () => { @@ -131,12 +146,14 @@ describe('UserService', () => { }, }, } as SessionClaims; - const syncedUser = await syncUser(sessionClaims); - expect(syncedUser).toBeDefined(); - expect(syncedUser.name).toBe('Updated Name'); - expect(syncedUser.email).toBe('test@email.com'); - expect(syncedUser.role).toBe('admin'); + const result = await syncUser(sessionClaims); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.name).toBe('Updated Name'); + expect(result.value.email).toBe('test@email.com'); + expect(result.value.role).toBe('admin'); }); test('can sync internal user', async () => { @@ -170,12 +187,14 @@ describe('UserService', () => { }, }, } as SessionClaims; - const syncedUser = await syncUser(sessionClaims); - expect(syncedUser).toBeDefined(); - expect(syncedUser.name).toBe('Updated Name'); - expect(syncedUser.email).toBe('test@email.com'); - expect(syncedUser.role).toBe('internal'); + const result = await syncUser(sessionClaims); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.name).toBe('Updated Name'); + expect(result.value.email).toBe('test@email.com'); + expect(result.value.role).toBe('internal'); }); test('can sync user', async () => { @@ -202,12 +221,14 @@ describe('UserService', () => { }, }, } as SessionClaims; - const syncedUser = await syncUser(sessionClaims); - expect(syncedUser).toBeDefined(); - expect(syncedUser.name).toBe('Updated Name'); - expect(syncedUser.email).toBe('test@email.com'); - expect(syncedUser.role).toBe('user'); + const result = await syncUser(sessionClaims); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.name).toBe('Updated Name'); + expect(result.value.email).toBe('test@email.com'); + expect(result.value.role).toBe('user'); }); test('can sync saving new user', async () => { @@ -234,11 +255,13 @@ describe('UserService', () => { }, }, } as SessionClaims; - const syncedUser = await syncUser(sessionClaims); - expect(syncedUser).toBeDefined(); - expect(syncedUser.name).toBe('Updated Name'); - expect(syncedUser.email).toBe('new@email.com'); - expect(syncedUser.role).toBe('user'); + const result = await syncUser(sessionClaims); + + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.name).toBe('Updated Name'); + expect(result.value.email).toBe('new@email.com'); + expect(result.value.role).toBe('user'); }); });