@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.
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+
5
+ // Mock dependencies
6
+ vi.mock('../../utils/logger.js', () => ({
7
+ createContextLogger: vi.fn().mockReturnValue({
8
+ info: vi.fn(),
9
+ error: vi.fn(),
10
+ warn: vi.fn(),
11
+ debug: vi.fn(),
12
+ }),
13
+ }));
14
+
15
+ // Mock crypto for multer's safe filename generation
16
+ vi.mock('crypto', () => ({
17
+ default: {
18
+ randomBytes: vi.fn().mockReturnValue({
19
+ toString: vi.fn().mockReturnValue('abcdef1234567890'),
20
+ }),
21
+ },
22
+ }));
23
+
24
+ // Mock os for temp directory
25
+ vi.mock('os', () => ({
26
+ default: {
27
+ tmpdir: vi.fn().mockReturnValue('/tmp'),
28
+ },
29
+ }));
30
+
31
+ // Mock fs for file operations
32
+ vi.mock('fs', () => ({
33
+ default: {
34
+ unlink: vi.fn((path, cb) => cb(null)),
35
+ existsSync: vi.fn().mockReturnValue(true),
36
+ },
37
+ }));
38
+
39
+ // Mock the image processing service
40
+ vi.mock('../../services/imageProcessingService.js', () => ({
41
+ processImage: vi.fn().mockResolvedValue('/tmp/processed-image.jpg'),
42
+ }));
43
+
44
+ // Mock the ghost service
45
+ vi.mock('../../services/ghostService.js', () => ({
46
+ uploadImage: vi.fn().mockResolvedValue({ url: 'https://ghost.com/image.jpg' }),
47
+ }));
48
+
49
+ // Import after mocks
50
+ import imageRoutes from '../imageRoutes.js';
51
+
52
+ function createTestApp() {
53
+ const app = express();
54
+ app.use(express.json());
55
+ app.use('/api/images', imageRoutes);
56
+ // Add error handler
57
+ app.use((err, req, res, _next) => {
58
+ res.status(err.status || 500).json({ error: err.message });
59
+ });
60
+ return app;
61
+ }
62
+
63
+ describe('imageRoutes', () => {
64
+ let app;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ app = createTestApp();
69
+ });
70
+
71
+ describe('POST /api/images', () => {
72
+ describe('rate limiting', () => {
73
+ it('should have rate limiting configured', () => {
74
+ // The route should be configured with rate limiting
75
+ // We verify indirectly by checking the route exists
76
+ expect(imageRoutes).toBeDefined();
77
+ });
78
+
79
+ it('should return rate limit headers', async () => {
80
+ const response = await request(app).post('/api/images');
81
+
82
+ // Rate limit headers should be present
83
+ expect(response.headers['ratelimit-limit']).toBeDefined();
84
+ expect(response.headers['ratelimit-remaining']).toBeDefined();
85
+ });
86
+ });
87
+
88
+ describe('file upload', () => {
89
+ it('should return 400 when no file is provided', async () => {
90
+ const response = await request(app).post('/api/images').send({});
91
+
92
+ // Without a file, the controller should return 400
93
+ expect(response.status).toBe(400);
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('route configuration', () => {
99
+ it('should export router', () => {
100
+ expect(imageRoutes).toBeDefined();
101
+ });
102
+
103
+ it('should handle POST requests', async () => {
104
+ const response = await request(app).post('/api/images');
105
+
106
+ // Route exists (not 404)
107
+ expect(response.status).not.toBe(404);
108
+ });
109
+
110
+ it('should not handle GET requests (no route defined)', async () => {
111
+ const response = await request(app).get('/api/images');
112
+
113
+ // Should return 404 since no GET route is defined
114
+ expect(response.status).toBe(404);
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+
5
+ // Mock dependencies
6
+ vi.mock('../../utils/logger.js', () => ({
7
+ createContextLogger: vi.fn().mockReturnValue({
8
+ info: vi.fn(),
9
+ error: vi.fn(),
10
+ warn: vi.fn(),
11
+ debug: vi.fn(),
12
+ }),
13
+ }));
14
+
15
+ const mockCreatePost = vi.fn();
16
+ vi.mock('../../controllers/postController.js', () => ({
17
+ createPost: (req, res, next) => mockCreatePost(req, res, next),
18
+ }));
19
+
20
+ // Import after mocks
21
+ import postRoutes from '../postRoutes.js';
22
+
23
+ function createTestApp() {
24
+ const app = express();
25
+ app.use(express.json());
26
+ app.use('/api/posts', postRoutes);
27
+ return app;
28
+ }
29
+
30
+ describe('postRoutes', () => {
31
+ let app;
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ app = createTestApp();
36
+ // Default mock response
37
+ mockCreatePost.mockImplementation((req, res) => {
38
+ res.status(201).json({ id: '1', title: req.body.title });
39
+ });
40
+ });
41
+
42
+ describe('POST /api/posts', () => {
43
+ describe('validation - title', () => {
44
+ it('should return 400 when title is missing', async () => {
45
+ const response = await request(app).post('/api/posts').send({ html: '<p>Content</p>' });
46
+
47
+ expect(response.status).toBe(400);
48
+ expect(response.body.errors).toBeDefined();
49
+ expect(response.body.errors.some((e) => e.path === 'title')).toBe(true);
50
+ });
51
+
52
+ it('should return 400 when title is empty string', async () => {
53
+ const response = await request(app)
54
+ .post('/api/posts')
55
+ .send({ title: '', html: '<p>Content</p>' });
56
+
57
+ expect(response.status).toBe(400);
58
+ expect(response.body.errors.some((e) => e.path === 'title')).toBe(true);
59
+ });
60
+
61
+ it('should return 400 when title is not a string', async () => {
62
+ const response = await request(app)
63
+ .post('/api/posts')
64
+ .send({ title: 123, html: '<p>Content</p>' });
65
+
66
+ expect(response.status).toBe(400);
67
+ });
68
+ });
69
+
70
+ describe('validation - html', () => {
71
+ it('should return 400 when html is missing', async () => {
72
+ const response = await request(app).post('/api/posts').send({ title: 'Test Post' });
73
+
74
+ expect(response.status).toBe(400);
75
+ expect(response.body.errors.some((e) => e.path === 'html')).toBe(true);
76
+ });
77
+
78
+ it('should return 400 when html is empty string', async () => {
79
+ const response = await request(app)
80
+ .post('/api/posts')
81
+ .send({ title: 'Test Post', html: '' });
82
+
83
+ expect(response.status).toBe(400);
84
+ expect(response.body.errors.some((e) => e.path === 'html')).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('validation - status', () => {
89
+ it('should accept valid status "draft"', async () => {
90
+ const response = await request(app)
91
+ .post('/api/posts')
92
+ .send({ title: 'Test', html: '<p>Content</p>', status: 'draft' });
93
+
94
+ expect(response.status).toBe(201);
95
+ });
96
+
97
+ it('should accept valid status "published"', async () => {
98
+ const response = await request(app)
99
+ .post('/api/posts')
100
+ .send({ title: 'Test', html: '<p>Content</p>', status: 'published' });
101
+
102
+ expect(response.status).toBe(201);
103
+ });
104
+
105
+ it('should accept valid status "scheduled"', async () => {
106
+ const response = await request(app)
107
+ .post('/api/posts')
108
+ .send({ title: 'Test', html: '<p>Content</p>', status: 'scheduled' });
109
+
110
+ expect(response.status).toBe(201);
111
+ });
112
+
113
+ it('should return 400 for invalid status', async () => {
114
+ const response = await request(app)
115
+ .post('/api/posts')
116
+ .send({ title: 'Test', html: '<p>Content</p>', status: 'invalid' });
117
+
118
+ expect(response.status).toBe(400);
119
+ expect(response.body.errors.some((e) => e.path === 'status')).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('validation - published_at', () => {
124
+ it('should accept valid ISO8601 date', async () => {
125
+ const response = await request(app).post('/api/posts').send({
126
+ title: 'Test',
127
+ html: '<p>Content</p>',
128
+ published_at: '2024-01-15T10:00:00Z',
129
+ });
130
+
131
+ expect(response.status).toBe(201);
132
+ });
133
+
134
+ it('should return 400 for invalid date format', async () => {
135
+ const response = await request(app).post('/api/posts').send({
136
+ title: 'Test',
137
+ html: '<p>Content</p>',
138
+ published_at: 'not-a-date',
139
+ });
140
+
141
+ expect(response.status).toBe(400);
142
+ expect(response.body.errors.some((e) => e.path === 'published_at')).toBe(true);
143
+ });
144
+ });
145
+
146
+ describe('validation - tags', () => {
147
+ it('should accept valid tags array', async () => {
148
+ const response = await request(app)
149
+ .post('/api/posts')
150
+ .send({ title: 'Test', html: '<p>Content</p>', tags: ['tech', 'news'] });
151
+
152
+ expect(response.status).toBe(201);
153
+ });
154
+
155
+ it('should return 400 when tags is not an array', async () => {
156
+ const response = await request(app)
157
+ .post('/api/posts')
158
+ .send({ title: 'Test', html: '<p>Content</p>', tags: 'tech' });
159
+
160
+ expect(response.status).toBe(400);
161
+ expect(response.body.errors.some((e) => e.path === 'tags')).toBe(true);
162
+ });
163
+ });
164
+
165
+ describe('validation - feature_image', () => {
166
+ it('should accept valid URL for feature_image', async () => {
167
+ const response = await request(app).post('/api/posts').send({
168
+ title: 'Test',
169
+ html: '<p>Content</p>',
170
+ feature_image: 'https://example.com/image.jpg',
171
+ });
172
+
173
+ expect(response.status).toBe(201);
174
+ });
175
+
176
+ it('should return 400 for invalid URL', async () => {
177
+ const response = await request(app).post('/api/posts').send({
178
+ title: 'Test',
179
+ html: '<p>Content</p>',
180
+ feature_image: 'not-a-url',
181
+ });
182
+
183
+ expect(response.status).toBe(400);
184
+ expect(response.body.errors.some((e) => e.path === 'feature_image')).toBe(true);
185
+ });
186
+ });
187
+
188
+ describe('validation - meta_title', () => {
189
+ it('should accept valid meta_title', async () => {
190
+ const response = await request(app)
191
+ .post('/api/posts')
192
+ .send({ title: 'Test', html: '<p>Content</p>', meta_title: 'SEO Title' });
193
+
194
+ expect(response.status).toBe(201);
195
+ });
196
+
197
+ it('should return 400 when meta_title exceeds 300 characters', async () => {
198
+ const response = await request(app)
199
+ .post('/api/posts')
200
+ .send({
201
+ title: 'Test',
202
+ html: '<p>Content</p>',
203
+ meta_title: 'a'.repeat(301),
204
+ });
205
+
206
+ expect(response.status).toBe(400);
207
+ expect(response.body.errors.some((e) => e.path === 'meta_title')).toBe(true);
208
+ });
209
+ });
210
+
211
+ describe('validation - meta_description', () => {
212
+ it('should accept valid meta_description', async () => {
213
+ const response = await request(app).post('/api/posts').send({
214
+ title: 'Test',
215
+ html: '<p>Content</p>',
216
+ meta_description: 'A brief description',
217
+ });
218
+
219
+ expect(response.status).toBe(201);
220
+ });
221
+
222
+ it('should return 400 when meta_description exceeds 500 characters', async () => {
223
+ const response = await request(app)
224
+ .post('/api/posts')
225
+ .send({
226
+ title: 'Test',
227
+ html: '<p>Content</p>',
228
+ meta_description: 'a'.repeat(501),
229
+ });
230
+
231
+ expect(response.status).toBe(400);
232
+ expect(response.body.errors.some((e) => e.path === 'meta_description')).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('successful post creation', () => {
237
+ it('should call createPost controller when validation passes', async () => {
238
+ const response = await request(app).post('/api/posts').send({
239
+ title: 'Test Post',
240
+ html: '<p>Content</p>',
241
+ });
242
+
243
+ expect(response.status).toBe(201);
244
+ expect(mockCreatePost).toHaveBeenCalled();
245
+ });
246
+
247
+ it('should pass validated body to controller', async () => {
248
+ await request(app).post('/api/posts').send({
249
+ title: 'My Title',
250
+ html: '<p>My Content</p>',
251
+ status: 'draft',
252
+ });
253
+
254
+ expect(mockCreatePost).toHaveBeenCalled();
255
+ const reqArg = mockCreatePost.mock.calls[0][0];
256
+ expect(reqArg.body.title).toBe('My Title');
257
+ expect(reqArg.body.html).toBe('<p>My Content</p>');
258
+ expect(reqArg.body.status).toBe('draft');
259
+ });
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import request from 'supertest';
3
+ import express from 'express';
4
+
5
+ // Mock dependencies
6
+ vi.mock('../../utils/logger.js', () => ({
7
+ createContextLogger: vi.fn().mockReturnValue({
8
+ info: vi.fn(),
9
+ error: vi.fn(),
10
+ warn: vi.fn(),
11
+ debug: vi.fn(),
12
+ }),
13
+ }));
14
+
15
+ const mockGetTags = vi.fn();
16
+ const mockCreateTag = vi.fn();
17
+ vi.mock('../../controllers/tagController.js', () => ({
18
+ getTags: (req, res, next) => mockGetTags(req, res, next),
19
+ createTag: (req, res, next) => mockCreateTag(req, res, next),
20
+ }));
21
+
22
+ // Import after mocks
23
+ import tagRoutes from '../tagRoutes.js';
24
+
25
+ function createTestApp() {
26
+ const app = express();
27
+ app.use(express.json());
28
+ app.use('/api/tags', tagRoutes);
29
+ return app;
30
+ }
31
+
32
+ describe('tagRoutes', () => {
33
+ let app;
34
+
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ app = createTestApp();
38
+ // Default mock responses
39
+ mockGetTags.mockImplementation((req, res) => {
40
+ res.status(200).json([{ id: '1', name: 'Test Tag' }]);
41
+ });
42
+ mockCreateTag.mockImplementation((req, res) => {
43
+ res.status(201).json({ id: '1', name: req.body.name });
44
+ });
45
+ });
46
+
47
+ describe('GET /api/tags', () => {
48
+ it('should call getTags controller', async () => {
49
+ const response = await request(app).get('/api/tags');
50
+
51
+ expect(response.status).toBe(200);
52
+ expect(mockGetTags).toHaveBeenCalled();
53
+ });
54
+
55
+ it('should pass query parameters to controller', async () => {
56
+ await request(app).get('/api/tags?name=Technology');
57
+
58
+ expect(mockGetTags).toHaveBeenCalled();
59
+ const reqArg = mockGetTags.mock.calls[0][0];
60
+ expect(reqArg.query.name).toBe('Technology');
61
+ });
62
+ });
63
+
64
+ describe('POST /api/tags', () => {
65
+ describe('validation - name', () => {
66
+ it('should return 400 when name is missing', async () => {
67
+ const response = await request(app).post('/api/tags').send({ description: 'A tag' });
68
+
69
+ expect(response.status).toBe(400);
70
+ expect(response.body.errors).toBeDefined();
71
+ expect(response.body.errors.some((e) => e.path === 'name')).toBe(true);
72
+ });
73
+
74
+ it('should return 400 when name is empty string', async () => {
75
+ const response = await request(app).post('/api/tags').send({ name: '' });
76
+
77
+ expect(response.status).toBe(400);
78
+ expect(response.body.errors.some((e) => e.path === 'name')).toBe(true);
79
+ });
80
+ });
81
+
82
+ describe('validation - description', () => {
83
+ it('should accept valid description', async () => {
84
+ const response = await request(app)
85
+ .post('/api/tags')
86
+ .send({ name: 'Tech', description: 'Technology articles' });
87
+
88
+ expect(response.status).toBe(201);
89
+ });
90
+
91
+ it('should return 400 when description exceeds 500 characters', async () => {
92
+ const response = await request(app)
93
+ .post('/api/tags')
94
+ .send({ name: 'Tech', description: 'a'.repeat(501) });
95
+
96
+ expect(response.status).toBe(400);
97
+ expect(response.body.errors.some((e) => e.path === 'description')).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe('validation - slug', () => {
102
+ it('should accept valid slug with lowercase letters', async () => {
103
+ const response = await request(app)
104
+ .post('/api/tags')
105
+ .send({ name: 'Tech', slug: 'technology' });
106
+
107
+ expect(response.status).toBe(201);
108
+ });
109
+
110
+ it('should accept valid slug with numbers', async () => {
111
+ const response = await request(app)
112
+ .post('/api/tags')
113
+ .send({ name: 'Tech', slug: 'tech123' });
114
+
115
+ expect(response.status).toBe(201);
116
+ });
117
+
118
+ it('should accept valid slug with hyphens', async () => {
119
+ const response = await request(app)
120
+ .post('/api/tags')
121
+ .send({ name: 'Tech News', slug: 'tech-news' });
122
+
123
+ expect(response.status).toBe(201);
124
+ });
125
+
126
+ it('should return 400 for slug with uppercase letters', async () => {
127
+ const response = await request(app)
128
+ .post('/api/tags')
129
+ .send({ name: 'Tech', slug: 'Technology' });
130
+
131
+ expect(response.status).toBe(400);
132
+ expect(response.body.errors.some((e) => e.path === 'slug')).toBe(true);
133
+ });
134
+
135
+ it('should return 400 for slug with spaces', async () => {
136
+ const response = await request(app)
137
+ .post('/api/tags')
138
+ .send({ name: 'Tech', slug: 'tech news' });
139
+
140
+ expect(response.status).toBe(400);
141
+ expect(response.body.errors.some((e) => e.path === 'slug')).toBe(true);
142
+ });
143
+
144
+ it('should return 400 for slug with underscores', async () => {
145
+ const response = await request(app)
146
+ .post('/api/tags')
147
+ .send({ name: 'Tech', slug: 'tech_news' });
148
+
149
+ expect(response.status).toBe(400);
150
+ expect(response.body.errors.some((e) => e.path === 'slug')).toBe(true);
151
+ });
152
+ });
153
+
154
+ describe('successful tag creation', () => {
155
+ it('should call createTag controller when validation passes', async () => {
156
+ const response = await request(app).post('/api/tags').send({ name: 'Technology' });
157
+
158
+ expect(response.status).toBe(201);
159
+ expect(mockCreateTag).toHaveBeenCalled();
160
+ });
161
+
162
+ it('should pass validated body to controller', async () => {
163
+ await request(app)
164
+ .post('/api/tags')
165
+ .send({ name: 'Tech', slug: 'tech', description: 'Technical articles' });
166
+
167
+ expect(mockCreateTag).toHaveBeenCalled();
168
+ const reqArg = mockCreateTag.mock.calls[0][0];
169
+ expect(reqArg.body.name).toBe('Tech');
170
+ expect(reqArg.body.slug).toBe('tech');
171
+ expect(reqArg.body.description).toBe('Technical articles');
172
+ });
173
+ });
174
+ });
175
+ });