@jgardner04/ghost-mcp-server 1.13.2 → 1.13.4

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 (34) hide show
  1. package/package.json +5 -13
  2. package/src/__tests__/helpers/mockGhostApi.js +36 -0
  3. package/src/__tests__/mcp_server.test.js +204 -117
  4. package/src/__tests__/mcp_server_pages.test.js +32 -18
  5. package/src/config/mcp-config.js +1 -1
  6. package/src/controllers/__tests__/tagController.test.js +12 -8
  7. package/src/controllers/tagController.js +2 -2
  8. package/src/errors/__tests__/index.test.js +3 -3
  9. package/src/errors/index.js +1 -1
  10. package/src/index.js +1 -1
  11. package/src/mcp_server.js +35 -31
  12. package/src/schemas/__tests__/postSchemas.test.js +19 -0
  13. package/src/schemas/__tests__/tagSchemas.test.js +1 -1
  14. package/src/schemas/common.js +2 -2
  15. package/src/schemas/memberSchemas.js +20 -8
  16. package/src/schemas/newsletterSchemas.js +10 -10
  17. package/src/schemas/pageSchemas.js +16 -11
  18. package/src/schemas/postSchemas.js +22 -15
  19. package/src/schemas/tagSchemas.js +12 -7
  20. package/src/schemas/tierSchemas.js +17 -8
  21. package/src/services/__tests__/ghostServiceImproved.members.test.js +31 -62
  22. package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +66 -69
  23. package/src/services/__tests__/ghostServiceImproved.pages.test.js +77 -48
  24. package/src/services/__tests__/ghostServiceImproved.posts.test.js +69 -55
  25. package/src/services/__tests__/ghostServiceImproved.tags.test.js +29 -66
  26. package/src/services/__tests__/ghostServiceImproved.tiers.test.js +12 -62
  27. package/src/services/__tests__/memberService.test.js +0 -28
  28. package/src/services/__tests__/tierService.test.js +0 -28
  29. package/src/services/ghostServiceImproved.js +117 -299
  30. package/src/services/imageProcessingService.js +1 -1
  31. package/src/services/memberService.js +0 -13
  32. package/src/services/tierService.js +0 -13
  33. package/src/utils/__tests__/nqlSanitizer.test.js +38 -0
  34. package/src/utils/nqlSanitizer.js +11 -0
@@ -1,41 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
3
3
  import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
4
+ import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
4
5
 
5
- // Mock the Ghost Admin API with pages support
6
- vi.mock('@tryghost/admin-api', () => ({
7
- default: vi.fn(function () {
8
- return {
9
- posts: {
10
- add: vi.fn(),
11
- browse: vi.fn(),
12
- read: vi.fn(),
13
- edit: vi.fn(),
14
- delete: vi.fn(),
15
- },
16
- pages: {
17
- add: vi.fn(),
18
- browse: vi.fn(),
19
- read: vi.fn(),
20
- edit: vi.fn(),
21
- delete: vi.fn(),
22
- },
23
- tags: {
24
- add: vi.fn(),
25
- browse: vi.fn(),
26
- read: vi.fn(),
27
- edit: vi.fn(),
28
- delete: vi.fn(),
29
- },
30
- site: {
31
- read: vi.fn(),
32
- },
33
- images: {
34
- upload: vi.fn(),
35
- },
36
- };
37
- }),
38
- }));
6
+ // Mock the Ghost Admin API using shared mock factory
7
+ vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
39
8
 
40
9
  // Mock dotenv
41
10
  vi.mock('dotenv', () => mockDotenv());
@@ -64,6 +33,7 @@ import {
64
33
  validators,
65
34
  } from '../ghostServiceImproved.js';
66
35
  import { createPageSchema, updatePageSchema } from '../../schemas/pageSchemas.js';
36
+ import { GhostAPIError, NotFoundError, ValidationError } from '../../errors/index.js';
67
37
 
68
38
  describe('ghostServiceImproved - Pages', () => {
69
39
  beforeEach(() => {
@@ -236,13 +206,13 @@ describe('ghostServiceImproved - Pages', () => {
236
206
  });
237
207
 
238
208
  it('should handle Ghost API validation errors (422)', async () => {
239
- const error422 = new Error('Validation failed');
209
+ const error422 = new GhostAPIError('pages.add', 'Validation failed', 422);
240
210
  error422.response = { status: 422 };
241
211
  api.pages.add.mockRejectedValue(error422);
242
212
 
243
- await expect(createPage({ title: 'Test', html: '<p>Content</p>' })).rejects.toThrow(
244
- 'Page creation failed due to validation errors'
245
- );
213
+ const rejection = createPage({ title: 'Test', html: '<p>Content</p>' });
214
+ await expect(rejection).rejects.toBeInstanceOf(ValidationError);
215
+ await expect(rejection).rejects.toThrow('Page creation failed due to validation errors');
246
216
  });
247
217
 
248
218
  it('should NOT include tags in page creation (pages do not support tags)', async () => {
@@ -292,8 +262,8 @@ describe('ghostServiceImproved - Pages', () => {
292
262
  expect(api.pages.read).toHaveBeenCalledWith({}, { id: pageId });
293
263
  // Should send ONLY updateData + updated_at, NOT the full existing page
294
264
  expect(api.pages.edit).toHaveBeenCalledWith(
295
- { title: 'Updated Title', updated_at: '2024-01-01T00:00:00.000Z' },
296
- { id: pageId }
265
+ { id: pageId, title: 'Updated Title', updated_at: '2024-01-01T00:00:00.000Z' },
266
+ {}
297
267
  );
298
268
  // Verify read-only fields are NOT sent
299
269
  const editCallData = api.pages.edit.mock.calls[0][0];
@@ -303,13 +273,13 @@ describe('ghostServiceImproved - Pages', () => {
303
273
  });
304
274
 
305
275
  it('should handle page not found (404)', async () => {
306
- const error404 = new Error('Page not found');
276
+ const error404 = new GhostAPIError('pages.read', 'Page not found', 404);
307
277
  error404.response = { status: 404 };
308
278
  api.pages.read.mockRejectedValue(error404);
309
279
 
310
- await expect(updatePage('nonexistent-id', { title: 'Updated' })).rejects.toThrow(
311
- 'Page not found'
312
- );
280
+ const rejection = updatePage('nonexistent-id', { title: 'Updated' });
281
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
282
+ await expect(rejection).rejects.toThrow('Page not found');
313
283
  });
314
284
 
315
285
  it('should preserve updated_at timestamp for conflict resolution', async () => {
@@ -341,6 +311,61 @@ describe('ghostServiceImproved - Pages', () => {
341
311
  'Page validation failed'
342
312
  );
343
313
  });
314
+
315
+ it('should reject past published_at when existing page is scheduled (no status in update)', async () => {
316
+ const pageId = 'page-123';
317
+ const existingPage = {
318
+ id: pageId,
319
+ title: 'Scheduled Page',
320
+ status: 'scheduled',
321
+ published_at: new Date(Date.now() + 86400000).toISOString(),
322
+ updated_at: '2024-01-01T00:00:00.000Z',
323
+ };
324
+ const pastDate = new Date(Date.now() - 86400000).toISOString();
325
+
326
+ api.pages.read.mockResolvedValue(existingPage);
327
+
328
+ await expect(updatePage(pageId, { published_at: pastDate })).rejects.toThrow(
329
+ 'Page validation failed'
330
+ );
331
+ });
332
+
333
+ it('should allow future published_at when existing page is scheduled (no status in update)', async () => {
334
+ const pageId = 'page-123';
335
+ const futureDate = new Date(Date.now() + 172800000).toISOString();
336
+ const existingPage = {
337
+ id: pageId,
338
+ title: 'Scheduled Page',
339
+ status: 'scheduled',
340
+ published_at: new Date(Date.now() + 86400000).toISOString(),
341
+ updated_at: '2024-01-01T00:00:00.000Z',
342
+ };
343
+
344
+ api.pages.read.mockResolvedValue(existingPage);
345
+ api.pages.edit.mockResolvedValue({ ...existingPage, published_at: futureDate });
346
+
347
+ const result = await updatePage(pageId, { published_at: futureDate });
348
+
349
+ expect(result).toBeDefined();
350
+ });
351
+
352
+ it('should allow published_at change when existing page is not scheduled', async () => {
353
+ const pageId = 'page-123';
354
+ const pastDate = new Date(Date.now() - 86400000).toISOString();
355
+ const existingPage = {
356
+ id: pageId,
357
+ title: 'Draft Page',
358
+ status: 'draft',
359
+ updated_at: '2024-01-01T00:00:00.000Z',
360
+ };
361
+
362
+ api.pages.read.mockResolvedValue(existingPage);
363
+ api.pages.edit.mockResolvedValue({ ...existingPage, published_at: pastDate });
364
+
365
+ const result = await updatePage(pageId, { published_at: pastDate });
366
+
367
+ expect(result).toBeDefined();
368
+ });
344
369
  });
345
370
 
346
371
  describe('deletePage', () => {
@@ -361,11 +386,13 @@ describe('ghostServiceImproved - Pages', () => {
361
386
  });
362
387
 
363
388
  it('should handle page not found (404)', async () => {
364
- const error404 = new Error('Page not found');
389
+ const error404 = new GhostAPIError('pages.delete', 'Page not found', 404);
365
390
  error404.response = { status: 404 };
366
391
  api.pages.delete.mockRejectedValue(error404);
367
392
 
368
- await expect(deletePage('nonexistent-id')).rejects.toThrow('Page not found');
393
+ const rejection = deletePage('nonexistent-id');
394
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
395
+ await expect(rejection).rejects.toThrow('Page not found');
369
396
  });
370
397
  });
371
398
 
@@ -409,11 +436,13 @@ describe('ghostServiceImproved - Pages', () => {
409
436
  });
410
437
 
411
438
  it('should handle page not found (404)', async () => {
412
- const error404 = new Error('Page not found');
439
+ const error404 = new GhostAPIError('pages.read', 'Page not found', 404);
413
440
  error404.response = { status: 404 };
414
441
  api.pages.read.mockRejectedValue(error404);
415
442
 
416
- await expect(getPage('nonexistent-id')).rejects.toThrow('Page not found');
443
+ const rejection = getPage('nonexistent-id');
444
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
445
+ await expect(rejection).rejects.toThrow('Page not found');
417
446
  });
418
447
  });
419
448
 
@@ -1,48 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
3
3
  import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
4
+ import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
4
5
 
5
- // Mock the Ghost Admin API with posts support
6
- vi.mock('@tryghost/admin-api', () => ({
7
- default: vi.fn(function () {
8
- return {
9
- posts: {
10
- add: vi.fn(),
11
- browse: vi.fn(),
12
- read: vi.fn(),
13
- edit: vi.fn(),
14
- delete: vi.fn(),
15
- },
16
- pages: {
17
- add: vi.fn(),
18
- browse: vi.fn(),
19
- read: vi.fn(),
20
- edit: vi.fn(),
21
- delete: vi.fn(),
22
- },
23
- tags: {
24
- add: vi.fn(),
25
- browse: vi.fn(),
26
- read: vi.fn(),
27
- edit: vi.fn(),
28
- delete: vi.fn(),
29
- },
30
- members: {
31
- add: vi.fn(),
32
- browse: vi.fn(),
33
- read: vi.fn(),
34
- edit: vi.fn(),
35
- delete: vi.fn(),
36
- },
37
- site: {
38
- read: vi.fn(),
39
- },
40
- images: {
41
- upload: vi.fn(),
42
- },
43
- };
44
- }),
45
- }));
6
+ // Mock the Ghost Admin API using shared mock factory
7
+ vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
46
8
 
47
9
  // Mock dotenv
48
10
  vi.mock('dotenv', () => mockDotenv());
@@ -62,6 +24,7 @@ vi.mock('fs/promises', () => ({
62
24
  // Import after setting up mocks
63
25
  import { updatePost, api, validators } from '../ghostServiceImproved.js';
64
26
  import { updatePostSchema } from '../../schemas/postSchemas.js';
27
+ import { GhostAPIError, NotFoundError } from '../../errors/index.js';
65
28
 
66
29
  describe('ghostServiceImproved - Posts (updatePost)', () => {
67
30
  beforeEach(() => {
@@ -95,8 +58,8 @@ describe('ghostServiceImproved - Posts (updatePost)', () => {
95
58
  expect(result).toEqual(expectedResult);
96
59
  // Should send ONLY updateData + updated_at, NOT the full existing post
97
60
  expect(api.posts.edit).toHaveBeenCalledWith(
98
- { title: 'Updated Title', updated_at: '2024-01-01T00:00:00.000Z' },
99
- { id: postId }
61
+ { id: postId, title: 'Updated Title', updated_at: '2024-01-01T00:00:00.000Z' },
62
+ {}
100
63
  );
101
64
  // Verify read-only fields are NOT sent
102
65
  const editCallData = api.posts.edit.mock.calls[0][0];
@@ -124,22 +87,18 @@ describe('ghostServiceImproved - Posts (updatePost)', () => {
124
87
  });
125
88
 
126
89
  it('should throw error when post ID is missing', async () => {
127
- await expect(updatePost(null, { title: 'Updated' })).rejects.toThrow(
128
- 'Post ID is required for update'
129
- );
130
- await expect(updatePost('', { title: 'Updated' })).rejects.toThrow(
131
- 'Post ID is required for update'
132
- );
90
+ await expect(updatePost(null, { title: 'Updated' })).rejects.toThrow('Post ID is required');
91
+ await expect(updatePost('', { title: 'Updated' })).rejects.toThrow('Post ID is required');
133
92
  });
134
93
 
135
94
  it('should handle post not found (404)', async () => {
136
- const error404 = new Error('Post not found');
95
+ const error404 = new GhostAPIError('posts.read', 'Post not found', 404);
137
96
  error404.response = { status: 404 };
138
97
  api.posts.read.mockRejectedValue(error404);
139
98
 
140
- await expect(updatePost('nonexistent-id', { title: 'Updated' })).rejects.toThrow(
141
- 'Post not found'
142
- );
99
+ const rejection = updatePost('nonexistent-id', { title: 'Updated' });
100
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
101
+ await expect(rejection).rejects.toThrow('Post not found');
143
102
  });
144
103
 
145
104
  it('should throw ValidationError when updating to scheduled without published_at', async () => {
@@ -173,10 +132,65 @@ describe('ghostServiceImproved - Posts (updatePost)', () => {
173
132
 
174
133
  expect(result).toBeDefined();
175
134
  expect(api.posts.edit).toHaveBeenCalledWith(
176
- { ...updateData, updated_at: existingPost.updated_at },
177
- { id: postId }
135
+ { id: postId, ...updateData, updated_at: existingPost.updated_at },
136
+ {}
178
137
  );
179
138
  });
139
+
140
+ it('should reject past published_at when existing post is scheduled (no status in update)', async () => {
141
+ const postId = 'post-123';
142
+ const existingPost = {
143
+ id: postId,
144
+ title: 'Scheduled Post',
145
+ status: 'scheduled',
146
+ published_at: new Date(Date.now() + 86400000).toISOString(),
147
+ updated_at: '2024-01-01T00:00:00.000Z',
148
+ };
149
+ const pastDate = new Date(Date.now() - 86400000).toISOString();
150
+
151
+ api.posts.read.mockResolvedValue(existingPost);
152
+
153
+ await expect(updatePost(postId, { published_at: pastDate })).rejects.toThrow(
154
+ 'Post validation failed'
155
+ );
156
+ });
157
+
158
+ it('should allow future published_at when existing post is scheduled (no status in update)', async () => {
159
+ const postId = 'post-123';
160
+ const futureDate = new Date(Date.now() + 172800000).toISOString();
161
+ const existingPost = {
162
+ id: postId,
163
+ title: 'Scheduled Post',
164
+ status: 'scheduled',
165
+ published_at: new Date(Date.now() + 86400000).toISOString(),
166
+ updated_at: '2024-01-01T00:00:00.000Z',
167
+ };
168
+
169
+ api.posts.read.mockResolvedValue(existingPost);
170
+ api.posts.edit.mockResolvedValue({ ...existingPost, published_at: futureDate });
171
+
172
+ const result = await updatePost(postId, { published_at: futureDate });
173
+
174
+ expect(result).toBeDefined();
175
+ });
176
+
177
+ it('should allow published_at change when existing post is not scheduled', async () => {
178
+ const postId = 'post-123';
179
+ const pastDate = new Date(Date.now() - 86400000).toISOString();
180
+ const existingPost = {
181
+ id: postId,
182
+ title: 'Draft Post',
183
+ status: 'draft',
184
+ updated_at: '2024-01-01T00:00:00.000Z',
185
+ };
186
+
187
+ api.posts.read.mockResolvedValue(existingPost);
188
+ api.posts.edit.mockResolvedValue({ ...existingPost, published_at: pastDate });
189
+
190
+ const result = await updatePost(postId, { published_at: pastDate });
191
+
192
+ expect(result).toBeDefined();
193
+ });
180
194
  });
181
195
 
182
196
  describe('validators.validateScheduledStatus', () => {
@@ -1,48 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
3
3
  import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
4
+ import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
4
5
 
5
- // Mock the Ghost Admin API with tags support
6
- vi.mock('@tryghost/admin-api', () => ({
7
- default: vi.fn(function () {
8
- return {
9
- posts: {
10
- add: vi.fn(),
11
- browse: vi.fn(),
12
- read: vi.fn(),
13
- edit: vi.fn(),
14
- delete: vi.fn(),
15
- },
16
- pages: {
17
- add: vi.fn(),
18
- browse: vi.fn(),
19
- read: vi.fn(),
20
- edit: vi.fn(),
21
- delete: vi.fn(),
22
- },
23
- tags: {
24
- add: vi.fn(),
25
- browse: vi.fn(),
26
- read: vi.fn(),
27
- edit: vi.fn(),
28
- delete: vi.fn(),
29
- },
30
- members: {
31
- add: vi.fn(),
32
- browse: vi.fn(),
33
- read: vi.fn(),
34
- edit: vi.fn(),
35
- delete: vi.fn(),
36
- },
37
- site: {
38
- read: vi.fn(),
39
- },
40
- images: {
41
- upload: vi.fn(),
42
- },
43
- };
44
- }),
45
- }));
6
+ // Mock the Ghost Admin API using shared mock factory
7
+ vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
46
8
 
47
9
  // Mock dotenv
48
10
  vi.mock('dotenv', () => mockDotenv());
@@ -69,6 +31,7 @@ import {
69
31
  api,
70
32
  ghostCircuitBreaker,
71
33
  } from '../ghostServiceImproved.js';
34
+ import { GhostAPIError, NotFoundError } from '../../errors/index.js';
72
35
 
73
36
  describe('ghostServiceImproved - Tags', () => {
74
37
  beforeEach(() => {
@@ -288,12 +251,13 @@ describe('ghostServiceImproved - Tags', () => {
288
251
  });
289
252
 
290
253
  it('should throw not found error when tag does not exist', async () => {
291
- api.tags.read.mockRejectedValue({
292
- response: { status: 404 },
293
- message: 'Tag not found',
294
- });
254
+ const error404 = new GhostAPIError('tags.read', 'Tag not found', 404);
255
+ error404.response = { status: 404 };
256
+ api.tags.read.mockRejectedValue(error404);
295
257
 
296
- await expect(getTag('non-existent')).rejects.toThrow();
258
+ const rejection = getTag('non-existent');
259
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
260
+ await expect(rejection).rejects.toThrow('Tag not found');
297
261
  });
298
262
  });
299
263
 
@@ -381,10 +345,9 @@ describe('ghostServiceImproved - Tags', () => {
381
345
  };
382
346
 
383
347
  // First call fails with duplicate error
384
- api.tags.add.mockRejectedValue({
385
- response: { status: 422 },
386
- message: 'Tag already exists',
387
- });
348
+ const error422 = new GhostAPIError('tags.add', 'Tag already exists', 422);
349
+ error422.response = { status: 422, data: { errors: [{ message: 'Tag already exists' }] } };
350
+ api.tags.add.mockRejectedValue(error422);
388
351
 
389
352
  // getTags returns existing tag when called with name filter
390
353
  api.tags.browse.mockResolvedValue([{ id: 'tag-1', name: 'JavaScript', slug: 'javascript' }]);
@@ -431,8 +394,8 @@ describe('ghostServiceImproved - Tags', () => {
431
394
  expect(api.tags.read).toHaveBeenCalled();
432
395
  // Should send ONLY updateData, NOT the full existing tag
433
396
  expect(api.tags.edit).toHaveBeenCalledWith(
434
- { name: 'Updated JavaScript', description: 'Updated description' },
435
- { id: tagId }
397
+ { id: tagId, name: 'Updated JavaScript', description: 'Updated description' },
398
+ {}
436
399
  );
437
400
  // Verify read-only fields are NOT sent
438
401
  const editCallData = api.tags.edit.mock.calls[0][0];
@@ -443,18 +406,17 @@ describe('ghostServiceImproved - Tags', () => {
443
406
  });
444
407
 
445
408
  it('should throw validation error for missing tag ID', async () => {
446
- await expect(updateTag(null, { name: 'Test' })).rejects.toThrow(
447
- 'Tag ID is required for update'
448
- );
409
+ await expect(updateTag(null, { name: 'Test' })).rejects.toThrow('Tag ID is required');
449
410
  });
450
411
 
451
412
  it('should throw not found error if tag does not exist', async () => {
452
- api.tags.read.mockRejectedValue({
453
- response: { status: 404 },
454
- message: 'Tag not found',
455
- });
413
+ const error404 = new GhostAPIError('tags.read', 'Tag not found', 404);
414
+ error404.response = { status: 404 };
415
+ api.tags.read.mockRejectedValue(error404);
456
416
 
457
- await expect(updateTag('non-existent', { name: 'Test' })).rejects.toThrow();
417
+ const rejection = updateTag('non-existent', { name: 'Test' });
418
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
419
+ await expect(rejection).rejects.toThrow('Tag not found');
458
420
  });
459
421
  });
460
422
 
@@ -471,16 +433,17 @@ describe('ghostServiceImproved - Tags', () => {
471
433
  });
472
434
 
473
435
  it('should throw validation error for missing tag ID', async () => {
474
- await expect(deleteTag(null)).rejects.toThrow('Tag ID is required for deletion');
436
+ await expect(deleteTag(null)).rejects.toThrow('Tag ID is required');
475
437
  });
476
438
 
477
439
  it('should throw not found error if tag does not exist', async () => {
478
- api.tags.delete.mockRejectedValue({
479
- response: { status: 404 },
480
- message: 'Tag not found',
481
- });
440
+ const error404 = new GhostAPIError('tags.delete', 'Tag not found', 404);
441
+ error404.response = { status: 404 };
442
+ api.tags.delete.mockRejectedValue(error404);
482
443
 
483
- await expect(deleteTag('non-existent')).rejects.toThrow();
444
+ const rejection = deleteTag('non-existent');
445
+ await expect(rejection).rejects.toBeInstanceOf(NotFoundError);
446
+ await expect(rejection).rejects.toThrow('Tag not found');
484
447
  });
485
448
  });
486
449
  });
@@ -1,55 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
3
3
  import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
4
+ import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
4
5
 
5
- // Mock the Ghost Admin API with tiers support
6
- vi.mock('@tryghost/admin-api', () => ({
7
- default: vi.fn(function () {
8
- return {
9
- posts: {
10
- add: vi.fn(),
11
- browse: vi.fn(),
12
- read: vi.fn(),
13
- edit: vi.fn(),
14
- delete: vi.fn(),
15
- },
16
- pages: {
17
- add: vi.fn(),
18
- browse: vi.fn(),
19
- read: vi.fn(),
20
- edit: vi.fn(),
21
- delete: vi.fn(),
22
- },
23
- tags: {
24
- add: vi.fn(),
25
- browse: vi.fn(),
26
- read: vi.fn(),
27
- edit: vi.fn(),
28
- delete: vi.fn(),
29
- },
30
- members: {
31
- add: vi.fn(),
32
- browse: vi.fn(),
33
- read: vi.fn(),
34
- edit: vi.fn(),
35
- delete: vi.fn(),
36
- },
37
- tiers: {
38
- add: vi.fn(),
39
- browse: vi.fn(),
40
- read: vi.fn(),
41
- edit: vi.fn(),
42
- delete: vi.fn(),
43
- },
44
- site: {
45
- read: vi.fn(),
46
- },
47
- images: {
48
- upload: vi.fn(),
49
- },
50
- };
51
- }),
52
- }));
6
+ // Mock the Ghost Admin API using shared mock factory
7
+ vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
53
8
 
54
9
  // Mock dotenv
55
10
  vi.mock('dotenv', () => mockDotenv());
@@ -270,14 +225,7 @@ describe('ghostServiceImproved - Tiers', () => {
270
225
 
271
226
  const result = await getTier('tier-1');
272
227
 
273
- expect(api.tiers.read).toHaveBeenCalledWith(
274
- expect.objectContaining({
275
- id: 'tier-1',
276
- }),
277
- expect.objectContaining({
278
- id: 'tier-1',
279
- })
280
- );
228
+ expect(api.tiers.read).toHaveBeenCalledWith({}, { id: 'tier-1' });
281
229
  expect(result).toEqual(mockTier);
282
230
  });
283
231
 
@@ -327,14 +275,16 @@ describe('ghostServiceImproved - Tiers', () => {
327
275
 
328
276
  const result = await updateTier('tier-1', updateData);
329
277
 
330
- expect(api.tiers.read).toHaveBeenCalledWith(
331
- expect.objectContaining({ id: 'tier-1' }),
332
- expect.objectContaining({ id: 'tier-1' })
333
- );
278
+ expect(api.tiers.read).toHaveBeenCalledWith({}, { id: 'tier-1' });
334
279
  // Should send ONLY updateData + updated_at, NOT the full existing tier
335
280
  expect(api.tiers.edit).toHaveBeenCalledWith(
336
- { name: 'Premium Plus', monthly_price: 1299, updated_at: '2024-01-01T00:00:00.000Z' },
337
- expect.objectContaining({ id: 'tier-1' })
281
+ {
282
+ id: 'tier-1',
283
+ name: 'Premium Plus',
284
+ monthly_price: 1299,
285
+ updated_at: '2024-01-01T00:00:00.000Z',
286
+ },
287
+ {}
338
288
  );
339
289
  // Verify read-only fields are NOT sent
340
290
  const editCallData = api.tiers.edit.mock.calls[0][0];
@@ -6,7 +6,6 @@ import {
6
6
  validateMemberLookup,
7
7
  validateSearchQuery,
8
8
  validateSearchOptions,
9
- sanitizeNqlValue,
10
9
  } from '../memberService.js';
11
10
 
12
11
  describe('memberService - Validation', () => {
@@ -447,31 +446,4 @@ describe('memberService - Validation', () => {
447
446
  );
448
447
  });
449
448
  });
450
-
451
- describe('sanitizeNqlValue', () => {
452
- it('should escape backslashes', () => {
453
- expect(sanitizeNqlValue('test\\value')).toBe('test\\\\value');
454
- });
455
-
456
- it('should escape single quotes', () => {
457
- expect(sanitizeNqlValue("test'value")).toBe("test\\'value");
458
- });
459
-
460
- it('should escape double quotes', () => {
461
- expect(sanitizeNqlValue('test"value')).toBe('test\\"value');
462
- });
463
-
464
- it('should handle multiple special characters', () => {
465
- expect(sanitizeNqlValue('test\'value"with\\chars')).toBe('test\\\'value\\"with\\\\chars');
466
- });
467
-
468
- it('should not modify strings without special characters', () => {
469
- expect(sanitizeNqlValue('normalvalue')).toBe('normalvalue');
470
- expect(sanitizeNqlValue('test@example.com')).toBe('test@example.com');
471
- });
472
-
473
- it('should handle empty string', () => {
474
- expect(sanitizeNqlValue('')).toBe('');
475
- });
476
- });
477
449
  });
@@ -3,7 +3,6 @@ import {
3
3
  validateTierData,
4
4
  validateTierUpdateData,
5
5
  validateTierQueryOptions,
6
- sanitizeNqlValue,
7
6
  } from '../tierService.js';
8
7
  import { ValidationError } from '../../errors/index.js';
9
8
 
@@ -342,31 +341,4 @@ describe('tierService - Validation', () => {
342
341
  ).not.toThrow();
343
342
  });
344
343
  });
345
-
346
- describe('sanitizeNqlValue', () => {
347
- it('should return value if undefined or null', () => {
348
- expect(sanitizeNqlValue(null)).toBe(null);
349
- expect(sanitizeNqlValue(undefined)).toBe(undefined);
350
- });
351
-
352
- it('should escape backslashes', () => {
353
- expect(sanitizeNqlValue('test\\value')).toBe('test\\\\value');
354
- });
355
-
356
- it('should escape single quotes', () => {
357
- expect(sanitizeNqlValue("test'value")).toBe("test\\'value");
358
- });
359
-
360
- it('should escape double quotes', () => {
361
- expect(sanitizeNqlValue('test"value')).toBe('test\\"value');
362
- });
363
-
364
- it('should escape multiple special characters', () => {
365
- expect(sanitizeNqlValue('test\\value"with\'quotes')).toBe('test\\\\value\\"with\\\'quotes');
366
- });
367
-
368
- it('should handle strings without special characters', () => {
369
- expect(sanitizeNqlValue('simple-value')).toBe('simple-value');
370
- });
371
- });
372
344
  });