@jgardner04/ghost-mcp-server 1.13.4 → 1.14.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.
Files changed (40) hide show
  1. package/README.md +68 -0
  2. package/package.json +7 -3
  3. package/src/__tests__/helpers/testUtils.js +15 -1
  4. package/src/__tests__/mcp_server.test.js +152 -1
  5. package/src/__tests__/mcp_server_pages.test.js +23 -6
  6. package/src/controllers/__tests__/imageController.test.js +2 -2
  7. package/src/controllers/imageController.js +11 -10
  8. package/src/mcp_server.js +647 -1203
  9. package/src/routes/__tests__/imageRoutes.test.js +2 -2
  10. package/src/schemas/__tests__/common.test.js +3 -3
  11. package/src/schemas/__tests__/pageSchemas.test.js +11 -2
  12. package/src/schemas/common.js +3 -2
  13. package/src/schemas/pageSchemas.js +1 -1
  14. package/src/schemas/postSchemas.js +1 -1
  15. package/src/services/__tests__/createResourceService.test.js +468 -0
  16. package/src/services/__tests__/ghostService.test.js +0 -19
  17. package/src/services/__tests__/ghostServiceImproved.members.test.js +0 -3
  18. package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +2 -2
  19. package/src/services/__tests__/ghostServiceImproved.pages.test.js +0 -3
  20. package/src/services/__tests__/ghostServiceImproved.posts.test.js +0 -1
  21. package/src/services/__tests__/ghostServiceImproved.tags.test.js +0 -3
  22. package/src/services/__tests__/ghostServiceImproved.tiers.test.js +3 -5
  23. package/src/services/__tests__/imageProcessingService.test.js +148 -177
  24. package/src/services/__tests__/images.test.js +78 -0
  25. package/src/services/createResourceService.js +138 -0
  26. package/src/services/ghostApiClient.js +240 -0
  27. package/src/services/ghostService.js +1 -19
  28. package/src/services/ghostServiceImproved.js +76 -915
  29. package/src/services/imageProcessingService.js +100 -56
  30. package/src/services/images.js +54 -0
  31. package/src/services/members.js +127 -0
  32. package/src/services/newsletters.js +63 -0
  33. package/src/services/pageService.js +2 -2
  34. package/src/services/pages.js +116 -0
  35. package/src/services/posts.js +116 -0
  36. package/src/services/tags.js +118 -0
  37. package/src/services/tiers.js +72 -0
  38. package/src/services/validators.js +218 -0
  39. package/src/utils/__tests__/imageInputResolver.test.js +134 -0
  40. package/src/utils/imageInputResolver.js +127 -0
@@ -41,8 +41,8 @@ vi.mock('../../services/imageProcessingService.js', () => ({
41
41
  processImage: vi.fn().mockResolvedValue('/tmp/processed-image.jpg'),
42
42
  }));
43
43
 
44
- // Mock the ghost service
45
- vi.mock('../../services/ghostService.js', () => ({
44
+ // Mock the image upload service (now in images.js, re-exported via ghostServiceImproved.js)
45
+ vi.mock('../../services/images.js', () => ({
46
46
  uploadImage: vi.fn().mockResolvedValue({ url: 'https://ghost.com/image.jpg' }),
47
47
  }));
48
48
 
@@ -336,12 +336,12 @@ describe('Common Schemas', () => {
336
336
  describe('featureImageAltSchema', () => {
337
337
  it('should accept valid alt text', () => {
338
338
  expect(() => featureImageAltSchema.parse('Image description')).not.toThrow();
339
- expect(() => featureImageAltSchema.parse('A'.repeat(125))).not.toThrow();
339
+ expect(() => featureImageAltSchema.parse('A'.repeat(191))).not.toThrow();
340
340
  expect(() => featureImageAltSchema.parse(undefined)).not.toThrow();
341
341
  });
342
342
 
343
- it('should reject too long alt text', () => {
344
- expect(() => featureImageAltSchema.parse('A'.repeat(126))).toThrow();
343
+ it('should reject too long alt text (>191, matches Ghost varchar(191))', () => {
344
+ expect(() => featureImageAltSchema.parse('A'.repeat(192))).toThrow();
345
345
  });
346
346
  });
347
347
 
@@ -115,16 +115,25 @@ describe('Page Schemas', () => {
115
115
  expect(() => createPageSchema.parse(invalidPage)).toThrow();
116
116
  });
117
117
 
118
- it('should reject page with too long feature_image_caption', () => {
118
+ it('should reject page with too long feature_image_caption (>5000)', () => {
119
119
  const invalidPage = {
120
120
  title: 'Page',
121
121
  html: '<p>Content</p>',
122
- feature_image_caption: 'A'.repeat(501),
122
+ feature_image_caption: 'A'.repeat(5001),
123
123
  };
124
124
 
125
125
  expect(() => createPageSchema.parse(invalidPage)).toThrow();
126
126
  });
127
127
 
128
+ it('should accept a 5000-char feature_image_caption', () => {
129
+ const page = {
130
+ title: 'Page',
131
+ html: '<p>Content</p>',
132
+ feature_image_caption: 'A'.repeat(5000),
133
+ };
134
+ expect(() => createPageSchema.parse(page)).not.toThrow();
135
+ });
136
+
128
137
  it('should reject page with too long og_title', () => {
129
138
  const invalidPage = {
130
139
  title: 'Page',
@@ -205,11 +205,12 @@ export const featureImageSchema = z.string().url('Invalid feature image URL').op
205
205
 
206
206
  /**
207
207
  * Feature image alt text validation schema
208
- * Optional alt text for accessibility
208
+ * Optional alt text for accessibility. Ghost's posts.feature_image_alt
209
+ * column is varchar(191); anything longer is rejected server-side.
209
210
  */
210
211
  export const featureImageAltSchema = z
211
212
  .string()
212
- .max(125, 'Feature image alt text cannot exceed 125 characters')
213
+ .max(191, 'Feature image alt text cannot exceed 191 characters')
213
214
  .optional();
214
215
 
215
216
  /**
@@ -44,7 +44,7 @@ export const createPageSchema = z.object({
44
44
  featured: featuredSchema,
45
45
  feature_image: featureImageSchema,
46
46
  feature_image_alt: featureImageAltSchema,
47
- feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
47
+ feature_image_caption: z.string().max(5000, 'Caption cannot exceed 5000 characters').optional(),
48
48
  excerpt: excerptSchema,
49
49
  custom_excerpt: customExcerptSchema,
50
50
  meta_title: metaTitleSchema,
@@ -42,7 +42,7 @@ export const createPostSchema = z.object({
42
42
  featured: featuredSchema,
43
43
  feature_image: featureImageSchema,
44
44
  feature_image_alt: featureImageAltSchema,
45
- feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
45
+ feature_image_caption: z.string().max(5000, 'Caption cannot exceed 5000 characters').optional(),
46
46
  excerpt: excerptSchema,
47
47
  custom_excerpt: customExcerptSchema,
48
48
  meta_title: metaTitleSchema,
@@ -0,0 +1,468 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
3
+ import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
4
+ import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
5
+
6
+ // Mock the Ghost Admin API using shared mock factory
7
+ vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
8
+
9
+ // Mock dotenv
10
+ vi.mock('dotenv', () => mockDotenv());
11
+
12
+ // Mock logger
13
+ vi.mock('../../utils/logger.js', () => ({
14
+ createContextLogger: createMockContextLogger(),
15
+ }));
16
+
17
+ // Mock fs for validators
18
+ vi.mock('fs/promises', () => ({
19
+ default: {
20
+ access: vi.fn(),
21
+ },
22
+ }));
23
+
24
+ import { createResourceService } from '../createResourceService.js';
25
+ import { api, ghostCircuitBreaker } from '../ghostApiClient.js';
26
+ import { GhostAPIError, ValidationError, NotFoundError } from '../../errors/index.js';
27
+
28
+ describe('createResourceService', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ // Reset circuit breaker state between tests so error tests don't trip it open
32
+ ghostCircuitBreaker.state = 'CLOSED';
33
+ ghostCircuitBreaker.failureCount = 0;
34
+ ghostCircuitBreaker.lastFailureTime = null;
35
+ ghostCircuitBreaker.nextAttempt = null;
36
+ });
37
+
38
+ describe('create', () => {
39
+ it('should create a resource via handleApiRequest', async () => {
40
+ const service = createResourceService({
41
+ resource: 'posts',
42
+ label: 'Post',
43
+ });
44
+
45
+ const data = { title: 'Test Post', html: '<p>Hello</p>' };
46
+ const expected = { id: '1', ...data };
47
+ api.posts.add.mockResolvedValue(expected);
48
+
49
+ const result = await service.create(data);
50
+
51
+ expect(result).toEqual(expected);
52
+ expect(api.posts.add).toHaveBeenCalledWith(data, {});
53
+ });
54
+
55
+ it('should merge createDefaults into data', async () => {
56
+ const service = createResourceService({
57
+ resource: 'posts',
58
+ label: 'Post',
59
+ createDefaults: { status: 'draft' },
60
+ });
61
+
62
+ const data = { title: 'Test Post', html: '<p>Hello</p>' };
63
+ api.posts.add.mockResolvedValue({ id: '1', ...data, status: 'draft' });
64
+
65
+ await service.create(data);
66
+
67
+ expect(api.posts.add).toHaveBeenCalledWith(
68
+ { status: 'draft', title: 'Test Post', html: '<p>Hello</p>' },
69
+ {}
70
+ );
71
+ });
72
+
73
+ it('should allow data to override createDefaults', async () => {
74
+ const service = createResourceService({
75
+ resource: 'posts',
76
+ label: 'Post',
77
+ createDefaults: { status: 'draft' },
78
+ });
79
+
80
+ const data = { title: 'Test', html: '<p>Hi</p>', status: 'published' };
81
+ api.posts.add.mockResolvedValue({ id: '1', ...data });
82
+
83
+ await service.create(data);
84
+
85
+ expect(api.posts.add).toHaveBeenCalledWith(
86
+ expect.objectContaining({ status: 'published' }),
87
+ {}
88
+ );
89
+ });
90
+
91
+ it('should pass createOptions to the API call', async () => {
92
+ const service = createResourceService({
93
+ resource: 'posts',
94
+ label: 'Post',
95
+ createOptions: { source: 'html' },
96
+ });
97
+
98
+ const data = { title: 'Test' };
99
+ api.posts.add.mockResolvedValue({ id: '1', ...data });
100
+
101
+ await service.create(data);
102
+
103
+ expect(api.posts.add).toHaveBeenCalledWith(data, { source: 'html' });
104
+ });
105
+
106
+ it('should merge caller options with createOptions', async () => {
107
+ const service = createResourceService({
108
+ resource: 'posts',
109
+ label: 'Post',
110
+ createOptions: { source: 'html' },
111
+ });
112
+
113
+ const data = { title: 'Test' };
114
+ api.posts.add.mockResolvedValue({ id: '1', ...data });
115
+
116
+ await service.create(data, { formats: 'mobiledoc' });
117
+
118
+ expect(api.posts.add).toHaveBeenCalledWith(data, { source: 'html', formats: 'mobiledoc' });
119
+ });
120
+
121
+ it('should call validateCreate before creating', async () => {
122
+ const validateCreate = vi.fn();
123
+ const service = createResourceService({
124
+ resource: 'posts',
125
+ label: 'Post',
126
+ validateCreate,
127
+ });
128
+
129
+ const data = { title: 'Test' };
130
+ api.posts.add.mockResolvedValue({ id: '1', ...data });
131
+
132
+ await service.create(data);
133
+
134
+ expect(validateCreate).toHaveBeenCalledWith(data);
135
+ expect(validateCreate).toHaveBeenCalledBefore(api.posts.add);
136
+ });
137
+
138
+ it('should support async validateCreate', async () => {
139
+ const validateCreate = vi.fn().mockResolvedValue(undefined);
140
+ const service = createResourceService({
141
+ resource: 'posts',
142
+ label: 'Post',
143
+ validateCreate,
144
+ });
145
+
146
+ const data = { title: 'Test' };
147
+ api.posts.add.mockResolvedValue({ id: '1', ...data });
148
+
149
+ await service.create(data);
150
+
151
+ expect(validateCreate).toHaveBeenCalledWith(data);
152
+ expect(api.posts.add).toHaveBeenCalled();
153
+ });
154
+
155
+ it('should not call API if validateCreate throws', async () => {
156
+ const validateCreate = vi.fn(() => {
157
+ throw new ValidationError('Invalid data');
158
+ });
159
+ const service = createResourceService({
160
+ resource: 'posts',
161
+ label: 'Post',
162
+ validateCreate,
163
+ });
164
+
165
+ await expect(service.create({ title: '' })).rejects.toThrow(ValidationError);
166
+ expect(api.posts.add).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('should convert 422 errors to ValidationError', async () => {
170
+ const service = createResourceService({
171
+ resource: 'posts',
172
+ label: 'Post',
173
+ });
174
+
175
+ const ghostError = new Error('Validation failed');
176
+ ghostError.response = { status: 422 };
177
+ api.posts.add.mockRejectedValue(ghostError);
178
+
179
+ await expect(service.create({ title: 'Test' })).rejects.toThrow(ValidationError);
180
+ });
181
+
182
+ it('should re-throw non-422 errors', async () => {
183
+ const service = createResourceService({
184
+ resource: 'posts',
185
+ label: 'Post',
186
+ });
187
+
188
+ const error = new Error('Server Error');
189
+ error.response = { status: 500 };
190
+ api.posts.add.mockRejectedValue(error);
191
+
192
+ await expect(service.create({ title: 'Test' })).rejects.toThrow(GhostAPIError);
193
+ });
194
+ });
195
+
196
+ describe('update', () => {
197
+ it('should update a resource with OCC', async () => {
198
+ const service = createResourceService({
199
+ resource: 'posts',
200
+ label: 'Post',
201
+ });
202
+
203
+ const existing = { id: 'post-1', title: 'Old', updated_at: '2024-01-01T00:00:00.000Z' };
204
+ const updated = { ...existing, title: 'New' };
205
+
206
+ api.posts.read.mockResolvedValue(existing);
207
+ api.posts.edit.mockResolvedValue(updated);
208
+
209
+ const result = await service.update('post-1', { title: 'New' });
210
+
211
+ expect(result).toEqual(updated);
212
+ expect(api.posts.read).toHaveBeenCalledWith({}, { id: 'post-1' });
213
+ expect(api.posts.edit).toHaveBeenCalledWith(
214
+ { id: 'post-1', title: 'New', updated_at: '2024-01-01T00:00:00.000Z' },
215
+ {}
216
+ );
217
+ });
218
+
219
+ it('should throw ValidationError if ID is missing', async () => {
220
+ const service = createResourceService({
221
+ resource: 'posts',
222
+ label: 'Post',
223
+ });
224
+
225
+ await expect(service.update(undefined, { title: 'New' })).rejects.toThrow(ValidationError);
226
+ await expect(service.update('', { title: 'New' })).rejects.toThrow('Post ID is required');
227
+ });
228
+
229
+ it('should call validateUpdate before updating', async () => {
230
+ const validateUpdate = vi.fn();
231
+ const service = createResourceService({
232
+ resource: 'posts',
233
+ label: 'Post',
234
+ validateUpdate,
235
+ });
236
+
237
+ const existing = { id: 'post-1', updated_at: '2024-01-01T00:00:00.000Z' };
238
+ api.posts.read.mockResolvedValue(existing);
239
+ api.posts.edit.mockResolvedValue(existing);
240
+
241
+ await service.update('post-1', { title: 'New' });
242
+
243
+ expect(validateUpdate).toHaveBeenCalledWith('post-1', { title: 'New' });
244
+ });
245
+
246
+ it('should support async validateUpdate', async () => {
247
+ const validateUpdate = vi.fn().mockResolvedValue(undefined);
248
+ const service = createResourceService({
249
+ resource: 'posts',
250
+ label: 'Post',
251
+ validateUpdate,
252
+ });
253
+
254
+ const existing = { id: 'post-1', updated_at: '2024-01-01T00:00:00.000Z' };
255
+ api.posts.read.mockResolvedValue(existing);
256
+ api.posts.edit.mockResolvedValue(existing);
257
+
258
+ await service.update('post-1', { title: 'New' });
259
+
260
+ expect(validateUpdate).toHaveBeenCalled();
261
+ });
262
+
263
+ it('should catch 422 on update when catch422OnUpdate is true', async () => {
264
+ const service = createResourceService({
265
+ resource: 'newsletters',
266
+ label: 'Newsletter',
267
+ catch422OnUpdate: true,
268
+ });
269
+
270
+ const existing = { id: 'nl-1', updated_at: '2024-01-01T00:00:00.000Z' };
271
+ api.newsletters.read.mockResolvedValue(existing);
272
+
273
+ const ghostError = new Error('Name already exists');
274
+ ghostError.response = { status: 422 };
275
+ api.newsletters.edit.mockRejectedValue(ghostError);
276
+
277
+ try {
278
+ await service.update('nl-1', { name: 'Dupe' });
279
+ expect.fail('Expected ValidationError to be thrown');
280
+ } catch (error) {
281
+ expect(error).toBeInstanceOf(ValidationError);
282
+ expect(error.message).toBe('Newsletter update failed');
283
+ }
284
+ });
285
+
286
+ it('should not catch 422 on update when catch422OnUpdate is false', async () => {
287
+ const service = createResourceService({
288
+ resource: 'posts',
289
+ label: 'Post',
290
+ catch422OnUpdate: false,
291
+ });
292
+
293
+ const existing = { id: 'post-1', updated_at: '2024-01-01T00:00:00.000Z' };
294
+ api.posts.read.mockResolvedValue(existing);
295
+
296
+ const ghostError = new Error('Something');
297
+ ghostError.response = { status: 422 };
298
+ api.posts.edit.mockRejectedValue(ghostError);
299
+
300
+ await expect(service.update('post-1', { title: 'New' })).rejects.toThrow(GhostAPIError);
301
+ });
302
+ });
303
+
304
+ describe('remove', () => {
305
+ it('should delete a resource by ID', async () => {
306
+ const service = createResourceService({
307
+ resource: 'posts',
308
+ label: 'Post',
309
+ });
310
+
311
+ api.posts.delete.mockResolvedValue({ id: 'post-1' });
312
+
313
+ const result = await service.remove('post-1');
314
+
315
+ expect(result).toEqual({ id: 'post-1' });
316
+ expect(api.posts.delete).toHaveBeenCalledWith('post-1', {});
317
+ });
318
+
319
+ it('should throw ValidationError if ID is missing', async () => {
320
+ const service = createResourceService({
321
+ resource: 'posts',
322
+ label: 'Post',
323
+ });
324
+
325
+ await expect(service.remove()).rejects.toThrow(ValidationError);
326
+ await expect(service.remove()).rejects.toThrow('Post ID is required');
327
+ });
328
+
329
+ it('should throw NotFoundError when resource does not exist', async () => {
330
+ const service = createResourceService({
331
+ resource: 'posts',
332
+ label: 'Post',
333
+ });
334
+
335
+ const ghostError = new Error('Not found');
336
+ ghostError.response = { status: 404 };
337
+ api.posts.delete.mockRejectedValue(ghostError);
338
+
339
+ await expect(service.remove('nonexistent')).rejects.toThrow(NotFoundError);
340
+ });
341
+ });
342
+
343
+ describe('getOne', () => {
344
+ it('should retrieve a resource by ID', async () => {
345
+ const service = createResourceService({
346
+ resource: 'posts',
347
+ label: 'Post',
348
+ });
349
+
350
+ const expected = { id: 'post-1', title: 'Test' };
351
+ api.posts.read.mockResolvedValue(expected);
352
+
353
+ const result = await service.getOne('post-1');
354
+
355
+ expect(result).toEqual(expected);
356
+ expect(api.posts.read).toHaveBeenCalledWith({}, { id: 'post-1' });
357
+ });
358
+
359
+ it('should pass options to read', async () => {
360
+ const service = createResourceService({
361
+ resource: 'posts',
362
+ label: 'Post',
363
+ });
364
+
365
+ api.posts.read.mockResolvedValue({ id: 'post-1' });
366
+
367
+ await service.getOne('post-1', { include: 'tags' });
368
+
369
+ expect(api.posts.read).toHaveBeenCalledWith({ include: 'tags' }, { id: 'post-1' });
370
+ });
371
+
372
+ it('should throw ValidationError if ID is missing', async () => {
373
+ const service = createResourceService({
374
+ resource: 'posts',
375
+ label: 'Post',
376
+ });
377
+
378
+ await expect(service.getOne()).rejects.toThrow(ValidationError);
379
+ });
380
+
381
+ it('should throw NotFoundError when resource does not exist', async () => {
382
+ const service = createResourceService({
383
+ resource: 'posts',
384
+ label: 'Post',
385
+ });
386
+
387
+ const ghostError = new Error('Not found');
388
+ ghostError.response = { status: 404 };
389
+ api.posts.read.mockRejectedValue(ghostError);
390
+
391
+ await expect(service.getOne('nonexistent')).rejects.toThrow(NotFoundError);
392
+ });
393
+ });
394
+
395
+ describe('getList', () => {
396
+ it('should list resources with defaults', async () => {
397
+ const service = createResourceService({
398
+ resource: 'posts',
399
+ label: 'Post',
400
+ listDefaults: { limit: 15, include: 'tags,authors' },
401
+ });
402
+
403
+ const expected = [{ id: '1' }, { id: '2' }];
404
+ api.posts.browse.mockResolvedValue(expected);
405
+
406
+ const result = await service.getList();
407
+
408
+ expect(result).toEqual(expected);
409
+ expect(api.posts.browse).toHaveBeenCalledWith({ limit: 15, include: 'tags,authors' }, {});
410
+ });
411
+
412
+ it('should allow overriding defaults', async () => {
413
+ const service = createResourceService({
414
+ resource: 'posts',
415
+ label: 'Post',
416
+ listDefaults: { limit: 15, include: 'tags,authors' },
417
+ });
418
+
419
+ api.posts.browse.mockResolvedValue([]);
420
+
421
+ await service.getList({ limit: 5, filter: 'status:published' });
422
+
423
+ expect(api.posts.browse).toHaveBeenCalledWith(
424
+ { limit: 5, include: 'tags,authors', filter: 'status:published' },
425
+ {}
426
+ );
427
+ });
428
+
429
+ it('should return empty array when API returns null/undefined', async () => {
430
+ const service = createResourceService({
431
+ resource: 'posts',
432
+ label: 'Post',
433
+ });
434
+
435
+ api.posts.browse.mockResolvedValue(null);
436
+
437
+ const result = await service.getList();
438
+
439
+ expect(result).toEqual([]);
440
+ });
441
+
442
+ it('should return empty array when API returns empty array', async () => {
443
+ const service = createResourceService({
444
+ resource: 'posts',
445
+ label: 'Post',
446
+ });
447
+
448
+ api.posts.browse.mockResolvedValue([]);
449
+
450
+ const result = await service.getList();
451
+
452
+ expect(result).toEqual([]);
453
+ });
454
+
455
+ it('should use default limit of 15 when no listDefaults provided', async () => {
456
+ const service = createResourceService({
457
+ resource: 'tags',
458
+ label: 'Tag',
459
+ });
460
+
461
+ api.tags.browse.mockResolvedValue([]);
462
+
463
+ await service.getList();
464
+
465
+ expect(api.tags.browse).toHaveBeenCalledWith({ limit: 15 }, {});
466
+ });
467
+ });
468
+ });
@@ -20,7 +20,6 @@ import {
20
20
  createTag,
21
21
  getTags,
22
22
  getSiteInfo,
23
- uploadImage,
24
23
  handleApiRequest,
25
24
  api,
26
25
  } from '../ghostService.js';
@@ -209,24 +208,6 @@ describe('ghostService', () => {
209
208
  });
210
209
  });
211
210
 
212
- describe('uploadImage', () => {
213
- it('should throw error when image path is missing', async () => {
214
- await expect(uploadImage()).rejects.toThrow('Image path is required for upload');
215
- await expect(uploadImage('')).rejects.toThrow('Image path is required for upload');
216
- });
217
-
218
- it('should successfully upload image with valid path', async () => {
219
- const imagePath = '/path/to/image.jpg';
220
- const expectedResult = { url: 'https://example.com/uploaded-image.jpg' };
221
- api.images.upload.mockResolvedValue(expectedResult);
222
-
223
- const result = await uploadImage(imagePath);
224
-
225
- expect(result).toEqual(expectedResult);
226
- expect(api.images.upload).toHaveBeenCalledWith({ file: imagePath });
227
- });
228
- });
229
-
230
211
  describe('createPost', () => {
231
212
  it('should throw error when title is missing', async () => {
232
213
  await expect(createPost({})).rejects.toThrow('Post title is required');
@@ -185,7 +185,6 @@ describe('ghostServiceImproved - Members', () => {
185
185
 
186
186
  it('should throw not found error if member does not exist', async () => {
187
187
  const error404 = new GhostAPIError('members.read', 'Member not found', 404);
188
- error404.response = { status: 404 };
189
188
  api.members.read.mockRejectedValue(error404);
190
189
 
191
190
  const rejection = updateMember('non-existent', { name: 'Test' });
@@ -212,7 +211,6 @@ describe('ghostServiceImproved - Members', () => {
212
211
 
213
212
  it('should throw not found error if member does not exist', async () => {
214
213
  const error404 = new GhostAPIError('members.delete', 'Member not found', 404);
215
- error404.response = { status: 404 };
216
214
  api.members.delete.mockRejectedValue(error404);
217
215
 
218
216
  const rejection = deleteMember('non-existent');
@@ -360,7 +358,6 @@ describe('ghostServiceImproved - Members', () => {
360
358
 
361
359
  it('should throw not found error when member not found by ID', async () => {
362
360
  const error404 = new GhostAPIError('members.read', 'Member not found', 404);
363
- error404.response = { status: 404 };
364
361
  api.members.read.mockRejectedValue(error404);
365
362
 
366
363
  const rejection = getMember({ id: 'non-existent' });
@@ -242,7 +242,7 @@ describe('ghostServiceImproved - Newsletter Operations', () => {
242
242
 
243
243
  it('should throw ValidationError if ID is missing', async () => {
244
244
  await expect(updateNewsletter()).rejects.toThrow(ValidationError);
245
- await expect(updateNewsletter()).rejects.toThrow('Newsletter ID is required for update');
245
+ await expect(updateNewsletter()).rejects.toThrow('Newsletter ID is required');
246
246
  expect(api.newsletters.read).not.toHaveBeenCalled();
247
247
  });
248
248
 
@@ -293,7 +293,7 @@ describe('ghostServiceImproved - Newsletter Operations', () => {
293
293
 
294
294
  it('should throw ValidationError if ID is missing', async () => {
295
295
  await expect(deleteNewsletter()).rejects.toThrow(ValidationError);
296
- await expect(deleteNewsletter()).rejects.toThrow('Newsletter ID is required for deletion');
296
+ await expect(deleteNewsletter()).rejects.toThrow('Newsletter ID is required');
297
297
  expect(api.newsletters.delete).not.toHaveBeenCalled();
298
298
  });
299
299
 
@@ -274,7 +274,6 @@ describe('ghostServiceImproved - Pages', () => {
274
274
 
275
275
  it('should handle page not found (404)', async () => {
276
276
  const error404 = new GhostAPIError('pages.read', 'Page not found', 404);
277
- error404.response = { status: 404 };
278
277
  api.pages.read.mockRejectedValue(error404);
279
278
 
280
279
  const rejection = updatePage('nonexistent-id', { title: 'Updated' });
@@ -387,7 +386,6 @@ describe('ghostServiceImproved - Pages', () => {
387
386
 
388
387
  it('should handle page not found (404)', async () => {
389
388
  const error404 = new GhostAPIError('pages.delete', 'Page not found', 404);
390
- error404.response = { status: 404 };
391
389
  api.pages.delete.mockRejectedValue(error404);
392
390
 
393
391
  const rejection = deletePage('nonexistent-id');
@@ -437,7 +435,6 @@ describe('ghostServiceImproved - Pages', () => {
437
435
 
438
436
  it('should handle page not found (404)', async () => {
439
437
  const error404 = new GhostAPIError('pages.read', 'Page not found', 404);
440
- error404.response = { status: 404 };
441
438
  api.pages.read.mockRejectedValue(error404);
442
439
 
443
440
  const rejection = getPage('nonexistent-id');
@@ -93,7 +93,6 @@ describe('ghostServiceImproved - Posts (updatePost)', () => {
93
93
 
94
94
  it('should handle post not found (404)', async () => {
95
95
  const error404 = new GhostAPIError('posts.read', 'Post not found', 404);
96
- error404.response = { status: 404 };
97
96
  api.posts.read.mockRejectedValue(error404);
98
97
 
99
98
  const rejection = updatePost('nonexistent-id', { title: 'Updated' });