@jgardner04/ghost-mcp-server 1.12.0 → 1.12.1
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 +199 -43
- package/package.json +2 -1
- package/src/__tests__/mcp_server_improved.test.js +69 -7
- package/src/mcp_server_improved.js +31 -58
- package/src/schemas/__tests__/memberSchemas.test.js +447 -0
- package/src/schemas/__tests__/newsletterSchemas.test.js +399 -0
- package/src/schemas/__tests__/pageSchemas.test.js +518 -0
- package/src/schemas/__tests__/tierSchemas.test.js +574 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createMemberSchema,
|
|
4
|
+
updateMemberSchema,
|
|
5
|
+
memberQuerySchema,
|
|
6
|
+
memberIdSchema,
|
|
7
|
+
memberEmailSchema,
|
|
8
|
+
memberOutputSchema,
|
|
9
|
+
labelOutputSchema,
|
|
10
|
+
newsletterOutputSchema,
|
|
11
|
+
memberSubscriptionSchema,
|
|
12
|
+
} from '../memberSchemas.js';
|
|
13
|
+
|
|
14
|
+
describe('Member Schemas', () => {
|
|
15
|
+
describe('createMemberSchema', () => {
|
|
16
|
+
it('should accept valid member creation data', () => {
|
|
17
|
+
const validMember = {
|
|
18
|
+
email: 'member@example.com',
|
|
19
|
+
name: 'John Doe',
|
|
20
|
+
subscribed: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(() => createMemberSchema.parse(validMember)).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should accept minimal member creation data (email only)', () => {
|
|
27
|
+
const minimalMember = {
|
|
28
|
+
email: 'minimal@example.com',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = createMemberSchema.parse(minimalMember);
|
|
32
|
+
expect(result.email).toBe('minimal@example.com');
|
|
33
|
+
expect(result.subscribed).toBe(true); // default
|
|
34
|
+
expect(result.comped).toBe(false); // default
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should accept member with all fields', () => {
|
|
38
|
+
const fullMember = {
|
|
39
|
+
email: 'full@example.com',
|
|
40
|
+
name: 'Jane Smith',
|
|
41
|
+
note: 'VIP customer',
|
|
42
|
+
subscribed: true,
|
|
43
|
+
comped: true,
|
|
44
|
+
labels: ['vip', 'newsletter'],
|
|
45
|
+
newsletters: ['507f1f77bcf86cd799439011'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(() => createMemberSchema.parse(fullMember)).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should reject member without email', () => {
|
|
52
|
+
const invalidMember = {
|
|
53
|
+
name: 'No Email',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(() => createMemberSchema.parse(invalidMember)).toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should reject member with invalid email', () => {
|
|
60
|
+
const invalidMember = {
|
|
61
|
+
email: 'not-an-email',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(() => createMemberSchema.parse(invalidMember)).toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject member with too long name', () => {
|
|
68
|
+
const invalidMember = {
|
|
69
|
+
email: 'test@example.com',
|
|
70
|
+
name: 'A'.repeat(192),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
expect(() => createMemberSchema.parse(invalidMember)).toThrow();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should reject member with too long note', () => {
|
|
77
|
+
const invalidMember = {
|
|
78
|
+
email: 'test@example.com',
|
|
79
|
+
note: 'A'.repeat(2001),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
expect(() => createMemberSchema.parse(invalidMember)).toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should reject member with invalid newsletter ID', () => {
|
|
86
|
+
const invalidMember = {
|
|
87
|
+
email: 'test@example.com',
|
|
88
|
+
newsletters: ['invalid-id'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(() => createMemberSchema.parse(invalidMember)).toThrow();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('updateMemberSchema', () => {
|
|
96
|
+
it('should accept partial member updates', () => {
|
|
97
|
+
const update = {
|
|
98
|
+
name: 'Updated Name',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
expect(() => updateMemberSchema.parse(update)).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should accept empty update object', () => {
|
|
105
|
+
expect(() => updateMemberSchema.parse({})).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should accept full member update', () => {
|
|
109
|
+
const update = {
|
|
110
|
+
email: 'updated@example.com',
|
|
111
|
+
name: 'Updated Name',
|
|
112
|
+
note: 'Updated note',
|
|
113
|
+
subscribed: false,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(() => updateMemberSchema.parse(update)).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('memberQuerySchema', () => {
|
|
121
|
+
it('should accept valid query parameters', () => {
|
|
122
|
+
const query = {
|
|
123
|
+
limit: 20,
|
|
124
|
+
page: 2,
|
|
125
|
+
filter: 'status:paid+subscribed:true',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
expect(() => memberQuerySchema.parse(query)).not.toThrow();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should accept query with include parameter', () => {
|
|
132
|
+
const query = {
|
|
133
|
+
include: 'labels,newsletters',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
expect(() => memberQuerySchema.parse(query)).not.toThrow();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should accept query with search parameter', () => {
|
|
140
|
+
const query = {
|
|
141
|
+
search: 'john',
|
|
142
|
+
limit: 10,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
expect(() => memberQuerySchema.parse(query)).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should accept query with order parameter', () => {
|
|
149
|
+
const query = {
|
|
150
|
+
order: 'created_at DESC',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(() => memberQuerySchema.parse(query)).not.toThrow();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should reject query with invalid filter characters', () => {
|
|
157
|
+
const query = {
|
|
158
|
+
filter: 'status;DROP TABLE',
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
expect(() => memberQuerySchema.parse(query)).toThrow();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should accept empty query object', () => {
|
|
165
|
+
const result = memberQuerySchema.parse({});
|
|
166
|
+
expect(result).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('memberIdSchema', () => {
|
|
171
|
+
it('should accept valid Ghost ID', () => {
|
|
172
|
+
const validId = {
|
|
173
|
+
id: '507f1f77bcf86cd799439011',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
expect(() => memberIdSchema.parse(validId)).not.toThrow();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should reject invalid Ghost ID', () => {
|
|
180
|
+
const invalidId = {
|
|
181
|
+
id: 'invalid-id',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
expect(() => memberIdSchema.parse(invalidId)).toThrow();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('memberEmailSchema', () => {
|
|
189
|
+
it('should accept valid email', () => {
|
|
190
|
+
const validEmail = {
|
|
191
|
+
email: 'test@example.com',
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
expect(() => memberEmailSchema.parse(validEmail)).not.toThrow();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should reject invalid email', () => {
|
|
198
|
+
const invalidEmail = {
|
|
199
|
+
email: 'not-an-email',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
expect(() => memberEmailSchema.parse(invalidEmail)).toThrow();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('labelOutputSchema', () => {
|
|
207
|
+
it('should accept valid label output from Ghost API', () => {
|
|
208
|
+
const apiLabel = {
|
|
209
|
+
id: '507f1f77bcf86cd799439011',
|
|
210
|
+
name: 'VIP',
|
|
211
|
+
slug: 'vip',
|
|
212
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
213
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
expect(() => labelOutputSchema.parse(apiLabel)).not.toThrow();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should reject label output without required fields', () => {
|
|
220
|
+
const invalidLabel = {
|
|
221
|
+
name: 'VIP',
|
|
222
|
+
slug: 'vip',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(() => labelOutputSchema.parse(invalidLabel)).toThrow();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('newsletterOutputSchema', () => {
|
|
230
|
+
it('should accept valid newsletter output from Ghost API', () => {
|
|
231
|
+
const apiNewsletter = {
|
|
232
|
+
id: '507f1f77bcf86cd799439011',
|
|
233
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
234
|
+
name: 'Weekly Newsletter',
|
|
235
|
+
description: 'Our weekly updates',
|
|
236
|
+
slug: 'weekly-newsletter',
|
|
237
|
+
sender_name: 'Blog Team',
|
|
238
|
+
sender_email: 'team@example.com',
|
|
239
|
+
sender_reply_to: 'newsletter',
|
|
240
|
+
status: 'active',
|
|
241
|
+
visibility: 'members',
|
|
242
|
+
subscribe_on_signup: true,
|
|
243
|
+
sort_order: 0,
|
|
244
|
+
header_image: null,
|
|
245
|
+
show_header_icon: true,
|
|
246
|
+
show_header_title: true,
|
|
247
|
+
title_font_category: 'sans-serif',
|
|
248
|
+
title_alignment: 'center',
|
|
249
|
+
show_feature_image: true,
|
|
250
|
+
body_font_category: 'sans-serif',
|
|
251
|
+
footer_content: null,
|
|
252
|
+
show_badge: true,
|
|
253
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
254
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
expect(() => newsletterOutputSchema.parse(apiNewsletter)).not.toThrow();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('memberSubscriptionSchema', () => {
|
|
262
|
+
it('should accept valid subscription object', () => {
|
|
263
|
+
const subscription = {
|
|
264
|
+
id: 'sub_123',
|
|
265
|
+
customer: {
|
|
266
|
+
id: 'cus_123',
|
|
267
|
+
name: 'John Doe',
|
|
268
|
+
email: 'john@example.com',
|
|
269
|
+
},
|
|
270
|
+
plan: {
|
|
271
|
+
id: 'plan_123',
|
|
272
|
+
nickname: 'Monthly Premium',
|
|
273
|
+
amount: 999,
|
|
274
|
+
interval: 'month',
|
|
275
|
+
currency: 'USD',
|
|
276
|
+
},
|
|
277
|
+
status: 'active',
|
|
278
|
+
start_date: '2024-01-15T10:30:00.000Z',
|
|
279
|
+
current_period_end: '2024-02-15T10:30:00.000Z',
|
|
280
|
+
cancel_at_period_end: false,
|
|
281
|
+
cancellation_reason: null,
|
|
282
|
+
trial_start_date: null,
|
|
283
|
+
trial_end_date: null,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
expect(() => memberSubscriptionSchema.parse(subscription)).not.toThrow();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should accept subscription with trial dates', () => {
|
|
290
|
+
const subscription = {
|
|
291
|
+
id: 'sub_123',
|
|
292
|
+
customer: {
|
|
293
|
+
id: 'cus_123',
|
|
294
|
+
name: null,
|
|
295
|
+
email: 'test@example.com',
|
|
296
|
+
},
|
|
297
|
+
plan: {
|
|
298
|
+
id: 'plan_123',
|
|
299
|
+
nickname: 'Yearly Premium',
|
|
300
|
+
amount: 9999,
|
|
301
|
+
interval: 'year',
|
|
302
|
+
currency: 'EUR',
|
|
303
|
+
},
|
|
304
|
+
status: 'trialing',
|
|
305
|
+
start_date: '2024-01-15T10:30:00.000Z',
|
|
306
|
+
current_period_end: '2024-02-15T10:30:00.000Z',
|
|
307
|
+
cancel_at_period_end: false,
|
|
308
|
+
cancellation_reason: null,
|
|
309
|
+
trial_start_date: '2024-01-15T10:30:00.000Z',
|
|
310
|
+
trial_end_date: '2024-01-22T10:30:00.000Z',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
expect(() => memberSubscriptionSchema.parse(subscription)).not.toThrow();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should reject subscription with invalid status', () => {
|
|
317
|
+
const subscription = {
|
|
318
|
+
id: 'sub_123',
|
|
319
|
+
customer: {
|
|
320
|
+
id: 'cus_123',
|
|
321
|
+
email: 'test@example.com',
|
|
322
|
+
},
|
|
323
|
+
plan: {
|
|
324
|
+
id: 'plan_123',
|
|
325
|
+
nickname: 'Monthly',
|
|
326
|
+
amount: 999,
|
|
327
|
+
interval: 'month',
|
|
328
|
+
currency: 'USD',
|
|
329
|
+
},
|
|
330
|
+
status: 'invalid_status',
|
|
331
|
+
start_date: '2024-01-15T10:30:00.000Z',
|
|
332
|
+
current_period_end: '2024-02-15T10:30:00.000Z',
|
|
333
|
+
cancel_at_period_end: false,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
expect(() => memberSubscriptionSchema.parse(subscription)).toThrow();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('memberOutputSchema', () => {
|
|
341
|
+
it('should accept valid member output from Ghost API', () => {
|
|
342
|
+
const apiMember = {
|
|
343
|
+
id: '507f1f77bcf86cd799439011',
|
|
344
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
345
|
+
email: 'member@example.com',
|
|
346
|
+
name: 'John Doe',
|
|
347
|
+
note: 'VIP customer',
|
|
348
|
+
geolocation: 'US',
|
|
349
|
+
enable_comment_notifications: true,
|
|
350
|
+
email_count: 10,
|
|
351
|
+
email_opened_count: 8,
|
|
352
|
+
email_open_rate: 0.8,
|
|
353
|
+
status: 'paid',
|
|
354
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
355
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
356
|
+
subscribed: true,
|
|
357
|
+
comped: false,
|
|
358
|
+
email_suppression: null,
|
|
359
|
+
labels: [],
|
|
360
|
+
subscriptions: [],
|
|
361
|
+
newsletters: [],
|
|
362
|
+
avatar_image: 'https://example.com/avatar.jpg',
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
expect(() => memberOutputSchema.parse(apiMember)).not.toThrow();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should accept member with null optional fields', () => {
|
|
369
|
+
const apiMember = {
|
|
370
|
+
id: '507f1f77bcf86cd799439011',
|
|
371
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
372
|
+
email: 'test@example.com',
|
|
373
|
+
name: null,
|
|
374
|
+
note: null,
|
|
375
|
+
geolocation: null,
|
|
376
|
+
enable_comment_notifications: false,
|
|
377
|
+
email_count: 0,
|
|
378
|
+
email_opened_count: 0,
|
|
379
|
+
email_open_rate: null,
|
|
380
|
+
status: 'free',
|
|
381
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
382
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
383
|
+
subscribed: false,
|
|
384
|
+
comped: false,
|
|
385
|
+
email_suppression: null,
|
|
386
|
+
avatar_image: null,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
expect(() => memberOutputSchema.parse(apiMember)).not.toThrow();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should accept member with email suppression', () => {
|
|
393
|
+
const apiMember = {
|
|
394
|
+
id: '507f1f77bcf86cd799439011',
|
|
395
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
396
|
+
email: 'suppressed@example.com',
|
|
397
|
+
name: 'Suppressed User',
|
|
398
|
+
note: null,
|
|
399
|
+
geolocation: null,
|
|
400
|
+
enable_comment_notifications: false,
|
|
401
|
+
email_count: 5,
|
|
402
|
+
email_opened_count: 0,
|
|
403
|
+
email_open_rate: 0,
|
|
404
|
+
status: 'free',
|
|
405
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
406
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
407
|
+
subscribed: false,
|
|
408
|
+
comped: false,
|
|
409
|
+
email_suppression: {
|
|
410
|
+
suppressed: true,
|
|
411
|
+
info: 'User unsubscribed',
|
|
412
|
+
},
|
|
413
|
+
avatar_image: null,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
expect(() => memberOutputSchema.parse(apiMember)).not.toThrow();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should reject member output with invalid status', () => {
|
|
420
|
+
const invalidMember = {
|
|
421
|
+
id: '507f1f77bcf86cd799439011',
|
|
422
|
+
uuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
423
|
+
email: 'test@example.com',
|
|
424
|
+
name: 'Test',
|
|
425
|
+
enable_comment_notifications: true,
|
|
426
|
+
email_count: 0,
|
|
427
|
+
email_opened_count: 0,
|
|
428
|
+
status: 'invalid_status',
|
|
429
|
+
created_at: '2024-01-15T10:30:00.000Z',
|
|
430
|
+
updated_at: '2024-01-15T10:30:00.000Z',
|
|
431
|
+
subscribed: false,
|
|
432
|
+
comped: false,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
expect(() => memberOutputSchema.parse(invalidMember)).toThrow();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should reject member output without required fields', () => {
|
|
439
|
+
const invalidMember = {
|
|
440
|
+
email: 'test@example.com',
|
|
441
|
+
name: 'Test',
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
expect(() => memberOutputSchema.parse(invalidMember)).toThrow();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
});
|