@jgardner04/ghost-mcp-server 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -20
- package/package.json +1 -1
- package/src/__tests__/mcp_server_pages.test.js +520 -0
- package/src/mcp_server_improved.js +467 -1
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +306 -0
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +561 -0
- package/src/services/__tests__/newsletterService.test.js +217 -0
- package/src/services/__tests__/pageService.test.js +400 -0
- package/src/services/ghostServiceImproved.js +371 -0
- package/src/services/newsletterService.js +47 -0
- package/src/services/pageService.js +121 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
|
|
5
|
+
// Mock dotenv
|
|
6
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
7
|
+
|
|
8
|
+
// Mock logger
|
|
9
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
10
|
+
createContextLogger: createMockContextLogger(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock ghostServiceImproved functions
|
|
14
|
+
vi.mock('../ghostServiceImproved.js', () => ({
|
|
15
|
+
createNewsletter: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Import after mocks are set up
|
|
19
|
+
import { createNewsletterService } from '../newsletterService.js';
|
|
20
|
+
import { createNewsletter } from '../ghostServiceImproved.js';
|
|
21
|
+
|
|
22
|
+
describe('newsletterService', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('createNewsletterService - validation', () => {
|
|
28
|
+
it('should accept valid input and create a newsletter', async () => {
|
|
29
|
+
const validInput = {
|
|
30
|
+
name: 'Weekly Newsletter',
|
|
31
|
+
};
|
|
32
|
+
const expectedNewsletter = { id: '1', name: 'Weekly Newsletter', slug: 'weekly-newsletter' };
|
|
33
|
+
createNewsletter.mockResolvedValue(expectedNewsletter);
|
|
34
|
+
|
|
35
|
+
const result = await createNewsletterService(validInput);
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual(expectedNewsletter);
|
|
38
|
+
expect(createNewsletter).toHaveBeenCalledWith(
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
name: 'Weekly Newsletter',
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should reject input with missing name', async () => {
|
|
46
|
+
const invalidInput = {};
|
|
47
|
+
|
|
48
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
49
|
+
'Invalid newsletter input: "name" is required'
|
|
50
|
+
);
|
|
51
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should accept all optional fields', async () => {
|
|
55
|
+
const fullInput = {
|
|
56
|
+
name: 'Monthly Newsletter',
|
|
57
|
+
description: 'Our monthly updates',
|
|
58
|
+
sender_name: 'John Doe',
|
|
59
|
+
sender_email: 'john@example.com',
|
|
60
|
+
sender_reply_to: 'newsletter',
|
|
61
|
+
subscribe_on_signup: true,
|
|
62
|
+
show_header_icon: true,
|
|
63
|
+
show_header_title: false,
|
|
64
|
+
};
|
|
65
|
+
const expectedNewsletter = { id: '1', ...fullInput };
|
|
66
|
+
createNewsletter.mockResolvedValue(expectedNewsletter);
|
|
67
|
+
|
|
68
|
+
const result = await createNewsletterService(fullInput);
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual(expectedNewsletter);
|
|
71
|
+
expect(createNewsletter).toHaveBeenCalledWith(expect.objectContaining(fullInput));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should validate sender_email is a valid email', async () => {
|
|
75
|
+
const invalidInput = {
|
|
76
|
+
name: 'Newsletter',
|
|
77
|
+
sender_email: 'not-an-email',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
81
|
+
'Invalid newsletter input:'
|
|
82
|
+
);
|
|
83
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should accept valid sender_email', async () => {
|
|
87
|
+
const validInput = {
|
|
88
|
+
name: 'Newsletter',
|
|
89
|
+
sender_email: 'valid@example.com',
|
|
90
|
+
};
|
|
91
|
+
createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
|
|
92
|
+
|
|
93
|
+
await createNewsletterService(validInput);
|
|
94
|
+
|
|
95
|
+
expect(createNewsletter).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
sender_email: 'valid@example.com',
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should validate sender_reply_to enum values', async () => {
|
|
103
|
+
const invalidInput = {
|
|
104
|
+
name: 'Newsletter',
|
|
105
|
+
sender_reply_to: 'invalid',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
109
|
+
'Invalid newsletter input:'
|
|
110
|
+
);
|
|
111
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should accept valid sender_reply_to values', async () => {
|
|
115
|
+
const validValues = ['newsletter', 'support'];
|
|
116
|
+
createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
|
|
117
|
+
|
|
118
|
+
for (const value of validValues) {
|
|
119
|
+
const input = {
|
|
120
|
+
name: 'Newsletter',
|
|
121
|
+
sender_reply_to: value,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await createNewsletterService(input);
|
|
125
|
+
|
|
126
|
+
expect(createNewsletter).toHaveBeenCalledWith(
|
|
127
|
+
expect.objectContaining({ sender_reply_to: value })
|
|
128
|
+
);
|
|
129
|
+
vi.clearAllMocks();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should validate subscribe_on_signup is boolean', async () => {
|
|
134
|
+
const invalidInput = {
|
|
135
|
+
name: 'Newsletter',
|
|
136
|
+
subscribe_on_signup: 'yes',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
140
|
+
'Invalid newsletter input:'
|
|
141
|
+
);
|
|
142
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should validate show_header_icon is boolean', async () => {
|
|
146
|
+
const invalidInput = {
|
|
147
|
+
name: 'Newsletter',
|
|
148
|
+
show_header_icon: 'true',
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
152
|
+
'Invalid newsletter input:'
|
|
153
|
+
);
|
|
154
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should validate show_header_title is boolean', async () => {
|
|
158
|
+
const invalidInput = {
|
|
159
|
+
name: 'Newsletter',
|
|
160
|
+
show_header_title: 1,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
await expect(createNewsletterService(invalidInput)).rejects.toThrow(
|
|
164
|
+
'Invalid newsletter input:'
|
|
165
|
+
);
|
|
166
|
+
expect(createNewsletter).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('createNewsletterService - defaults and transformations', () => {
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
createNewsletter.mockResolvedValue({ id: '1', name: 'Newsletter' });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should pass through all provided fields', async () => {
|
|
176
|
+
const input = {
|
|
177
|
+
name: 'Newsletter',
|
|
178
|
+
description: 'Test description',
|
|
179
|
+
sender_name: 'Sender',
|
|
180
|
+
sender_email: 'sender@example.com',
|
|
181
|
+
sender_reply_to: 'support',
|
|
182
|
+
subscribe_on_signup: false,
|
|
183
|
+
show_header_icon: false,
|
|
184
|
+
show_header_title: true,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await createNewsletterService(input);
|
|
188
|
+
|
|
189
|
+
expect(createNewsletter).toHaveBeenCalledWith(expect.objectContaining(input));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle minimal input', async () => {
|
|
193
|
+
const input = {
|
|
194
|
+
name: 'Simple Newsletter',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
await createNewsletterService(input);
|
|
198
|
+
|
|
199
|
+
expect(createNewsletter).toHaveBeenCalledWith(
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
name: 'Simple Newsletter',
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('createNewsletterService - error handling', () => {
|
|
208
|
+
it('should propagate errors from ghostServiceImproved', async () => {
|
|
209
|
+
const input = {
|
|
210
|
+
name: 'Newsletter',
|
|
211
|
+
};
|
|
212
|
+
createNewsletter.mockRejectedValue(new Error('Ghost API error'));
|
|
213
|
+
|
|
214
|
+
await expect(createNewsletterService(input)).rejects.toThrow('Ghost API error');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
|
|
5
|
+
// Mock dotenv
|
|
6
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
7
|
+
|
|
8
|
+
// Mock logger
|
|
9
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
10
|
+
createContextLogger: createMockContextLogger(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock ghostServiceImproved functions
|
|
14
|
+
vi.mock('../ghostServiceImproved.js', () => ({
|
|
15
|
+
createPage: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Import after mocks are set up
|
|
19
|
+
import { createPageService } from '../pageService.js';
|
|
20
|
+
import { createPage } from '../ghostServiceImproved.js';
|
|
21
|
+
|
|
22
|
+
describe('pageService', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('createPageService - validation', () => {
|
|
28
|
+
it('should accept valid input and create a page', async () => {
|
|
29
|
+
const validInput = {
|
|
30
|
+
title: 'Test Page',
|
|
31
|
+
html: '<p>Test content</p>',
|
|
32
|
+
};
|
|
33
|
+
const expectedPage = { id: '1', title: 'Test Page', status: 'draft' };
|
|
34
|
+
createPage.mockResolvedValue(expectedPage);
|
|
35
|
+
|
|
36
|
+
const result = await createPageService(validInput);
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual(expectedPage);
|
|
39
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
title: 'Test Page',
|
|
42
|
+
html: '<p>Test content</p>',
|
|
43
|
+
status: 'draft',
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should reject input with missing title', async () => {
|
|
49
|
+
const invalidInput = {
|
|
50
|
+
html: '<p>Test content</p>',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await expect(createPageService(invalidInput)).rejects.toThrow(
|
|
54
|
+
'Invalid page input: "title" is required'
|
|
55
|
+
);
|
|
56
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject input with missing html', async () => {
|
|
60
|
+
const invalidInput = {
|
|
61
|
+
title: 'Test Page',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await expect(createPageService(invalidInput)).rejects.toThrow(
|
|
65
|
+
'Invalid page input: "html" is required'
|
|
66
|
+
);
|
|
67
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should reject input with invalid status', async () => {
|
|
71
|
+
const invalidInput = {
|
|
72
|
+
title: 'Test Page',
|
|
73
|
+
html: '<p>Content</p>',
|
|
74
|
+
status: 'invalid-status',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await expect(createPageService(invalidInput)).rejects.toThrow(
|
|
78
|
+
'Invalid page input: "status" must be one of [draft, published, scheduled]'
|
|
79
|
+
);
|
|
80
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should accept valid status values', async () => {
|
|
84
|
+
const statuses = ['draft', 'published', 'scheduled'];
|
|
85
|
+
createPage.mockResolvedValue({ id: '1', title: 'Test' });
|
|
86
|
+
|
|
87
|
+
for (const status of statuses) {
|
|
88
|
+
const input = {
|
|
89
|
+
title: 'Test Page',
|
|
90
|
+
html: '<p>Content</p>',
|
|
91
|
+
status,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
await createPageService(input);
|
|
95
|
+
|
|
96
|
+
expect(createPage).toHaveBeenCalledWith(expect.objectContaining({ status }));
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should reject tags field (pages do not support tags)', async () => {
|
|
102
|
+
const invalidInput = {
|
|
103
|
+
title: 'Test Page',
|
|
104
|
+
html: '<p>Content</p>',
|
|
105
|
+
tags: ['tag1', 'tag2'],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Tags field should cause validation error since it's not in the schema
|
|
109
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
110
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should validate feature_image is a valid URI', async () => {
|
|
114
|
+
const invalidInput = {
|
|
115
|
+
title: 'Test Page',
|
|
116
|
+
html: '<p>Content</p>',
|
|
117
|
+
feature_image: 'not-a-valid-url',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
121
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should accept valid feature_image URI', async () => {
|
|
125
|
+
const validInput = {
|
|
126
|
+
title: 'Test Page',
|
|
127
|
+
html: '<p>Content</p>',
|
|
128
|
+
feature_image: 'https://example.com/image.jpg',
|
|
129
|
+
};
|
|
130
|
+
createPage.mockResolvedValue({ id: '1', title: 'Test' });
|
|
131
|
+
|
|
132
|
+
await createPageService(validInput);
|
|
133
|
+
|
|
134
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
feature_image: 'https://example.com/image.jpg',
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should validate title max length', async () => {
|
|
142
|
+
const invalidInput = {
|
|
143
|
+
title: 'a'.repeat(256), // 256 chars exceeds max of 255
|
|
144
|
+
html: '<p>Content</p>',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
148
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should validate custom_excerpt max length', async () => {
|
|
152
|
+
const invalidInput = {
|
|
153
|
+
title: 'Test Page',
|
|
154
|
+
html: '<p>Content</p>',
|
|
155
|
+
custom_excerpt: 'a'.repeat(501), // 501 chars exceeds max of 500
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
159
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should validate meta_title max length', async () => {
|
|
163
|
+
const invalidInput = {
|
|
164
|
+
title: 'Test Page',
|
|
165
|
+
html: '<p>Content</p>',
|
|
166
|
+
meta_title: 'a'.repeat(71), // 71 chars exceeds max of 70
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
170
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should validate meta_description max length', async () => {
|
|
174
|
+
const invalidInput = {
|
|
175
|
+
title: 'Test Page',
|
|
176
|
+
html: '<p>Content</p>',
|
|
177
|
+
meta_description: 'a'.repeat(161), // 161 chars exceeds max of 160
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
181
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should validate published_at is ISO date format', async () => {
|
|
185
|
+
const invalidInput = {
|
|
186
|
+
title: 'Test Page',
|
|
187
|
+
html: '<p>Content</p>',
|
|
188
|
+
published_at: 'invalid-date',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await expect(createPageService(invalidInput)).rejects.toThrow('Invalid page input:');
|
|
192
|
+
expect(createPage).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should accept valid published_at ISO date', async () => {
|
|
196
|
+
const validInput = {
|
|
197
|
+
title: 'Test Page',
|
|
198
|
+
html: '<p>Content</p>',
|
|
199
|
+
published_at: '2024-12-31T12:00:00.000Z',
|
|
200
|
+
};
|
|
201
|
+
createPage.mockResolvedValue({ id: '1', title: 'Test' });
|
|
202
|
+
|
|
203
|
+
await createPageService(validInput);
|
|
204
|
+
|
|
205
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
206
|
+
expect.objectContaining({
|
|
207
|
+
published_at: '2024-12-31T12:00:00.000Z',
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('createPageService - metadata generation', () => {
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
createPage.mockResolvedValue({ id: '1', title: 'Test' });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should default meta_title to title when not provided', async () => {
|
|
219
|
+
const input = {
|
|
220
|
+
title: 'My Page Title',
|
|
221
|
+
html: '<p>Content</p>',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await createPageService(input);
|
|
225
|
+
|
|
226
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
meta_title: 'My Page Title',
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should use provided meta_title instead of defaulting', async () => {
|
|
234
|
+
const input = {
|
|
235
|
+
title: 'My Page Title',
|
|
236
|
+
html: '<p>Content</p>',
|
|
237
|
+
meta_title: 'Custom Meta Title',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await createPageService(input);
|
|
241
|
+
|
|
242
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
meta_title: 'Custom Meta Title',
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should default meta_description to custom_excerpt when provided', async () => {
|
|
250
|
+
const input = {
|
|
251
|
+
title: 'Test Page',
|
|
252
|
+
html: '<p>Content</p>',
|
|
253
|
+
custom_excerpt: 'This is my custom excerpt',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
await createPageService(input);
|
|
257
|
+
|
|
258
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
259
|
+
expect.objectContaining({
|
|
260
|
+
meta_description: 'This is my custom excerpt',
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should generate meta_description from HTML when not provided', async () => {
|
|
266
|
+
const input = {
|
|
267
|
+
title: 'Test Page',
|
|
268
|
+
html: '<p>This is the page content that will be used for meta description.</p>',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await createPageService(input);
|
|
272
|
+
|
|
273
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
meta_description: expect.stringContaining('This is the page content'),
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should use provided meta_description over custom_excerpt', async () => {
|
|
281
|
+
const input = {
|
|
282
|
+
title: 'Test Page',
|
|
283
|
+
html: '<p>Content</p>',
|
|
284
|
+
custom_excerpt: 'This is the excerpt',
|
|
285
|
+
meta_description: 'This is the explicit meta description',
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
await createPageService(input);
|
|
289
|
+
|
|
290
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
291
|
+
expect.objectContaining({
|
|
292
|
+
meta_description: 'This is the explicit meta description',
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should strip HTML tags when generating meta_description', async () => {
|
|
298
|
+
const input = {
|
|
299
|
+
title: 'Test Page',
|
|
300
|
+
html: '<h1>Heading</h1><p><strong>Bold</strong> and <em>italic</em> text</p>',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await createPageService(input);
|
|
304
|
+
|
|
305
|
+
const calledWith = createPage.mock.calls[0][0];
|
|
306
|
+
expect(calledWith.meta_description).not.toContain('<');
|
|
307
|
+
expect(calledWith.meta_description).not.toContain('>');
|
|
308
|
+
expect(calledWith.meta_description).toContain('Heading');
|
|
309
|
+
expect(calledWith.meta_description).toContain('Bold');
|
|
310
|
+
expect(calledWith.meta_description).toContain('italic');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should truncate meta_description to 500 characters', async () => {
|
|
314
|
+
const longContent = 'a'.repeat(600);
|
|
315
|
+
const input = {
|
|
316
|
+
title: 'Test Page',
|
|
317
|
+
html: `<p>${longContent}</p>`,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
await createPageService(input);
|
|
321
|
+
|
|
322
|
+
const calledWith = createPage.mock.calls[0][0];
|
|
323
|
+
expect(calledWith.meta_description.length).toBeLessThanOrEqual(500);
|
|
324
|
+
expect(calledWith.meta_description).toContain('...');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should handle empty HTML content gracefully', async () => {
|
|
328
|
+
const input = {
|
|
329
|
+
title: 'Test Page',
|
|
330
|
+
html: '',
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Empty html should fail validation
|
|
334
|
+
await expect(createPageService(input)).rejects.toThrow('Invalid page input:');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('createPageService - complete page creation', () => {
|
|
339
|
+
it('should create page with all optional fields', async () => {
|
|
340
|
+
const fullInput = {
|
|
341
|
+
title: 'Complete Page',
|
|
342
|
+
html: '<p>Full content</p>',
|
|
343
|
+
custom_excerpt: 'Page excerpt',
|
|
344
|
+
status: 'published',
|
|
345
|
+
published_at: '2024-12-31T12:00:00.000Z',
|
|
346
|
+
feature_image: 'https://example.com/image.jpg',
|
|
347
|
+
feature_image_alt: 'Alt text',
|
|
348
|
+
feature_image_caption: 'Image caption',
|
|
349
|
+
meta_title: 'SEO Title',
|
|
350
|
+
meta_description: 'SEO Description',
|
|
351
|
+
};
|
|
352
|
+
const expectedPage = { id: '1', ...fullInput };
|
|
353
|
+
createPage.mockResolvedValue(expectedPage);
|
|
354
|
+
|
|
355
|
+
const result = await createPageService(fullInput);
|
|
356
|
+
|
|
357
|
+
expect(result).toEqual(expectedPage);
|
|
358
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
359
|
+
expect.objectContaining({
|
|
360
|
+
title: 'Complete Page',
|
|
361
|
+
html: '<p>Full content</p>',
|
|
362
|
+
custom_excerpt: 'Page excerpt',
|
|
363
|
+
status: 'published',
|
|
364
|
+
published_at: '2024-12-31T12:00:00.000Z',
|
|
365
|
+
feature_image: 'https://example.com/image.jpg',
|
|
366
|
+
feature_image_alt: 'Alt text',
|
|
367
|
+
feature_image_caption: 'Image caption',
|
|
368
|
+
meta_title: 'SEO Title',
|
|
369
|
+
meta_description: 'SEO Description',
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should default status to draft when not provided', async () => {
|
|
375
|
+
const input = {
|
|
376
|
+
title: 'Test Page',
|
|
377
|
+
html: '<p>Content</p>',
|
|
378
|
+
};
|
|
379
|
+
createPage.mockResolvedValue({ id: '1', title: 'Test', status: 'draft' });
|
|
380
|
+
|
|
381
|
+
await createPageService(input);
|
|
382
|
+
|
|
383
|
+
expect(createPage).toHaveBeenCalledWith(
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
status: 'draft',
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should propagate errors from ghostServiceImproved', async () => {
|
|
391
|
+
const input = {
|
|
392
|
+
title: 'Test Page',
|
|
393
|
+
html: '<p>Content</p>',
|
|
394
|
+
};
|
|
395
|
+
createPage.mockRejectedValue(new Error('Ghost API error'));
|
|
396
|
+
|
|
397
|
+
await expect(createPageService(input)).rejects.toThrow('Ghost API error');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|