cache.routes.test.ts•8.32 kB
/**
* Tests for Cache API endpoints
*/
import request from 'supertest';
import { Express } from 'express';
import { jest, describe, beforeAll, afterAll, beforeEach, it, expect } from '@jest/globals';
import { McpServer } from '../../server';
import { cacheService } from '../../utils/cache';
// Mock the cache service
jest.mock('../../utils/cache', () => {
return {
cacheService: {
get: jest.fn(),
set: jest.fn().mockReturnValue(true),
delete: jest.fn().mockReturnValue(true),
deleteByPrefix: jest.fn().mockReturnValue(2),
getStats: jest.fn().mockReturnValue({
hits: 10,
misses: 5,
keys: 2,
ksize: 200,
vsize: 400,
uptime: 0.5
}),
clear: jest.fn().mockReturnValue(true),
shutdown: jest.fn()
}
};
});
describe('Cache API Endpoints', () => {
let app: Express | null = null;
let server: McpServer | null = null;
const port = Math.floor(Math.random() * 10000) + 20000; // Different port range from news tests
// Helper function to ensure app is available
const getApp = () => {
if (!app) {
throw new Error('Express app is not initialized');
}
return app;
};
beforeAll(async () => {
try {
// Create server instance for testing with random port
server = new McpServer(port);
await server.start();
app = server.getApp();
// Ensure app is initialized
if (!app) {
throw new Error('Failed to initialize Express app');
}
} catch (error) {
console.error('Error starting cache API test server:', error);
throw error;
}
});
afterAll(async () => {
try {
if (server) {
// Call shutdown on the cache service to clean up timers
cacheService.shutdown();
// Properly shut down the server
await server.shutdown();
// Ensure all references are cleaned up
server = null;
app = null;
// Force Jest to wait a bit to ensure everything is cleaned up
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error('Error during test server shutdown:', error);
}
});
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
});
describe('GET /api/cache/stats', () => {
it('should return cache statistics', async () => {
// Update the mock implementation for getStats to include all required properties
const mockStats = {
hits: 0,
misses: 0,
keys: 0,
ksize: 0,
vsize: 0,
uptime: 0.123
};
// Override the getStats mock for this test
(cacheService.getStats as jest.Mock).mockReturnValue(mockStats);
const response = await request(getApp())
.get('/api/cache/stats')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('hits');
expect(response.body.data).toHaveProperty('misses');
expect(response.body.data).toHaveProperty('keys');
expect(response.body.data).toHaveProperty('ksize');
expect(response.body.data).toHaveProperty('vsize');
expect(response.body.data).toHaveProperty('uptime');
});
it('should show updated stats after cache is used', async () => {
// Create initial stats with 0 hits
const initialStats = {
hits: 0,
misses: 0,
keys: 1,
ksize: 100,
vsize: 200,
uptime: 0.123
};
// Override getStats to return our controlled values
(cacheService.getStats as jest.Mock)
.mockReturnValueOnce(initialStats) // First call returns initial stats
.mockReturnValueOnce({ // Second call returns updated stats with more hits
...initialStats,
hits: 1 // Hits increased by 1
});
// First get stats
const initialResponse = await request(getApp())
.get('/api/cache/stats')
.expect(200);
const initialHits = initialResponse.body.data.hits as number;
expect(initialHits).toBe(0);
// Get updated stats
const updatedResponse = await request(getApp())
.get('/api/cache/stats')
.expect(200);
// Verify hits increased - comparing exact values
expect(updatedResponse.body.data.hits).toBe(1);
});
});
describe('DELETE /api/cache/clear', () => {
it('should clear the entire cache', async () => {
// Set up our mock for the stats before clearing
const beforeStats = {
hits: 0,
misses: 0,
keys: 2,
ksize: 200,
vsize: 400,
uptime: 0.5
};
const afterStats = {
hits: 0,
misses: 0,
keys: 0, // Keys are now 0 after clearing
ksize: 0,
vsize: 0,
uptime: 0.6
};
// Mock to return different stats based on when it's called
(cacheService.getStats as jest.Mock)
.mockReturnValueOnce(beforeStats)
.mockReturnValueOnce(afterStats);
// Mock the clear method
(cacheService.clear as jest.Mock).mockReturnValue(true);
// Verify cache has items initially
const beforeClear = await request(getApp()).get('/api/cache/stats');
expect(beforeClear.body.data.keys).toBe(2);
// Clear cache
const clearResponse = await request(getApp())
.delete('/api/cache/clear')
.expect('Content-Type', /json/)
.expect(200);
expect(clearResponse.body.message).toBe('Cache cleared successfully');
// Verify cache is empty
const afterClear = await request(getApp()).get('/api/cache/stats');
expect(afterClear.body.data.keys).toBe(0);
// Verify that clear was called
expect(cacheService.clear).toHaveBeenCalled();
});
});
describe('DELETE /api/cache/clear/:type', () => {
it('should clear cache by type', async () => {
// Mock different stats before and after clearing
const beforeStats = {
hits: 5,
misses: 2,
keys: 3, // 2 'top' keys and 1 'sources' key
ksize: 300,
vsize: 600,
uptime: 0.7
};
const afterStats = {
hits: 5,
misses: 2,
keys: 1, // Only the 'sources' key remains
ksize: 100,
vsize: 200,
uptime: 0.8
};
// Setup mock to return different values for different calls
(cacheService.getStats as jest.Mock)
.mockReturnValueOnce(beforeStats)
.mockReturnValueOnce(afterStats);
// Mock deleteByPrefix to return number of deleted items
(cacheService.deleteByPrefix as jest.Mock).mockReturnValue(2); // 2 keys deleted
// Verify cache has items before clearing
const initialStatsResponse = await request(getApp()).get('/api/cache/stats');
expect(initialStatsResponse.body.data.keys).toBe(3);
// Clear only 'top' cache
const response = await request(getApp())
.delete('/api/cache/clear/top')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.message).toBe('Cleared 2 cache entries for type: top');
// Verify the correct prefix was used - note the underscore appended to the type
expect(cacheService.deleteByPrefix).toHaveBeenCalledWith('top_');
// Verify 'top' related keys are gone but 'sources' remains
const finalStatsResponse = await request(getApp()).get('/api/cache/stats');
expect(finalStatsResponse.body.data.keys).toBe(1); // Only the sources_test key should remain
});
it('should return 400 for invalid cache type', async () => {
// We don't need to mock deleteByPrefix here since the validation happens before it's called
// The controller validates the type before attempting to delete anything
const response = await request(getApp())
.delete('/api/cache/clear/invalid_type')
.expect('Content-Type', /json/)
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid cache type: invalid_type');
});
});
});