@jgardner04/ghost-mcp-server 1.1.12 → 1.2.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 +2 -1
- package/src/__tests__/helpers/mockExpress.js +38 -0
- package/src/__tests__/index.test.js +312 -0
- package/src/__tests__/mcp_server.test.js +381 -0
- package/src/__tests__/mcp_server_improved.test.js +440 -0
- package/src/config/__tests__/mcp-config.test.js +311 -0
- package/src/controllers/__tests__/imageController.test.js +572 -0
- package/src/controllers/__tests__/postController.test.js +236 -0
- package/src/controllers/__tests__/tagController.test.js +222 -0
- package/src/mcp_server_improved.js +105 -1
- package/src/middleware/__tests__/errorMiddleware.test.js +1113 -0
- package/src/resources/__tests__/ResourceManager.test.js +977 -0
- package/src/routes/__tests__/imageRoutes.test.js +117 -0
- package/src/routes/__tests__/postRoutes.test.js +262 -0
- package/src/routes/__tests__/tagRoutes.test.js +175 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
|
|
4
|
+
// Mock dependencies
|
|
5
|
+
vi.mock('fs', () => ({
|
|
6
|
+
default: {
|
|
7
|
+
unlink: vi.fn((path, cb) => cb(null)),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('os', () => ({
|
|
12
|
+
default: {
|
|
13
|
+
tmpdir: vi.fn().mockReturnValue('/tmp'),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('crypto', () => ({
|
|
18
|
+
default: {
|
|
19
|
+
randomBytes: vi.fn().mockReturnValue({
|
|
20
|
+
toString: vi.fn().mockReturnValue('abcdef1234567890'),
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockUploadGhostImage = vi.fn();
|
|
26
|
+
const mockProcessImage = vi.fn();
|
|
27
|
+
|
|
28
|
+
vi.mock('../../services/ghostService.js', () => ({
|
|
29
|
+
uploadImage: (...args) => mockUploadGhostImage(...args),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('../../services/imageProcessingService.js', () => ({
|
|
33
|
+
processImage: (...args) => mockProcessImage(...args),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
37
|
+
createContextLogger: vi.fn().mockReturnValue({
|
|
38
|
+
info: vi.fn(),
|
|
39
|
+
error: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
debug: vi.fn(),
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Import after mocks are set up
|
|
46
|
+
import { upload, handleImageUpload } from '../imageController.js';
|
|
47
|
+
import fs from 'fs';
|
|
48
|
+
|
|
49
|
+
// Helper to create mock request
|
|
50
|
+
function createMockRequest(overrides = {}) {
|
|
51
|
+
return {
|
|
52
|
+
file: null,
|
|
53
|
+
body: {},
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Helper to create mock response
|
|
59
|
+
function createMockResponse() {
|
|
60
|
+
const res = {
|
|
61
|
+
status: vi.fn().mockReturnThis(),
|
|
62
|
+
json: vi.fn().mockReturnThis(),
|
|
63
|
+
};
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Helper to create mock next
|
|
68
|
+
function createMockNext() {
|
|
69
|
+
return vi.fn();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('imageController', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('upload (multer instance)', () => {
|
|
78
|
+
it('should be defined', () => {
|
|
79
|
+
expect(upload).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should have single method', () => {
|
|
83
|
+
expect(typeof upload.single).toBe('function');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('handleImageUpload', () => {
|
|
88
|
+
describe('validation', () => {
|
|
89
|
+
it('should return 400 when no file uploaded', async () => {
|
|
90
|
+
const req = createMockRequest({ file: null });
|
|
91
|
+
const res = createMockResponse();
|
|
92
|
+
const next = createMockNext();
|
|
93
|
+
|
|
94
|
+
await handleImageUpload(req, res, next);
|
|
95
|
+
|
|
96
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
97
|
+
expect(res.json).toHaveBeenCalledWith({ message: 'No image file uploaded.' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return 400 when file validation fails - missing size', async () => {
|
|
101
|
+
const req = createMockRequest({
|
|
102
|
+
file: {
|
|
103
|
+
originalname: 'test.jpg',
|
|
104
|
+
mimetype: 'image/jpeg',
|
|
105
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
106
|
+
// missing size - required field
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const res = createMockResponse();
|
|
110
|
+
const next = createMockNext();
|
|
111
|
+
|
|
112
|
+
await handleImageUpload(req, res, next);
|
|
113
|
+
|
|
114
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
115
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
message: expect.stringContaining('File validation failed'),
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return 400 when file size exceeds limit', async () => {
|
|
123
|
+
const req = createMockRequest({
|
|
124
|
+
file: {
|
|
125
|
+
originalname: 'test.jpg',
|
|
126
|
+
mimetype: 'image/jpeg',
|
|
127
|
+
size: 15 * 1024 * 1024, // 15MB - exceeds 10MB limit
|
|
128
|
+
path: '/tmp/test.jpg',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const res = createMockResponse();
|
|
132
|
+
const next = createMockNext();
|
|
133
|
+
|
|
134
|
+
await handleImageUpload(req, res, next);
|
|
135
|
+
|
|
136
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
137
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
message: expect.stringContaining('File validation failed'),
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return 400 when mimetype is invalid', async () => {
|
|
145
|
+
const req = createMockRequest({
|
|
146
|
+
file: {
|
|
147
|
+
originalname: 'test.txt',
|
|
148
|
+
mimetype: 'text/plain',
|
|
149
|
+
size: 1000,
|
|
150
|
+
path: '/tmp/test.txt',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const res = createMockResponse();
|
|
154
|
+
const next = createMockNext();
|
|
155
|
+
|
|
156
|
+
await handleImageUpload(req, res, next);
|
|
157
|
+
|
|
158
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return 400 for invalid alt text', async () => {
|
|
162
|
+
const req = createMockRequest({
|
|
163
|
+
file: {
|
|
164
|
+
originalname: 'test.jpg',
|
|
165
|
+
mimetype: 'image/jpeg',
|
|
166
|
+
size: 1000,
|
|
167
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
168
|
+
},
|
|
169
|
+
body: {
|
|
170
|
+
alt: 'a'.repeat(501), // exceeds 500 char limit
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const res = createMockResponse();
|
|
174
|
+
const next = createMockNext();
|
|
175
|
+
|
|
176
|
+
mockProcessImage.mockResolvedValue('/tmp/processed.jpg');
|
|
177
|
+
|
|
178
|
+
await handleImageUpload(req, res, next);
|
|
179
|
+
|
|
180
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
181
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
message: expect.stringContaining('Invalid alt text'),
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('security', () => {
|
|
190
|
+
it('should reject file path outside upload directory', async () => {
|
|
191
|
+
const req = createMockRequest({
|
|
192
|
+
file: {
|
|
193
|
+
originalname: 'test.jpg',
|
|
194
|
+
mimetype: 'image/jpeg',
|
|
195
|
+
size: 1000,
|
|
196
|
+
path: '/etc/passwd', // Path traversal attempt
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const res = createMockResponse();
|
|
200
|
+
const next = createMockNext();
|
|
201
|
+
|
|
202
|
+
await handleImageUpload(req, res, next);
|
|
203
|
+
|
|
204
|
+
expect(next).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
message: expect.stringContaining('Security violation'),
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('successful upload', () => {
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
mockProcessImage.mockResolvedValue('/tmp/processed-123.jpg');
|
|
215
|
+
mockUploadGhostImage.mockResolvedValue({
|
|
216
|
+
url: 'https://ghost.com/content/images/image.jpg',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should process and upload image successfully', async () => {
|
|
221
|
+
const req = createMockRequest({
|
|
222
|
+
file: {
|
|
223
|
+
originalname: 'test.jpg',
|
|
224
|
+
mimetype: 'image/jpeg',
|
|
225
|
+
size: 1000,
|
|
226
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
227
|
+
},
|
|
228
|
+
body: {},
|
|
229
|
+
});
|
|
230
|
+
const res = createMockResponse();
|
|
231
|
+
const next = createMockNext();
|
|
232
|
+
|
|
233
|
+
await handleImageUpload(req, res, next);
|
|
234
|
+
|
|
235
|
+
expect(mockProcessImage).toHaveBeenCalledWith('/tmp/mcp-upload-123-abc.jpg', '/tmp');
|
|
236
|
+
expect(mockUploadGhostImage).toHaveBeenCalled();
|
|
237
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
238
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({
|
|
240
|
+
url: 'https://ghost.com/content/images/image.jpg',
|
|
241
|
+
alt: expect.any(String),
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should use provided alt text', async () => {
|
|
247
|
+
const req = createMockRequest({
|
|
248
|
+
file: {
|
|
249
|
+
originalname: 'test.jpg',
|
|
250
|
+
mimetype: 'image/jpeg',
|
|
251
|
+
size: 1000,
|
|
252
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
253
|
+
},
|
|
254
|
+
body: {
|
|
255
|
+
alt: 'Custom alt text',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const res = createMockResponse();
|
|
259
|
+
const next = createMockNext();
|
|
260
|
+
|
|
261
|
+
await handleImageUpload(req, res, next);
|
|
262
|
+
|
|
263
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
alt: 'Custom alt text',
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should generate default alt text from filename', async () => {
|
|
271
|
+
const req = createMockRequest({
|
|
272
|
+
file: {
|
|
273
|
+
originalname: 'beautiful-sunset-photo.jpg',
|
|
274
|
+
mimetype: 'image/jpeg',
|
|
275
|
+
size: 1000,
|
|
276
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
277
|
+
},
|
|
278
|
+
body: {},
|
|
279
|
+
});
|
|
280
|
+
const res = createMockResponse();
|
|
281
|
+
const next = createMockNext();
|
|
282
|
+
|
|
283
|
+
await handleImageUpload(req, res, next);
|
|
284
|
+
|
|
285
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
alt: 'beautiful sunset photo',
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should accept empty alt text', async () => {
|
|
293
|
+
const req = createMockRequest({
|
|
294
|
+
file: {
|
|
295
|
+
originalname: 'test.jpg',
|
|
296
|
+
mimetype: 'image/jpeg',
|
|
297
|
+
size: 1000,
|
|
298
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
299
|
+
},
|
|
300
|
+
body: {
|
|
301
|
+
alt: '',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const res = createMockResponse();
|
|
305
|
+
const next = createMockNext();
|
|
306
|
+
|
|
307
|
+
await handleImageUpload(req, res, next);
|
|
308
|
+
|
|
309
|
+
// Should use default alt when empty string provided
|
|
310
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
311
|
+
expect.objectContaining({
|
|
312
|
+
alt: expect.any(String),
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('error handling', () => {
|
|
319
|
+
it('should pass non-multer errors to next', async () => {
|
|
320
|
+
const req = createMockRequest({
|
|
321
|
+
file: {
|
|
322
|
+
originalname: 'test.jpg',
|
|
323
|
+
mimetype: 'image/jpeg',
|
|
324
|
+
size: 1000,
|
|
325
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
const res = createMockResponse();
|
|
329
|
+
const next = createMockNext();
|
|
330
|
+
|
|
331
|
+
mockProcessImage.mockRejectedValue(new Error('Processing failed'));
|
|
332
|
+
|
|
333
|
+
await handleImageUpload(req, res, next);
|
|
334
|
+
|
|
335
|
+
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle multer errors with 400 status', async () => {
|
|
339
|
+
const req = createMockRequest({
|
|
340
|
+
file: {
|
|
341
|
+
originalname: 'test.jpg',
|
|
342
|
+
mimetype: 'image/jpeg',
|
|
343
|
+
size: 1000,
|
|
344
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
const res = createMockResponse();
|
|
348
|
+
const next = createMockNext();
|
|
349
|
+
|
|
350
|
+
const multerError = new multer.MulterError('LIMIT_FILE_SIZE');
|
|
351
|
+
mockProcessImage.mockRejectedValue(multerError);
|
|
352
|
+
|
|
353
|
+
await handleImageUpload(req, res, next);
|
|
354
|
+
|
|
355
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should handle upload service errors', async () => {
|
|
359
|
+
const req = createMockRequest({
|
|
360
|
+
file: {
|
|
361
|
+
originalname: 'test.jpg',
|
|
362
|
+
mimetype: 'image/jpeg',
|
|
363
|
+
size: 1000,
|
|
364
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const res = createMockResponse();
|
|
368
|
+
const next = createMockNext();
|
|
369
|
+
|
|
370
|
+
mockProcessImage.mockResolvedValue('/tmp/processed.jpg');
|
|
371
|
+
mockUploadGhostImage.mockRejectedValue(new Error('Upload to Ghost failed'));
|
|
372
|
+
|
|
373
|
+
await handleImageUpload(req, res, next);
|
|
374
|
+
|
|
375
|
+
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('cleanup', () => {
|
|
380
|
+
it('should cleanup temp files on success', async () => {
|
|
381
|
+
const req = createMockRequest({
|
|
382
|
+
file: {
|
|
383
|
+
originalname: 'test.jpg',
|
|
384
|
+
mimetype: 'image/jpeg',
|
|
385
|
+
size: 1000,
|
|
386
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
const res = createMockResponse();
|
|
390
|
+
const next = createMockNext();
|
|
391
|
+
|
|
392
|
+
mockProcessImage.mockResolvedValue('/tmp/processed-123.jpg');
|
|
393
|
+
mockUploadGhostImage.mockResolvedValue({ url: 'https://ghost.com/image.jpg' });
|
|
394
|
+
|
|
395
|
+
await handleImageUpload(req, res, next);
|
|
396
|
+
|
|
397
|
+
expect(fs.unlink).toHaveBeenCalled();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should cleanup temp files on error', async () => {
|
|
401
|
+
const req = createMockRequest({
|
|
402
|
+
file: {
|
|
403
|
+
originalname: 'test.jpg',
|
|
404
|
+
mimetype: 'image/jpeg',
|
|
405
|
+
size: 1000,
|
|
406
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const res = createMockResponse();
|
|
410
|
+
const next = createMockNext();
|
|
411
|
+
|
|
412
|
+
mockProcessImage.mockRejectedValue(new Error('Processing failed'));
|
|
413
|
+
|
|
414
|
+
await handleImageUpload(req, res, next);
|
|
415
|
+
|
|
416
|
+
expect(fs.unlink).toHaveBeenCalled();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should handle cleanup errors gracefully', async () => {
|
|
420
|
+
fs.unlink.mockImplementation((path, cb) => cb(new Error('Unlink failed')));
|
|
421
|
+
|
|
422
|
+
const req = createMockRequest({
|
|
423
|
+
file: {
|
|
424
|
+
originalname: 'test.jpg',
|
|
425
|
+
mimetype: 'image/jpeg',
|
|
426
|
+
size: 1000,
|
|
427
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
const res = createMockResponse();
|
|
431
|
+
const next = createMockNext();
|
|
432
|
+
|
|
433
|
+
mockProcessImage.mockResolvedValue('/tmp/processed-123.jpg');
|
|
434
|
+
mockUploadGhostImage.mockResolvedValue({ url: 'https://ghost.com/image.jpg' });
|
|
435
|
+
|
|
436
|
+
// Should not throw even when cleanup fails
|
|
437
|
+
await handleImageUpload(req, res, next);
|
|
438
|
+
|
|
439
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('file type support', () => {
|
|
444
|
+
const supportedTypes = [
|
|
445
|
+
{ name: 'test.jpg', mimetype: 'image/jpeg' },
|
|
446
|
+
{ name: 'test.jpeg', mimetype: 'image/jpeg' },
|
|
447
|
+
{ name: 'test.png', mimetype: 'image/png' },
|
|
448
|
+
{ name: 'test.gif', mimetype: 'image/gif' },
|
|
449
|
+
{ name: 'test.webp', mimetype: 'image/webp' },
|
|
450
|
+
{ name: 'test.svg', mimetype: 'image/svg+xml' },
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
supportedTypes.forEach(({ name, mimetype }) => {
|
|
454
|
+
it(`should accept ${mimetype} files`, async () => {
|
|
455
|
+
mockProcessImage.mockResolvedValue('/tmp/processed.jpg');
|
|
456
|
+
mockUploadGhostImage.mockResolvedValue({ url: 'https://ghost.com/image.jpg' });
|
|
457
|
+
|
|
458
|
+
const req = createMockRequest({
|
|
459
|
+
file: {
|
|
460
|
+
originalname: name,
|
|
461
|
+
mimetype: mimetype,
|
|
462
|
+
size: 1000,
|
|
463
|
+
path: `/tmp/mcp-upload-123-abc${name.substring(name.lastIndexOf('.'))}`,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
const res = createMockResponse();
|
|
467
|
+
const next = createMockNext();
|
|
468
|
+
|
|
469
|
+
await handleImageUpload(req, res, next);
|
|
470
|
+
|
|
471
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('getDefaultAltText (tested indirectly)', () => {
|
|
478
|
+
beforeEach(() => {
|
|
479
|
+
mockProcessImage.mockResolvedValue('/tmp/processed.jpg');
|
|
480
|
+
mockUploadGhostImage.mockResolvedValue({ url: 'https://ghost.com/image.jpg' });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should convert dashes to spaces', async () => {
|
|
484
|
+
const req = createMockRequest({
|
|
485
|
+
file: {
|
|
486
|
+
originalname: 'my-beautiful-image.jpg',
|
|
487
|
+
mimetype: 'image/jpeg',
|
|
488
|
+
size: 1000,
|
|
489
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
490
|
+
},
|
|
491
|
+
body: {},
|
|
492
|
+
});
|
|
493
|
+
const res = createMockResponse();
|
|
494
|
+
const next = createMockNext();
|
|
495
|
+
|
|
496
|
+
await handleImageUpload(req, res, next);
|
|
497
|
+
|
|
498
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
499
|
+
expect.objectContaining({
|
|
500
|
+
alt: 'my beautiful image',
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should convert underscores to spaces', async () => {
|
|
506
|
+
const req = createMockRequest({
|
|
507
|
+
file: {
|
|
508
|
+
originalname: 'my_great_photo.png',
|
|
509
|
+
mimetype: 'image/png',
|
|
510
|
+
size: 1000,
|
|
511
|
+
path: '/tmp/mcp-upload-123-abc.png',
|
|
512
|
+
},
|
|
513
|
+
body: {},
|
|
514
|
+
});
|
|
515
|
+
const res = createMockResponse();
|
|
516
|
+
const next = createMockNext();
|
|
517
|
+
|
|
518
|
+
await handleImageUpload(req, res, next);
|
|
519
|
+
|
|
520
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
521
|
+
expect.objectContaining({
|
|
522
|
+
alt: 'my great photo',
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should fallback to "Uploaded image" for files without name', async () => {
|
|
528
|
+
const req = createMockRequest({
|
|
529
|
+
file: {
|
|
530
|
+
originalname: '.jpg',
|
|
531
|
+
mimetype: 'image/jpeg',
|
|
532
|
+
size: 1000,
|
|
533
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
534
|
+
},
|
|
535
|
+
body: {},
|
|
536
|
+
});
|
|
537
|
+
const res = createMockResponse();
|
|
538
|
+
const next = createMockNext();
|
|
539
|
+
|
|
540
|
+
await handleImageUpload(req, res, next);
|
|
541
|
+
|
|
542
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
543
|
+
expect.objectContaining({
|
|
544
|
+
alt: 'Uploaded image',
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should sanitize path separators from filename', async () => {
|
|
550
|
+
const req = createMockRequest({
|
|
551
|
+
file: {
|
|
552
|
+
originalname: 'path/to/image.jpg', // Contains path separator
|
|
553
|
+
mimetype: 'image/jpeg',
|
|
554
|
+
size: 1000,
|
|
555
|
+
path: '/tmp/mcp-upload-123-abc.jpg',
|
|
556
|
+
},
|
|
557
|
+
body: {},
|
|
558
|
+
});
|
|
559
|
+
const res = createMockResponse();
|
|
560
|
+
const next = createMockNext();
|
|
561
|
+
|
|
562
|
+
await handleImageUpload(req, res, next);
|
|
563
|
+
|
|
564
|
+
// Should sanitize path separators
|
|
565
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
566
|
+
expect.objectContaining({
|
|
567
|
+
alt: expect.any(String),
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
});
|