Features: - 400+ tools covering entire GoHighLevel API - Contact management, conversations, opportunities, calendars - Invoices, payments, products, store management - Social media, email marketing, workflows, and more - Self-host or use managed solution at mcp.localbosses.org
549 lines
17 KiB
TypeScript
549 lines
17 KiB
TypeScript
/**
|
|
* Unit Tests for Blog Tools
|
|
* Tests all 7 blog management MCP tools
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
import { BlogTools } from '../../src/tools/blog-tools.js';
|
|
import { MockGHLApiClient, mockBlogPost, mockBlogSite, mockBlogAuthor, mockBlogCategory } from '../mocks/ghl-api-client.mock.js';
|
|
|
|
describe('BlogTools', () => {
|
|
let blogTools: BlogTools;
|
|
let mockGhlClient: MockGHLApiClient;
|
|
|
|
beforeEach(() => {
|
|
mockGhlClient = new MockGHLApiClient();
|
|
blogTools = new BlogTools(mockGhlClient as any);
|
|
});
|
|
|
|
describe('getToolDefinitions', () => {
|
|
it('should return 7 blog tool definitions', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
expect(tools).toHaveLength(7);
|
|
|
|
const toolNames = tools.map(tool => tool.name);
|
|
expect(toolNames).toEqual([
|
|
'create_blog_post',
|
|
'update_blog_post',
|
|
'get_blog_posts',
|
|
'get_blog_sites',
|
|
'get_blog_authors',
|
|
'get_blog_categories',
|
|
'check_url_slug'
|
|
]);
|
|
});
|
|
|
|
it('should have proper schema definitions for all tools', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
|
|
tools.forEach(tool => {
|
|
expect(tool.name).toBeDefined();
|
|
expect(tool.description).toBeDefined();
|
|
expect(tool.inputSchema).toBeDefined();
|
|
expect(tool.inputSchema.type).toBe('object');
|
|
expect(tool.inputSchema.properties).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('executeTool', () => {
|
|
it('should route tool calls correctly', async () => {
|
|
const createSpy = jest.spyOn(blogTools as any, 'createBlogPost');
|
|
const getSitesSpy = jest.spyOn(blogTools as any, 'getBlogSites');
|
|
|
|
await blogTools.executeTool('create_blog_post', {
|
|
title: 'Test Post',
|
|
blogId: 'blog_123',
|
|
content: '<h1>Test</h1>',
|
|
description: 'Test description',
|
|
imageUrl: 'https://example.com/image.jpg',
|
|
imageAltText: 'Test image',
|
|
urlSlug: 'test-post',
|
|
author: 'author_123',
|
|
categories: ['cat_123']
|
|
});
|
|
await blogTools.executeTool('get_blog_sites', {});
|
|
|
|
expect(createSpy).toHaveBeenCalled();
|
|
expect(getSitesSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should throw error for unknown tool', async () => {
|
|
await expect(
|
|
blogTools.executeTool('unknown_tool', {})
|
|
).rejects.toThrow('Unknown tool: unknown_tool');
|
|
});
|
|
});
|
|
|
|
describe('create_blog_post', () => {
|
|
const validBlogPostData = {
|
|
title: 'Test Blog Post',
|
|
blogId: 'blog_123',
|
|
content: '<h1>Test Content</h1><p>This is a test blog post.</p>',
|
|
description: 'Test blog post description',
|
|
imageUrl: 'https://example.com/test-image.jpg',
|
|
imageAltText: 'Test image alt text',
|
|
urlSlug: 'test-blog-post',
|
|
author: 'author_123',
|
|
categories: ['cat_123', 'cat_456'],
|
|
tags: ['test', 'blog']
|
|
};
|
|
|
|
it('should create blog post successfully', async () => {
|
|
const result = await blogTools.executeTool('create_blog_post', validBlogPostData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.blogPost).toBeDefined();
|
|
expect(result.blogPost.title).toBe(validBlogPostData.title);
|
|
expect(result.message).toContain('created successfully');
|
|
});
|
|
|
|
it('should set default status to DRAFT if not provided', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'createBlogPost');
|
|
|
|
await blogTools.executeTool('create_blog_post', validBlogPostData);
|
|
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: 'DRAFT'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should set publishedAt when status is PUBLISHED', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'createBlogPost');
|
|
|
|
await blogTools.executeTool('create_blog_post', {
|
|
...validBlogPostData,
|
|
status: 'PUBLISHED'
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: 'PUBLISHED',
|
|
publishedAt: expect.any(String)
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should use custom publishedAt if provided', async () => {
|
|
const customDate = '2024-06-01T12:00:00.000Z';
|
|
const spy = jest.spyOn(mockGhlClient, 'createBlogPost');
|
|
|
|
await blogTools.executeTool('create_blog_post', {
|
|
...validBlogPostData,
|
|
publishedAt: customDate
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
publishedAt: customDate
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle API errors', async () => {
|
|
const mockError = new Error('GHL API Error (400): Invalid blog data');
|
|
jest.spyOn(mockGhlClient, 'createBlogPost').mockRejectedValueOnce(mockError);
|
|
|
|
await expect(
|
|
blogTools.executeTool('create_blog_post', validBlogPostData)
|
|
).rejects.toThrow('Failed to create blog post');
|
|
});
|
|
});
|
|
|
|
describe('update_blog_post', () => {
|
|
it('should update blog post successfully', async () => {
|
|
const updateData = {
|
|
postId: 'post_123',
|
|
blogId: 'blog_123',
|
|
title: 'Updated Title',
|
|
status: 'PUBLISHED' as const
|
|
};
|
|
|
|
const result = await blogTools.executeTool('update_blog_post', updateData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.blogPost).toBeDefined();
|
|
expect(result.message).toBe('Blog post updated successfully');
|
|
});
|
|
|
|
it('should handle partial updates', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'updateBlogPost');
|
|
|
|
await blogTools.executeTool('update_blog_post', {
|
|
postId: 'post_123',
|
|
blogId: 'blog_123',
|
|
title: 'New Title'
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith('post_123', {
|
|
locationId: 'test_location_123',
|
|
blogId: 'blog_123',
|
|
title: 'New Title'
|
|
});
|
|
});
|
|
|
|
it('should include all provided fields', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'updateBlogPost');
|
|
|
|
const updateData = {
|
|
postId: 'post_123',
|
|
blogId: 'blog_123',
|
|
title: 'Updated Title',
|
|
content: '<h1>Updated Content</h1>',
|
|
status: 'PUBLISHED' as const,
|
|
tags: ['updated', 'test']
|
|
};
|
|
|
|
await blogTools.executeTool('update_blog_post', updateData);
|
|
|
|
expect(spy).toHaveBeenCalledWith('post_123', {
|
|
locationId: 'test_location_123',
|
|
blogId: 'blog_123',
|
|
title: 'Updated Title',
|
|
rawHTML: '<h1>Updated Content</h1>',
|
|
status: 'PUBLISHED',
|
|
tags: ['updated', 'test']
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('get_blog_posts', () => {
|
|
it('should get blog posts successfully', async () => {
|
|
const result = await blogTools.executeTool('get_blog_posts', {
|
|
blogId: 'blog_123'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.posts).toBeDefined();
|
|
expect(Array.isArray(result.posts)).toBe(true);
|
|
expect(result.count).toBeDefined();
|
|
expect(result.message).toContain('Retrieved');
|
|
});
|
|
|
|
it('should use default parameters', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'getBlogPosts');
|
|
|
|
await blogTools.executeTool('get_blog_posts', {
|
|
blogId: 'blog_123'
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith({
|
|
locationId: 'test_location_123',
|
|
blogId: 'blog_123',
|
|
limit: 10,
|
|
offset: 0,
|
|
searchTerm: undefined,
|
|
status: undefined
|
|
});
|
|
});
|
|
|
|
it('should handle search and filtering', async () => {
|
|
const result = await blogTools.executeTool('get_blog_posts', {
|
|
blogId: 'blog_123',
|
|
limit: 5,
|
|
offset: 10,
|
|
searchTerm: 'test',
|
|
status: 'PUBLISHED'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.posts).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('get_blog_sites', () => {
|
|
it('should get blog sites successfully', async () => {
|
|
const result = await blogTools.executeTool('get_blog_sites', {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.sites).toBeDefined();
|
|
expect(Array.isArray(result.sites)).toBe(true);
|
|
expect(result.count).toBeDefined();
|
|
expect(result.message).toContain('Retrieved');
|
|
});
|
|
|
|
it('should use default parameters', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'getBlogSites');
|
|
|
|
await blogTools.executeTool('get_blog_sites', {});
|
|
|
|
expect(spy).toHaveBeenCalledWith({
|
|
locationId: 'test_location_123',
|
|
skip: 0,
|
|
limit: 10,
|
|
searchTerm: undefined
|
|
});
|
|
});
|
|
|
|
it('should handle custom parameters', async () => {
|
|
const result = await blogTools.executeTool('get_blog_sites', {
|
|
limit: 5,
|
|
skip: 2,
|
|
searchTerm: 'main blog'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.sites).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('get_blog_authors', () => {
|
|
it('should get blog authors successfully', async () => {
|
|
const result = await blogTools.executeTool('get_blog_authors', {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.authors).toBeDefined();
|
|
expect(Array.isArray(result.authors)).toBe(true);
|
|
expect(result.count).toBeDefined();
|
|
expect(result.message).toContain('Retrieved');
|
|
});
|
|
|
|
it('should use default parameters', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'getBlogAuthors');
|
|
|
|
await blogTools.executeTool('get_blog_authors', {});
|
|
|
|
expect(spy).toHaveBeenCalledWith({
|
|
locationId: 'test_location_123',
|
|
limit: 10,
|
|
offset: 0
|
|
});
|
|
});
|
|
|
|
it('should handle custom pagination', async () => {
|
|
const result = await blogTools.executeTool('get_blog_authors', {
|
|
limit: 20,
|
|
offset: 5
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.authors).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('get_blog_categories', () => {
|
|
it('should get blog categories successfully', async () => {
|
|
const result = await blogTools.executeTool('get_blog_categories', {});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.categories).toBeDefined();
|
|
expect(Array.isArray(result.categories)).toBe(true);
|
|
expect(result.count).toBeDefined();
|
|
expect(result.message).toContain('Retrieved');
|
|
});
|
|
|
|
it('should use default parameters', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'getBlogCategories');
|
|
|
|
await blogTools.executeTool('get_blog_categories', {});
|
|
|
|
expect(spy).toHaveBeenCalledWith({
|
|
locationId: 'test_location_123',
|
|
limit: 10,
|
|
offset: 0
|
|
});
|
|
});
|
|
|
|
it('should handle custom pagination', async () => {
|
|
const result = await blogTools.executeTool('get_blog_categories', {
|
|
limit: 15,
|
|
offset: 3
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.categories).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('check_url_slug', () => {
|
|
it('should check available URL slug successfully', async () => {
|
|
const result = await blogTools.executeTool('check_url_slug', {
|
|
urlSlug: 'new-blog-post'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.urlSlug).toBe('new-blog-post');
|
|
expect(result.exists).toBe(false);
|
|
expect(result.available).toBe(true);
|
|
expect(result.message).toContain('is available');
|
|
});
|
|
|
|
it('should detect existing URL slug', async () => {
|
|
const result = await blogTools.executeTool('check_url_slug', {
|
|
urlSlug: 'existing-slug'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.urlSlug).toBe('existing-slug');
|
|
expect(result.exists).toBe(true);
|
|
expect(result.available).toBe(false);
|
|
expect(result.message).toContain('is already in use');
|
|
});
|
|
|
|
it('should handle post ID exclusion for updates', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'checkUrlSlugExists');
|
|
|
|
await blogTools.executeTool('check_url_slug', {
|
|
urlSlug: 'test-slug',
|
|
postId: 'post_123'
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith({
|
|
locationId: 'test_location_123',
|
|
urlSlug: 'test-slug',
|
|
postId: 'post_123'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should propagate API client errors', async () => {
|
|
const mockError = new Error('Network timeout');
|
|
jest.spyOn(mockGhlClient, 'createBlogPost').mockRejectedValueOnce(mockError);
|
|
|
|
await expect(
|
|
blogTools.executeTool('create_blog_post', {
|
|
title: 'Test',
|
|
blogId: 'blog_123',
|
|
content: 'content',
|
|
description: 'desc',
|
|
imageUrl: 'url',
|
|
imageAltText: 'alt',
|
|
urlSlug: 'slug',
|
|
author: 'author',
|
|
categories: ['cat']
|
|
})
|
|
).rejects.toThrow('Failed to create blog post: Error: Network timeout');
|
|
});
|
|
|
|
it('should handle blog not found errors', async () => {
|
|
const mockError = new Error('GHL API Error (404): Blog not found');
|
|
jest.spyOn(mockGhlClient, 'getBlogPosts').mockRejectedValueOnce(mockError);
|
|
|
|
await expect(
|
|
blogTools.executeTool('get_blog_posts', { blogId: 'not_found' })
|
|
).rejects.toThrow('Failed to get blog posts');
|
|
});
|
|
|
|
it('should handle invalid blog post data', async () => {
|
|
const mockError = new Error('GHL API Error (422): Invalid blog post data');
|
|
jest.spyOn(mockGhlClient, 'updateBlogPost').mockRejectedValueOnce(mockError);
|
|
|
|
await expect(
|
|
blogTools.executeTool('update_blog_post', {
|
|
postId: 'post_123',
|
|
blogId: 'blog_123',
|
|
title: ''
|
|
})
|
|
).rejects.toThrow('Failed to update blog post');
|
|
});
|
|
});
|
|
|
|
describe('input validation', () => {
|
|
it('should validate required fields in create_blog_post', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
const createTool = tools.find(tool => tool.name === 'create_blog_post');
|
|
|
|
expect(createTool?.inputSchema.required).toEqual([
|
|
'title', 'blogId', 'content', 'description',
|
|
'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories'
|
|
]);
|
|
});
|
|
|
|
it('should validate required fields in update_blog_post', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
const updateTool = tools.find(tool => tool.name === 'update_blog_post');
|
|
|
|
expect(updateTool?.inputSchema.required).toEqual(['postId', 'blogId']);
|
|
});
|
|
|
|
it('should validate blog post status enum', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
const createTool = tools.find(tool => tool.name === 'create_blog_post');
|
|
|
|
expect(createTool?.inputSchema.properties.status.enum).toEqual([
|
|
'DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'
|
|
]);
|
|
});
|
|
|
|
it('should validate URL slug requirement', () => {
|
|
const tools = blogTools.getToolDefinitions();
|
|
const checkSlugTool = tools.find(tool => tool.name === 'check_url_slug');
|
|
|
|
expect(checkSlugTool?.inputSchema.required).toEqual(['urlSlug']);
|
|
});
|
|
});
|
|
|
|
describe('data transformation', () => {
|
|
it('should transform content to rawHTML in create request', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'createBlogPost');
|
|
|
|
await blogTools.executeTool('create_blog_post', {
|
|
title: 'Test',
|
|
blogId: 'blog_123',
|
|
content: '<h1>Test Content</h1>',
|
|
description: 'desc',
|
|
imageUrl: 'url',
|
|
imageAltText: 'alt',
|
|
urlSlug: 'slug',
|
|
author: 'author',
|
|
categories: ['cat']
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
rawHTML: '<h1>Test Content</h1>'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should transform content to rawHTML in update request', async () => {
|
|
const spy = jest.spyOn(mockGhlClient, 'updateBlogPost');
|
|
|
|
await blogTools.executeTool('update_blog_post', {
|
|
postId: 'post_123',
|
|
blogId: 'blog_123',
|
|
content: '<h2>Updated Content</h2>'
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledWith('post_123',
|
|
expect.objectContaining({
|
|
rawHTML: '<h2>Updated Content</h2>'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should include location ID in all requests', async () => {
|
|
const createSpy = jest.spyOn(mockGhlClient, 'createBlogPost');
|
|
const getSitesSpy = jest.spyOn(mockGhlClient, 'getBlogSites');
|
|
|
|
await blogTools.executeTool('create_blog_post', {
|
|
title: 'Test',
|
|
blogId: 'blog_123',
|
|
content: 'content',
|
|
description: 'desc',
|
|
imageUrl: 'url',
|
|
imageAltText: 'alt',
|
|
urlSlug: 'slug',
|
|
author: 'author',
|
|
categories: ['cat']
|
|
});
|
|
|
|
await blogTools.executeTool('get_blog_sites', {});
|
|
|
|
expect(createSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
locationId: 'test_location_123'
|
|
})
|
|
);
|
|
|
|
expect(getSitesSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
locationId: 'test_location_123'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|