@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgardner04/ghost-mcp-server",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "description": "A Model Context Protocol (MCP) server for interacting with Ghost CMS via the Admin API",
5
5
  "author": "Jonathan Gardner",
6
6
  "type": "module",
@@ -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
  }