feat: implement article management features with CRUD operations
Some checks failed
Build and Test / run-test (20.x) (push) Has been cancelled
Some checks failed
Build and Test / run-test (20.x) (push) Has been cancelled
This commit is contained in:
33
migrations/1775782403581-adds-articles-table.ts
Normal file
33
migrations/1775782403581-adds-articles-table.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddsArticlesTable1775782403581 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`--sql
|
||||
CREATE TABLE articles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
cover_image_url TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
external_id UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
|
||||
|
||||
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_articles_updated_at
|
||||
BEFORE UPDATE on articles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
--end-sql`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`--sql
|
||||
DROP TABLE articles CASCADE;
|
||||
--end-sql`);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(png|jpg|jpeg|gif|webp|svg|ico)$": "<rootDir>/tests/setup/__mocks__/fileMock.js",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"^~/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { ArticleEntity } from '@/lib/feature/article/article.entity';
|
||||
export { UserEntity } from '@/lib/feature/user/user.entity';
|
||||
|
||||
58
src/lib/feature/article/article.entity.ts
Normal file
58
src/lib/feature/article/article.entity.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { UserEntity } from '@/lib/feature/user/user.entity';
|
||||
import type { Relation } from '@/utils/types/relation';
|
||||
import type { UUIDv4 } from '@/utils/types/uuid';
|
||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity('articles')
|
||||
export class ArticleEntity {
|
||||
@PrimaryColumn({
|
||||
type: 'bigint',
|
||||
primary: true,
|
||||
generated: true,
|
||||
})
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, unique: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'cover_image_url', type: 'text' })
|
||||
coverImageUrl: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
content: string;
|
||||
|
||||
@Column({ name: 'author_id', type: 'bigint' })
|
||||
authorId: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'author_id' })
|
||||
author: Relation<UserEntity>;
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'timestamp with time zone',
|
||||
default: () => 'now()',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@Column({
|
||||
name: 'updated_at',
|
||||
type: 'timestamp with time zone',
|
||||
default: () => 'now()',
|
||||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({
|
||||
name: 'external_id',
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
default: () => 'gen_random_uuid()',
|
||||
})
|
||||
externalId: UUIDv4;
|
||||
}
|
||||
61
src/lib/feature/article/article.external.ts
Normal file
61
src/lib/feature/article/article.external.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
ArticleModel,
|
||||
CreateArticleModel,
|
||||
PaginatedArticlesResult,
|
||||
UpdateArticleModel,
|
||||
} from '@/lib/feature/article/article.model';
|
||||
import * as service from '@/lib/feature/article/article.service';
|
||||
import { getSessionData } from '@/lib/session/session-storage';
|
||||
import { UUIDv4 } from '@/utils/types/uuid';
|
||||
|
||||
export const getArticleByExternalId = async (
|
||||
externalId: UUIDv4
|
||||
): Promise<ArticleModel | null> => {
|
||||
return await service.getArticleByExternalId(externalId);
|
||||
};
|
||||
|
||||
export const getArticleBySlug = async (
|
||||
slug: string
|
||||
): Promise<ArticleModel | null> => {
|
||||
return await service.getArticleBySlug(slug);
|
||||
};
|
||||
|
||||
export const getArticlesPaginated = async (
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<PaginatedArticlesResult> => {
|
||||
return await service.getArticlesPaginated(page, pageSize);
|
||||
};
|
||||
|
||||
export const saveArticle = async (
|
||||
article: CreateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
const session = await getSessionData();
|
||||
if (!session || !session?.user || session?.user.role !== 'admin') {
|
||||
throw new Error('Unauthorized: Only admin users can save articles.');
|
||||
}
|
||||
article.authorId = session.user.id;
|
||||
|
||||
return await service.saveArticle(article);
|
||||
};
|
||||
|
||||
export const updateArticle = async (
|
||||
articleId: string,
|
||||
article: UpdateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
const session = await getSessionData();
|
||||
if (!session || !session?.user || session?.user.role !== 'admin') {
|
||||
throw new Error('Unauthorized: Only admin users can save articles.');
|
||||
}
|
||||
return await service.updateArticle(articleId, article);
|
||||
};
|
||||
|
||||
export const deleteArticle = async (articleId: string): Promise<void> => {
|
||||
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);
|
||||
};
|
||||
45
src/lib/feature/article/article.model.ts
Normal file
45
src/lib/feature/article/article.model.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Pagination } from '@/utils/types/pagination';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateArticleModel = z.object({
|
||||
title: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string(),
|
||||
coverImageUrl: z.string(),
|
||||
content: z.string(),
|
||||
authorId: z.string().optional(),
|
||||
});
|
||||
export type CreateArticleModel = z.infer<typeof CreateArticleModel>;
|
||||
|
||||
export const UpdateArticleModel = z.object({
|
||||
title: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
coverImageUrl: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
});
|
||||
export type UpdateArticleModel = z.infer<typeof UpdateArticleModel>;
|
||||
|
||||
export const ArticleModel = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string(),
|
||||
coverImageUrl: z.string(),
|
||||
content: z.string(),
|
||||
authorId: z.string(),
|
||||
externalId: z.uuid(),
|
||||
});
|
||||
export type ArticleModel = z.infer<typeof ArticleModel>;
|
||||
|
||||
export const ArticleInfoModel = z.object({
|
||||
externalId: z.uuid(),
|
||||
title: z.string(),
|
||||
slug: z.string(),
|
||||
description: z.string(),
|
||||
coverImageUrl: z.string(),
|
||||
});
|
||||
export type ArticleInfoModel = z.infer<typeof ArticleInfoModel>;
|
||||
|
||||
export const PaginatedArticlesResult = Pagination(ArticleModel);
|
||||
export type PaginatedArticlesResult = z.infer<typeof PaginatedArticlesResult>;
|
||||
177
src/lib/feature/article/article.service.ts
Normal file
177
src/lib/feature/article/article.service.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { getRepository } from '@/lib/db/client';
|
||||
import { ArticleEntity } from '@/lib/feature/article/article.entity';
|
||||
import {
|
||||
ArticleModel,
|
||||
CreateArticleModel,
|
||||
PaginatedArticlesResult,
|
||||
UpdateArticleModel,
|
||||
} from '@/lib/feature/article/article.model';
|
||||
import { UUIDv4 } from '@/utils/types/uuid';
|
||||
|
||||
export const articleEntityToModel = (
|
||||
articleEntity: ArticleEntity
|
||||
): ArticleModel => {
|
||||
return {
|
||||
id: articleEntity.id,
|
||||
title: articleEntity.title,
|
||||
slug: articleEntity.slug,
|
||||
description: articleEntity.description,
|
||||
coverImageUrl: articleEntity.coverImageUrl,
|
||||
content: articleEntity.content,
|
||||
authorId: articleEntity.authorId,
|
||||
externalId: articleEntity.externalId,
|
||||
};
|
||||
};
|
||||
|
||||
/** Retrieves an artible by its external ID.
|
||||
* @param externalId - The external ID of the article to retrieve.
|
||||
* @returns {Promise<ArticleModel | null>} The article model if found, otherwise null.
|
||||
* */
|
||||
export const getArticleByExternalId = async (
|
||||
externalId: UUIDv4
|
||||
): Promise<ArticleModel | null> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const articleEntity = await articleRepository.findOneBy({
|
||||
externalId: externalId,
|
||||
});
|
||||
|
||||
if (!articleEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return articleEntityToModel(articleEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves an article by its slug.
|
||||
* @param slug - The slug of the article to retrieve.
|
||||
* @returns {Promise<ArticleModel | null>} The article model if found, otherwise null.
|
||||
*/
|
||||
export const getArticleBySlug = async (
|
||||
slug: string
|
||||
): Promise<ArticleModel | null> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const articleEntity = await articleRepository.findOneBy({ slug });
|
||||
|
||||
if (!articleEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return articleEntityToModel(articleEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all articles by a given author ID.
|
||||
* @param authorId - The ID of the author.
|
||||
* @returns {Promise<ArticleModel[]>} A list of article models.
|
||||
*/
|
||||
export const getArticlesByAuthorId = async (
|
||||
authorId: string
|
||||
): Promise<ArticleModel[]> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const articleEntities = await articleRepository.findBy({ authorId });
|
||||
|
||||
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<PaginatedArticlesResult>} The paginated result.
|
||||
*/
|
||||
export const getArticlesPaginated = async (
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<PaginatedArticlesResult> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const [articleEntities, total] = await articleRepository.findAndCount({
|
||||
order: { createdAt: 'DESC' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
data: articleEntities.map(articleEntityToModel),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a new article to the database.
|
||||
* @param article - The article data to save.
|
||||
* @returns {Promise<ArticleModel>} The saved article model.
|
||||
* @throws {Error} If an article with the same slug already exists.
|
||||
*/
|
||||
export const saveArticle = async (
|
||||
article: CreateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
if (!article.authorId) {
|
||||
throw new Error('Author ID is required to save an article');
|
||||
}
|
||||
|
||||
if (!!(await articleRepository.findOneBy({ slug: article.slug }))) {
|
||||
throw new Error(`Article with slug ${article.slug} already exists`);
|
||||
}
|
||||
|
||||
const newArticle = articleRepository.create(article);
|
||||
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<ArticleModel>} The updated article model.
|
||||
* @throws {Error} If the article with the given ID does not exist.
|
||||
*/
|
||||
export const updateArticle = async (
|
||||
articleId: string,
|
||||
article: UpdateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const existingArticle = await articleRepository.findOneBy({
|
||||
id: articleId,
|
||||
});
|
||||
if (!existingArticle) {
|
||||
throw new Error(`Article with ID ${articleId} not found`);
|
||||
}
|
||||
|
||||
if (!!article.title) existingArticle.title = article.title;
|
||||
if (!!article.slug) existingArticle.slug = article.slug;
|
||||
if (!!article.description)
|
||||
existingArticle.description = article.description;
|
||||
if (!!article.coverImageUrl)
|
||||
existingArticle.coverImageUrl = article.coverImageUrl;
|
||||
if (!!article.content) existingArticle.content = article.content;
|
||||
|
||||
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<void> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const existingArticle = await articleRepository.findOneBy({
|
||||
id: articleId,
|
||||
});
|
||||
if (!existingArticle) {
|
||||
throw new Error(`Article with ID ${articleId} not found`);
|
||||
}
|
||||
|
||||
await articleRepository.remove(existingArticle);
|
||||
};
|
||||
232
tests/lib/feature/article/article.service.test.ts
Normal file
232
tests/lib/feature/article/article.service.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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`
|
||||
);
|
||||
});
|
||||
});
|
||||
1
tests/setup/__mocks__/fileMock.js
Normal file
1
tests/setup/__mocks__/fileMock.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'test-file-stub';
|
||||
Reference in New Issue
Block a user