news-routes-reliable.test.ts•9.96 kB
/**
* Reliable tests for News API endpoints
* This implementation properly handles server lifecycle and mocks database
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach, jest } from '@jest/globals';
import request from 'supertest';
import { Express } from 'express';
import { McpServer } from '../../server';
import { cacheService } from '../../utils/cache';
// Import axios to have its type information
import axios from 'axios';
// Mock axios for external API calls
jest.mock('axios');
const mockedAxios = jest.requireMock('axios') as jest.Mocked<typeof axios>;
// Mock cache service to prevent side effects
jest.mock('../../utils/cache', () => {
const mockCache = new Map();
return {
cacheService: {
get: jest.fn().mockImplementation((key) => mockCache.get(key)),
set: jest.fn().mockImplementation((key, value, ttl) => mockCache.set(key, value)),
clear: jest.fn().mockImplementation(() => mockCache.clear()),
getStats: jest.fn().mockReturnValue({ hits: 0, misses: 0, keys: 0 }),
getTTL: jest.fn().mockReturnValue(300)
}
};
});
// Mock database connection
jest.mock('../../utils/db', () => {
return {
connectDb: jest.fn().mockResolvedValue(undefined),
disconnectDb: jest.fn().mockResolvedValue(undefined),
prisma: {
article: {
findMany: jest.fn().mockResolvedValue([
{
id: 1,
uuid: '123e4567-e89b-12d3-a456-426614174001',
title: 'Test Article 1',
description: 'Test description 1',
content: 'Test content 1',
url: 'http://example.com/1',
image_url: 'http://example.com/image1.jpg',
source: 'Test Source',
categories: ['general'],
published_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 2,
uuid: '123e4567-e89b-12d3-a456-426614174002',
title: 'Test Article 2',
description: 'Test description 2',
content: 'Test content 2',
url: 'http://example.com/2',
image_url: 'http://example.com/image2.jpg',
source: 'Test Source',
categories: ['technology'],
published_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
]),
findUnique: jest.fn().mockImplementation((args) => {
if (args?.where?.uuid === '123e4567-e89b-12d3-a456-426614174001') {
return Promise.resolve({
id: 1,
uuid: '123e4567-e89b-12d3-a456-426614174001',
title: 'Test Article 1',
description: 'Test description 1',
content: 'Test content 1',
url: 'http://example.com/1',
image_url: 'http://example.com/image1.jpg',
source: 'Test Source',
categories: ['general'],
published_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
}
return Promise.resolve(null);
}),
count: jest.fn().mockResolvedValue(2)
}
}
};
});
describe('News API Routes', () => {
let app: Express;
let server: McpServer;
let mockPort: number;
// Setup before all tests
beforeAll(async () => {
// Use a random port to avoid conflicts
mockPort = Math.floor(Math.random() * 10000) + 20000;
// Store original PORT
const originalPort = process.env.PORT;
// Set PORT environment variable to our random port
process.env.PORT = mockPort.toString();
try {
// Create new server instance
server = new McpServer(mockPort);
// Get Express app for testing
app = server.getApp();
// Start server without actually binding to port (we'll use supertest)
jest.spyOn(server, 'start').mockImplementation(async () => {
// This is a mock implementation that doesn't actually start the server
// but simulates the successful server start
return Promise.resolve();
});
// Call start to initialize database connection
await server.start();
// Restore original PORT
if (originalPort) {
process.env.PORT = originalPort;
} else {
delete process.env.PORT;
}
} catch (error) {
console.error('Error in test setup:', error);
throw error;
}
});
// Cleanup after all tests
afterAll(async () => {
try {
if (server) {
await server.shutdown();
}
} catch (error) {
console.error('Error in test teardown:', error);
}
});
// Reset mocks and clear cache before each test
beforeEach(() => {
cacheService.clear();
mockedAxios.get.mockReset();
// Define mock article for UUID test
const testArticle = {
id: 1,
uuid: '123e4567-e89b-12d3-a456-426614174001',
title: 'Test Article 1',
description: 'Test description 1',
content: 'Test content 1',
url: 'http://example.com/1',
image_url: 'http://example.com/image1.jpg',
source: 'Test Source',
categories: ['general'],
published_at: new Date().toISOString(),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// Set up default mock response for article by UUID
mockedAxios.get.mockImplementation((url: string, config?: any) => {
if (url.includes('123e4567-e89b-12d3-a456-426614174001')) {
return Promise.resolve({
status: 200,
statusText: 'OK',
headers: {},
config: config || {},
data: {
data: testArticle
}
});
} else if (url.includes('00000000-0000-0000-0000-000000000000')) {
// For non-existent article, simulate a 404 error
const error = new Error('Article not found - 404');
(error as any).response = { status: 404 };
return Promise.reject(error);
} else {
// Default response for other URLs (e.g., top news, all news)
return Promise.resolve({
status: 200,
statusText: 'OK',
headers: {},
config: config || {},
data: {
data: [testArticle],
meta: {
found: 1,
returned: 1,
page: 1,
total_pages: 1
}
}
});
}
});
});
// Test GET /api/news/top endpoint
test('GET /api/news/top should return top news articles', async () => {
const response = await request(app)
.get('/api/news/top')
.expect('Content-Type', /json/)
.expect(200);
// Verify response structure with nested data
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);
expect(response.body.data.data.length).toBeGreaterThan(0);
// Check first article structure
const article = response.body.data.data[0];
expect(article).toHaveProperty('title');
expect(article).toHaveProperty('source');
expect(article).toHaveProperty('uuid');
});
// Test filtering by category
test('GET /api/news/top should filter by category', async () => {
const response = await request(app)
.get('/api/news/top?category=technology')
.expect('Content-Type', /json/)
.expect(200);
// Verify response structure
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);
});
// Test /api/news/all endpoint with pagination
test('GET /api/news/all should return paginated articles', async () => {
const response = await request(app)
.get('/api/news/all?page=1&limit=10')
.expect('Content-Type', /json/)
.expect(200);
// Verify response structure with nested data
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);
// Check pagination metadata
expect(response.body.data.meta).toBeDefined();
expect(response.body.data.meta).toHaveProperty('found');
expect(response.body.data.meta).toHaveProperty('returned');
expect(response.body.data.meta).toHaveProperty('page');
});
// Test article retrieval by UUID
test('GET /api/news/uuid/:uuid should return a specific article', async () => {
const testUuid = '123e4567-e89b-12d3-a456-426614174001';
const response = await request(app)
.get(`/api/news/uuid/${testUuid}`)
.expect('Content-Type', /json/)
.expect(200);
// Verify response structure with nested data
expect(response.body.success).toBe(true);
expect(response.body.data).toBeDefined();
expect(response.body.data.data).toBeDefined();
expect(response.body.data.data).toHaveProperty('uuid', testUuid);
expect(response.body.data.data).toHaveProperty('title', 'Test Article 1');
});
// Test 404 for non-existent article
test('GET /api/news/uuid/:uuid should return 404 for non-existent article', async () => {
// Use a properly formatted UUID that doesn't exist
const nonExistentUuid = '00000000-0000-0000-0000-000000000000';
const response = await request(app)
.get(`/api/news/uuid/${nonExistentUuid}`)
.expect('Content-Type', /json/)
.expect(404);
// Verify response structure
expect(response.body.success).toBe(false);
expect(response.body.error).toBeDefined();
});
});