Skip to main content
Glama
pageService.test.js12.4 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mockDotenv } from '../../__tests__/helpers/testUtils.js'; import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js'; // Mock dotenv vi.mock('dotenv', () => mockDotenv()); // Mock logger vi.mock('../../utils/logger.js', () => ({ createContextLogger: createMockContextLogger(), })); // Mock ghostServiceImproved functions vi.mock('../ghostServiceImproved.js', () => ({ createPage: vi.fn(), })); // Import after mocks are set up import { createPageService } from '../pageService.js'; import { createPage } from '../ghostServiceImproved.js'; describe('pageService', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('createPageService - validation', () => { it('should accept valid input and create a page', async () => { const validInput = { title: 'Test Page', html: '<p>Test content</p>', }; const expectedPage = { id: '1', title: 'Test Page', status: 'draft' }; createPage.mockResolvedValue(expectedPage); const result = await createPageService(validInput); expect(result).toEqual(expectedPage); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ title: 'Test Page', html: '<p>Test content</p>', status: 'draft', }) ); }); it('should reject input with missing title', async () => { const invalidInput = { html: '<p>Test content</p>', }; await expect(createPageService(invalidInput)).rejects.toThrow( 'Invalid page input: "title" is required' ); expect(createPage).not.toHaveBeenCalled(); }); it('should reject input with missing html', async () => { const invalidInput = { title: 'Test Page', }; await expect(createPageService(invalidInput)).rejects.toThrow( 'Invalid page input: "html" is required' ); expect(createPage).not.toHaveBeenCalled(); }); it('should reject input with invalid status', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', status: 'invalid-status', }; await expect(createPageService(invalidInput)).rejects.toThrow( 'Invalid page input: "status" must be one of [draft, published, scheduled]' ); expect(createPage).not.toHaveBeenCalled(); }); it('should accept valid status values', async () => { const statuses = ['draft', 'published', 'scheduled']; createPage.mockResolvedValue({ id: '1', title: 'Test' }); for (const status of statuses) { const input = { title: 'Test Page', html: '<p>Content</p>', status, }; await createPageService(input); expect(createPage).toHaveBeenCalledWith(expect.objectContaining({ status })); vi.clearAllMocks(); } }); it('should reject tags field (pages do not support tags)', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', tags: ['tag1', 'tag2'], }; // Tags field should cause validation error since it's not in the schema await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should validate feature_image is a valid URI', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', feature_image: 'not-a-valid-url', }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should accept valid feature_image URI', async () => { const validInput = { title: 'Test Page', html: '<p>Content</p>', feature_image: 'https://example.com/image.jpg', }; createPage.mockResolvedValue({ id: '1', title: 'Test' }); await createPageService(validInput); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ feature_image: 'https://example.com/image.jpg', }) ); }); it('should validate title max length', async () => { const invalidInput = { title: 'a'.repeat(256), // 256 chars exceeds max of 255 html: '<p>Content</p>', }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should validate custom_excerpt max length', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', custom_excerpt: 'a'.repeat(501), // 501 chars exceeds max of 500 }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should validate meta_title max length', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', meta_title: 'a'.repeat(71), // 71 chars exceeds max of 70 }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should validate meta_description max length', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', meta_description: 'a'.repeat(161), // 161 chars exceeds max of 160 }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should validate published_at is ISO date format', async () => { const invalidInput = { title: 'Test Page', html: '<p>Content</p>', published_at: 'invalid-date', }; await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:'); expect(createPage).not.toHaveBeenCalled(); }); it('should accept valid published_at ISO date', async () => { const validInput = { title: 'Test Page', html: '<p>Content</p>', published_at: '2024-12-31T12:00:00.000Z', }; createPage.mockResolvedValue({ id: '1', title: 'Test' }); await createPageService(validInput); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ published_at: '2024-12-31T12:00:00.000Z', }) ); }); }); describe('createPageService - metadata generation', () => { beforeEach(() => { createPage.mockResolvedValue({ id: '1', title: 'Test' }); }); it('should default meta_title to title when not provided', async () => { const input = { title: 'My Page Title', html: '<p>Content</p>', }; await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ meta_title: 'My Page Title', }) ); }); it('should use provided meta_title instead of defaulting', async () => { const input = { title: 'My Page Title', html: '<p>Content</p>', meta_title: 'Custom Meta Title', }; await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ meta_title: 'Custom Meta Title', }) ); }); it('should default meta_description to custom_excerpt when provided', async () => { const input = { title: 'Test Page', html: '<p>Content</p>', custom_excerpt: 'This is my custom excerpt', }; await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ meta_description: 'This is my custom excerpt', }) ); }); it('should generate meta_description from HTML when not provided', async () => { const input = { title: 'Test Page', html: '<p>This is the page content that will be used for meta description.</p>', }; await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ meta_description: expect.stringContaining('This is the page content'), }) ); }); it('should use provided meta_description over custom_excerpt', async () => { const input = { title: 'Test Page', html: '<p>Content</p>', custom_excerpt: 'This is the excerpt', meta_description: 'This is the explicit meta description', }; await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ meta_description: 'This is the explicit meta description', }) ); }); it('should strip HTML tags when generating meta_description', async () => { const input = { title: 'Test Page', html: '<h1>Heading</h1><p><strong>Bold</strong> and <em>italic</em> text</p>', }; await createPageService(input); const calledWith = createPage.mock.calls[0][0]; expect(calledWith.meta_description).not.toContain('<'); expect(calledWith.meta_description).not.toContain('>'); expect(calledWith.meta_description).toContain('Heading'); expect(calledWith.meta_description).toContain('Bold'); expect(calledWith.meta_description).toContain('italic'); }); it('should truncate meta_description to 500 characters', async () => { const longContent = 'a'.repeat(600); const input = { title: 'Test Page', html: `<p>${longContent}</p>`, }; await createPageService(input); const calledWith = createPage.mock.calls[0][0]; expect(calledWith.meta_description.length).toBeLessThanOrEqual(500); expect(calledWith.meta_description).toContain('...'); }); it('should handle empty HTML content gracefully', async () => { const input = { title: 'Test Page', html: '', }; // Empty html should fail validation await expect(createPageService(input)).rejects.toThrow('Invalid page input:'); }); }); describe('createPageService - complete page creation', () => { it('should create page with all optional fields', async () => { const fullInput = { title: 'Complete Page', html: '<p>Full content</p>', custom_excerpt: 'Page excerpt', status: 'published', published_at: '2024-12-31T12:00:00.000Z', feature_image: 'https://example.com/image.jpg', feature_image_alt: 'Alt text', feature_image_caption: 'Image caption', meta_title: 'SEO Title', meta_description: 'SEO Description', }; const expectedPage = { id: '1', ...fullInput }; createPage.mockResolvedValue(expectedPage); const result = await createPageService(fullInput); expect(result).toEqual(expectedPage); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ title: 'Complete Page', html: '<p>Full content</p>', custom_excerpt: 'Page excerpt', status: 'published', published_at: '2024-12-31T12:00:00.000Z', feature_image: 'https://example.com/image.jpg', feature_image_alt: 'Alt text', feature_image_caption: 'Image caption', meta_title: 'SEO Title', meta_description: 'SEO Description', }) ); }); it('should default status to draft when not provided', async () => { const input = { title: 'Test Page', html: '<p>Content</p>', }; createPage.mockResolvedValue({ id: '1', title: 'Test', status: 'draft' }); await createPageService(input); expect(createPage).toHaveBeenCalledWith( expect.objectContaining({ status: 'draft', }) ); }); it('should propagate errors from ghostServiceImproved', async () => { const input = { title: 'Test Page', html: '<p>Content</p>', }; createPage.mockRejectedValue(new Error('Ghost API error')); await expect(createPageService(input)).rejects.toThrow('Ghost API error'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jgardner04/Ghost-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server