@jgardner04/ghost-mcp-server 1.13.5 → 1.14.1
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/__tests__/mcp_server.test.js +83 -0
- package/src/controllers/__tests__/imageController.test.js +2 -2
- package/src/controllers/imageController.js +11 -10
- package/src/mcp_server.js +270 -84
- package/src/routes/__tests__/imageRoutes.test.js +2 -2
- package/src/schemas/__tests__/common.test.js +3 -3
- package/src/schemas/__tests__/pageSchemas.test.js +11 -2
- package/src/schemas/common.js +3 -2
- package/src/schemas/pageSchemas.js +1 -1
- package/src/schemas/postSchemas.js +1 -1
- package/src/services/__tests__/ghostService.test.js +0 -19
- package/src/services/__tests__/imageProcessingService.test.js +148 -177
- package/src/services/__tests__/images.test.js +78 -0
- package/src/services/ghostService.js +1 -19
- package/src/services/imageProcessingService.js +100 -56
- package/src/services/images.js +34 -7
- package/src/services/pageService.js +2 -2
- package/src/utils/__tests__/formatErrorResponse.test.js +158 -0
- package/src/utils/__tests__/imageInputResolver.test.js +134 -0
- package/src/utils/__tests__/sanitizeErrorPayload.test.js +130 -0
- package/src/utils/__tests__/validation.test.js +13 -7
- package/src/utils/formatErrorResponse.js +63 -0
- package/src/utils/imageInputResolver.js +127 -0
- package/src/utils/sanitizeErrorPayload.js +67 -0
- package/src/utils/validation.js +2 -4
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
createTag,
|
|
21
21
|
getTags,
|
|
22
22
|
getSiteInfo,
|
|
23
|
-
uploadImage,
|
|
24
23
|
handleApiRequest,
|
|
25
24
|
api,
|
|
26
25
|
} from '../ghostService.js';
|
|
@@ -209,24 +208,6 @@ describe('ghostService', () => {
|
|
|
209
208
|
});
|
|
210
209
|
});
|
|
211
210
|
|
|
212
|
-
describe('uploadImage', () => {
|
|
213
|
-
it('should throw error when image path is missing', async () => {
|
|
214
|
-
await expect(uploadImage()).rejects.toThrow('Image path is required for upload');
|
|
215
|
-
await expect(uploadImage('')).rejects.toThrow('Image path is required for upload');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should successfully upload image with valid path', async () => {
|
|
219
|
-
const imagePath = '/path/to/image.jpg';
|
|
220
|
-
const expectedResult = { url: 'https://example.com/uploaded-image.jpg' };
|
|
221
|
-
api.images.upload.mockResolvedValue(expectedResult);
|
|
222
|
-
|
|
223
|
-
const result = await uploadImage(imagePath);
|
|
224
|
-
|
|
225
|
-
expect(result).toEqual(expectedResult);
|
|
226
|
-
expect(api.images.upload).toHaveBeenCalledWith({ file: imagePath });
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
211
|
describe('createPost', () => {
|
|
231
212
|
it('should throw error when title is missing', async () => {
|
|
232
213
|
await expect(createPost({})).rejects.toThrow('Post title is required');
|
|
@@ -1,204 +1,175 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
vi.mock('dotenv', () => mockDotenv());
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
9
8
|
vi.mock('../../utils/logger.js', () => ({
|
|
10
9
|
createContextLogger: createMockContextLogger(),
|
|
11
10
|
}));
|
|
12
11
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
12
|
+
// Import under test (real sharp, real filesystem)
|
|
13
|
+
const { processImage } = await import('../imageProcessingService.js');
|
|
14
|
+
|
|
15
|
+
const tmpRoot = path.join(os.tmpdir(), `ghost-mcp-img-test-${Date.now()}`);
|
|
16
|
+
const fixtures = {};
|
|
17
|
+
|
|
18
|
+
async function makeRasterFixture(format, width, height = width) {
|
|
19
|
+
const file = path.join(tmpRoot, `in-${format}-${width}.${format === 'jpeg' ? 'jpg' : format}`);
|
|
20
|
+
await sharp({
|
|
21
|
+
create: {
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
channels: format === 'png' ? 4 : 3,
|
|
25
|
+
background: { r: 10, g: 50, b: 150, alpha: 1 },
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
[format]()
|
|
29
|
+
.toFile(file);
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
await fs.mkdir(tmpRoot, { recursive: true });
|
|
35
|
+
|
|
36
|
+
fixtures.pngSmall = await makeRasterFixture('png', 800);
|
|
37
|
+
fixtures.pngLarge = await makeRasterFixture('png', 2000);
|
|
38
|
+
fixtures.jpegSmall = await makeRasterFixture('jpeg', 800);
|
|
39
|
+
fixtures.jpegLarge = await makeRasterFixture('jpeg', 2000);
|
|
40
|
+
fixtures.webpSmall = await makeRasterFixture('webp', 800);
|
|
41
|
+
fixtures.webpLarge = await makeRasterFixture('webp', 2000);
|
|
42
|
+
|
|
43
|
+
// SVG: write minimal vector source
|
|
44
|
+
fixtures.svg = path.join(tmpRoot, 'in.svg');
|
|
45
|
+
await fs.writeFile(
|
|
46
|
+
fixtures.svg,
|
|
47
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#39f"/></svg>'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// GIF: sharp cannot reliably create animated GIFs across versions; use a
|
|
51
|
+
// known-valid 1x1 transparent GIF89a byte sequence for a deterministic fixture.
|
|
52
|
+
fixtures.gif = path.join(tmpRoot, 'in.gif');
|
|
53
|
+
await fs.writeFile(
|
|
54
|
+
fixtures.gif,
|
|
55
|
+
Buffer.from(
|
|
56
|
+
'47494638396101000100800000ffffff00000021f90401000001002c00000000010001000002024401003b',
|
|
57
|
+
'hex'
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ICO: minimal 1x1 ICO header (used only for passthrough byte comparison).
|
|
62
|
+
fixtures.ico = path.join(tmpRoot, 'in.ico');
|
|
63
|
+
await fs.writeFile(
|
|
64
|
+
fixtures.ico,
|
|
65
|
+
Buffer.from(
|
|
66
|
+
'00000100010001010000010020003000000016000000280000000100000002000000010020000000000004000000000000000000000000000000000000000000000000000000000000000000000000',
|
|
67
|
+
'hex'
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
});
|
|
44
71
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const actual = await vi.importActual('path');
|
|
48
|
-
return {
|
|
49
|
-
default: actual.default,
|
|
50
|
-
...actual,
|
|
51
|
-
};
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
52
74
|
});
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fs.existsSync.mockReturnValue(true);
|
|
76
|
+
async function metaOf(file) {
|
|
77
|
+
return sharp(file).metadata();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('processImage — format passthrough', () => {
|
|
81
|
+
it('PNG <= 1200: preserves format and passes through unmodified', async () => {
|
|
82
|
+
const out = await processImage(fixtures.pngSmall, tmpRoot);
|
|
83
|
+
expect(path.extname(out).toLowerCase()).toBe('.png');
|
|
84
|
+
const m = await metaOf(out);
|
|
85
|
+
expect(m.format).toBe('png');
|
|
86
|
+
expect(m.width).toBe(800);
|
|
66
87
|
});
|
|
67
88
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
});
|
|
89
|
+
it('PNG > 1200: resized but still PNG (no JPEG conversion)', async () => {
|
|
90
|
+
const out = await processImage(fixtures.pngLarge, tmpRoot);
|
|
91
|
+
expect(path.extname(out).toLowerCase()).toBe('.png');
|
|
92
|
+
const m = await metaOf(out);
|
|
93
|
+
expect(m.format).toBe('png');
|
|
94
|
+
expect(m.width).toBe(1200);
|
|
92
95
|
});
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
});
|
|
97
|
+
it('JPEG <= 1200: passes through as JPEG', async () => {
|
|
98
|
+
const out = await processImage(fixtures.jpegSmall, tmpRoot);
|
|
99
|
+
expect(path.extname(out).toLowerCase()).toMatch(/\.jpe?g$/);
|
|
100
|
+
const m = await metaOf(out);
|
|
101
|
+
expect(m.format).toBe('jpeg');
|
|
102
|
+
expect(m.width).toBe(800);
|
|
113
103
|
});
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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);
|
|
105
|
+
it('JPEG > 1200: resized as JPEG', async () => {
|
|
106
|
+
const out = await processImage(fixtures.jpegLarge, tmpRoot);
|
|
107
|
+
const m = await metaOf(out);
|
|
108
|
+
expect(m.format).toBe('jpeg');
|
|
109
|
+
expect(m.width).toBe(1200);
|
|
110
|
+
});
|
|
143
111
|
|
|
144
|
-
|
|
112
|
+
it('WEBP <= 1200: passes through as WEBP', async () => {
|
|
113
|
+
const out = await processImage(fixtures.webpSmall, tmpRoot);
|
|
114
|
+
const m = await metaOf(out);
|
|
115
|
+
expect(m.format).toBe('webp');
|
|
116
|
+
expect(m.width).toBe(800);
|
|
117
|
+
});
|
|
145
118
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
119
|
+
it('WEBP > 1200: resized as WEBP', async () => {
|
|
120
|
+
const out = await processImage(fixtures.webpLarge, tmpRoot);
|
|
121
|
+
const m = await metaOf(out);
|
|
122
|
+
expect(m.format).toBe('webp');
|
|
123
|
+
expect(m.width).toBe(1200);
|
|
124
|
+
});
|
|
149
125
|
|
|
150
|
-
|
|
151
|
-
|
|
126
|
+
it('SVG: passthrough — output bytes identical to input (never touched by sharp)', async () => {
|
|
127
|
+
const out = await processImage(fixtures.svg, tmpRoot);
|
|
128
|
+
expect(path.extname(out).toLowerCase()).toBe('.svg');
|
|
129
|
+
const [a, b] = await Promise.all([fs.readFile(fixtures.svg), fs.readFile(out)]);
|
|
130
|
+
expect(a.equals(b)).toBe(true);
|
|
131
|
+
});
|
|
152
132
|
|
|
153
|
-
|
|
154
|
-
|
|
133
|
+
it('GIF: passthrough — output bytes identical to input (animation-safe)', async () => {
|
|
134
|
+
const out = await processImage(fixtures.gif, tmpRoot);
|
|
135
|
+
expect(path.extname(out).toLowerCase()).toBe('.gif');
|
|
136
|
+
const [a, b] = await Promise.all([fs.readFile(fixtures.gif), fs.readFile(out)]);
|
|
137
|
+
expect(a.equals(b)).toBe(true);
|
|
138
|
+
});
|
|
155
139
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
it('ICO: passthrough — output bytes identical to input', async () => {
|
|
141
|
+
const out = await processImage(fixtures.ico, tmpRoot, { purpose: 'icon' });
|
|
142
|
+
expect(path.extname(out).toLowerCase()).toBe('.ico');
|
|
143
|
+
const [a, b] = await Promise.all([fs.readFile(fixtures.ico), fs.readFile(out)]);
|
|
144
|
+
expect(a.equals(b)).toBe(true);
|
|
145
|
+
});
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
it('output path is distinct from input path', async () => {
|
|
148
|
+
const out = await processImage(fixtures.pngSmall, tmpRoot);
|
|
149
|
+
expect(out).not.toBe(fixtures.pngSmall);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
165
152
|
|
|
166
|
-
|
|
153
|
+
describe('processImage — validation & errors', () => {
|
|
154
|
+
it('rejects missing inputPath', async () => {
|
|
155
|
+
await expect(processImage(undefined, tmpRoot)).rejects.toThrow(/Invalid processing parameters/);
|
|
156
|
+
});
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
158
|
+
it('rejects missing outputDir', async () => {
|
|
159
|
+
await expect(processImage(fixtures.pngSmall, undefined)).rejects.toThrow(
|
|
160
|
+
/Invalid processing parameters/
|
|
161
|
+
);
|
|
162
|
+
});
|
|
171
163
|
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
it('throws when input file does not exist', async () => {
|
|
165
|
+
await expect(processImage('/nonexistent/xyz.png', tmpRoot)).rejects.toThrow(
|
|
166
|
+
/Input file does not exist/
|
|
167
|
+
);
|
|
174
168
|
});
|
|
175
169
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
});
|
|
170
|
+
it('wraps sharp failures with context', async () => {
|
|
171
|
+
const bogus = path.join(tmpRoot, 'bogus.png');
|
|
172
|
+
await fs.writeFile(bogus, 'not an image');
|
|
173
|
+
await expect(processImage(bogus, tmpRoot)).rejects.toThrow(/Image processing failed/);
|
|
203
174
|
});
|
|
204
175
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockGhostApiModule } from '../../__tests__/helpers/mockGhostApi.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
5
|
+
|
|
6
|
+
vi.mock('@tryghost/admin-api', () => mockGhostApiModule());
|
|
7
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
8
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
9
|
+
createContextLogger: createMockContextLogger(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Stub path validation so these tests focus on argument shaping, not FS.
|
|
13
|
+
vi.mock('../validators.js', () => ({
|
|
14
|
+
validators: { validateImagePath: vi.fn().mockResolvedValue(undefined) },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { uploadImage } from '../images.js';
|
|
18
|
+
import { api } from '../ghostApiClient.js';
|
|
19
|
+
import { ValidationError } from '../../errors/index.js';
|
|
20
|
+
|
|
21
|
+
describe('images.uploadImage (domain module used by MCP server)', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('forwards purpose and ref to the SDK', async () => {
|
|
27
|
+
const expected = { url: 'https://cdn/x.png', ref: 'original.png' };
|
|
28
|
+
api.images.upload.mockResolvedValue(expected);
|
|
29
|
+
|
|
30
|
+
const result = await uploadImage('/tmp/x.png', {
|
|
31
|
+
purpose: 'icon',
|
|
32
|
+
ref: 'original.png',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual(expected);
|
|
36
|
+
expect(api.images.upload).toHaveBeenCalledWith({
|
|
37
|
+
file: '/tmp/x.png',
|
|
38
|
+
purpose: 'icon',
|
|
39
|
+
ref: 'original.png',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('omits purpose/ref when not provided', async () => {
|
|
44
|
+
api.images.upload.mockResolvedValue({ url: 'https://cdn/x.png' });
|
|
45
|
+
await uploadImage('/tmp/x.png');
|
|
46
|
+
expect(api.images.upload).toHaveBeenCalledWith({ file: '/tmp/x.png' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('accepts each allowed purpose value', async () => {
|
|
50
|
+
api.images.upload.mockResolvedValue({ url: 'https://cdn/x' });
|
|
51
|
+
for (const purpose of ['image', 'profile_image', 'icon']) {
|
|
52
|
+
await uploadImage('/tmp/x.png', { purpose });
|
|
53
|
+
}
|
|
54
|
+
expect(api.images.upload).toHaveBeenCalledTimes(3);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('rejects unknown purpose', async () => {
|
|
58
|
+
await expect(uploadImage('/tmp/x.png', { purpose: 'banner' })).rejects.toBeInstanceOf(
|
|
59
|
+
ValidationError
|
|
60
|
+
);
|
|
61
|
+
expect(api.images.upload).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects non-string ref', async () => {
|
|
65
|
+
await expect(uploadImage('/tmp/x.png', { ref: 42 })).rejects.toThrow(/must be a string/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects ref longer than 200 chars', async () => {
|
|
69
|
+
await expect(uploadImage('/tmp/x.png', { ref: 'x'.repeat(201) })).rejects.toThrow(
|
|
70
|
+
/cannot exceed 200/
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts ref of exactly 200 chars', async () => {
|
|
75
|
+
api.images.upload.mockResolvedValue({ url: 'https://cdn/x' });
|
|
76
|
+
await expect(uploadImage('/tmp/x.png', { ref: 'x'.repeat(200) })).resolves.toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -148,24 +148,6 @@ const createPost = async (postData, options = { source: 'html' }) => {
|
|
|
148
148
|
return handleApiRequest('posts', 'add', dataWithDefaults, options);
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
* Uploads an image to Ghost.
|
|
153
|
-
* Requires the image file path.
|
|
154
|
-
* @param {string} imagePath - The local path to the image file.
|
|
155
|
-
* @returns {Promise<object>} The result from the image upload API call, typically includes the URL of the uploaded image.
|
|
156
|
-
*/
|
|
157
|
-
const uploadImage = async (imagePath) => {
|
|
158
|
-
if (!imagePath) {
|
|
159
|
-
throw new Error('Image path is required for upload.');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// The Ghost Admin API expects an object with a 'file' property containing the path
|
|
163
|
-
const imageData = { file: imagePath };
|
|
164
|
-
|
|
165
|
-
// Use the handleApiRequest function for consistency
|
|
166
|
-
return handleApiRequest('images', 'upload', imageData);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
151
|
/**
|
|
170
152
|
* Creates a new tag in Ghost.
|
|
171
153
|
* @param {object} tagData - Data for the new tag (e.g., { name: 'New Tag', slug: 'new-tag' }).
|
|
@@ -209,4 +191,4 @@ const getTags = async (options = {}) => {
|
|
|
209
191
|
// Add other content management functions here (createTag, etc.)
|
|
210
192
|
|
|
211
193
|
// Export the API client instance and any service functions
|
|
212
|
-
export { api, getSiteInfo, handleApiRequest, createPost,
|
|
194
|
+
export { api, getSiteInfo, handleApiRequest, createPost, createTag, getTags };
|