Skip to main content
Glama

News Aggregator API

news.routes.test.ts12.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'); } }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Malachi-devel/the-news-api-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server