get-highlights.test.ts•7.54 kB
import { GetHighlightsTool } from '../../src/tools/get-highlights.js';
import { ReadwiseAPI } from '../../src/api/readwise-api.js';
import { Logger } from '../../src/utils/logger.js';
import { Highlight, PaginatedResponse } from '../../src/types/index.js';
import { validateNumberRange } from '../../src/types/validation.js';
// Get mocked instances of validateNumberRange
// This will make our tests match what the implementation actually does
jest.mock('../../src/types/validation', () => {
return {
validateNumberRange: jest.fn().mockImplementation((params, field, min, max, message) => {
// Mock implementation that ensures it handles validation correctly
// 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 };
}),
validationSuccess: jest.fn().mockReturnValue({ success: true }),
validationError: jest.fn().mockImplementation((field, message) => {
return {
success: false,
errors: [{ field, message }]
};
})
};
});
// Mocking ReadwiseAPI and Logger
jest.mock('../../src/api/readwise-api');
jest.mock('../../src/utils/logger');
describe('GetHighlightsTool', () => {
let tool: GetHighlightsTool;
let mockApi: jest.Mocked<ReadwiseAPI>;
let mockLogger: jest.Mocked<Logger>;
beforeEach(() => {
jest.clearAllMocks();
// Create mock API
mockApi = {
getHighlights: 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 GetHighlightsTool(mockApi, mockLogger);
});
it('should have the correct name and description', () => {
expect(tool.name).toBe('get_highlights');
expect(tool.description).toBe('Retrieve highlights from your Readwise library');
expect(tool.parameters).toHaveProperty('properties.page');
expect(tool.parameters).toHaveProperty('properties.page_size');
});
it('should validate parameters correctly', () => {
// Reset mock before we start
(validateNumberRange as jest.Mock).mockReset();
// Test valid parameters
expect(tool.validate({})).toEqual(expect.objectContaining({ success: true }));
// Test that validate calls validateNumberRange with expected parameters
// Test each validation case individually
// Test page parameter validations
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page: 1 });
expect(validateNumberRange).toHaveBeenCalledWith(
{ page: 1 }, 'page', 1, undefined, 'Page must be a positive number'
);
// For page: 0, the validation won't occur due to JavaScript treating 0 as falsy
// in the !params.page condition in the validate method.
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page: 0 });
expect(validateNumberRange).not.toHaveBeenCalled();
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page: -1 });
expect(validateNumberRange).toHaveBeenCalledWith(
{ page: -1 }, 'page', 1, undefined, 'Page must be a positive number'
);
// Test page_size parameter validations
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page_size: 10 });
expect(validateNumberRange).toHaveBeenCalledWith(
{ page_size: 10 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100'
);
// For page_size: 0, the validation won't occur due to JavaScript treating 0 as falsy
// in the !params.page_size condition in the validate method.
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page_size: 0 });
expect(validateNumberRange).not.toHaveBeenCalled();
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page_size: -10 });
expect(validateNumberRange).toHaveBeenCalledWith(
{ page_size: -10 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100'
);
(validateNumberRange as jest.Mock).mockReset();
tool.validate({ page_size: 101 });
expect(validateNumberRange).toHaveBeenCalledWith(
{ page_size: 101 }, 'page_size', 1, 100, 'Page size must be a number between 1 and 100'
);
});
it('should execute and return highlights', async () => {
// Set up mock response
const mockResponse: PaginatedResponse<Highlight> = {
count: 2,
next: null,
previous: null,
results: [
{
id: '1',
text: 'Test Highlight 1',
book_id: '123',
location: 42,
location_type: 'page',
highlighted_at: '2023-01-01T12:00:00Z',
updated_at: '2023-01-01T12:00:00Z',
created_at: '2023-01-01T12:00:00Z',
color: 'yellow',
note: '',
tags: ['test'],
url: 'https://example.com'
},
{
id: '2',
text: 'Test Highlight 2',
book_id: '123',
location: 84,
location_type: 'page',
highlighted_at: '2023-01-02T12:00:00Z',
updated_at: '2023-01-02T12:00:00Z',
created_at: '2023-01-02T12:00:00Z',
color: 'blue',
note: 'This is a note',
tags: ['test', 'important'],
url: 'https://example.com'
}
]
};
mockApi.getHighlights.mockResolvedValue(mockResponse);
// Execute the tool
const result = await tool.execute({});
// Verify the result
expect(result).toEqual({ result: mockResponse });
expect(mockApi.getHighlights).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.getHighlights.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.getHighlights.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 highlights'
});
expect(mockLogger.error).toHaveBeenCalled();
});
});