@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,477 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
validateMemberData,
|
|
4
|
+
validateMemberUpdateData,
|
|
5
|
+
validateMemberQueryOptions,
|
|
6
|
+
validateMemberLookup,
|
|
7
|
+
validateSearchQuery,
|
|
8
|
+
validateSearchOptions,
|
|
9
|
+
sanitizeNqlValue,
|
|
10
|
+
} from '../memberService.js';
|
|
11
|
+
|
|
12
|
+
describe('memberService - Validation', () => {
|
|
13
|
+
describe('validateMemberData', () => {
|
|
14
|
+
it('should validate required email field', () => {
|
|
15
|
+
expect(() => validateMemberData({})).toThrow('Member validation failed');
|
|
16
|
+
expect(() => validateMemberData({ email: '' })).toThrow('Member validation failed');
|
|
17
|
+
expect(() => validateMemberData({ email: ' ' })).toThrow('Member validation failed');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should validate email format', () => {
|
|
21
|
+
expect(() => validateMemberData({ email: 'invalid-email' })).toThrow(
|
|
22
|
+
'Member validation failed'
|
|
23
|
+
);
|
|
24
|
+
expect(() => validateMemberData({ email: 'test@' })).toThrow('Member validation failed');
|
|
25
|
+
expect(() => validateMemberData({ email: '@test.com' })).toThrow('Member validation failed');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should accept valid email', () => {
|
|
29
|
+
expect(() => validateMemberData({ email: 'test@example.com' })).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should accept optional name field', () => {
|
|
33
|
+
expect(() =>
|
|
34
|
+
validateMemberData({ email: 'test@example.com', name: 'John Doe' })
|
|
35
|
+
).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should accept optional note field', () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
validateMemberData({ email: 'test@example.com', note: 'Test note' })
|
|
41
|
+
).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should accept optional labels array', () => {
|
|
45
|
+
expect(() =>
|
|
46
|
+
validateMemberData({ email: 'test@example.com', labels: ['premium', 'newsletter'] })
|
|
47
|
+
).not.toThrow();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should validate labels is an array', () => {
|
|
51
|
+
expect(() => validateMemberData({ email: 'test@example.com', labels: 'premium' })).toThrow(
|
|
52
|
+
'Member validation failed'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should accept optional newsletters array', () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
validateMemberData({
|
|
59
|
+
email: 'test@example.com',
|
|
60
|
+
newsletters: [{ id: 'newsletter-1' }],
|
|
61
|
+
})
|
|
62
|
+
).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should validate newsletter objects have id field', () => {
|
|
66
|
+
expect(() =>
|
|
67
|
+
validateMemberData({
|
|
68
|
+
email: 'test@example.com',
|
|
69
|
+
newsletters: [{ name: 'Newsletter' }],
|
|
70
|
+
})
|
|
71
|
+
).toThrow('Member validation failed');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should accept optional subscribed boolean', () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
validateMemberData({ email: 'test@example.com', subscribed: true })
|
|
77
|
+
).not.toThrow();
|
|
78
|
+
expect(() =>
|
|
79
|
+
validateMemberData({ email: 'test@example.com', subscribed: false })
|
|
80
|
+
).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should validate subscribed is a boolean', () => {
|
|
84
|
+
expect(() => validateMemberData({ email: 'test@example.com', subscribed: 'yes' })).toThrow(
|
|
85
|
+
'Member validation failed'
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should validate name length', () => {
|
|
90
|
+
const longName = 'a'.repeat(192); // Exceeds MAX_NAME_LENGTH (191)
|
|
91
|
+
expect(() => validateMemberData({ email: 'test@example.com', name: longName })).toThrow(
|
|
92
|
+
'Member validation failed'
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should accept name at max length', () => {
|
|
97
|
+
const maxName = 'a'.repeat(191); // At MAX_NAME_LENGTH
|
|
98
|
+
expect(() => validateMemberData({ email: 'test@example.com', name: maxName })).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should validate note length', () => {
|
|
102
|
+
const longNote = 'a'.repeat(2001); // Exceeds MAX_NOTE_LENGTH (2000)
|
|
103
|
+
expect(() => validateMemberData({ email: 'test@example.com', note: longNote })).toThrow(
|
|
104
|
+
'Member validation failed'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should accept note at max length', () => {
|
|
109
|
+
const maxNote = 'a'.repeat(2000); // At MAX_NOTE_LENGTH
|
|
110
|
+
expect(() => validateMemberData({ email: 'test@example.com', note: maxNote })).not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should sanitize HTML in note field', () => {
|
|
114
|
+
const memberData = {
|
|
115
|
+
email: 'test@example.com',
|
|
116
|
+
note: '<script>alert("xss")</script>Test note',
|
|
117
|
+
};
|
|
118
|
+
validateMemberData(memberData);
|
|
119
|
+
expect(memberData.note).toBe('Test note'); // HTML should be stripped
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should validate label length', () => {
|
|
123
|
+
const longLabel = 'a'.repeat(192); // Exceeds MAX_LABEL_LENGTH (191)
|
|
124
|
+
expect(() => validateMemberData({ email: 'test@example.com', labels: [longLabel] })).toThrow(
|
|
125
|
+
'Member validation failed'
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should reject empty string labels', () => {
|
|
130
|
+
expect(() => validateMemberData({ email: 'test@example.com', labels: [''] })).toThrow(
|
|
131
|
+
'Member validation failed'
|
|
132
|
+
);
|
|
133
|
+
expect(() => validateMemberData({ email: 'test@example.com', labels: [' '] })).toThrow(
|
|
134
|
+
'Member validation failed'
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should reject non-string labels', () => {
|
|
139
|
+
expect(() => validateMemberData({ email: 'test@example.com', labels: [123] })).toThrow(
|
|
140
|
+
'Member validation failed'
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should reject empty newsletter IDs', () => {
|
|
145
|
+
expect(() =>
|
|
146
|
+
validateMemberData({ email: 'test@example.com', newsletters: [{ id: '' }] })
|
|
147
|
+
).toThrow('Member validation failed');
|
|
148
|
+
expect(() =>
|
|
149
|
+
validateMemberData({ email: 'test@example.com', newsletters: [{ id: ' ' }] })
|
|
150
|
+
).toThrow('Member validation failed');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('validateMemberUpdateData', () => {
|
|
155
|
+
it('should validate email format if provided', () => {
|
|
156
|
+
expect(() => validateMemberUpdateData({ email: 'invalid-email' })).toThrow(
|
|
157
|
+
'Member validation failed'
|
|
158
|
+
);
|
|
159
|
+
expect(() => validateMemberUpdateData({ email: 'test@example.com' })).not.toThrow();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should accept update with only name', () => {
|
|
163
|
+
expect(() => validateMemberUpdateData({ name: 'John Doe' })).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should accept update with only note', () => {
|
|
167
|
+
expect(() => validateMemberUpdateData({ note: 'Updated note' })).not.toThrow();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should accept update with only labels', () => {
|
|
171
|
+
expect(() => validateMemberUpdateData({ labels: ['premium'] })).not.toThrow();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should validate labels is an array if provided', () => {
|
|
175
|
+
expect(() => validateMemberUpdateData({ labels: 'premium' })).toThrow(
|
|
176
|
+
'Member validation failed'
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should accept update with only newsletters', () => {
|
|
181
|
+
expect(() =>
|
|
182
|
+
validateMemberUpdateData({ newsletters: [{ id: 'newsletter-1' }] })
|
|
183
|
+
).not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should validate newsletter objects have id field if provided', () => {
|
|
187
|
+
expect(() => validateMemberUpdateData({ newsletters: [{ name: 'Newsletter' }] })).toThrow(
|
|
188
|
+
'Member validation failed'
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should allow empty update object', () => {
|
|
193
|
+
expect(() => validateMemberUpdateData({})).not.toThrow();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should validate name length in updates', () => {
|
|
197
|
+
const longName = 'a'.repeat(192); // Exceeds MAX_NAME_LENGTH (191)
|
|
198
|
+
expect(() => validateMemberUpdateData({ name: longName })).toThrow(
|
|
199
|
+
'Member validation failed'
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should accept name at max length in updates', () => {
|
|
204
|
+
const maxName = 'a'.repeat(191); // At MAX_NAME_LENGTH
|
|
205
|
+
expect(() => validateMemberUpdateData({ name: maxName })).not.toThrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should validate note length in updates', () => {
|
|
209
|
+
const longNote = 'a'.repeat(2001); // Exceeds MAX_NOTE_LENGTH (2000)
|
|
210
|
+
expect(() => validateMemberUpdateData({ note: longNote })).toThrow(
|
|
211
|
+
'Member validation failed'
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should accept note at max length in updates', () => {
|
|
216
|
+
const maxNote = 'a'.repeat(2000); // At MAX_NOTE_LENGTH
|
|
217
|
+
expect(() => validateMemberUpdateData({ note: maxNote })).not.toThrow();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should sanitize HTML in note field for updates', () => {
|
|
221
|
+
const updateData = { note: '<script>alert("xss")</script>Updated note' };
|
|
222
|
+
validateMemberUpdateData(updateData);
|
|
223
|
+
expect(updateData.note).toBe('Updated note'); // HTML should be stripped
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should validate label length in updates', () => {
|
|
227
|
+
const longLabel = 'a'.repeat(192); // Exceeds MAX_LABEL_LENGTH (191)
|
|
228
|
+
expect(() => validateMemberUpdateData({ labels: [longLabel] })).toThrow(
|
|
229
|
+
'Member validation failed'
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should reject empty string labels in updates', () => {
|
|
234
|
+
expect(() => validateMemberUpdateData({ labels: [''] })).toThrow('Member validation failed');
|
|
235
|
+
expect(() => validateMemberUpdateData({ labels: [' '] })).toThrow(
|
|
236
|
+
'Member validation failed'
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should reject non-string labels in updates', () => {
|
|
241
|
+
expect(() => validateMemberUpdateData({ labels: [123] })).toThrow('Member validation failed');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should reject empty newsletter IDs in updates', () => {
|
|
245
|
+
expect(() => validateMemberUpdateData({ newsletters: [{ id: '' }] })).toThrow(
|
|
246
|
+
'Member validation failed'
|
|
247
|
+
);
|
|
248
|
+
expect(() => validateMemberUpdateData({ newsletters: [{ id: ' ' }] })).toThrow(
|
|
249
|
+
'Member validation failed'
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('validateMemberQueryOptions', () => {
|
|
255
|
+
it('should accept empty options', () => {
|
|
256
|
+
expect(() => validateMemberQueryOptions({})).not.toThrow();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should accept valid limit within bounds', () => {
|
|
260
|
+
expect(() => validateMemberQueryOptions({ limit: 1 })).not.toThrow();
|
|
261
|
+
expect(() => validateMemberQueryOptions({ limit: 50 })).not.toThrow();
|
|
262
|
+
expect(() => validateMemberQueryOptions({ limit: 100 })).not.toThrow();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should reject limit below minimum', () => {
|
|
266
|
+
expect(() => validateMemberQueryOptions({ limit: 0 })).toThrow(
|
|
267
|
+
'Member query validation failed'
|
|
268
|
+
);
|
|
269
|
+
expect(() => validateMemberQueryOptions({ limit: -1 })).toThrow(
|
|
270
|
+
'Member query validation failed'
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should reject limit above maximum', () => {
|
|
275
|
+
expect(() => validateMemberQueryOptions({ limit: 101 })).toThrow(
|
|
276
|
+
'Member query validation failed'
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should accept valid page number', () => {
|
|
281
|
+
expect(() => validateMemberQueryOptions({ page: 1 })).not.toThrow();
|
|
282
|
+
expect(() => validateMemberQueryOptions({ page: 100 })).not.toThrow();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should reject page below minimum', () => {
|
|
286
|
+
expect(() => validateMemberQueryOptions({ page: 0 })).toThrow(
|
|
287
|
+
'Member query validation failed'
|
|
288
|
+
);
|
|
289
|
+
expect(() => validateMemberQueryOptions({ page: -1 })).toThrow(
|
|
290
|
+
'Member query validation failed'
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should accept valid filter strings', () => {
|
|
295
|
+
expect(() => validateMemberQueryOptions({ filter: 'status:free' })).not.toThrow();
|
|
296
|
+
expect(() => validateMemberQueryOptions({ filter: 'status:paid' })).not.toThrow();
|
|
297
|
+
expect(() => validateMemberQueryOptions({ filter: 'subscribed:true' })).not.toThrow();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should reject empty filter string', () => {
|
|
301
|
+
expect(() => validateMemberQueryOptions({ filter: '' })).toThrow(
|
|
302
|
+
'Member query validation failed'
|
|
303
|
+
);
|
|
304
|
+
expect(() => validateMemberQueryOptions({ filter: ' ' })).toThrow(
|
|
305
|
+
'Member query validation failed'
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should accept valid order strings', () => {
|
|
310
|
+
expect(() => validateMemberQueryOptions({ order: 'created_at desc' })).not.toThrow();
|
|
311
|
+
expect(() => validateMemberQueryOptions({ order: 'email asc' })).not.toThrow();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should reject empty order string', () => {
|
|
315
|
+
expect(() => validateMemberQueryOptions({ order: '' })).toThrow(
|
|
316
|
+
'Member query validation failed'
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should accept valid include strings', () => {
|
|
321
|
+
expect(() => validateMemberQueryOptions({ include: 'labels' })).not.toThrow();
|
|
322
|
+
expect(() => validateMemberQueryOptions({ include: 'newsletters' })).not.toThrow();
|
|
323
|
+
expect(() => validateMemberQueryOptions({ include: 'labels,newsletters' })).not.toThrow();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should reject empty include string', () => {
|
|
327
|
+
expect(() => validateMemberQueryOptions({ include: '' })).toThrow(
|
|
328
|
+
'Member query validation failed'
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should validate multiple options together', () => {
|
|
333
|
+
expect(() =>
|
|
334
|
+
validateMemberQueryOptions({
|
|
335
|
+
limit: 50,
|
|
336
|
+
page: 2,
|
|
337
|
+
filter: 'status:paid',
|
|
338
|
+
order: 'created_at desc',
|
|
339
|
+
include: 'labels,newsletters',
|
|
340
|
+
})
|
|
341
|
+
).not.toThrow();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('validateMemberLookup', () => {
|
|
346
|
+
it('should accept valid id', () => {
|
|
347
|
+
expect(() => validateMemberLookup({ id: '12345' })).not.toThrow();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should accept valid email', () => {
|
|
351
|
+
expect(() => validateMemberLookup({ email: 'test@example.com' })).not.toThrow();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should reject when both id and email are missing', () => {
|
|
355
|
+
expect(() => validateMemberLookup({})).toThrow('Member lookup validation failed');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should reject empty id', () => {
|
|
359
|
+
expect(() => validateMemberLookup({ id: '' })).toThrow('Member lookup validation failed');
|
|
360
|
+
expect(() => validateMemberLookup({ id: ' ' })).toThrow('Member lookup validation failed');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should reject invalid email format', () => {
|
|
364
|
+
expect(() => validateMemberLookup({ email: 'invalid-email' })).toThrow(
|
|
365
|
+
'Member lookup validation failed'
|
|
366
|
+
);
|
|
367
|
+
expect(() => validateMemberLookup({ email: 'test@' })).toThrow(
|
|
368
|
+
'Member lookup validation failed'
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should accept when both id and email provided (id takes precedence)', () => {
|
|
373
|
+
expect(() => validateMemberLookup({ id: '12345', email: 'test@example.com' })).not.toThrow();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should return normalized params with lookupType', () => {
|
|
377
|
+
const resultId = validateMemberLookup({ id: '12345' });
|
|
378
|
+
expect(resultId).toEqual({ id: '12345', lookupType: 'id' });
|
|
379
|
+
|
|
380
|
+
const resultEmail = validateMemberLookup({ email: 'test@example.com' });
|
|
381
|
+
expect(resultEmail).toEqual({ email: 'test@example.com', lookupType: 'email' });
|
|
382
|
+
|
|
383
|
+
// ID takes precedence when both provided
|
|
384
|
+
const resultBoth = validateMemberLookup({ id: '12345', email: 'test@example.com' });
|
|
385
|
+
expect(resultBoth).toEqual({ id: '12345', lookupType: 'id' });
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('validateSearchQuery', () => {
|
|
390
|
+
it('should accept valid search query', () => {
|
|
391
|
+
expect(() => validateSearchQuery('john')).not.toThrow();
|
|
392
|
+
expect(() => validateSearchQuery('john@example.com')).not.toThrow();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should reject empty search query', () => {
|
|
396
|
+
expect(() => validateSearchQuery('')).toThrow('Search query validation failed');
|
|
397
|
+
expect(() => validateSearchQuery(' ')).toThrow('Search query validation failed');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should reject non-string search query', () => {
|
|
401
|
+
expect(() => validateSearchQuery(123)).toThrow('Search query validation failed');
|
|
402
|
+
expect(() => validateSearchQuery(null)).toThrow('Search query validation failed');
|
|
403
|
+
expect(() => validateSearchQuery(undefined)).toThrow('Search query validation failed');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should return sanitized query', () => {
|
|
407
|
+
const result = validateSearchQuery('john');
|
|
408
|
+
expect(result).toBe('john');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should trim whitespace from query', () => {
|
|
412
|
+
const result = validateSearchQuery(' john ');
|
|
413
|
+
expect(result).toBe('john');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('validateSearchOptions', () => {
|
|
418
|
+
it('should accept empty options', () => {
|
|
419
|
+
expect(() => validateSearchOptions({})).not.toThrow();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should accept valid limit within bounds (1-50)', () => {
|
|
423
|
+
expect(() => validateSearchOptions({ limit: 1 })).not.toThrow();
|
|
424
|
+
expect(() => validateSearchOptions({ limit: 25 })).not.toThrow();
|
|
425
|
+
expect(() => validateSearchOptions({ limit: 50 })).not.toThrow();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should reject limit below minimum', () => {
|
|
429
|
+
expect(() => validateSearchOptions({ limit: 0 })).toThrow('Search options validation failed');
|
|
430
|
+
expect(() => validateSearchOptions({ limit: -1 })).toThrow(
|
|
431
|
+
'Search options validation failed'
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should reject limit above maximum (50)', () => {
|
|
436
|
+
expect(() => validateSearchOptions({ limit: 51 })).toThrow(
|
|
437
|
+
'Search options validation failed'
|
|
438
|
+
);
|
|
439
|
+
expect(() => validateSearchOptions({ limit: 100 })).toThrow(
|
|
440
|
+
'Search options validation failed'
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should reject non-number limit', () => {
|
|
445
|
+
expect(() => validateSearchOptions({ limit: 'ten' })).toThrow(
|
|
446
|
+
'Search options validation failed'
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
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
|
+
});
|