@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,268 @@
|
|
|
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
|
+
let consoleSpy;
|
|
14
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
15
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
16
|
+
|
|
17
|
+
// =========================================================================
|
|
18
|
+
// Shared mock data
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
const pluginsList = [
|
|
22
|
+
{
|
|
23
|
+
plugin: 'akismet/akismet.php',
|
|
24
|
+
name: 'Akismet',
|
|
25
|
+
version: '5.3',
|
|
26
|
+
status: 'active',
|
|
27
|
+
author: { rendered: 'Automattic' },
|
|
28
|
+
description: { rendered: '<p>Anti-spam plugin</p>' },
|
|
29
|
+
plugin_uri: 'https://akismet.com',
|
|
30
|
+
requires_wp: '6.0',
|
|
31
|
+
requires_php: '7.4',
|
|
32
|
+
network_only: false,
|
|
33
|
+
textdomain: 'akismet',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
plugin: 'hello-dolly/hello.php',
|
|
37
|
+
name: 'Hello Dolly',
|
|
38
|
+
version: '1.7',
|
|
39
|
+
status: 'inactive',
|
|
40
|
+
author: { rendered: 'Matt Mullenweg' },
|
|
41
|
+
description: { rendered: '<p>Hello Dolly</p>' },
|
|
42
|
+
plugin_uri: '',
|
|
43
|
+
requires_wp: '',
|
|
44
|
+
requires_php: '',
|
|
45
|
+
network_only: false,
|
|
46
|
+
textdomain: 'hello-dolly',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// =========================================================================
|
|
51
|
+
// wp_list_plugins
|
|
52
|
+
// =========================================================================
|
|
53
|
+
|
|
54
|
+
describe('wp_list_plugins', () => {
|
|
55
|
+
it('SUCCESS — returns total, active, inactive counts and plugins', async () => {
|
|
56
|
+
mockSuccess(pluginsList);
|
|
57
|
+
|
|
58
|
+
const res = await call('wp_list_plugins');
|
|
59
|
+
const data = parseResult(res);
|
|
60
|
+
|
|
61
|
+
expect(data.total).toBe(2);
|
|
62
|
+
expect(data.active).toBe(1);
|
|
63
|
+
expect(data.inactive).toBe(1);
|
|
64
|
+
expect(data.plugins).toHaveLength(2);
|
|
65
|
+
|
|
66
|
+
const akismet = data.plugins.find(p => p.plugin === 'akismet/akismet.php');
|
|
67
|
+
expect(akismet.name).toBe('Akismet');
|
|
68
|
+
expect(akismet.version).toBe('5.3');
|
|
69
|
+
expect(akismet.status).toBe('active');
|
|
70
|
+
expect(akismet.description).toBe('Anti-spam plugin');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('SUCCESS — status filter is passed to URL', async () => {
|
|
74
|
+
mockSuccess([pluginsList[0]]);
|
|
75
|
+
|
|
76
|
+
await call('wp_list_plugins', { status: 'active' });
|
|
77
|
+
|
|
78
|
+
// Verify the fetch URL includes the status parameter
|
|
79
|
+
const fetchUrl = fetch.mock.calls[0][0];
|
|
80
|
+
expect(fetchUrl).toContain('status=active');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('ERROR 403 — returns specific Administrator role message', async () => {
|
|
84
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
85
|
+
|
|
86
|
+
const res = await call('wp_list_plugins');
|
|
87
|
+
expect(res.isError).toBe(true);
|
|
88
|
+
expect(res.content[0].text).toContain('Administrator role');
|
|
89
|
+
expect(res.content[0].text).toContain('activate_plugins');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('AUDIT — logs list action on success', async () => {
|
|
93
|
+
mockSuccess(pluginsList);
|
|
94
|
+
|
|
95
|
+
await call('wp_list_plugins');
|
|
96
|
+
|
|
97
|
+
const logs = getAuditLogs();
|
|
98
|
+
const entry = logs.find(l => l.tool === 'wp_list_plugins');
|
|
99
|
+
expect(entry).toBeDefined();
|
|
100
|
+
expect(entry.status).toBe('success');
|
|
101
|
+
expect(entry.action).toBe('list');
|
|
102
|
+
expect(entry.target_type).toBe('plugin');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// =========================================================================
|
|
107
|
+
// wp_activate_plugin
|
|
108
|
+
// =========================================================================
|
|
109
|
+
|
|
110
|
+
describe('wp_activate_plugin', () => {
|
|
111
|
+
const activatedPlugin = {
|
|
112
|
+
plugin: 'hello-dolly/hello.php',
|
|
113
|
+
name: 'Hello Dolly',
|
|
114
|
+
version: '1.7',
|
|
115
|
+
status: 'active',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
it('SUCCESS — activates plugin and returns status active', async () => {
|
|
119
|
+
mockSuccess(activatedPlugin);
|
|
120
|
+
|
|
121
|
+
const res = await call('wp_activate_plugin', { plugin: 'hello-dolly/hello.php' });
|
|
122
|
+
const data = parseResult(res);
|
|
123
|
+
|
|
124
|
+
expect(data.success).toBe(true);
|
|
125
|
+
expect(data.plugin.status).toBe('active');
|
|
126
|
+
expect(data.plugin.name).toBe('Hello Dolly');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('GOVERNANCE — blocked by WP_READ_ONLY', async () => {
|
|
130
|
+
const original = process.env.WP_READ_ONLY;
|
|
131
|
+
process.env.WP_READ_ONLY = 'true';
|
|
132
|
+
try {
|
|
133
|
+
const res = await call('wp_activate_plugin', { plugin: 'hello-dolly/hello.php' });
|
|
134
|
+
expect(res.isError).toBe(true);
|
|
135
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
136
|
+
expect(res.content[0].text).toContain('wp_activate_plugin');
|
|
137
|
+
} finally {
|
|
138
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
139
|
+
else process.env.WP_READ_ONLY = original;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('GOVERNANCE — blocked by WP_DISABLE_PLUGIN_MANAGEMENT', async () => {
|
|
144
|
+
const original = process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
145
|
+
process.env.WP_DISABLE_PLUGIN_MANAGEMENT = 'true';
|
|
146
|
+
try {
|
|
147
|
+
const res = await call('wp_activate_plugin', { plugin: 'hello-dolly/hello.php' });
|
|
148
|
+
expect(res.isError).toBe(true);
|
|
149
|
+
expect(res.content[0].text).toContain('WP_DISABLE_PLUGIN_MANAGEMENT');
|
|
150
|
+
expect(res.content[0].text).toContain('wp_activate_plugin');
|
|
151
|
+
} finally {
|
|
152
|
+
if (original === undefined) delete process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
153
|
+
else process.env.WP_DISABLE_PLUGIN_MANAGEMENT = original;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('ERROR 403 — returns specific Administrator role message', async () => {
|
|
158
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
159
|
+
|
|
160
|
+
const res = await call('wp_activate_plugin', { plugin: 'hello-dolly/hello.php' });
|
|
161
|
+
expect(res.isError).toBe(true);
|
|
162
|
+
expect(res.content[0].text).toContain('Administrator role');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('ERROR 404 — returns message mentioning wp_list_plugins', async () => {
|
|
166
|
+
mockError(404, '{"code":"rest_plugin_not_found"}');
|
|
167
|
+
|
|
168
|
+
const res = await call('wp_activate_plugin', { plugin: 'nonexistent/plugin.php' });
|
|
169
|
+
expect(res.isError).toBe(true);
|
|
170
|
+
expect(res.content[0].text).toContain('wp_list_plugins');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('AUDIT — logs activate action on success', async () => {
|
|
174
|
+
mockSuccess(activatedPlugin);
|
|
175
|
+
|
|
176
|
+
await call('wp_activate_plugin', { plugin: 'hello-dolly/hello.php' });
|
|
177
|
+
|
|
178
|
+
const logs = getAuditLogs();
|
|
179
|
+
const entry = logs.find(l => l.tool === 'wp_activate_plugin');
|
|
180
|
+
expect(entry).toBeDefined();
|
|
181
|
+
expect(entry.status).toBe('success');
|
|
182
|
+
expect(entry.action).toBe('activate');
|
|
183
|
+
expect(entry.target).toBe('hello-dolly/hello.php');
|
|
184
|
+
expect(entry.target_type).toBe('plugin');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// =========================================================================
|
|
189
|
+
// wp_deactivate_plugin
|
|
190
|
+
// =========================================================================
|
|
191
|
+
|
|
192
|
+
describe('wp_deactivate_plugin', () => {
|
|
193
|
+
const deactivatedPlugin = {
|
|
194
|
+
plugin: 'akismet/akismet.php',
|
|
195
|
+
name: 'Akismet',
|
|
196
|
+
version: '5.3',
|
|
197
|
+
status: 'inactive',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
it('SUCCESS — deactivates plugin and returns status inactive', async () => {
|
|
201
|
+
mockSuccess(deactivatedPlugin);
|
|
202
|
+
|
|
203
|
+
const res = await call('wp_deactivate_plugin', { plugin: 'akismet/akismet.php' });
|
|
204
|
+
const data = parseResult(res);
|
|
205
|
+
|
|
206
|
+
expect(data.success).toBe(true);
|
|
207
|
+
expect(data.plugin.status).toBe('inactive');
|
|
208
|
+
expect(data.plugin.name).toBe('Akismet');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('GOVERNANCE — blocked by WP_READ_ONLY', async () => {
|
|
212
|
+
const original = process.env.WP_READ_ONLY;
|
|
213
|
+
process.env.WP_READ_ONLY = 'true';
|
|
214
|
+
try {
|
|
215
|
+
const res = await call('wp_deactivate_plugin', { plugin: 'akismet/akismet.php' });
|
|
216
|
+
expect(res.isError).toBe(true);
|
|
217
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
218
|
+
expect(res.content[0].text).toContain('wp_deactivate_plugin');
|
|
219
|
+
} finally {
|
|
220
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
221
|
+
else process.env.WP_READ_ONLY = original;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('GOVERNANCE — blocked by WP_DISABLE_PLUGIN_MANAGEMENT', async () => {
|
|
226
|
+
const original = process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
227
|
+
process.env.WP_DISABLE_PLUGIN_MANAGEMENT = 'true';
|
|
228
|
+
try {
|
|
229
|
+
const res = await call('wp_deactivate_plugin', { plugin: 'akismet/akismet.php' });
|
|
230
|
+
expect(res.isError).toBe(true);
|
|
231
|
+
expect(res.content[0].text).toContain('WP_DISABLE_PLUGIN_MANAGEMENT');
|
|
232
|
+
expect(res.content[0].text).toContain('wp_deactivate_plugin');
|
|
233
|
+
} finally {
|
|
234
|
+
if (original === undefined) delete process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
235
|
+
else process.env.WP_DISABLE_PLUGIN_MANAGEMENT = original;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('ERROR 403 — returns specific Administrator role message', async () => {
|
|
240
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
241
|
+
|
|
242
|
+
const res = await call('wp_deactivate_plugin', { plugin: 'akismet/akismet.php' });
|
|
243
|
+
expect(res.isError).toBe(true);
|
|
244
|
+
expect(res.content[0].text).toContain('Administrator role');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('ERROR 404 — returns message mentioning wp_list_plugins', async () => {
|
|
248
|
+
mockError(404, '{"code":"rest_plugin_not_found"}');
|
|
249
|
+
|
|
250
|
+
const res = await call('wp_deactivate_plugin', { plugin: 'nonexistent/plugin.php' });
|
|
251
|
+
expect(res.isError).toBe(true);
|
|
252
|
+
expect(res.content[0].text).toContain('wp_list_plugins');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('AUDIT — logs deactivate action on success', async () => {
|
|
256
|
+
mockSuccess(deactivatedPlugin);
|
|
257
|
+
|
|
258
|
+
await call('wp_deactivate_plugin', { plugin: 'akismet/akismet.php' });
|
|
259
|
+
|
|
260
|
+
const logs = getAuditLogs();
|
|
261
|
+
const entry = logs.find(l => l.tool === 'wp_deactivate_plugin');
|
|
262
|
+
expect(entry).toBeDefined();
|
|
263
|
+
expect(entry.status).toBe('success');
|
|
264
|
+
expect(entry.action).toBe('deactivate');
|
|
265
|
+
expect(entry.target).toBe('akismet/akismet.php');
|
|
266
|
+
expect(entry.target_type).toBe('plugin');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
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_posts
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('wp_list_posts', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
20
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
21
|
+
|
|
22
|
+
it('returns formatted post list on success', async () => {
|
|
23
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
24
|
+
{ id: 1, title: { rendered: 'Post 1' }, status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/post-1', author: 1, categories: [1], tags: [], excerpt: { rendered: 'excerpt' } }
|
|
25
|
+
]));
|
|
26
|
+
|
|
27
|
+
const result = await call('wp_list_posts');
|
|
28
|
+
const data = parseResult(result);
|
|
29
|
+
|
|
30
|
+
expect(data.total).toBe(1);
|
|
31
|
+
expect(data.posts[0].id).toBe(1);
|
|
32
|
+
expect(data.posts[0].title).toBe('Post 1');
|
|
33
|
+
expect(data.posts[0].status).toBe('publish');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('logs audit entry with status success', async () => {
|
|
37
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
38
|
+
{ id: 1, title: { rendered: 'Post 1' }, status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/post-1', author: 1, categories: [1], tags: [], excerpt: { rendered: 'excerpt' } }
|
|
39
|
+
]));
|
|
40
|
+
|
|
41
|
+
await call('wp_list_posts');
|
|
42
|
+
const logs = getAuditLogs(consoleSpy);
|
|
43
|
+
const entry = logs.find(l => l.tool === 'wp_list_posts');
|
|
44
|
+
|
|
45
|
+
expect(entry).toBeDefined();
|
|
46
|
+
expect(entry.status).toBe('success');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ────────────────────────────────────────────────────────────
|
|
51
|
+
// wp_get_post
|
|
52
|
+
// ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe('wp_get_post', () => {
|
|
55
|
+
let consoleSpy;
|
|
56
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
57
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
58
|
+
|
|
59
|
+
it('returns full post data on success', async () => {
|
|
60
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
61
|
+
id: 1, title: { rendered: 'Post 1' }, content: { rendered: '<p>Content</p>' }, excerpt: { rendered: 'excerpt' },
|
|
62
|
+
status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/post-1',
|
|
63
|
+
slug: 'post-1', categories: [1], tags: [], author: 1, featured_media: 0, comment_status: 'open', meta: {}
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const result = await call('wp_get_post', { id: 1 });
|
|
67
|
+
const data = parseResult(result);
|
|
68
|
+
|
|
69
|
+
expect(data.id).toBe(1);
|
|
70
|
+
expect(data.title).toBe('Post 1');
|
|
71
|
+
expect(data.content).toBe('<p>Content</p>');
|
|
72
|
+
expect(data.slug).toBe('post-1');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns error on 404', async () => {
|
|
76
|
+
fetch.mockResolvedValue(mockError(404));
|
|
77
|
+
|
|
78
|
+
const result = await call('wp_get_post', { id: 999 });
|
|
79
|
+
|
|
80
|
+
expect(result.isError).toBe(true);
|
|
81
|
+
expect(result.content[0].text).toContain('404');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('logs audit with tool name', async () => {
|
|
85
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
86
|
+
id: 1, title: { rendered: 'Post 1' }, content: { rendered: '<p>Content</p>' }, excerpt: { rendered: 'excerpt' },
|
|
87
|
+
status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/post-1',
|
|
88
|
+
slug: 'post-1', categories: [1], tags: [], author: 1, featured_media: 0, comment_status: 'open', meta: {}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
await call('wp_get_post', { id: 1 });
|
|
92
|
+
const logs = getAuditLogs(consoleSpy);
|
|
93
|
+
const entry = logs.find(l => l.tool === 'wp_get_post');
|
|
94
|
+
|
|
95
|
+
expect(entry).toBeDefined();
|
|
96
|
+
expect(entry.status).toBe('success');
|
|
97
|
+
expect(entry.target).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ────────────────────────────────────────────────────────────
|
|
102
|
+
// wp_create_post
|
|
103
|
+
// ────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('wp_create_post', () => {
|
|
106
|
+
let consoleSpy;
|
|
107
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
108
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
109
|
+
|
|
110
|
+
it('creates a post and returns success shape', async () => {
|
|
111
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
112
|
+
id: 2, title: { rendered: 'New Post' }, status: 'draft', link: 'https://test.example.com/?p=2', slug: 'new-post'
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const result = await call('wp_create_post', { title: 'New Post', content: 'Body text' });
|
|
116
|
+
const data = parseResult(result);
|
|
117
|
+
|
|
118
|
+
expect(data.success).toBe(true);
|
|
119
|
+
expect(data.post.id).toBe(2);
|
|
120
|
+
expect(data.post.title).toBe('New Post');
|
|
121
|
+
expect(data.post.status).toBe('draft');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
125
|
+
process.env.WP_READ_ONLY = 'true';
|
|
126
|
+
try {
|
|
127
|
+
const result = await call('wp_create_post', { title: 'T', content: 'C' });
|
|
128
|
+
|
|
129
|
+
expect(result.isError).toBe(true);
|
|
130
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
131
|
+
|
|
132
|
+
const logs = getAuditLogs(consoleSpy);
|
|
133
|
+
const entry = logs.find(l => l.tool === 'wp_create_post');
|
|
134
|
+
expect(entry).toBeDefined();
|
|
135
|
+
expect(entry.status).toBe('blocked');
|
|
136
|
+
} finally {
|
|
137
|
+
delete process.env.WP_READ_ONLY;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('logs audit on success', async () => {
|
|
142
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
143
|
+
id: 2, title: { rendered: 'New Post' }, status: 'draft', link: 'https://test.example.com/?p=2', slug: 'new-post'
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
await call('wp_create_post', { title: 'New Post', content: 'Body' });
|
|
147
|
+
const logs = getAuditLogs(consoleSpy);
|
|
148
|
+
const entry = logs.find(l => l.tool === 'wp_create_post');
|
|
149
|
+
|
|
150
|
+
expect(entry).toBeDefined();
|
|
151
|
+
expect(entry.status).toBe('success');
|
|
152
|
+
expect(entry.action).toBe('create');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ────────────────────────────────────────────────────────────
|
|
157
|
+
// wp_update_post
|
|
158
|
+
// ────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('wp_update_post', () => {
|
|
161
|
+
let consoleSpy;
|
|
162
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
163
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
164
|
+
|
|
165
|
+
it('updates a post and returns success shape', async () => {
|
|
166
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
167
|
+
id: 1, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/post-1', modified: '2024-01-02'
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
const result = await call('wp_update_post', { id: 1, title: 'Updated' });
|
|
171
|
+
const data = parseResult(result);
|
|
172
|
+
|
|
173
|
+
expect(data.success).toBe(true);
|
|
174
|
+
expect(data.post.id).toBe(1);
|
|
175
|
+
expect(data.post.title).toBe('Updated');
|
|
176
|
+
expect(data.post.modified).toBe('2024-01-02');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns error on 404', async () => {
|
|
180
|
+
fetch.mockResolvedValue(mockError(404));
|
|
181
|
+
|
|
182
|
+
const result = await call('wp_update_post', { id: 999, title: 'Ghost' });
|
|
183
|
+
|
|
184
|
+
expect(result.isError).toBe(true);
|
|
185
|
+
expect(result.content[0].text).toContain('404');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
189
|
+
process.env.WP_READ_ONLY = 'true';
|
|
190
|
+
try {
|
|
191
|
+
const result = await call('wp_update_post', { id: 1, title: 'T' });
|
|
192
|
+
|
|
193
|
+
expect(result.isError).toBe(true);
|
|
194
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
195
|
+
} finally {
|
|
196
|
+
delete process.env.WP_READ_ONLY;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('logs audit with tool name and target', async () => {
|
|
201
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
202
|
+
id: 1, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/post-1', modified: '2024-01-02'
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
await call('wp_update_post', { id: 1, title: 'Updated' });
|
|
206
|
+
const logs = getAuditLogs(consoleSpy);
|
|
207
|
+
const entry = logs.find(l => l.tool === 'wp_update_post');
|
|
208
|
+
|
|
209
|
+
expect(entry).toBeDefined();
|
|
210
|
+
expect(entry.status).toBe('success');
|
|
211
|
+
expect(entry.target).toBe(1);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ────────────────────────────────────────────────────────────
|
|
216
|
+
// wp_delete_post
|
|
217
|
+
// ────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('wp_delete_post', () => {
|
|
220
|
+
let consoleSpy;
|
|
221
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
222
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
223
|
+
|
|
224
|
+
it('trashes a post and returns success shape', async () => {
|
|
225
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
226
|
+
id: 1, title: { rendered: 'Post 1' }, status: 'trash'
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
const result = await call('wp_delete_post', { id: 1 });
|
|
230
|
+
const data = parseResult(result);
|
|
231
|
+
|
|
232
|
+
expect(data.success).toBe(true);
|
|
233
|
+
expect(data.post.id).toBe(1);
|
|
234
|
+
expect(data.post.status).toBe('trash');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('returns error on 404', async () => {
|
|
238
|
+
fetch.mockResolvedValue(mockError(404));
|
|
239
|
+
|
|
240
|
+
const result = await call('wp_delete_post', { id: 999 });
|
|
241
|
+
|
|
242
|
+
expect(result.isError).toBe(true);
|
|
243
|
+
expect(result.content[0].text).toContain('404');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('is blocked by WP_READ_ONLY', async () => {
|
|
247
|
+
process.env.WP_READ_ONLY = 'true';
|
|
248
|
+
try {
|
|
249
|
+
const result = await call('wp_delete_post', { id: 1 });
|
|
250
|
+
|
|
251
|
+
expect(result.isError).toBe(true);
|
|
252
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
253
|
+
} finally {
|
|
254
|
+
delete process.env.WP_READ_ONLY;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('logs audit with correct action', async () => {
|
|
259
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
260
|
+
id: 1, title: { rendered: 'Post 1' }, status: 'trash'
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
await call('wp_delete_post', { id: 1 });
|
|
264
|
+
const logs = getAuditLogs(consoleSpy);
|
|
265
|
+
const entry = logs.find(l => l.tool === 'wp_delete_post');
|
|
266
|
+
|
|
267
|
+
expect(entry).toBeDefined();
|
|
268
|
+
expect(entry.status).toBe('success');
|
|
269
|
+
expect(entry.action).toBe('trash');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ────────────────────────────────────────────────────────────
|
|
274
|
+
// wp_search
|
|
275
|
+
// ────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
describe('wp_search', () => {
|
|
278
|
+
let consoleSpy;
|
|
279
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
280
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
281
|
+
|
|
282
|
+
it('returns search results on success', async () => {
|
|
283
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
284
|
+
{ id: 1, title: 'Post 1', url: 'https://test.example.com/post-1', type: 'post', subtype: 'post' }
|
|
285
|
+
]));
|
|
286
|
+
|
|
287
|
+
const result = await call('wp_search', { search: 'Post' });
|
|
288
|
+
const data = parseResult(result);
|
|
289
|
+
|
|
290
|
+
expect(data.query).toBe('Post');
|
|
291
|
+
expect(data.total).toBe(1);
|
|
292
|
+
expect(data.results[0].id).toBe(1);
|
|
293
|
+
expect(data.results[0].title).toBe('Post 1');
|
|
294
|
+
expect(data.results[0].type).toBe('post');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('logs audit with search action', async () => {
|
|
298
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
299
|
+
{ id: 1, title: 'Post 1', url: 'https://test.example.com/post-1', type: 'post', subtype: 'post' }
|
|
300
|
+
]));
|
|
301
|
+
|
|
302
|
+
await call('wp_search', { search: 'Post' });
|
|
303
|
+
const logs = getAuditLogs(consoleSpy);
|
|
304
|
+
const entry = logs.find(l => l.tool === 'wp_search');
|
|
305
|
+
|
|
306
|
+
expect(entry).toBeDefined();
|
|
307
|
+
expect(entry.status).toBe('success');
|
|
308
|
+
expect(entry.action).toBe('search');
|
|
309
|
+
});
|
|
310
|
+
});
|