@jgardner04/ghost-mcp-server 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp_server_improved.js +236 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +548 -0
- package/src/services/__tests__/memberService.test.js +477 -0
- package/src/services/ghostServiceImproved.js +215 -0
- package/src/services/memberService.js +392 -0
|
@@ -0,0 +1,548 @@
|
|
|
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
|
+
|
|
5
|
+
// Mock the Ghost Admin API with members 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
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Mock dotenv
|
|
48
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
49
|
+
|
|
50
|
+
// Mock logger
|
|
51
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
52
|
+
createContextLogger: createMockContextLogger(),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Mock fs for validateImagePath
|
|
56
|
+
vi.mock('fs/promises', () => ({
|
|
57
|
+
default: {
|
|
58
|
+
access: vi.fn(),
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Import after setting up mocks
|
|
63
|
+
import {
|
|
64
|
+
createMember,
|
|
65
|
+
updateMember,
|
|
66
|
+
deleteMember,
|
|
67
|
+
getMembers,
|
|
68
|
+
getMember,
|
|
69
|
+
searchMembers,
|
|
70
|
+
api,
|
|
71
|
+
} from '../ghostServiceImproved.js';
|
|
72
|
+
|
|
73
|
+
describe('ghostServiceImproved - Members', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Reset all mocks before each test
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('createMember', () => {
|
|
80
|
+
it('should create a member with required email', async () => {
|
|
81
|
+
const memberData = {
|
|
82
|
+
email: 'test@example.com',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const mockCreatedMember = {
|
|
86
|
+
id: 'member-1',
|
|
87
|
+
email: 'test@example.com',
|
|
88
|
+
status: 'free',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
api.members.add.mockResolvedValue(mockCreatedMember);
|
|
92
|
+
|
|
93
|
+
const result = await createMember(memberData);
|
|
94
|
+
|
|
95
|
+
expect(api.members.add).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
email: 'test@example.com',
|
|
98
|
+
}),
|
|
99
|
+
expect.any(Object)
|
|
100
|
+
);
|
|
101
|
+
expect(result).toEqual(mockCreatedMember);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should create a member with optional fields', async () => {
|
|
105
|
+
const memberData = {
|
|
106
|
+
email: 'test@example.com',
|
|
107
|
+
name: 'John Doe',
|
|
108
|
+
note: 'Test member',
|
|
109
|
+
labels: ['premium', 'newsletter'],
|
|
110
|
+
newsletters: [{ id: 'newsletter-1' }],
|
|
111
|
+
subscribed: true,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const mockCreatedMember = {
|
|
115
|
+
id: 'member-1',
|
|
116
|
+
...memberData,
|
|
117
|
+
status: 'free',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
api.members.add.mockResolvedValue(mockCreatedMember);
|
|
121
|
+
|
|
122
|
+
const result = await createMember(memberData);
|
|
123
|
+
|
|
124
|
+
expect(api.members.add).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining(memberData),
|
|
126
|
+
expect.any(Object)
|
|
127
|
+
);
|
|
128
|
+
expect(result).toEqual(mockCreatedMember);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should throw validation error for missing email', async () => {
|
|
132
|
+
await expect(createMember({})).rejects.toThrow('Member validation failed');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw validation error for invalid email', async () => {
|
|
136
|
+
await expect(createMember({ email: 'invalid-email' })).rejects.toThrow(
|
|
137
|
+
'Member validation failed'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw validation error for invalid labels type', async () => {
|
|
142
|
+
await expect(
|
|
143
|
+
createMember({
|
|
144
|
+
email: 'test@example.com',
|
|
145
|
+
labels: 'premium',
|
|
146
|
+
})
|
|
147
|
+
).rejects.toThrow('Member validation failed');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle Ghost API errors', async () => {
|
|
151
|
+
const memberData = {
|
|
152
|
+
email: 'test@example.com',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
api.members.add.mockRejectedValue(new Error('Ghost API Error'));
|
|
156
|
+
|
|
157
|
+
await expect(createMember(memberData)).rejects.toThrow();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('updateMember', () => {
|
|
162
|
+
it('should update a member with valid ID and data', async () => {
|
|
163
|
+
const memberId = 'member-1';
|
|
164
|
+
const updateData = {
|
|
165
|
+
name: 'Jane Doe',
|
|
166
|
+
note: 'Updated note',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const mockExistingMember = {
|
|
170
|
+
id: memberId,
|
|
171
|
+
email: 'test@example.com',
|
|
172
|
+
name: 'John Doe',
|
|
173
|
+
updated_at: '2023-01-01T00:00:00.000Z',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const mockUpdatedMember = {
|
|
177
|
+
...mockExistingMember,
|
|
178
|
+
...updateData,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
api.members.read.mockResolvedValue(mockExistingMember);
|
|
182
|
+
api.members.edit.mockResolvedValue(mockUpdatedMember);
|
|
183
|
+
|
|
184
|
+
const result = await updateMember(memberId, updateData);
|
|
185
|
+
|
|
186
|
+
expect(api.members.read).toHaveBeenCalledWith(expect.any(Object), { id: memberId });
|
|
187
|
+
expect(api.members.edit).toHaveBeenCalledWith(
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
...mockExistingMember,
|
|
190
|
+
...updateData,
|
|
191
|
+
}),
|
|
192
|
+
expect.objectContaining({ id: memberId })
|
|
193
|
+
);
|
|
194
|
+
expect(result).toEqual(mockUpdatedMember);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should update member email if provided', async () => {
|
|
198
|
+
const memberId = 'member-1';
|
|
199
|
+
const updateData = {
|
|
200
|
+
email: 'newemail@example.com',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const mockExistingMember = {
|
|
204
|
+
id: memberId,
|
|
205
|
+
email: 'test@example.com',
|
|
206
|
+
updated_at: '2023-01-01T00:00:00.000Z',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const mockUpdatedMember = {
|
|
210
|
+
...mockExistingMember,
|
|
211
|
+
email: 'newemail@example.com',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
api.members.read.mockResolvedValue(mockExistingMember);
|
|
215
|
+
api.members.edit.mockResolvedValue(mockUpdatedMember);
|
|
216
|
+
|
|
217
|
+
const result = await updateMember(memberId, updateData);
|
|
218
|
+
|
|
219
|
+
expect(result.email).toBe('newemail@example.com');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw validation error for missing member ID', async () => {
|
|
223
|
+
await expect(updateMember(null, { name: 'Test' })).rejects.toThrow(
|
|
224
|
+
'Member ID is required for update'
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should throw validation error for invalid email in update', async () => {
|
|
229
|
+
await expect(updateMember('member-1', { email: 'invalid-email' })).rejects.toThrow(
|
|
230
|
+
'Member validation failed'
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw not found error if member does not exist', async () => {
|
|
235
|
+
api.members.read.mockRejectedValue({
|
|
236
|
+
response: { status: 404 },
|
|
237
|
+
message: 'Member not found',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await expect(updateMember('non-existent', { name: 'Test' })).rejects.toThrow();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('deleteMember', () => {
|
|
245
|
+
it('should delete a member with valid ID', async () => {
|
|
246
|
+
const memberId = 'member-1';
|
|
247
|
+
|
|
248
|
+
api.members.delete.mockResolvedValue({ deleted: true });
|
|
249
|
+
|
|
250
|
+
const result = await deleteMember(memberId);
|
|
251
|
+
|
|
252
|
+
expect(api.members.delete).toHaveBeenCalledWith(memberId, expect.any(Object));
|
|
253
|
+
expect(result).toEqual({ deleted: true });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should throw validation error for missing member ID', async () => {
|
|
257
|
+
await expect(deleteMember(null)).rejects.toThrow('Member ID is required for deletion');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should throw not found error if member does not exist', async () => {
|
|
261
|
+
api.members.delete.mockRejectedValue({
|
|
262
|
+
response: { status: 404 },
|
|
263
|
+
message: 'Member not found',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await expect(deleteMember('non-existent')).rejects.toThrow();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('getMembers', () => {
|
|
271
|
+
it('should return all members with default options', async () => {
|
|
272
|
+
const mockMembers = [
|
|
273
|
+
{ id: 'member-1', email: 'test1@example.com', status: 'free' },
|
|
274
|
+
{ id: 'member-2', email: 'test2@example.com', status: 'paid' },
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
278
|
+
|
|
279
|
+
const result = await getMembers();
|
|
280
|
+
|
|
281
|
+
expect(api.members.browse).toHaveBeenCalled();
|
|
282
|
+
expect(result).toEqual(mockMembers);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should accept pagination options', async () => {
|
|
286
|
+
const mockMembers = [{ id: 'member-1', email: 'test1@example.com' }];
|
|
287
|
+
|
|
288
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
289
|
+
|
|
290
|
+
await getMembers({ limit: 50, page: 2 });
|
|
291
|
+
|
|
292
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
293
|
+
expect.objectContaining({
|
|
294
|
+
limit: 50,
|
|
295
|
+
page: 2,
|
|
296
|
+
}),
|
|
297
|
+
expect.any(Object)
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should accept filter options', async () => {
|
|
302
|
+
const mockMembers = [{ id: 'member-1', email: 'test1@example.com', status: 'paid' }];
|
|
303
|
+
|
|
304
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
305
|
+
|
|
306
|
+
await getMembers({ filter: 'status:paid' });
|
|
307
|
+
|
|
308
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
309
|
+
expect.objectContaining({
|
|
310
|
+
filter: 'status:paid',
|
|
311
|
+
}),
|
|
312
|
+
expect.any(Object)
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should accept order options', async () => {
|
|
317
|
+
const mockMembers = [{ id: 'member-1', email: 'test1@example.com' }];
|
|
318
|
+
|
|
319
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
320
|
+
|
|
321
|
+
await getMembers({ order: 'created_at desc' });
|
|
322
|
+
|
|
323
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
324
|
+
expect.objectContaining({
|
|
325
|
+
order: 'created_at desc',
|
|
326
|
+
}),
|
|
327
|
+
expect.any(Object)
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should accept include options', async () => {
|
|
332
|
+
const mockMembers = [
|
|
333
|
+
{ id: 'member-1', email: 'test1@example.com', labels: [], newsletters: [] },
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
337
|
+
|
|
338
|
+
await getMembers({ include: 'labels,newsletters' });
|
|
339
|
+
|
|
340
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
341
|
+
expect.objectContaining({
|
|
342
|
+
include: 'labels,newsletters',
|
|
343
|
+
}),
|
|
344
|
+
expect.any(Object)
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should throw validation error for invalid limit', async () => {
|
|
349
|
+
await expect(getMembers({ limit: 0 })).rejects.toThrow('Member query validation failed');
|
|
350
|
+
await expect(getMembers({ limit: 101 })).rejects.toThrow('Member query validation failed');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should throw validation error for invalid page', async () => {
|
|
354
|
+
await expect(getMembers({ page: 0 })).rejects.toThrow('Member query validation failed');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should return empty array when no members found', async () => {
|
|
358
|
+
api.members.browse.mockResolvedValue([]);
|
|
359
|
+
|
|
360
|
+
const result = await getMembers();
|
|
361
|
+
|
|
362
|
+
expect(result).toEqual([]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should handle Ghost API errors', async () => {
|
|
366
|
+
api.members.browse.mockRejectedValue(new Error('Ghost API Error'));
|
|
367
|
+
|
|
368
|
+
await expect(getMembers()).rejects.toThrow();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('getMember', () => {
|
|
373
|
+
it('should get member by ID', async () => {
|
|
374
|
+
const mockMember = {
|
|
375
|
+
id: 'member-1',
|
|
376
|
+
email: 'test@example.com',
|
|
377
|
+
name: 'John Doe',
|
|
378
|
+
status: 'free',
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
api.members.read.mockResolvedValue(mockMember);
|
|
382
|
+
|
|
383
|
+
const result = await getMember({ id: 'member-1' });
|
|
384
|
+
|
|
385
|
+
expect(api.members.read).toHaveBeenCalledWith(expect.any(Object), { id: 'member-1' });
|
|
386
|
+
expect(result).toEqual(mockMember);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should get member by email', async () => {
|
|
390
|
+
const mockMember = {
|
|
391
|
+
id: 'member-1',
|
|
392
|
+
email: 'test@example.com',
|
|
393
|
+
name: 'John Doe',
|
|
394
|
+
status: 'free',
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
api.members.browse.mockResolvedValue([mockMember]);
|
|
398
|
+
|
|
399
|
+
const result = await getMember({ email: 'test@example.com' });
|
|
400
|
+
|
|
401
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
402
|
+
expect.objectContaining({
|
|
403
|
+
filter: expect.stringContaining('test@example.com'),
|
|
404
|
+
}),
|
|
405
|
+
expect.any(Object)
|
|
406
|
+
);
|
|
407
|
+
expect(result).toEqual(mockMember);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should throw validation error when neither id nor email provided', async () => {
|
|
411
|
+
await expect(getMember({})).rejects.toThrow('Member lookup validation failed');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should throw validation error for invalid email format', async () => {
|
|
415
|
+
await expect(getMember({ email: 'invalid-email' })).rejects.toThrow(
|
|
416
|
+
'Member lookup validation failed'
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should throw not found error when member not found by ID', async () => {
|
|
421
|
+
api.members.read.mockRejectedValue({
|
|
422
|
+
response: { status: 404 },
|
|
423
|
+
message: 'Member not found',
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await expect(getMember({ id: 'non-existent' })).rejects.toThrow();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should throw not found error when member not found by email', async () => {
|
|
430
|
+
api.members.browse.mockResolvedValue([]);
|
|
431
|
+
|
|
432
|
+
await expect(getMember({ email: 'notfound@example.com' })).rejects.toThrow(
|
|
433
|
+
'Member not found'
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should prioritize ID over email when both provided', async () => {
|
|
438
|
+
const mockMember = {
|
|
439
|
+
id: 'member-1',
|
|
440
|
+
email: 'test@example.com',
|
|
441
|
+
status: 'free',
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
api.members.read.mockResolvedValue(mockMember);
|
|
445
|
+
|
|
446
|
+
await getMember({ id: 'member-1', email: 'test@example.com' });
|
|
447
|
+
|
|
448
|
+
// Should use read (ID) instead of browse (email)
|
|
449
|
+
expect(api.members.read).toHaveBeenCalled();
|
|
450
|
+
expect(api.members.browse).not.toHaveBeenCalled();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('searchMembers', () => {
|
|
455
|
+
it('should search members by query', async () => {
|
|
456
|
+
const mockMembers = [{ id: 'member-1', email: 'john@example.com', name: 'John Doe' }];
|
|
457
|
+
|
|
458
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
459
|
+
|
|
460
|
+
const result = await searchMembers('john');
|
|
461
|
+
|
|
462
|
+
expect(api.members.browse).toHaveBeenCalled();
|
|
463
|
+
expect(result).toEqual(mockMembers);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should apply default limit of 15', async () => {
|
|
467
|
+
const mockMembers = [];
|
|
468
|
+
|
|
469
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
470
|
+
|
|
471
|
+
await searchMembers('test');
|
|
472
|
+
|
|
473
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
474
|
+
expect.objectContaining({
|
|
475
|
+
limit: 15,
|
|
476
|
+
}),
|
|
477
|
+
expect.any(Object)
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should accept custom limit', async () => {
|
|
482
|
+
const mockMembers = [];
|
|
483
|
+
|
|
484
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
485
|
+
|
|
486
|
+
await searchMembers('test', { limit: 25 });
|
|
487
|
+
|
|
488
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
489
|
+
expect.objectContaining({
|
|
490
|
+
limit: 25,
|
|
491
|
+
}),
|
|
492
|
+
expect.any(Object)
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should throw validation error for empty query', async () => {
|
|
497
|
+
await expect(searchMembers('')).rejects.toThrow('Search query validation failed');
|
|
498
|
+
await expect(searchMembers(' ')).rejects.toThrow('Search query validation failed');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should throw validation error for non-string query', async () => {
|
|
502
|
+
await expect(searchMembers(123)).rejects.toThrow('Search query validation failed');
|
|
503
|
+
await expect(searchMembers(null)).rejects.toThrow('Search query validation failed');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should throw validation error for invalid limit', async () => {
|
|
507
|
+
await expect(searchMembers('test', { limit: 0 })).rejects.toThrow(
|
|
508
|
+
'Search options validation failed'
|
|
509
|
+
);
|
|
510
|
+
await expect(searchMembers('test', { limit: 51 })).rejects.toThrow(
|
|
511
|
+
'Search options validation failed'
|
|
512
|
+
);
|
|
513
|
+
await expect(searchMembers('test', { limit: 100 })).rejects.toThrow(
|
|
514
|
+
'Search options validation failed'
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should sanitize query to prevent NQL injection', async () => {
|
|
519
|
+
const mockMembers = [];
|
|
520
|
+
|
|
521
|
+
api.members.browse.mockResolvedValue(mockMembers);
|
|
522
|
+
|
|
523
|
+
// Query with special NQL characters
|
|
524
|
+
await searchMembers("test'value");
|
|
525
|
+
|
|
526
|
+
expect(api.members.browse).toHaveBeenCalledWith(
|
|
527
|
+
expect.objectContaining({
|
|
528
|
+
filter: expect.stringContaining("\\'"),
|
|
529
|
+
}),
|
|
530
|
+
expect.any(Object)
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should return empty array when no matches found', async () => {
|
|
535
|
+
api.members.browse.mockResolvedValue([]);
|
|
536
|
+
|
|
537
|
+
const result = await searchMembers('nonexistent');
|
|
538
|
+
|
|
539
|
+
expect(result).toEqual([]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle Ghost API errors', async () => {
|
|
543
|
+
api.members.browse.mockRejectedValue(new Error('Ghost API Error'));
|
|
544
|
+
|
|
545
|
+
await expect(searchMembers('test')).rejects.toThrow();
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
});
|