feat: wrap article and user service functions with TypedResult for improved error handling
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user