get-documents.test.ts•7.14 kB
import { GetDocumentsTool } from '../../src/tools/get-documents.js';
import { ReadwiseAPI } from '../../src/api/readwise-api.js';
import { Logger } from '../../src/utils/logger.js';
import { Document, PaginatedResponse } from '../../src/types/index.js';
// Mock the ReadwiseAPI
jest.mock('../../src/api/readwise-api');
// Get mocked instances of validateNumberRange
// This will make our tests match what the implementation actually does
jest.mock('../../src/types/validation', () => {
const original = jest.requireActual('../../src/types/validation');
return {
...original,
validateNumberRange: jest.fn().mockImplementation((params, field, min, max, message) => {
// Mock implementation that ensures it handles page: 0 as error and returns consistent objects
// Skip validation if field is not present
if (params[field] === undefined || params[field] === null) {
return { success: true };
}
const value = Number(params[field]);
if (isNaN(value)) {
return { success: false, errors: [{ field, message: message || `${field} must be a number` }] };
}
if (min !== undefined && value < min) {
return { success: false, errors: [{ field, message: message || `${field} must be at least ${min}` }] };
}
if (max !== undefined && value > max) {
return { success: false, errors: [{ field, message: message || `${field} must be at most ${max}` }] };
}
return { success: true };
})
};
});
describe('GetDocumentsTool', () => {
let tool: GetDocumentsTool;
let mockApi: jest.Mocked<ReadwiseAPI>;
let mockLogger: jest.Mocked<Logger>;
beforeEach(() => {
// Create mock API
mockApi = {
getDocuments: jest.fn()
} as unknown as jest.Mocked<ReadwiseAPI>;
// Create mock logger
mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
} as unknown as jest.Mocked<Logger>;
// Create the tool
tool = new GetDocumentsTool(mockApi, mockLogger);
});
afterEach(() => {
jest.resetAllMocks();
});
it('should have the correct name and description', () => {
expect(tool.name).toBe('get_documents');
expect(tool.description).toBeDefined();
expect(tool.parameters).toBeDefined();
});
it('should validate parameters correctly', () => {
const validateNumberRange = jest.requireMock('../../src/types/validation').validateNumberRange;
// No mocked validateNumberRange calls should have happened yet
expect(validateNumberRange).not.toHaveBeenCalled();
// Test valid parameters - these should pass without calling validateNumberRange
expect(tool.validate({})).toEqual(expect.objectContaining({ success: true }));
// These should call validateNumberRange but still pass because our mock returns success
validateNumberRange.mockReturnValueOnce({ success: true });
expect(tool.validate({ page: 1 })).toEqual(expect.objectContaining({ success: true }));
validateNumberRange.mockReturnValueOnce({ success: true });
expect(tool.validate({ page_size: 10 })).toEqual(expect.objectContaining({ success: true }));
validateNumberRange.mockReturnValueOnce({ success: true });
validateNumberRange.mockReturnValueOnce({ success: true });
expect(tool.validate({ page: 1, page_size: 10 })).toEqual(expect.objectContaining({ success: true }));
// Test invalid parameters - these should fail
validateNumberRange.mockReturnValueOnce({
success: false,
errors: [{ field: 'page', message: 'Page must be a positive number' }]
});
expect(tool.validate({ page: 0 })).toEqual(expect.objectContaining({ success: false }));
validateNumberRange.mockReturnValueOnce({
success: false,
errors: [{ field: 'page', message: 'Page must be a positive number' }]
});
expect(tool.validate({ page: -1 })).toEqual(expect.objectContaining({ success: false }));
validateNumberRange.mockReturnValueOnce({
success: false,
errors: [{ field: 'page_size', message: 'Page size must be a number between 1 and 100' }]
});
expect(tool.validate({ page_size: 0 })).toEqual(expect.objectContaining({ success: false }));
validateNumberRange.mockReturnValueOnce({
success: false,
errors: [{ field: 'page_size', message: 'Page size must be a number between 1 and 100' }]
});
expect(tool.validate({ page_size: -10 })).toEqual(expect.objectContaining({ success: false }));
validateNumberRange.mockReturnValueOnce({
success: false,
errors: [{ field: 'page_size', message: 'Page size must be a number between 1 and 100' }]
});
expect(tool.validate({ page_size: 101 })).toEqual(expect.objectContaining({ success: false }));
});
it('should execute and return documents', async () => {
// Set up mock response
const mockResponse: PaginatedResponse<Document> = {
count: 2,
next: null,
previous: null,
results: [
{
id: '1',
title: 'Test Document 1',
author: 'Test Author 1',
source: 'article',
url: 'https://example.com/article1',
created_at: '2023-01-01T12:00:00Z',
updated_at: '2023-01-01T12:00:00Z'
},
{
id: '2',
title: 'Test Document 2',
author: 'Test Author 2',
source: 'pdf',
url: 'https://example.com/article2',
created_at: '2023-01-02T12:00:00Z',
updated_at: '2023-01-02T12:00:00Z'
}
]
};
mockApi.getDocuments.mockResolvedValue(mockResponse);
// Execute the tool
const result = await tool.execute({});
// Verify the result
expect(result).toEqual({ result: mockResponse });
expect(mockApi.getDocuments).toHaveBeenCalledWith({});
expect(mockLogger.debug).toHaveBeenCalled();
});
it('should handle API errors correctly', async () => {
// Set up mock error
const apiError = {
type: 'api',
details: {
status: 401,
code: 'api_error',
message: 'Invalid token'
}
};
mockApi.getDocuments.mockRejectedValue(apiError);
// Execute the tool and expect it to reject
await expect(tool.execute({})).rejects.toEqual(apiError);
expect(mockLogger.error).toHaveBeenCalled();
});
it('should handle unexpected errors correctly', async () => {
// Spy on the error logger
mockLogger.error = jest.fn();
// Set up the API mock to throw an unexpected error
mockApi.getDocuments.mockImplementation(() => {
throw new Error('Unexpected error');
});
// Execute the tool and expect it to not throw but return error in the result
const result = await tool.execute({});
expect(result).toEqual({
result: { count: 0, next: null, previous: null, results: [] },
success: false,
error: 'An unexpected error occurred while fetching documents'
});
expect(mockLogger.error).toHaveBeenCalled();
});
});