@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,306 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
|
|
4
|
+
// Mock dotenv before other imports
|
|
5
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
6
|
+
|
|
7
|
+
// Create a mock Ghost Admin API
|
|
8
|
+
vi.mock('@tryghost/admin-api', () => {
|
|
9
|
+
const mockNewslettersApi = {
|
|
10
|
+
browse: vi.fn(),
|
|
11
|
+
read: vi.fn(),
|
|
12
|
+
add: vi.fn(),
|
|
13
|
+
edit: vi.fn(),
|
|
14
|
+
delete: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
default: class {
|
|
19
|
+
constructor() {
|
|
20
|
+
return {
|
|
21
|
+
newsletters: mockNewslettersApi,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
mockNewslettersApi,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Import after mocks are set up
|
|
30
|
+
import {
|
|
31
|
+
getNewsletters,
|
|
32
|
+
getNewsletter,
|
|
33
|
+
createNewsletter,
|
|
34
|
+
updateNewsletter,
|
|
35
|
+
deleteNewsletter,
|
|
36
|
+
} from '../ghostServiceImproved.js';
|
|
37
|
+
import { ValidationError, NotFoundError } from '../../errors/index.js';
|
|
38
|
+
|
|
39
|
+
// Get the mock API
|
|
40
|
+
const { mockNewslettersApi } = await vi.importMock('@tryghost/admin-api');
|
|
41
|
+
|
|
42
|
+
describe('ghostServiceImproved - Newsletter Operations', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getNewsletters', () => {
|
|
48
|
+
it('should retrieve all newsletters', async () => {
|
|
49
|
+
const mockNewsletters = [
|
|
50
|
+
{ id: '1', name: 'Newsletter 1', slug: 'newsletter-1' },
|
|
51
|
+
{ id: '2', name: 'Newsletter 2', slug: 'newsletter-2' },
|
|
52
|
+
];
|
|
53
|
+
mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
|
|
54
|
+
|
|
55
|
+
const result = await getNewsletters();
|
|
56
|
+
|
|
57
|
+
expect(result).toEqual(mockNewsletters);
|
|
58
|
+
expect(mockNewslettersApi.browse).toHaveBeenCalledWith({ limit: 'all' }, {});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should support custom limit', async () => {
|
|
62
|
+
const mockNewsletters = [{ id: '1', name: 'Newsletter 1' }];
|
|
63
|
+
mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
|
|
64
|
+
|
|
65
|
+
await getNewsletters({ limit: 5 });
|
|
66
|
+
|
|
67
|
+
expect(mockNewslettersApi.browse).toHaveBeenCalledWith({ limit: 5 }, {});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should support filter option', async () => {
|
|
71
|
+
const mockNewsletters = [{ id: '1', name: 'Active Newsletter' }];
|
|
72
|
+
mockNewslettersApi.browse.mockResolvedValue(mockNewsletters);
|
|
73
|
+
|
|
74
|
+
await getNewsletters({ filter: 'status:active' });
|
|
75
|
+
|
|
76
|
+
expect(mockNewslettersApi.browse).toHaveBeenCalledWith(
|
|
77
|
+
{ limit: 'all', filter: 'status:active' },
|
|
78
|
+
{}
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle empty results', async () => {
|
|
83
|
+
mockNewslettersApi.browse.mockResolvedValue([]);
|
|
84
|
+
|
|
85
|
+
const result = await getNewsletters();
|
|
86
|
+
|
|
87
|
+
expect(result).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should propagate API errors', async () => {
|
|
91
|
+
mockNewslettersApi.browse.mockRejectedValue(new Error('API Error'));
|
|
92
|
+
|
|
93
|
+
await expect(getNewsletters()).rejects.toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getNewsletter', () => {
|
|
98
|
+
it('should retrieve a newsletter by ID', async () => {
|
|
99
|
+
const mockNewsletter = { id: 'newsletter-123', name: 'My Newsletter' };
|
|
100
|
+
mockNewslettersApi.read.mockResolvedValue(mockNewsletter);
|
|
101
|
+
|
|
102
|
+
const result = await getNewsletter('newsletter-123');
|
|
103
|
+
|
|
104
|
+
expect(result).toEqual(mockNewsletter);
|
|
105
|
+
expect(mockNewslettersApi.read).toHaveBeenCalledWith({}, { id: 'newsletter-123' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw ValidationError if ID is missing', async () => {
|
|
109
|
+
await expect(getNewsletter()).rejects.toThrow(ValidationError);
|
|
110
|
+
await expect(getNewsletter()).rejects.toThrow('Newsletter ID is required');
|
|
111
|
+
expect(mockNewslettersApi.read).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should throw NotFoundError when newsletter does not exist', async () => {
|
|
115
|
+
const ghostError = new Error('Not found');
|
|
116
|
+
ghostError.response = { status: 404 };
|
|
117
|
+
mockNewslettersApi.read.mockRejectedValue(ghostError);
|
|
118
|
+
|
|
119
|
+
await expect(getNewsletter('nonexistent')).rejects.toThrow(NotFoundError);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('createNewsletter', () => {
|
|
124
|
+
it('should create a newsletter with valid data', async () => {
|
|
125
|
+
const newsletterData = {
|
|
126
|
+
name: 'Weekly Newsletter',
|
|
127
|
+
description: 'Our weekly updates',
|
|
128
|
+
};
|
|
129
|
+
const createdNewsletter = { id: '1', ...newsletterData };
|
|
130
|
+
mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
|
|
131
|
+
|
|
132
|
+
const result = await createNewsletter(newsletterData);
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual(createdNewsletter);
|
|
135
|
+
expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should create newsletter with sender email', async () => {
|
|
139
|
+
const newsletterData = {
|
|
140
|
+
name: 'Newsletter',
|
|
141
|
+
sender_name: 'John Doe',
|
|
142
|
+
sender_email: 'john@example.com',
|
|
143
|
+
sender_reply_to: 'newsletter',
|
|
144
|
+
};
|
|
145
|
+
const createdNewsletter = { id: '1', ...newsletterData };
|
|
146
|
+
mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
|
|
147
|
+
|
|
148
|
+
const result = await createNewsletter(newsletterData);
|
|
149
|
+
|
|
150
|
+
expect(result).toEqual(createdNewsletter);
|
|
151
|
+
expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should create newsletter with display options', async () => {
|
|
155
|
+
const newsletterData = {
|
|
156
|
+
name: 'Newsletter',
|
|
157
|
+
subscribe_on_signup: true,
|
|
158
|
+
show_header_icon: true,
|
|
159
|
+
show_header_title: false,
|
|
160
|
+
};
|
|
161
|
+
const createdNewsletter = { id: '1', ...newsletterData };
|
|
162
|
+
mockNewslettersApi.add.mockResolvedValue(createdNewsletter);
|
|
163
|
+
|
|
164
|
+
const result = await createNewsletter(newsletterData);
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual(createdNewsletter);
|
|
167
|
+
expect(mockNewslettersApi.add).toHaveBeenCalledWith(newsletterData, {});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw ValidationError if name is missing', async () => {
|
|
171
|
+
const invalidData = { description: 'No name' };
|
|
172
|
+
|
|
173
|
+
await expect(createNewsletter(invalidData)).rejects.toThrow(ValidationError);
|
|
174
|
+
await expect(createNewsletter(invalidData)).rejects.toThrow('Newsletter validation failed');
|
|
175
|
+
expect(mockNewslettersApi.add).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should throw ValidationError if name is empty', async () => {
|
|
179
|
+
const invalidData = { name: '' };
|
|
180
|
+
|
|
181
|
+
await expect(createNewsletter(invalidData)).rejects.toThrow(ValidationError);
|
|
182
|
+
await expect(createNewsletter(invalidData)).rejects.toThrow('Newsletter validation failed');
|
|
183
|
+
expect(mockNewslettersApi.add).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle Ghost API validation errors', async () => {
|
|
187
|
+
const newsletterData = { name: 'Newsletter' };
|
|
188
|
+
const ghostError = new Error('Validation failed');
|
|
189
|
+
ghostError.response = { status: 422 };
|
|
190
|
+
mockNewslettersApi.add.mockRejectedValue(ghostError);
|
|
191
|
+
|
|
192
|
+
await expect(createNewsletter(newsletterData)).rejects.toThrow();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('updateNewsletter', () => {
|
|
197
|
+
it('should update a newsletter successfully', async () => {
|
|
198
|
+
const existingNewsletter = {
|
|
199
|
+
id: 'newsletter-123',
|
|
200
|
+
name: 'Old Name',
|
|
201
|
+
updated_at: '2024-01-01T00:00:00.000Z',
|
|
202
|
+
};
|
|
203
|
+
const updateData = { name: 'New Name' };
|
|
204
|
+
const updatedNewsletter = { ...existingNewsletter, ...updateData };
|
|
205
|
+
|
|
206
|
+
mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
|
|
207
|
+
mockNewslettersApi.edit.mockResolvedValue(updatedNewsletter);
|
|
208
|
+
|
|
209
|
+
const result = await updateNewsletter('newsletter-123', updateData);
|
|
210
|
+
|
|
211
|
+
expect(result).toEqual(updatedNewsletter);
|
|
212
|
+
expect(mockNewslettersApi.read).toHaveBeenCalledWith({}, { id: 'newsletter-123' });
|
|
213
|
+
expect(mockNewslettersApi.edit).toHaveBeenCalledWith(
|
|
214
|
+
{
|
|
215
|
+
...existingNewsletter,
|
|
216
|
+
...updateData,
|
|
217
|
+
},
|
|
218
|
+
{ id: 'newsletter-123' }
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should update newsletter with email settings', async () => {
|
|
223
|
+
const existingNewsletter = {
|
|
224
|
+
id: 'newsletter-123',
|
|
225
|
+
name: 'Newsletter',
|
|
226
|
+
updated_at: '2024-01-01T00:00:00.000Z',
|
|
227
|
+
};
|
|
228
|
+
const updateData = {
|
|
229
|
+
sender_name: 'Updated Sender',
|
|
230
|
+
sender_email: 'updated@example.com',
|
|
231
|
+
subscribe_on_signup: false,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
|
|
235
|
+
mockNewslettersApi.edit.mockResolvedValue({ ...existingNewsletter, ...updateData });
|
|
236
|
+
|
|
237
|
+
await updateNewsletter('newsletter-123', updateData);
|
|
238
|
+
|
|
239
|
+
expect(mockNewslettersApi.edit).toHaveBeenCalledWith(expect.objectContaining(updateData), {
|
|
240
|
+
id: 'newsletter-123',
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should throw ValidationError if ID is missing', async () => {
|
|
245
|
+
await expect(updateNewsletter()).rejects.toThrow(ValidationError);
|
|
246
|
+
await expect(updateNewsletter()).rejects.toThrow('Newsletter ID is required for update');
|
|
247
|
+
expect(mockNewslettersApi.read).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should throw NotFoundError if newsletter does not exist', async () => {
|
|
251
|
+
const ghostError = new Error('Not found');
|
|
252
|
+
ghostError.response = { status: 404 };
|
|
253
|
+
mockNewslettersApi.read.mockRejectedValue(ghostError);
|
|
254
|
+
|
|
255
|
+
await expect(updateNewsletter('nonexistent', { name: 'New Name' })).rejects.toThrow(
|
|
256
|
+
NotFoundError
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should preserve updated_at from existing newsletter', async () => {
|
|
261
|
+
const existingNewsletter = {
|
|
262
|
+
id: 'newsletter-123',
|
|
263
|
+
name: 'Newsletter',
|
|
264
|
+
updated_at: '2024-01-01T00:00:00.000Z',
|
|
265
|
+
};
|
|
266
|
+
const updateData = { description: 'Updated description' };
|
|
267
|
+
|
|
268
|
+
mockNewslettersApi.read.mockResolvedValue(existingNewsletter);
|
|
269
|
+
mockNewslettersApi.edit.mockResolvedValue({ ...existingNewsletter, ...updateData });
|
|
270
|
+
|
|
271
|
+
await updateNewsletter('newsletter-123', updateData);
|
|
272
|
+
|
|
273
|
+
expect(mockNewslettersApi.edit).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
updated_at: '2024-01-01T00:00:00.000Z',
|
|
276
|
+
}),
|
|
277
|
+
{ id: 'newsletter-123' }
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('deleteNewsletter', () => {
|
|
283
|
+
it('should delete a newsletter successfully', async () => {
|
|
284
|
+
mockNewslettersApi.delete.mockResolvedValue({ id: 'newsletter-123' });
|
|
285
|
+
|
|
286
|
+
const result = await deleteNewsletter('newsletter-123');
|
|
287
|
+
|
|
288
|
+
expect(result).toEqual({ id: 'newsletter-123' });
|
|
289
|
+
expect(mockNewslettersApi.delete).toHaveBeenCalledWith('newsletter-123', {});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should throw ValidationError if ID is missing', async () => {
|
|
293
|
+
await expect(deleteNewsletter()).rejects.toThrow(ValidationError);
|
|
294
|
+
await expect(deleteNewsletter()).rejects.toThrow('Newsletter ID is required for deletion');
|
|
295
|
+
expect(mockNewslettersApi.delete).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should throw NotFoundError if newsletter does not exist', async () => {
|
|
299
|
+
const ghostError = new Error('Not found');
|
|
300
|
+
ghostError.response = { status: 404 };
|
|
301
|
+
mockNewslettersApi.delete.mockRejectedValue(ghostError);
|
|
302
|
+
|
|
303
|
+
await expect(deleteNewsletter('nonexistent')).rejects.toThrow(NotFoundError);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|