@jgardner04/ghost-mcp-server 1.13.4 → 1.13.5
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 +68 -0
- package/package.json +7 -3
- package/src/__tests__/helpers/testUtils.js +15 -1
- package/src/__tests__/mcp_server.test.js +69 -1
- package/src/__tests__/mcp_server_pages.test.js +23 -6
- package/src/mcp_server.js +393 -1143
- package/src/services/__tests__/createResourceService.test.js +468 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +2 -2
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.posts.test.js +0 -1
- package/src/services/__tests__/ghostServiceImproved.tags.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.tiers.test.js +3 -5
- package/src/services/createResourceService.js +138 -0
- package/src/services/ghostApiClient.js +240 -0
- package/src/services/ghostServiceImproved.js +76 -915
- package/src/services/images.js +27 -0
- package/src/services/members.js +127 -0
- package/src/services/newsletters.js +63 -0
- package/src/services/pages.js +116 -0
- package/src/services/posts.js +116 -0
- package/src/services/tags.js +118 -0
- package/src/services/tiers.js +72 -0
- package/src/services/validators.js +218 -0
|
@@ -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
|
+
});
|
|
@@ -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
|
|
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
|
|
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' });
|
|
@@ -252,7 +252,6 @@ describe('ghostServiceImproved - Tags', () => {
|
|
|
252
252
|
|
|
253
253
|
it('should throw not found error when tag does not exist', async () => {
|
|
254
254
|
const error404 = new GhostAPIError('tags.read', 'Tag not found', 404);
|
|
255
|
-
error404.response = { status: 404 };
|
|
256
255
|
api.tags.read.mockRejectedValue(error404);
|
|
257
256
|
|
|
258
257
|
const rejection = getTag('non-existent');
|
|
@@ -411,7 +410,6 @@ describe('ghostServiceImproved - Tags', () => {
|
|
|
411
410
|
|
|
412
411
|
it('should throw not found error if tag does not exist', async () => {
|
|
413
412
|
const error404 = new GhostAPIError('tags.read', 'Tag not found', 404);
|
|
414
|
-
error404.response = { status: 404 };
|
|
415
413
|
api.tags.read.mockRejectedValue(error404);
|
|
416
414
|
|
|
417
415
|
const rejection = updateTag('non-existent', { name: 'Test' });
|
|
@@ -438,7 +436,6 @@ describe('ghostServiceImproved - Tags', () => {
|
|
|
438
436
|
|
|
439
437
|
it('should throw not found error if tag does not exist', async () => {
|
|
440
438
|
const error404 = new GhostAPIError('tags.delete', 'Tag not found', 404);
|
|
441
|
-
error404.response = { status: 404 };
|
|
442
439
|
api.tags.delete.mockRejectedValue(error404);
|
|
443
440
|
|
|
444
441
|
const rejection = deleteTag('non-existent');
|
|
@@ -295,9 +295,7 @@ describe('ghostServiceImproved - Tiers', () => {
|
|
|
295
295
|
});
|
|
296
296
|
|
|
297
297
|
it('should throw ValidationError when ID is missing', async () => {
|
|
298
|
-
await expect(updateTier('', { name: 'Updated' })).rejects.toThrow(
|
|
299
|
-
'Tier ID is required for update'
|
|
300
|
-
);
|
|
298
|
+
await expect(updateTier('', { name: 'Updated' })).rejects.toThrow('Tier ID is required');
|
|
301
299
|
});
|
|
302
300
|
|
|
303
301
|
it('should throw ValidationError for invalid update data', async () => {
|
|
@@ -327,11 +325,11 @@ describe('ghostServiceImproved - Tiers', () => {
|
|
|
327
325
|
});
|
|
328
326
|
|
|
329
327
|
it('should throw ValidationError when ID is missing', async () => {
|
|
330
|
-
await expect(deleteTier()).rejects.toThrow('Tier ID is required
|
|
328
|
+
await expect(deleteTier()).rejects.toThrow('Tier ID is required');
|
|
331
329
|
});
|
|
332
330
|
|
|
333
331
|
it('should throw ValidationError when ID is empty string', async () => {
|
|
334
|
-
await expect(deleteTier('')).rejects.toThrow('Tier ID is required
|
|
332
|
+
await expect(deleteTier('')).rejects.toThrow('Tier ID is required');
|
|
335
333
|
});
|
|
336
334
|
|
|
337
335
|
it('should throw NotFoundError when tier does not exist', async () => {
|