news.routes.test.ts•12.5 kB
/**
* Tests for News API endpoints
*/
import request from 'supertest';
import { Express } from 'express';
import axios from 'axios';
import { createServer } from '../../server';
import { cacheService } from '../../services/cache.service';
import { NewsService } from '../../services/news.service';
import { Server as McpServer } from 'http';
import { prisma } from '../../utils/db';
import { cacheService } from '../../utils/cache';
import axios from 'axios';
// Mock axios to prevent real API calls
jest.mock('axios');
// Use a simpler approach to avoid TypeScript errors with the mocked axios
const mockedAxios = axios as jest.MockedObject<typeof axios>;
// Define types for our mock responses to help TypeScript
type MockNewsResponse = {
data: {
data: Array<{
uuid: string;
title: string;
description: string;
content: string;
url: string;
image_url: string;
source: string;
categories: string[];
published_at: string;
created_at: string;
updated_at: string;
}>;
meta: {
found: number;
returned: number;
page?: number;
total_pages?: number;
};
};
};
// Define default response outside beforeEach to use it in tests
let defaultResponse: MockNewsResponse;
describe('News API Endpoints', () => {
let app: Express;
let server: McpServer;
let port: number;
// Use a unique high-range port for each test run to avoid conflicts
// Choose a port in the 30000-40000 range to minimize conflicts with other services
port = Math.floor(Math.random() * 10000) + 30000;
beforeAll(async () => {
try {
// Create server instance for testing with random port
server = new McpServer(port);
// Only create the app but don't start the server
// We'll use supertest to handle the server lifecycle
app = server.getApp();
console.log(`News API test using port ${port}`);
} catch (error) {
console.error('Error setting up news API test:', error);
throw error;
}
});
afterAll(async () => {
try {
console.log('Cleaning up after news API tests');
// No need to shutdown the server since we're not starting it
// Just clear any resources that might have been allocated
await prisma.$disconnect();
} catch (error) {
console.error('Error cleaning up after news API tests:', error);
}
});
// Sample test data
const mockArticles = [
{
uuid: '123e4567-e89b-12d3-a456-426614174000',
title: 'Test Article 1',
description: 'Test description 1',
content: 'Test content 1',
url: 'https://example.com/test1',
image_url: 'https://example.com/test1.jpg',
source: 'Test Source',
categories: ['technology'],
published_at: '2023-01-01T12:00:00Z',
created_at: '2023-01-01T12:00:00Z',
updated_at: '2023-01-01T12:00:00Z'
},
{
uuid: '223e4567-e89b-12d3-a456-426614174001',
title: 'Test Article 2',
description: 'Test description 2',
content: 'Test content 2',
url: 'https://example.com/test2',
image_url: 'https://example.com/test2.jpg',
source: 'Test Source',
categories: ['business'],
published_at: '2023-01-02T12:00:00Z',
created_at: '2023-01-02T12:00:00Z',
updated_at: '2023-01-02T12:00:00Z'
}
];
// Mock sources data
const mockSources = [
{
id: 'source-1',
name: 'Test Source 1',
url: 'https://example.com/source1',
category: 'technology',
language: 'en',
country: 'us'
},
{
id: 'source-2',
name: 'Test Source 2',
url: 'https://example.com/source2',
category: 'business',
language: 'en',
country: 'uk'
}
];
beforeEach(async () => {
// Clear cache before each test
cacheService.clear();
// Reset all mocks
jest.clearAllMocks();
// Use a simpler approach to avoid TypeScript errors - reset the mock and then define behavior for specific calls
mockedAxios.get.mockReset();
// Initialize the default mock response for all endpoints
defaultResponse = {
data: {
data: mockArticles,
meta: {
found: mockArticles.length,
returned: mockArticles.length,
page: 1,
total_pages: 1
}
}
};
// Set the default response for all endpoints
mockedAxios.get.mockResolvedValue(defaultResponse as any);
// We'll override the mock for specific endpoints in individual tests if needed
});
describe('GET /api/news/top', () => {
it('should return a list of top news articles', async () => {
const response = await request(app)
.get('/api/news/top')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
// The controller returns the service response in body.data
// Service returns { data: Array<Article>, meta: {...} }
expect(response.body.data.data).toBeDefined();
expect(Array.isArray(response.body.data.data)).toBe(true);
expect(response.body.data.meta).toBeDefined();
// Check that the articles have the expected properties
if (response.body.data.data.length > 0) {
const article = response.body.data.data[0];
expect(article).toHaveProperty('uuid');
expect(article).toHaveProperty('title');
}
});
it('should filter articles by category', async () => {
// Use a category that's likely to exist in the test data
const category = 'technology';
// Set up a specific mock for the category filter test
const categoryFilteredArticles = mockArticles.filter(a => a.categories.includes(category));
const categoryResponse: MockNewsResponse = {
data: {
data: categoryFilteredArticles,
meta: {
found: categoryFilteredArticles.length,
returned: categoryFilteredArticles.length,
page: 1,
total_pages: 1
}
}
};
// Mock the axios call with category parameter
mockedAxios.get.mockImplementationOnce((url) => {
if (url.includes(`category=${category}`)) {
return Promise.resolve(categoryResponse as any);
}
return Promise.resolve(defaultResponse as any);
});
const response = await request(app)
.get(`/api/news/top?category=${category}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
expect(response.body.data.data).toBeDefined();
expect(Array.isArray(response.body.data.data)).toBe(true);
// This test might be skipped if no articles are returned
if (response.body.data.data.length === 0) {
console.log('No articles returned for category filtering test');
return;
}
// Verify all returned articles have the specified category
response.body.data.data.forEach((article: any) => {
expect(article.categories).toContain(category);
});
});
it('should use cache for repeated requests', async () => {
// Spy on the newsService.getTopNews method
const getTopNewsSpy = jest.spyOn(newsService, 'getTopNews');
// First request - should call the service
const firstResponse = await request(app)
.get('/api/news/top')
.expect(200);
expect(firstResponse.body.success).toBe(true);
expect(firstResponse.body.data).toBeDefined();
expect(firstResponse.body.data.data).toBeDefined();
// Second request - should use cache
const secondResponse = await request(app)
.get('/api/news/top')
.expect(200);
expect(secondResponse.body.success).toBe(true);
expect(secondResponse.body.data).toBeDefined();
expect(secondResponse.body.data.data).toBeDefined();
// Service should only be called once as the second request should hit the cache
expect(getTopNewsSpy).toHaveBeenCalledTimes(1);
// Verify both responses have the same structure
expect(JSON.stringify(firstResponse.body)).toBe(JSON.stringify(secondResponse.body));
});
});
describe('GET /api/news/all', () => {
it('should return all news articles with pagination', async () => {
const limit = 5;
const response = await request(app)
.get(`/api/news/all?limit=${limit}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
// Data is returned directly in response.body.data
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.data.length).toBeLessThanOrEqual(limit);
});
it('should filter by date range', async () => {
const fromDate = new Date();
fromDate.setDate(fromDate.getDate() - 30); // Use a wider date range for testing
const response = await request(app)
.get(`/api/news/all?fromDate=${fromDate.toISOString()}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
// This test might be skipped if no articles are returned
if (response.body.data.length === 0) {
console.log('No articles returned for date filtering test');
return;
}
// Check if articles have a publication date in the correct format
expect(response.body.data[0]).toHaveProperty('published_at');
// Verify the date filtering works if there are articles
if (response.body.data.length > 0) {
// Check at least one article has a date we can parse
const hasValidDate = response.body.data.some(
(article: any) => article.published_at && new Date(article.published_at).getTime() > 0
);
expect(hasValidDate).toBe(true);
}
});
});
describe('GET /api/news/uuid/:uuid', () => {
it('should return a specific article by UUID', async () => {
// Use a known UUID from our mock data
const testUuid = '123e4567-e89b-12d3-a456-426614174000';
// Override the default mock for this specific test using type casting to avoid TS errors
const singleArticleResponse = {
data: {
data: mockArticles[0]
}
};
mockedAxios.get.mockResolvedValueOnce(singleArticleResponse as any);
// Make the request
const response = await request(app)
.get(`/api/news/uuid/${testUuid}`)
.expect('Content-Type', /json/)
.expect(200);
// Verify the response
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('uuid', testUuid);
expect(response.body.data).toHaveProperty('title', 'Test Article 1');
});
it('should return 404 for non-existent UUID', async () => {
// UUID that doesn't exist
const nonExistentUuid = '00000000-0000-0000-0000-000000000000';
// Create an axios error to simulate a 404 response
const axiosError = new Error('Article not found') as any;
axiosError.response = { status: 404, statusText: 'Not Found' };
// Use the error for this specific test
mockedAxios.get.mockRejectedValueOnce(axiosError);
// Make the request and expect 404
const response = await request(app)
.get(`/api/news/uuid/${nonExistentUuid}`)
.expect('Content-Type', /json/)
.expect(404);
// Verify error response format
expect(response.body.success).toBe(false);
expect(response.body.error).toBeDefined();
});
});
describe('GET /api/news/sources', () => {
it('should return a list of news sources', async () => {
const response = await request(app)
.get('/api/news/sources')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
// Sources are returned directly in response.body.data
expect(Array.isArray(response.body.data)).toBe(true);
// Verify response contains expected source properties
if (response.body.data.length > 0) {
expect(response.body.data[0]).toHaveProperty('domain');
expect(response.body.data[0]).toHaveProperty('source_id');
}
});
});
});