news.controller.test.ts•11.7 kB
/**
* Unit tests for the NewsController
*
* These tests focus on controller behavior in isolation by properly
* mocking dependencies and verifying correct request handling.
*/
import { describe, test, expect, beforeEach, jest, afterEach } from '@jest/globals';
import { Request, Response } from 'express';
import { cacheService } from '../../utils/cache';
// Create properly typed mock service functions
const mockGetTopNews = jest.fn().mockImplementation((params: any) => Promise.resolve({
data: [],
meta: { found: 0, returned: 0 }
}));
const mockGetAllNews = jest.fn().mockImplementation((params: any) => Promise.resolve({
data: [],
meta: { found: 0, returned: 0 }
}));
const mockGetNewsByUuid = jest.fn().mockImplementation((uuid: string) => Promise.resolve({
data: { uuid }
}));
const mockGetNewsSources = jest.fn().mockImplementation((params: any) => Promise.resolve({
data: [],
meta: { found: 0, returned: 0 }
}));
// Create a mock NewsService class before importing the controller
const mockNewsServiceInstance = {
getTopNews: mockGetTopNews,
getAllNews: mockGetAllNews,
getNewsByUuid: mockGetNewsByUuid,
getNewsSources: mockGetNewsSources
};
// Mock the entire news.service module
jest.mock('../../services/news.service', () => {
return {
// When NewsController creates a new NewsService(), it will get our mockNewsServiceInstance
NewsService: jest.fn().mockImplementation(() => mockNewsServiceInstance)
};
});
// Now import the controller (after mocks are set up)
import { NewsController } from '../../controllers/news.controller';
describe('NewsController', () => {
// Test subjects
let controller: NewsController;
let req: Partial<Request>;
let res: Partial<Response>;
// Mock data
const mockArticles = [
{
uuid: 'test-uuid-1',
title: 'Test Article 1',
description: 'Test description 1',
content: 'Test content 1',
source: 'Test Source',
url: 'https://example.com/article1',
image_url: 'https://example.com/image1.jpg',
categories: ['general'],
published_at: '2025-05-01T12:00:00Z',
created_at: '2025-05-01T12:00:00Z',
updated_at: '2025-05-01T12:00:00Z'
},
{
uuid: 'test-uuid-2',
title: 'Test Article 2',
description: 'Test description 2',
content: 'Test content 2',
source: 'Test Source',
url: 'https://example.com/article2',
image_url: 'https://example.com/image2.jpg',
categories: ['technology'],
published_at: '2025-05-02T12:00:00Z',
created_at: '2025-05-02T12:00:00Z',
updated_at: '2025-05-02T12:00:00Z'
}
];
const mockSources = [
{
id: 1,
name: 'Test Source 1',
url: 'https://testsource1.com'
},
{
id: 2,
name: 'Test Source 2',
url: 'https://testsource2.com'
}
];
// Setup before each test
beforeEach(() => {
// Clear mocks
jest.clearAllMocks();
mockGetTopNews.mockReset();
mockGetAllNews.mockReset();
mockGetNewsByUuid.mockReset();
mockGetNewsSources.mockReset();
// Clear cache
cacheService.clear();
// Create new controller instance
controller = new NewsController();
// Create mock request and response objects with proper typing
req = {
query: {},
params: {}
} as Partial<Request>;
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
} as Partial<Response>;
// Cast the mocks to any to avoid TypeScript errors with mock functions
(res.status as any).mockReturnThis();
(res.json as any).mockReturnValue(res);
});
// Clean up after tests
afterEach(() => {
jest.clearAllMocks();
});
// Test suite for getTopNews
describe('getTopNews', () => {
test('should return top news articles with 200 status', async () => {
// Arrange
mockGetTopNews.mockResolvedValueOnce({
data: mockArticles,
meta: {
found: mockArticles.length,
returned: mockArticles.length
}
});
// Act
await controller.getTopNews(req as Request, res as Response);
// Assert
expect(mockGetTopNews).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: mockArticles,
meta: expect.objectContaining({
found: mockArticles.length,
returned: mockArticles.length
})
});
});
test('should handle query parameters correctly', async () => {
// Arrange
req.query = {
categories: 'technology',
language: 'en',
limit: '10'
};
mockGetTopNews.mockResolvedValueOnce({
data: [mockArticles[1]],
meta: {
found: 1,
returned: 1
}
});
// Act
await controller.getTopNews(req as Request, res as Response);
// Assert
expect(mockGetTopNews).toHaveBeenCalledWith(expect.objectContaining({
categories: 'technology',
language: 'en',
limit: 10
}));
expect(res.status).toHaveBeenCalledWith(200);
});
test('should handle error from service gracefully', async () => {
// Arrange
const error = new Error('Service error');
mockGetTopNews.mockRejectedValueOnce(error);
// Act
await controller.getTopNews(req as Request, res as Response);
// Assert
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: expect.any(String)
});
});
});
// Test suite for getAllNews
describe('getAllNews', () => {
test('should return paginated news with 200 status', async () => {
// Arrange
req.query = {
page: '1',
limit: '10'
};
mockGetAllNews.mockResolvedValueOnce({
data: mockArticles,
meta: {
found: 20,
returned: mockArticles.length,
page: 1,
limit: 10
}
});
// Act
await controller.getAllNews(req as Request, res as Response);
// Assert
expect(mockGetAllNews).toHaveBeenCalledWith(expect.objectContaining({
page: 1,
limit: 10
}));
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: mockArticles,
meta: expect.objectContaining({
page: 1,
limit: 10
})
});
});
test('should handle search query parameter', async () => {
// Arrange
req.query = {
search: 'test',
search_fields: 'title,description'
};
mockGetAllNews.mockResolvedValueOnce({
data: mockArticles,
meta: {
found: mockArticles.length,
returned: mockArticles.length
}
});
// Act
await controller.getAllNews(req as Request, res as Response);
// Assert
expect(mockGetAllNews).toHaveBeenCalledWith(expect.objectContaining({
search: 'test',
search_fields: 'title,description'
}));
expect(res.status).toHaveBeenCalledWith(200);
});
test('should handle date filtering correctly', async () => {
// Arrange
const testDate = '2025-05-01';
req.query = {
published_after: testDate
};
mockGetAllNews.mockResolvedValueOnce({
data: [mockArticles[1]],
meta: {
found: 1,
returned: 1
}
});
// Act
await controller.getAllNews(req as Request, res as Response);
// Assert
expect(mockGetAllNews).toHaveBeenCalledWith(expect.objectContaining({
published_after: testDate
}));
expect(res.status).toHaveBeenCalledWith(200);
});
test('should handle error from service gracefully', async () => {
// Arrange
const error = new Error('Service error');
mockGetAllNews.mockRejectedValueOnce(error);
// Act
await controller.getAllNews(req as Request, res as Response);
// Assert
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: expect.any(String)
});
});
});
// Test suite for getNewsByUuid
describe('getNewsByUuid', () => {
test('should return article when found with 200 status', async () => {
// Arrange
req.params = { uuid: 'test-uuid-1' };
mockGetNewsByUuid.mockResolvedValueOnce({
data: mockArticles[0]
});
// Act
await controller.getNewsByUuid(req as Request, res as Response);
// Assert
expect(mockGetNewsByUuid).toHaveBeenCalledWith('test-uuid-1');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: mockArticles[0]
});
});
test('should return 404 when article not found', async () => {
// Arrange
req.params = { uuid: 'non-existent-uuid' };
const error = new Error('Article not found');
mockGetNewsByUuid.mockRejectedValueOnce(error);
// Act
await controller.getNewsByUuid(req as Request, res as Response);
// Assert
expect(mockGetNewsByUuid).toHaveBeenCalledWith('non-existent-uuid');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: expect.any(String)
});
});
});
// Test suite for getNewsSources
describe('getNewsSources', () => {
test('should return sources with 200 status', async () => {
// Arrange
mockGetNewsSources.mockResolvedValueOnce({
data: mockSources,
meta: {
found: mockSources.length,
returned: mockSources.length
}
});
// Act
await controller.getNewsSources(req as Request, res as Response);
// Assert
expect(mockGetNewsSources).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: mockSources,
meta: expect.objectContaining({
found: mockSources.length,
returned: mockSources.length
})
});
});
test('should handle pagination parameters', async () => {
// Arrange
req.query = {
page: '2',
limit: '5'
};
mockGetNewsSources.mockResolvedValueOnce({
data: mockSources,
meta: {
found: 10,
returned: 2,
page: 2,
limit: 5
}
});
// Act
await controller.getNewsSources(req as Request, res as Response);
// Assert
expect(mockGetNewsSources).toHaveBeenCalledWith(expect.objectContaining({
page: 2,
limit: 5
}));
expect(res.status).toHaveBeenCalledWith(200);
});
test('should handle error from service gracefully', async () => {
// Arrange
const error = new Error('Service error');
mockGetNewsSources.mockRejectedValueOnce(error);
// Act
await controller.getNewsSources(req as Request, res as Response);
// Assert
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: expect.any(String)
});
});
});
});