@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,260 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } 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
|
+
// 1. WP_READ_ONLY blocks ALL 13 write tools
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('WP_READ_ONLY=true', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
process.env.WP_READ_ONLY = 'true';
|
|
22
|
+
});
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
delete process.env.WP_READ_ONLY;
|
|
25
|
+
});
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
fetch.mockReset();
|
|
28
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
consoleSpy.mockRestore();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const writeTools = [
|
|
35
|
+
{ name: 'wp_create_post', args: { title: 'T', content: 'C' } },
|
|
36
|
+
{ name: 'wp_update_post', args: { id: 1, title: 'T' } },
|
|
37
|
+
{ name: 'wp_delete_post', args: { id: 1 } },
|
|
38
|
+
{ name: 'wp_create_page', args: { title: 'T', content: 'C' } },
|
|
39
|
+
{ name: 'wp_update_page', args: { id: 1, title: 'T' } },
|
|
40
|
+
{ name: 'wp_upload_media', args: { url: 'https://example.com/img.png' } },
|
|
41
|
+
{ name: 'wp_create_comment', args: { post: 1, content: 'C' } },
|
|
42
|
+
{ name: 'wp_create_taxonomy_term', args: { taxonomy: 'category', name: 'T' } },
|
|
43
|
+
{ name: 'wp_update_seo_meta', args: { id: 1, title: 'SEO title' } },
|
|
44
|
+
{ name: 'wp_activate_plugin', args: { plugin: 'test/test.php' } },
|
|
45
|
+
{ name: 'wp_deactivate_plugin', args: { plugin: 'test/test.php' } },
|
|
46
|
+
{ name: 'wp_restore_revision', args: { post_id: 1, revision_id: 2 } },
|
|
47
|
+
{ name: 'wp_delete_revision', args: { post_id: 1, revision_id: 2 } },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
it.each(writeTools)('blocks $name', async ({ name: toolName, args }) => {
|
|
51
|
+
const result = await call(toolName, args);
|
|
52
|
+
|
|
53
|
+
expect(result.isError).toBe(true);
|
|
54
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
55
|
+
|
|
56
|
+
const logs = getAuditLogs(consoleSpy);
|
|
57
|
+
const entry = logs.find(l => l.tool === toolName);
|
|
58
|
+
expect(entry).toBeDefined();
|
|
59
|
+
expect(entry.status).toBe('blocked');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does NOT block fetch (no network calls made)', () => {
|
|
63
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ────────────────────────────────────────────────────────────
|
|
68
|
+
// 2. WP_DRAFT_ONLY blocks publish status on create/update post
|
|
69
|
+
// ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe('WP_DRAFT_ONLY=true', () => {
|
|
72
|
+
let consoleSpy;
|
|
73
|
+
|
|
74
|
+
beforeAll(() => {
|
|
75
|
+
process.env.WP_DRAFT_ONLY = 'true';
|
|
76
|
+
});
|
|
77
|
+
afterAll(() => {
|
|
78
|
+
delete process.env.WP_DRAFT_ONLY;
|
|
79
|
+
});
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
fetch.mockReset();
|
|
82
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
consoleSpy.mockRestore();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('blocks wp_create_post with status:publish', async () => {
|
|
89
|
+
const result = await call('wp_create_post', { title: 'T', content: 'C', status: 'publish' });
|
|
90
|
+
|
|
91
|
+
expect(result.isError).toBe(true);
|
|
92
|
+
expect(result.content[0].text).toContain('DRAFT-ONLY');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('blocks wp_update_post with status:publish', async () => {
|
|
96
|
+
const result = await call('wp_update_post', { id: 1, status: 'publish' });
|
|
97
|
+
|
|
98
|
+
expect(result.isError).toBe(true);
|
|
99
|
+
expect(result.content[0].text).toContain('DRAFT-ONLY');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('allows wp_create_post with status:draft', async () => {
|
|
103
|
+
fetch.mockResolvedValue(mockSuccess({ id: 2, title: { rendered: 'T' }, status: 'draft', link: 'https://test.example.com/?p=2', slug: 'test' }));
|
|
104
|
+
const result = await call('wp_create_post', { title: 'T', content: 'C', status: 'draft' });
|
|
105
|
+
|
|
106
|
+
expect(result.isError).toBeUndefined();
|
|
107
|
+
const data = parseResult(result);
|
|
108
|
+
expect(data.success).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ────────────────────────────────────────────────────────────
|
|
113
|
+
// 3. WP_DISABLE_DELETE blocks delete tools
|
|
114
|
+
// ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe('WP_DISABLE_DELETE=true', () => {
|
|
117
|
+
let consoleSpy;
|
|
118
|
+
|
|
119
|
+
beforeAll(() => {
|
|
120
|
+
process.env.WP_DISABLE_DELETE = 'true';
|
|
121
|
+
});
|
|
122
|
+
afterAll(() => {
|
|
123
|
+
delete process.env.WP_DISABLE_DELETE;
|
|
124
|
+
});
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
fetch.mockReset();
|
|
127
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
128
|
+
});
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
consoleSpy.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('blocks wp_delete_post', async () => {
|
|
134
|
+
const result = await call('wp_delete_post', { id: 1 });
|
|
135
|
+
|
|
136
|
+
expect(result.isError).toBe(true);
|
|
137
|
+
expect(result.content[0].text).toContain('WP_DISABLE_DELETE');
|
|
138
|
+
|
|
139
|
+
const logs = getAuditLogs(consoleSpy);
|
|
140
|
+
const entry = logs.find(l => l.tool === 'wp_delete_post');
|
|
141
|
+
expect(entry).toBeDefined();
|
|
142
|
+
expect(entry.status).toBe('blocked');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('blocks wp_delete_revision', async () => {
|
|
146
|
+
const result = await call('wp_delete_revision', { post_id: 1, revision_id: 2 });
|
|
147
|
+
|
|
148
|
+
expect(result.isError).toBe(true);
|
|
149
|
+
expect(result.content[0].text).toContain('WP_DISABLE_DELETE');
|
|
150
|
+
|
|
151
|
+
const logs = getAuditLogs(consoleSpy);
|
|
152
|
+
const entry = logs.find(l => l.tool === 'wp_delete_revision');
|
|
153
|
+
expect(entry).toBeDefined();
|
|
154
|
+
expect(entry.status).toBe('blocked');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ────────────────────────────────────────────────────────────
|
|
159
|
+
// 4. WP_DISABLE_PLUGIN_MANAGEMENT blocks plugin write tools
|
|
160
|
+
// ────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
describe('WP_DISABLE_PLUGIN_MANAGEMENT=true', () => {
|
|
163
|
+
let consoleSpy;
|
|
164
|
+
|
|
165
|
+
beforeAll(() => {
|
|
166
|
+
process.env.WP_DISABLE_PLUGIN_MANAGEMENT = 'true';
|
|
167
|
+
});
|
|
168
|
+
afterAll(() => {
|
|
169
|
+
delete process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
170
|
+
});
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
fetch.mockReset();
|
|
173
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
174
|
+
});
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
consoleSpy.mockRestore();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('blocks wp_activate_plugin', async () => {
|
|
180
|
+
const result = await call('wp_activate_plugin', { plugin: 'test/test.php' });
|
|
181
|
+
|
|
182
|
+
expect(result.isError).toBe(true);
|
|
183
|
+
expect(result.content[0].text).toContain('WP_DISABLE_PLUGIN_MANAGEMENT');
|
|
184
|
+
|
|
185
|
+
const logs = getAuditLogs(consoleSpy);
|
|
186
|
+
const entry = logs.find(l => l.tool === 'wp_activate_plugin');
|
|
187
|
+
expect(entry).toBeDefined();
|
|
188
|
+
expect(entry.status).toBe('blocked');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('blocks wp_deactivate_plugin', async () => {
|
|
192
|
+
const result = await call('wp_deactivate_plugin', { plugin: 'test/test.php' });
|
|
193
|
+
|
|
194
|
+
expect(result.isError).toBe(true);
|
|
195
|
+
expect(result.content[0].text).toContain('WP_DISABLE_PLUGIN_MANAGEMENT');
|
|
196
|
+
|
|
197
|
+
const logs = getAuditLogs(consoleSpy);
|
|
198
|
+
const entry = logs.find(l => l.tool === 'wp_deactivate_plugin');
|
|
199
|
+
expect(entry).toBeDefined();
|
|
200
|
+
expect(entry.status).toBe('blocked');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('does NOT block wp_list_plugins', async () => {
|
|
204
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
205
|
+
{
|
|
206
|
+
plugin: 'test/test.php',
|
|
207
|
+
name: 'Test',
|
|
208
|
+
version: '1.0',
|
|
209
|
+
status: 'active',
|
|
210
|
+
author: 'Test',
|
|
211
|
+
description: { rendered: 'desc' }
|
|
212
|
+
}
|
|
213
|
+
]));
|
|
214
|
+
|
|
215
|
+
const result = await call('wp_list_plugins', {});
|
|
216
|
+
|
|
217
|
+
expect(result.isError).toBeUndefined();
|
|
218
|
+
const data = parseResult(result);
|
|
219
|
+
expect(data.total).toBe(1);
|
|
220
|
+
expect(data.plugins[0].name).toBe('Test');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ────────────────────────────────────────────────────────────
|
|
225
|
+
// 5. Governance flag combinations
|
|
226
|
+
// ────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
describe('Governance flag combinations', () => {
|
|
229
|
+
let consoleSpy;
|
|
230
|
+
|
|
231
|
+
beforeEach(() => {
|
|
232
|
+
fetch.mockReset();
|
|
233
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
234
|
+
});
|
|
235
|
+
afterEach(() => {
|
|
236
|
+
delete process.env.WP_READ_ONLY;
|
|
237
|
+
delete process.env.WP_DISABLE_PLUGIN_MANAGEMENT;
|
|
238
|
+
consoleSpy.mockRestore();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('WP_DISABLE_PLUGIN_MANAGEMENT=true + WP_READ_ONLY=false still blocks wp_activate_plugin', async () => {
|
|
242
|
+
process.env.WP_DISABLE_PLUGIN_MANAGEMENT = 'true';
|
|
243
|
+
process.env.WP_READ_ONLY = 'false';
|
|
244
|
+
|
|
245
|
+
const result = await call('wp_activate_plugin', { plugin: 'test/test.php' });
|
|
246
|
+
|
|
247
|
+
expect(result.isError).toBe(true);
|
|
248
|
+
expect(result.content[0].text).toContain('WP_DISABLE_PLUGIN_MANAGEMENT');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('WP_READ_ONLY=true + WP_DISABLE_PLUGIN_MANAGEMENT=false still blocks wp_activate_plugin (via readOnly)', async () => {
|
|
252
|
+
process.env.WP_READ_ONLY = 'true';
|
|
253
|
+
process.env.WP_DISABLE_PLUGIN_MANAGEMENT = 'false';
|
|
254
|
+
|
|
255
|
+
const result = await call('wp_activate_plugin', { plugin: 'test/test.php' });
|
|
256
|
+
|
|
257
|
+
expect(result.isError).toBe(true);
|
|
258
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
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_comments
|
|
15
|
+
// ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('wp_list_comments', () => {
|
|
18
|
+
let consoleSpy;
|
|
19
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
20
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
21
|
+
|
|
22
|
+
it('returns formatted comment list on success', async () => {
|
|
23
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
24
|
+
{ id: 1, post: 1, parent: 0, author_name: 'John', date: '2024-01-01', status: 'approved', content: { rendered: '<p>Great post!</p>' }, link: 'https://test.example.com/post-1#comment-1' }
|
|
25
|
+
]));
|
|
26
|
+
|
|
27
|
+
const result = await call('wp_list_comments');
|
|
28
|
+
const data = parseResult(result);
|
|
29
|
+
|
|
30
|
+
expect(data.total).toBe(1);
|
|
31
|
+
expect(data.page).toBe(1);
|
|
32
|
+
expect(data.comments).toHaveLength(1);
|
|
33
|
+
expect(data.comments[0].id).toBe(1);
|
|
34
|
+
expect(data.comments[0].post).toBe(1);
|
|
35
|
+
expect(data.comments[0].parent).toBe(0);
|
|
36
|
+
expect(data.comments[0].author_name).toBe('John');
|
|
37
|
+
expect(data.comments[0].date).toBe('2024-01-01');
|
|
38
|
+
expect(data.comments[0].status).toBe('approved');
|
|
39
|
+
expect(data.comments[0].content).toBe('Great post!');
|
|
40
|
+
expect(data.comments[0].link).toBe('https://test.example.com/post-1#comment-1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns error on 403', async () => {
|
|
44
|
+
fetch.mockResolvedValue(mockError(403));
|
|
45
|
+
|
|
46
|
+
const result = await call('wp_list_comments');
|
|
47
|
+
|
|
48
|
+
expect(result.isError).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns error on 404', async () => {
|
|
52
|
+
fetch.mockResolvedValue(mockError(404));
|
|
53
|
+
|
|
54
|
+
const result = await call('wp_list_comments');
|
|
55
|
+
|
|
56
|
+
expect(result.isError).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('logs audit entry with status success', async () => {
|
|
60
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
61
|
+
{ id: 1, post: 1, parent: 0, author_name: 'John', date: '2024-01-01', status: 'approved', content: { rendered: '<p>Great post!</p>' }, link: 'https://test.example.com/post-1#comment-1' }
|
|
62
|
+
]));
|
|
63
|
+
|
|
64
|
+
await call('wp_list_comments');
|
|
65
|
+
const logs = getAuditLogs(consoleSpy);
|
|
66
|
+
const entry = logs.find(l => l.tool === 'wp_list_comments');
|
|
67
|
+
|
|
68
|
+
expect(entry).toBeDefined();
|
|
69
|
+
expect(entry.status).toBe('success');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('passes post filter parameter correctly', async () => {
|
|
73
|
+
fetch.mockResolvedValue(mockSuccess([
|
|
74
|
+
{ id: 1, post: 5, parent: 0, author_name: 'Jane', date: '2024-02-01', status: 'approved', content: { rendered: '<p>Filtered</p>' }, link: 'https://test.example.com/post-5#comment-1' }
|
|
75
|
+
]));
|
|
76
|
+
|
|
77
|
+
const result = await call('wp_list_comments', { post: 5 });
|
|
78
|
+
const data = parseResult(result);
|
|
79
|
+
|
|
80
|
+
expect(data.total).toBe(1);
|
|
81
|
+
expect(data.comments[0].post).toBe(5);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ────────────────────────────────────────────────────────────
|
|
86
|
+
// wp_create_comment
|
|
87
|
+
// ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe('wp_create_comment', () => {
|
|
90
|
+
let consoleSpy;
|
|
91
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
|
|
92
|
+
afterEach(() => { consoleSpy.mockRestore(); });
|
|
93
|
+
|
|
94
|
+
it('creates a comment on success', async () => {
|
|
95
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
96
|
+
id: 2, post: 1, status: 'approved', content: { rendered: '<p>Nice article</p>' }
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const result = await call('wp_create_comment', { post: 1, content: 'Nice article' });
|
|
100
|
+
const data = parseResult(result);
|
|
101
|
+
|
|
102
|
+
expect(data.success).toBe(true);
|
|
103
|
+
expect(data.message).toBe('Comment created');
|
|
104
|
+
expect(data.comment.id).toBe(2);
|
|
105
|
+
expect(data.comment.post).toBe(1);
|
|
106
|
+
expect(data.comment.status).toBe('approved');
|
|
107
|
+
expect(data.comment.content).toBe('Nice article');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('is blocked in READ-ONLY mode (governance)', async () => {
|
|
111
|
+
process.env.WP_READ_ONLY = 'true';
|
|
112
|
+
|
|
113
|
+
const result = await call('wp_create_comment', { post: 1, content: 'Blocked comment' });
|
|
114
|
+
|
|
115
|
+
expect(result.isError).toBe(true);
|
|
116
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
117
|
+
|
|
118
|
+
delete process.env.WP_READ_ONLY;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns error on 403', async () => {
|
|
122
|
+
fetch.mockResolvedValue(mockError(403));
|
|
123
|
+
|
|
124
|
+
const result = await call('wp_create_comment', { post: 1, content: 'Forbidden' });
|
|
125
|
+
|
|
126
|
+
expect(result.isError).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns error on 404', async () => {
|
|
130
|
+
fetch.mockResolvedValue(mockError(404));
|
|
131
|
+
|
|
132
|
+
const result = await call('wp_create_comment', { post: 999, content: 'Missing post' });
|
|
133
|
+
|
|
134
|
+
expect(result.isError).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('logs audit entry with status success on create', async () => {
|
|
138
|
+
fetch.mockResolvedValue(mockSuccess({
|
|
139
|
+
id: 2, post: 1, status: 'approved', content: { rendered: '<p>Nice article</p>' }
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
await call('wp_create_comment', { post: 1, content: 'Nice article' });
|
|
143
|
+
const logs = getAuditLogs(consoleSpy);
|
|
144
|
+
const entry = logs.find(l => l.tool === 'wp_create_comment');
|
|
145
|
+
|
|
146
|
+
expect(entry).toBeDefined();
|
|
147
|
+
expect(entry.status).toBe('success');
|
|
148
|
+
expect(entry.target).toBe(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('logs audit entry with status blocked in READ-ONLY mode', async () => {
|
|
152
|
+
process.env.WP_READ_ONLY = 'true';
|
|
153
|
+
|
|
154
|
+
await call('wp_create_comment', { post: 1, content: 'Blocked' });
|
|
155
|
+
const logs = getAuditLogs(consoleSpy);
|
|
156
|
+
const entry = logs.find(l => l.tool === 'wp_create_comment');
|
|
157
|
+
|
|
158
|
+
expect(entry).toBeDefined();
|
|
159
|
+
expect(entry.status).toBe('blocked');
|
|
160
|
+
|
|
161
|
+
delete process.env.WP_READ_ONLY;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('requires post and content fields', async () => {
|
|
165
|
+
const result = await call('wp_create_comment', {});
|
|
166
|
+
|
|
167
|
+
expect(result.isError).toBe(true);
|
|
168
|
+
expect(result.content[0].text).toContain('post');
|
|
169
|
+
});
|
|
170
|
+
});
|