@adsim/wordpress-mcp-server 1.0.0 → 3.0.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/.env.example +8 -0
- package/.github/workflows/ci.yml +20 -0
- package/LICENSE +1 -1
- package/README.md +596 -135
- package/index.js +1367 -0
- package/package.json +21 -33
- package/src/auth/bearer.js +72 -0
- package/src/transport/http.js +264 -0
- package/tests/helpers/mockWpRequest.js +135 -0
- package/tests/unit/governance.test.js +260 -0
- package/tests/unit/tools/comments.test.js +170 -0
- package/tests/unit/tools/media.test.js +279 -0
- package/tests/unit/tools/pages.test.js +222 -0
- package/tests/unit/tools/plugins.test.js +268 -0
- package/tests/unit/tools/posts.test.js +310 -0
- package/tests/unit/tools/revisions.test.js +299 -0
- package/tests/unit/tools/search.test.js +190 -0
- package/tests/unit/tools/seo.test.js +248 -0
- package/tests/unit/tools/site.test.js +133 -0
- package/tests/unit/tools/taxonomies.test.js +220 -0
- package/tests/unit/tools/themes.test.js +163 -0
- package/tests/unit/tools/users.test.js +113 -0
- package/tests/unit/transport/http.test.js +300 -0
- package/vitest.config.js +12 -0
- package/dist/constants.d.ts +0 -13
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -10
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -33
- package/dist/index.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -308
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -191
- package/dist/schemas/index.js.map +0 -1
- package/dist/services/formatters.d.ts +0 -22
- package/dist/services/formatters.d.ts.map +0 -1
- package/dist/services/formatters.js +0 -52
- package/dist/services/formatters.js.map +0 -1
- package/dist/services/wp-client.d.ts +0 -38
- package/dist/services/wp-client.d.ts.map +0 -1
- package/dist/services/wp-client.js +0 -102
- package/dist/services/wp-client.js.map +0 -1
- package/dist/tools/content.d.ts +0 -4
- package/dist/tools/content.d.ts.map +0 -1
- package/dist/tools/content.js +0 -196
- package/dist/tools/content.js.map +0 -1
- package/dist/tools/posts.d.ts +0 -4
- package/dist/tools/posts.d.ts.map +0 -1
- package/dist/tools/posts.js +0 -179
- package/dist/tools/posts.js.map +0 -1
- package/dist/tools/seo.d.ts +0 -4
- package/dist/tools/seo.d.ts.map +0 -1
- package/dist/tools/seo.js +0 -241
- package/dist/tools/seo.js.map +0 -1
- package/dist/tools/taxonomy.d.ts +0 -4
- package/dist/tools/taxonomy.d.ts.map +0 -1
- package/dist/tools/taxonomy.js +0 -82
- package/dist/tools/taxonomy.js.map +0 -1
- package/dist/types.d.ts +0 -160
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall } from '../../../index.js';
|
|
7
|
+
import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ────────────────────────────────────────────────────────────
|
|
14
|
+
// wp_list_media
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('wp_list_media', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
20
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
21
|
+
|
|
22
|
+
it('returns formatted media list on success', async () => {
|
|
23
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
24
|
+
{ id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg', source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt', media_details: { width: 800, height: 600 } }
|
|
25
|
+
]));
|
|
26
|
+
|
|
27
|
+
const result = await call('wp_list_media');
|
|
28
|
+
const data = parseResult(result);
|
|
29
|
+
|
|
30
|
+
expect(data.total).toBe(1);
|
|
31
|
+
expect(data.page).toBe(1);
|
|
32
|
+
expect(data.media).toHaveLength(1);
|
|
33
|
+
expect(data.media[0].id).toBe(100);
|
|
34
|
+
expect(data.media[0].title).toBe('Image 1');
|
|
35
|
+
expect(data.media[0].mime_type).toBe('image/jpeg');
|
|
36
|
+
expect(data.media[0].source_url).toBe('https://test.example.com/wp-content/uploads/img.jpg');
|
|
37
|
+
expect(data.media[0].alt_text).toBe('Alt');
|
|
38
|
+
expect(data.media[0].width).toBe(800);
|
|
39
|
+
expect(data.media[0].height).toBe(600);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns error on 403', async () => {
|
|
43
|
+
fetch.mockResolvedValue(mockError(403));
|
|
44
|
+
|
|
45
|
+
const result = await call('wp_list_media');
|
|
46
|
+
|
|
47
|
+
expect(result.isError).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns error on 404', async () => {
|
|
51
|
+
fetch.mockResolvedValue(mockError(404));
|
|
52
|
+
|
|
53
|
+
const result = await call('wp_list_media');
|
|
54
|
+
|
|
55
|
+
expect(result.isError).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('logs audit entry with status success', async () => {
|
|
59
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
60
|
+
{ id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg', source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt', media_details: { width: 800, height: 600 } }
|
|
61
|
+
]));
|
|
62
|
+
|
|
63
|
+
await call('wp_list_media');
|
|
64
|
+
const logs = getAuditLogs(consoleSpy);
|
|
65
|
+
const entry = logs.find(l => l.tool === 'wp_list_media');
|
|
66
|
+
|
|
67
|
+
expect(entry).toBeDefined();
|
|
68
|
+
expect(entry.status).toBe('success');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ────────────────────────────────────────────────────────────
|
|
73
|
+
// wp_get_media
|
|
74
|
+
// ────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe('wp_get_media', () => {
|
|
77
|
+
let consoleSpy;
|
|
78
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
79
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
80
|
+
|
|
81
|
+
it('returns full media details on success', async () => {
|
|
82
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
83
|
+
id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg',
|
|
84
|
+
source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt',
|
|
85
|
+
caption: { rendered: 'Caption' },
|
|
86
|
+
media_details: {
|
|
87
|
+
width: 800, height: 600, file: '2024/01/img.jpg',
|
|
88
|
+
sizes: {
|
|
89
|
+
thumbnail: { source_url: 'https://test.example.com/wp-content/uploads/img-150x150.jpg', width: 150, height: 150 }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
const result = await call('wp_get_media', { id: 100 });
|
|
95
|
+
const data = parseResult(result);
|
|
96
|
+
|
|
97
|
+
expect(data.id).toBe(100);
|
|
98
|
+
expect(data.title).toBe('Image 1');
|
|
99
|
+
expect(data.mime_type).toBe('image/jpeg');
|
|
100
|
+
expect(data.source_url).toBe('https://test.example.com/wp-content/uploads/img.jpg');
|
|
101
|
+
expect(data.alt_text).toBe('Alt');
|
|
102
|
+
expect(data.caption).toBe('Caption');
|
|
103
|
+
expect(data.width).toBe(800);
|
|
104
|
+
expect(data.height).toBe(600);
|
|
105
|
+
expect(data.file).toBe('2024/01/img.jpg');
|
|
106
|
+
expect(data.sizes.thumbnail).toBeDefined();
|
|
107
|
+
expect(data.sizes.thumbnail.url).toBe('https://test.example.com/wp-content/uploads/img-150x150.jpg');
|
|
108
|
+
expect(data.sizes.thumbnail.width).toBe(150);
|
|
109
|
+
expect(data.sizes.thumbnail.height).toBe(150);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns error on 403', async () => {
|
|
113
|
+
fetch.mockResolvedValue(mockError(403));
|
|
114
|
+
|
|
115
|
+
const result = await call('wp_get_media', { id: 100 });
|
|
116
|
+
|
|
117
|
+
expect(result.isError).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns error on 404', async () => {
|
|
121
|
+
fetch.mockResolvedValue(mockError(404));
|
|
122
|
+
|
|
123
|
+
const result = await call('wp_get_media', { id: 999 });
|
|
124
|
+
|
|
125
|
+
expect(result.isError).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('logs audit entry with status success', async () => {
|
|
129
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
130
|
+
id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg',
|
|
131
|
+
source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt',
|
|
132
|
+
caption: { rendered: 'Caption' },
|
|
133
|
+
media_details: { width: 800, height: 600, file: '2024/01/img.jpg', sizes: {} }
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
await call('wp_get_media', { id: 100 });
|
|
137
|
+
const logs = getAuditLogs(consoleSpy);
|
|
138
|
+
const entry = logs.find(l => l.tool === 'wp_get_media');
|
|
139
|
+
|
|
140
|
+
expect(entry).toBeDefined();
|
|
141
|
+
expect(entry.status).toBe('success');
|
|
142
|
+
expect(entry.target).toBe(100);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ────────────────────────────────────────────────────────────
|
|
147
|
+
// wp_upload_media
|
|
148
|
+
// ────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe('wp_upload_media', () => {
|
|
151
|
+
let consoleSpy;
|
|
152
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
153
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
154
|
+
|
|
155
|
+
it('downloads file and uploads to WP on success', async () => {
|
|
156
|
+
let callCount = 0;
|
|
157
|
+
fetch.mockImplementation(() => {
|
|
158
|
+
callCount++;
|
|
159
|
+
if (callCount === 1) {
|
|
160
|
+
// First call: download the remote file
|
|
161
|
+
return Promise.resolve({
|
|
162
|
+
ok: true,
|
|
163
|
+
status: 200,
|
|
164
|
+
headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
|
|
165
|
+
buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
|
|
166
|
+
json: () => Promise.resolve({}),
|
|
167
|
+
text: () => Promise.resolve('')
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Second call: upload to WP API
|
|
171
|
+
return Promise.resolve(mockSuccess({
|
|
172
|
+
id: 101,
|
|
173
|
+
source_url: 'https://test.example.com/wp-content/uploads/uploaded.jpg',
|
|
174
|
+
mime_type: 'image/jpeg'
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
179
|
+
const data = parseResult(result);
|
|
180
|
+
|
|
181
|
+
expect(data.success).toBe(true);
|
|
182
|
+
expect(data.media.id).toBe(101);
|
|
183
|
+
expect(data.media.source_url).toBe('https://test.example.com/wp-content/uploads/uploaded.jpg');
|
|
184
|
+
expect(data.media.mime_type).toBe('image/jpeg');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('is blocked in READ-ONLY mode (governance)', async () => {
|
|
188
|
+
process.env.WP_READ_ONLY = 'true';
|
|
189
|
+
|
|
190
|
+
const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
191
|
+
|
|
192
|
+
expect(result.isError).toBe(true);
|
|
193
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
194
|
+
|
|
195
|
+
delete process.env.WP_READ_ONLY;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns error when download fails with 403', async () => {
|
|
199
|
+
fetch.mockImplementation(() => {
|
|
200
|
+
return Promise.resolve({
|
|
201
|
+
ok: false,
|
|
202
|
+
status: 403,
|
|
203
|
+
headers: { get: () => null },
|
|
204
|
+
buffer: () => Promise.resolve(Buffer.from('')),
|
|
205
|
+
json: () => Promise.resolve({ message: 'Forbidden' }),
|
|
206
|
+
text: () => Promise.resolve('Forbidden')
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
211
|
+
|
|
212
|
+
expect(result.isError).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('returns error when WP upload API returns 403', async () => {
|
|
216
|
+
let callCount = 0;
|
|
217
|
+
fetch.mockImplementation(() => {
|
|
218
|
+
callCount++;
|
|
219
|
+
if (callCount === 1) {
|
|
220
|
+
return Promise.resolve({
|
|
221
|
+
ok: true,
|
|
222
|
+
status: 200,
|
|
223
|
+
headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
|
|
224
|
+
buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
|
|
225
|
+
json: () => Promise.resolve({}),
|
|
226
|
+
text: () => Promise.resolve('')
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return Promise.resolve(mockError(403));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
233
|
+
|
|
234
|
+
expect(result.isError).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('logs audit entry with status success on upload', async () => {
|
|
238
|
+
let callCount = 0;
|
|
239
|
+
fetch.mockImplementation(() => {
|
|
240
|
+
callCount++;
|
|
241
|
+
if (callCount === 1) {
|
|
242
|
+
return Promise.resolve({
|
|
243
|
+
ok: true,
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
|
|
246
|
+
buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
|
|
247
|
+
json: () => Promise.resolve({}),
|
|
248
|
+
text: () => Promise.resolve('')
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return Promise.resolve(mockSuccess({
|
|
252
|
+
id: 101,
|
|
253
|
+
source_url: 'https://test.example.com/wp-content/uploads/uploaded.jpg',
|
|
254
|
+
mime_type: 'image/jpeg'
|
|
255
|
+
}));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
259
|
+
const logs = getAuditLogs(consoleSpy);
|
|
260
|
+
const entry = logs.find(l => l.tool === 'wp_upload_media');
|
|
261
|
+
|
|
262
|
+
expect(entry).toBeDefined();
|
|
263
|
+
expect(entry.status).toBe('success');
|
|
264
|
+
expect(entry.target).toBe(101);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('logs audit entry with status blocked in READ-ONLY mode', async () => {
|
|
268
|
+
process.env.WP_READ_ONLY = 'true';
|
|
269
|
+
|
|
270
|
+
await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
|
|
271
|
+
const logs = getAuditLogs(consoleSpy);
|
|
272
|
+
const entry = logs.find(l => l.tool === 'wp_upload_media');
|
|
273
|
+
|
|
274
|
+
expect(entry).toBeDefined();
|
|
275
|
+
expect(entry.status).toBe('blocked');
|
|
276
|
+
|
|
277
|
+
delete process.env.WP_READ_ONLY;
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall } from '../../../index.js';
|
|
7
|
+
import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ────────────────────────────────────────────────────────────
|
|
14
|
+
// wp_list_pages
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('wp_list_pages', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
20
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
21
|
+
|
|
22
|
+
it('returns formatted page list on success', async () => {
|
|
23
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
24
|
+
{ id: 10, title: { rendered: 'About' }, status: 'publish', date: '2024-01-01', link: 'https://test.example.com/about', parent: 0, menu_order: 1, template: '', excerpt: { rendered: 'About page' } }
|
|
25
|
+
]));
|
|
26
|
+
|
|
27
|
+
const result = await call('wp_list_pages');
|
|
28
|
+
const data = parseResult(result);
|
|
29
|
+
|
|
30
|
+
expect(data.total).toBe(1);
|
|
31
|
+
expect(data.pages[0].id).toBe(10);
|
|
32
|
+
expect(data.pages[0].title).toBe('About');
|
|
33
|
+
expect(data.pages[0].parent).toBe(0);
|
|
34
|
+
expect(data.pages[0].menu_order).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('logs audit entry with status success', async () => {
|
|
38
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
39
|
+
{ id: 10, title: { rendered: 'About' }, status: 'publish', date: '2024-01-01', link: 'https://test.example.com/about', parent: 0, menu_order: 1, template: '', excerpt: { rendered: 'About page' } }
|
|
40
|
+
]));
|
|
41
|
+
|
|
42
|
+
await call('wp_list_pages');
|
|
43
|
+
const logs = getAuditLogs(consoleSpy);
|
|
44
|
+
const entry = logs.find(l => l.tool === 'wp_list_pages');
|
|
45
|
+
|
|
46
|
+
expect(entry).toBeDefined();
|
|
47
|
+
expect(entry.status).toBe('success');
|
|
48
|
+
expect(entry.action).toBe('list');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ────────────────────────────────────────────────────────────
|
|
53
|
+
// wp_get_page
|
|
54
|
+
// ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe('wp_get_page', () => {
|
|
57
|
+
let consoleSpy;
|
|
58
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
59
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
60
|
+
|
|
61
|
+
it('returns full page data on success', async () => {
|
|
62
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
63
|
+
id: 10, title: { rendered: 'About' }, content: { rendered: '<p>About us</p>' }, excerpt: { rendered: 'About page' },
|
|
64
|
+
status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/about',
|
|
65
|
+
slug: 'about', parent: 0, menu_order: 1, template: '', author: 1, featured_media: 0, meta: {}
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const result = await call('wp_get_page', { id: 10 });
|
|
69
|
+
const data = parseResult(result);
|
|
70
|
+
|
|
71
|
+
expect(data.id).toBe(10);
|
|
72
|
+
expect(data.title).toBe('About');
|
|
73
|
+
expect(data.content).toBe('<p>About us</p>');
|
|
74
|
+
expect(data.slug).toBe('about');
|
|
75
|
+
expect(data.parent).toBe(0);
|
|
76
|
+
expect(data.template).toBe('');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns error on 404', async () => {
|
|
80
|
+
fetch.mockResolvedValue(mockError(404));
|
|
81
|
+
|
|
82
|
+
const result = await call('wp_get_page', { id: 999 });
|
|
83
|
+
|
|
84
|
+
expect(result.isError).toBe(true);
|
|
85
|
+
expect(result.content[0].text).toContain('404');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('logs audit with target and target_type', async () => {
|
|
89
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
90
|
+
id: 10, title: { rendered: 'About' }, content: { rendered: '<p>About us</p>' }, excerpt: { rendered: 'About page' },
|
|
91
|
+
status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/about',
|
|
92
|
+
slug: 'about', parent: 0, menu_order: 1, template: '', author: 1, featured_media: 0, meta: {}
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
await call('wp_get_page', { id: 10 });
|
|
96
|
+
const logs = getAuditLogs(consoleSpy);
|
|
97
|
+
const entry = logs.find(l => l.tool === 'wp_get_page');
|
|
98
|
+
|
|
99
|
+
expect(entry).toBeDefined();
|
|
100
|
+
expect(entry.status).toBe('success');
|
|
101
|
+
expect(entry.target).toBe(10);
|
|
102
|
+
expect(entry.target_type).toBe('page');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ────────────────────────────────────────────────────────────
|
|
107
|
+
// wp_create_page
|
|
108
|
+
// ────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('wp_create_page', () => {
|
|
111
|
+
let consoleSpy;
|
|
112
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
113
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
114
|
+
|
|
115
|
+
it('creates a page and returns success shape', async () => {
|
|
116
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
117
|
+
id: 11, title: { rendered: 'New Page' }, status: 'draft', link: 'https://test.example.com/?page_id=11', parent: 0
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const result = await call('wp_create_page', { title: 'New Page', content: 'Page body' });
|
|
121
|
+
const data = parseResult(result);
|
|
122
|
+
|
|
123
|
+
expect(data.success).toBe(true);
|
|
124
|
+
expect(data.page.id).toBe(11);
|
|
125
|
+
expect(data.page.title).toBe('New Page');
|
|
126
|
+
expect(data.page.status).toBe('draft');
|
|
127
|
+
expect(data.page.parent).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
131
|
+
process.env.WP_READ_ONLY = 'true';
|
|
132
|
+
try {
|
|
133
|
+
const result = await call('wp_create_page', { title: 'T', content: 'C' });
|
|
134
|
+
|
|
135
|
+
expect(result.isError).toBe(true);
|
|
136
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
137
|
+
|
|
138
|
+
const logs = getAuditLogs(consoleSpy);
|
|
139
|
+
const entry = logs.find(l => l.tool === 'wp_create_page');
|
|
140
|
+
expect(entry).toBeDefined();
|
|
141
|
+
expect(entry.status).toBe('blocked');
|
|
142
|
+
} finally {
|
|
143
|
+
delete process.env.WP_READ_ONLY;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('logs audit on success', async () => {
|
|
148
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
149
|
+
id: 11, title: { rendered: 'New Page' }, status: 'draft', link: 'https://test.example.com/?page_id=11', parent: 0
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
await call('wp_create_page', { title: 'New Page', content: 'Body' });
|
|
153
|
+
const logs = getAuditLogs(consoleSpy);
|
|
154
|
+
const entry = logs.find(l => l.tool === 'wp_create_page');
|
|
155
|
+
|
|
156
|
+
expect(entry).toBeDefined();
|
|
157
|
+
expect(entry.status).toBe('success');
|
|
158
|
+
expect(entry.action).toBe('create');
|
|
159
|
+
expect(entry.target_type).toBe('page');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ────────────────────────────────────────────────────────────
|
|
164
|
+
// wp_update_page
|
|
165
|
+
// ────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe('wp_update_page', () => {
|
|
168
|
+
let consoleSpy;
|
|
169
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
170
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
171
|
+
|
|
172
|
+
it('updates a page and returns success shape', async () => {
|
|
173
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
174
|
+
id: 10, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/about', modified: '2024-01-02'
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const result = await call('wp_update_page', { id: 10, title: 'Updated' });
|
|
178
|
+
const data = parseResult(result);
|
|
179
|
+
|
|
180
|
+
expect(data.success).toBe(true);
|
|
181
|
+
expect(data.page.id).toBe(10);
|
|
182
|
+
expect(data.page.title).toBe('Updated');
|
|
183
|
+
expect(data.page.modified).toBe('2024-01-02');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('returns error on 404', async () => {
|
|
187
|
+
fetch.mockResolvedValue(mockError(404));
|
|
188
|
+
|
|
189
|
+
const result = await call('wp_update_page', { id: 999, title: 'Ghost' });
|
|
190
|
+
|
|
191
|
+
expect(result.isError).toBe(true);
|
|
192
|
+
expect(result.content[0].text).toContain('404');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
196
|
+
process.env.WP_READ_ONLY = 'true';
|
|
197
|
+
try {
|
|
198
|
+
const result = await call('wp_update_page', { id: 10, title: 'T' });
|
|
199
|
+
|
|
200
|
+
expect(result.isError).toBe(true);
|
|
201
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
202
|
+
} finally {
|
|
203
|
+
delete process.env.WP_READ_ONLY;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('logs audit with target and action', async () => {
|
|
208
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
209
|
+
id: 10, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/about', modified: '2024-01-02'
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
await call('wp_update_page', { id: 10, title: 'Updated' });
|
|
213
|
+
const logs = getAuditLogs(consoleSpy);
|
|
214
|
+
const entry = logs.find(l => l.tool === 'wp_update_page');
|
|
215
|
+
|
|
216
|
+
expect(entry).toBeDefined();
|
|
217
|
+
expect(entry.status).toBe('success');
|
|
218
|
+
expect(entry.action).toBe('update');
|
|
219
|
+
expect(entry.target).toBe(10);
|
|
220
|
+
expect(entry.target_type).toBe('page');
|
|
221
|
+
});
|
|
222
|
+
});
|