@jgardner04/ghost-mcp-server 1.1.10 → 1.1.11
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
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
|
|
5
|
+
// Mock dotenv
|
|
6
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
7
|
+
|
|
8
|
+
// Mock logger
|
|
9
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
10
|
+
createContextLogger: createMockContextLogger(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock sharp with chainable API
|
|
14
|
+
const mockMetadata = vi.fn();
|
|
15
|
+
const mockResize = vi.fn();
|
|
16
|
+
const mockJpeg = vi.fn();
|
|
17
|
+
const mockToFile = vi.fn();
|
|
18
|
+
|
|
19
|
+
const createMockSharp = () => {
|
|
20
|
+
const instance = {
|
|
21
|
+
metadata: mockMetadata,
|
|
22
|
+
resize: mockResize,
|
|
23
|
+
jpeg: mockJpeg,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Make methods chainable
|
|
27
|
+
mockResize.mockReturnValue(instance);
|
|
28
|
+
mockJpeg.mockReturnValue(instance);
|
|
29
|
+
instance.toFile = mockToFile;
|
|
30
|
+
|
|
31
|
+
return instance;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
vi.mock('sharp', () => ({
|
|
35
|
+
default: vi.fn(() => createMockSharp()),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Mock fs
|
|
39
|
+
vi.mock('fs', () => ({
|
|
40
|
+
default: {
|
|
41
|
+
existsSync: vi.fn(),
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock path - use actual implementation but allow spying
|
|
46
|
+
vi.mock('path', async () => {
|
|
47
|
+
const actual = await vi.importActual('path');
|
|
48
|
+
return {
|
|
49
|
+
default: actual.default,
|
|
50
|
+
...actual,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Import after mocks are set up
|
|
55
|
+
import { processImage } from '../imageProcessingService.js';
|
|
56
|
+
import sharp from 'sharp';
|
|
57
|
+
import fs from 'fs';
|
|
58
|
+
|
|
59
|
+
describe('imageProcessingService', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
// Reset mock implementations
|
|
63
|
+
mockMetadata.mockResolvedValue({ width: 800, size: 100000 });
|
|
64
|
+
mockToFile.mockResolvedValue();
|
|
65
|
+
fs.existsSync.mockReturnValue(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Input Validation', () => {
|
|
69
|
+
it('should accept valid inputPath and outputDir', async () => {
|
|
70
|
+
const inputPath = '/tmp/test-image.jpg';
|
|
71
|
+
const outputDir = '/tmp/output';
|
|
72
|
+
|
|
73
|
+
await processImage(inputPath, outputDir);
|
|
74
|
+
|
|
75
|
+
expect(sharp).toHaveBeenCalledWith(inputPath);
|
|
76
|
+
expect(mockToFile).toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should reject missing inputPath', async () => {
|
|
80
|
+
await expect(processImage(undefined, '/tmp/output')).rejects.toThrow(
|
|
81
|
+
'Invalid processing parameters'
|
|
82
|
+
);
|
|
83
|
+
expect(sharp).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should reject missing outputDir', async () => {
|
|
87
|
+
await expect(processImage('/tmp/test.jpg', undefined)).rejects.toThrow(
|
|
88
|
+
'Invalid processing parameters'
|
|
89
|
+
);
|
|
90
|
+
expect(sharp).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Path Security', () => {
|
|
95
|
+
it('should resolve paths correctly', async () => {
|
|
96
|
+
const inputPath = './relative/path/image.jpg';
|
|
97
|
+
const outputDir = './output';
|
|
98
|
+
|
|
99
|
+
await processImage(inputPath, outputDir);
|
|
100
|
+
|
|
101
|
+
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringContaining('image.jpg'));
|
|
102
|
+
expect(mockToFile).toHaveBeenCalledWith(expect.stringContaining('output'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw error when input file does not exist', async () => {
|
|
106
|
+
fs.existsSync.mockReturnValue(false);
|
|
107
|
+
|
|
108
|
+
await expect(processImage('/tmp/nonexistent.jpg', '/tmp/output')).rejects.toThrow(
|
|
109
|
+
'Input file does not exist'
|
|
110
|
+
);
|
|
111
|
+
expect(sharp).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Image Processing', () => {
|
|
116
|
+
it('should not resize image when width <= MAX_WIDTH (1200)', async () => {
|
|
117
|
+
mockMetadata.mockResolvedValue({ width: 1000, size: 100000 });
|
|
118
|
+
|
|
119
|
+
await processImage('/tmp/small-image.jpg', '/tmp/output');
|
|
120
|
+
|
|
121
|
+
expect(mockResize).not.toHaveBeenCalled();
|
|
122
|
+
expect(mockJpeg).toHaveBeenCalledWith({ quality: 80 });
|
|
123
|
+
expect(mockToFile).toHaveBeenCalled();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should resize image when width > MAX_WIDTH (1200)', async () => {
|
|
127
|
+
mockMetadata.mockResolvedValue({ width: 2000, size: 200000 });
|
|
128
|
+
|
|
129
|
+
await processImage('/tmp/large-image.jpg', '/tmp/output');
|
|
130
|
+
|
|
131
|
+
expect(mockResize).toHaveBeenCalledWith({ width: 1200 });
|
|
132
|
+
expect(mockJpeg).toHaveBeenCalledWith({ quality: 80 });
|
|
133
|
+
expect(mockToFile).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should generate output filename with timestamp and processed prefix', async () => {
|
|
137
|
+
const inputPath = '/tmp/test-photo.jpg';
|
|
138
|
+
const outputDir = '/tmp/output';
|
|
139
|
+
|
|
140
|
+
// Mock Date.now to get predictable filename
|
|
141
|
+
const mockTimestamp = 1234567890;
|
|
142
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
143
|
+
|
|
144
|
+
await processImage(inputPath, outputDir);
|
|
145
|
+
|
|
146
|
+
expect(mockToFile).toHaveBeenCalledWith(
|
|
147
|
+
expect.stringMatching(/processed-1234567890-test-photo\.jpg$/)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
vi.restoreAllMocks();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should convert image to JPEG with quality setting of 80', async () => {
|
|
154
|
+
await processImage('/tmp/image.png', '/tmp/output');
|
|
155
|
+
|
|
156
|
+
expect(mockJpeg).toHaveBeenCalledWith({ quality: 80 });
|
|
157
|
+
expect(mockToFile).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle images with multiple dots in filename', async () => {
|
|
161
|
+
const inputPath = '/tmp/my.test.image.png';
|
|
162
|
+
const outputDir = '/tmp/output';
|
|
163
|
+
const mockTimestamp = 9999999999;
|
|
164
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
165
|
+
|
|
166
|
+
await processImage(inputPath, outputDir);
|
|
167
|
+
|
|
168
|
+
expect(mockToFile).toHaveBeenCalledWith(
|
|
169
|
+
expect.stringMatching(/processed-9999999999-my\.test\.image\.jpg$/)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
vi.restoreAllMocks();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Error Handling', () => {
|
|
177
|
+
it('should catch and re-throw sharp processing failures', async () => {
|
|
178
|
+
const processingError = new Error('Sharp processing failed');
|
|
179
|
+
mockMetadata.mockRejectedValue(processingError);
|
|
180
|
+
|
|
181
|
+
await expect(processImage('/tmp/corrupt.jpg', '/tmp/output')).rejects.toThrow(
|
|
182
|
+
'Image processing failed: Sharp processing failed'
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should include original error message in re-thrown error', async () => {
|
|
187
|
+
const originalMessage = 'Input buffer contains unsupported image format';
|
|
188
|
+
mockToFile.mockRejectedValue(new Error(originalMessage));
|
|
189
|
+
|
|
190
|
+
await expect(processImage('/tmp/bad-format.dat', '/tmp/output')).rejects.toThrow(
|
|
191
|
+
`Image processing failed: ${originalMessage}`
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle errors during JPEG conversion', async () => {
|
|
196
|
+
const conversionError = new Error('JPEG conversion failed');
|
|
197
|
+
mockToFile.mockRejectedValue(conversionError);
|
|
198
|
+
|
|
199
|
+
await expect(processImage('/tmp/image.jpg', '/tmp/output')).rejects.toThrow(
|
|
200
|
+
'Image processing failed: JPEG conversion failed'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -28,8 +28,8 @@ const processImage = async (inputPath, outputDir) => {
|
|
|
28
28
|
if (error) {
|
|
29
29
|
logger.error('Invalid processing parameters', {
|
|
30
30
|
error: error.details[0].message,
|
|
31
|
-
inputPath: path.basename(inputPath),
|
|
32
|
-
outputDir: path.basename(outputDir),
|
|
31
|
+
inputPath: inputPath ? path.basename(inputPath) : 'undefined',
|
|
32
|
+
outputDir: outputDir ? path.basename(outputDir) : 'undefined',
|
|
33
33
|
});
|
|
34
34
|
throw new Error('Invalid processing parameters');
|
|
35
35
|
}
|