@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,299 @@
|
|
|
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 revisionsList = [
|
|
22
|
+
{
|
|
23
|
+
id: 101,
|
|
24
|
+
parent: 1,
|
|
25
|
+
date: '2024-01-01',
|
|
26
|
+
date_gmt: '2024-01-01T00:00:00',
|
|
27
|
+
modified: '2024-01-01',
|
|
28
|
+
modified_gmt: '2024-01-01T00:00:00',
|
|
29
|
+
author: 1,
|
|
30
|
+
title: { rendered: 'Revision 1' },
|
|
31
|
+
excerpt: { rendered: '<p>Excerpt</p>' },
|
|
32
|
+
slug: '1-revision-v1',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 102,
|
|
36
|
+
parent: 1,
|
|
37
|
+
date: '2024-01-02',
|
|
38
|
+
date_gmt: '2024-01-02T00:00:00',
|
|
39
|
+
modified: '2024-01-02',
|
|
40
|
+
modified_gmt: '2024-01-02T00:00:00',
|
|
41
|
+
author: 1,
|
|
42
|
+
title: { rendered: 'Revision 2' },
|
|
43
|
+
excerpt: { rendered: '' },
|
|
44
|
+
slug: '1-revision-v2',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const singleRevision = {
|
|
49
|
+
id: 101,
|
|
50
|
+
parent: 1,
|
|
51
|
+
date: '2024-01-01',
|
|
52
|
+
author: 1,
|
|
53
|
+
title: { rendered: 'Revision 1' },
|
|
54
|
+
content: { rendered: '<p>Old content</p>' },
|
|
55
|
+
excerpt: { rendered: '<p>Old excerpt</p>' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const restoreRevisionData = {
|
|
59
|
+
id: 101,
|
|
60
|
+
parent: 1,
|
|
61
|
+
title: { raw: 'Old Title', rendered: 'Old Title' },
|
|
62
|
+
content: { raw: '<p>Old content</p>', rendered: '<p>Old content</p>' },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const restoreUpdateResponse = {
|
|
66
|
+
id: 1,
|
|
67
|
+
title: { rendered: 'Old Title' },
|
|
68
|
+
status: 'publish',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// =========================================================================
|
|
72
|
+
// wp_list_revisions
|
|
73
|
+
// =========================================================================
|
|
74
|
+
|
|
75
|
+
describe('wp_list_revisions', () => {
|
|
76
|
+
it('SUCCESS — returns total, post_id, post_type, note, and revisions without content', async () => {
|
|
77
|
+
mockSuccess(revisionsList);
|
|
78
|
+
|
|
79
|
+
const res = await call('wp_list_revisions', { post_id: 1 });
|
|
80
|
+
const data = parseResult(res);
|
|
81
|
+
|
|
82
|
+
expect(data.total).toBe(2);
|
|
83
|
+
expect(data.post_id).toBe(1);
|
|
84
|
+
expect(data.post_type).toBe('post');
|
|
85
|
+
expect(data.note).toContain('wp_get_revision');
|
|
86
|
+
expect(data.revisions).toHaveLength(2);
|
|
87
|
+
|
|
88
|
+
// Revisions should not include content field
|
|
89
|
+
const rev = data.revisions[0];
|
|
90
|
+
expect(rev.id).toBe(101);
|
|
91
|
+
expect(rev.parent).toBe(1);
|
|
92
|
+
expect(rev.date).toBe('2024-01-01');
|
|
93
|
+
expect(rev.author).toBe(1);
|
|
94
|
+
expect(rev.title).toBe('Revision 1');
|
|
95
|
+
expect(rev.slug).toBe('1-revision-v1');
|
|
96
|
+
expect(rev).not.toHaveProperty('content');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('ERROR 404 — returns message about "no revisions available"', async () => {
|
|
100
|
+
mockError(404, '{"code":"rest_post_not_found"}');
|
|
101
|
+
|
|
102
|
+
const res = await call('wp_list_revisions', { post_id: 999 });
|
|
103
|
+
expect(res.isError).toBe(true);
|
|
104
|
+
expect(res.content[0].text).toContain('no revisions available');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('AUDIT — logs list action on success', async () => {
|
|
108
|
+
mockSuccess(revisionsList);
|
|
109
|
+
|
|
110
|
+
await call('wp_list_revisions', { post_id: 1 });
|
|
111
|
+
|
|
112
|
+
const logs = getAuditLogs();
|
|
113
|
+
const entry = logs.find(l => l.tool === 'wp_list_revisions');
|
|
114
|
+
expect(entry).toBeDefined();
|
|
115
|
+
expect(entry.status).toBe('success');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// =========================================================================
|
|
120
|
+
// wp_get_revision
|
|
121
|
+
// =========================================================================
|
|
122
|
+
|
|
123
|
+
describe('wp_get_revision', () => {
|
|
124
|
+
it('SUCCESS — returns full revision with content', async () => {
|
|
125
|
+
mockSuccess(singleRevision);
|
|
126
|
+
|
|
127
|
+
const res = await call('wp_get_revision', { post_id: 1, revision_id: 101 });
|
|
128
|
+
const data = parseResult(res);
|
|
129
|
+
|
|
130
|
+
expect(data.id).toBe(101);
|
|
131
|
+
expect(data.parent).toBe(1);
|
|
132
|
+
expect(data.date).toBe('2024-01-01');
|
|
133
|
+
expect(data.author).toBe(1);
|
|
134
|
+
expect(data.title).toBe('Revision 1');
|
|
135
|
+
expect(data.content).toBe('<p>Old content</p>');
|
|
136
|
+
expect(data.excerpt).toBe('<p>Old excerpt</p>');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('ERROR 404 — returns message mentioning wp_list_revisions', async () => {
|
|
140
|
+
mockError(404, '{"code":"rest_revision_not_found"}');
|
|
141
|
+
|
|
142
|
+
const res = await call('wp_get_revision', { post_id: 1, revision_id: 999 });
|
|
143
|
+
expect(res.isError).toBe(true);
|
|
144
|
+
expect(res.content[0].text).toContain('wp_list_revisions');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('AUDIT — logs read action on success', async () => {
|
|
148
|
+
mockSuccess(singleRevision);
|
|
149
|
+
|
|
150
|
+
await call('wp_get_revision', { post_id: 1, revision_id: 101 });
|
|
151
|
+
|
|
152
|
+
const logs = getAuditLogs();
|
|
153
|
+
const entry = logs.find(l => l.tool === 'wp_get_revision');
|
|
154
|
+
expect(entry).toBeDefined();
|
|
155
|
+
expect(entry.status).toBe('success');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// =========================================================================
|
|
160
|
+
// wp_restore_revision
|
|
161
|
+
// =========================================================================
|
|
162
|
+
|
|
163
|
+
describe('wp_restore_revision', () => {
|
|
164
|
+
it('SUCCESS — restores revision and verifies PUT/POST call with revision data', async () => {
|
|
165
|
+
// First call: GET revision data
|
|
166
|
+
mockSuccess(restoreRevisionData);
|
|
167
|
+
// Second call: POST update to post
|
|
168
|
+
mockSuccess(restoreUpdateResponse);
|
|
169
|
+
|
|
170
|
+
const res = await call('wp_restore_revision', { post_id: 1, revision_id: 101 });
|
|
171
|
+
const data = parseResult(res);
|
|
172
|
+
|
|
173
|
+
expect(data.restored).toBe(true);
|
|
174
|
+
expect(data.post_id).toBe(1);
|
|
175
|
+
expect(data.revision_id).toBe(101);
|
|
176
|
+
expect(data.title).toBe('Old Title');
|
|
177
|
+
|
|
178
|
+
// Verify the second fetch call sent the revision's title and content
|
|
179
|
+
expect(fetch.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
180
|
+
const updateCall = fetch.mock.calls[1];
|
|
181
|
+
const body = JSON.parse(updateCall[1].body);
|
|
182
|
+
expect(body.title).toBe('Old Title');
|
|
183
|
+
expect(body.content).toBe('<p>Old content</p>');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('GOVERNANCE — blocked by WP_READ_ONLY', async () => {
|
|
187
|
+
const original = process.env.WP_READ_ONLY;
|
|
188
|
+
process.env.WP_READ_ONLY = 'true';
|
|
189
|
+
try {
|
|
190
|
+
const res = await call('wp_restore_revision', { post_id: 1, revision_id: 101 });
|
|
191
|
+
expect(res.isError).toBe(true);
|
|
192
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
193
|
+
} finally {
|
|
194
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
195
|
+
else process.env.WP_READ_ONLY = original;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('ERROR 404 on GET revision — does not attempt PUT', async () => {
|
|
200
|
+
mockError(404, '{"code":"rest_revision_not_found"}');
|
|
201
|
+
|
|
202
|
+
const res = await call('wp_restore_revision', { post_id: 1, revision_id: 999 });
|
|
203
|
+
expect(res.isError).toBe(true);
|
|
204
|
+
|
|
205
|
+
// Only one fetch call should have been made (the GET), no PUT/POST
|
|
206
|
+
expect(fetch.mock.calls).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('AUDIT — logs entries on success', async () => {
|
|
210
|
+
mockSuccess(restoreRevisionData);
|
|
211
|
+
mockSuccess(restoreUpdateResponse);
|
|
212
|
+
|
|
213
|
+
await call('wp_restore_revision', { post_id: 1, revision_id: 101 });
|
|
214
|
+
|
|
215
|
+
const logs = getAuditLogs();
|
|
216
|
+
const entries = logs.filter(l => l.tool === 'wp_restore_revision');
|
|
217
|
+
expect(entries.length).toBeGreaterThanOrEqual(1);
|
|
218
|
+
expect(entries.some(e => e.status === 'success')).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// =========================================================================
|
|
223
|
+
// wp_delete_revision
|
|
224
|
+
// =========================================================================
|
|
225
|
+
|
|
226
|
+
describe('wp_delete_revision', () => {
|
|
227
|
+
it('SUCCESS — returns deleted true with revision_id and post_id', async () => {
|
|
228
|
+
mockSuccess({ deleted: true });
|
|
229
|
+
|
|
230
|
+
const res = await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
231
|
+
const data = parseResult(res);
|
|
232
|
+
|
|
233
|
+
expect(data.deleted).toBe(true);
|
|
234
|
+
expect(data.revision_id).toBe(101);
|
|
235
|
+
expect(data.post_id).toBe(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('SUCCESS — force=true is present in the fetch URL', async () => {
|
|
239
|
+
mockSuccess({ deleted: true });
|
|
240
|
+
|
|
241
|
+
await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
242
|
+
|
|
243
|
+
const fetchUrl = fetch.mock.calls[0][0];
|
|
244
|
+
expect(fetchUrl).toContain('force=true');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('GOVERNANCE — blocked by WP_READ_ONLY', async () => {
|
|
248
|
+
const original = process.env.WP_READ_ONLY;
|
|
249
|
+
process.env.WP_READ_ONLY = 'true';
|
|
250
|
+
try {
|
|
251
|
+
const res = await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
252
|
+
expect(res.isError).toBe(true);
|
|
253
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
254
|
+
} finally {
|
|
255
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
256
|
+
else process.env.WP_READ_ONLY = original;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('GOVERNANCE — blocked by WP_DISABLE_DELETE', async () => {
|
|
261
|
+
const original = process.env.WP_DISABLE_DELETE;
|
|
262
|
+
process.env.WP_DISABLE_DELETE = 'true';
|
|
263
|
+
try {
|
|
264
|
+
const res = await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
265
|
+
expect(res.isError).toBe(true);
|
|
266
|
+
expect(res.content[0].text).toContain('WP_DISABLE_DELETE');
|
|
267
|
+
} finally {
|
|
268
|
+
if (original === undefined) delete process.env.WP_DISABLE_DELETE;
|
|
269
|
+
else process.env.WP_DISABLE_DELETE = original;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('ERROR 404 — returns revision not found message', async () => {
|
|
274
|
+
mockError(404, '{"code":"rest_revision_not_found"}');
|
|
275
|
+
|
|
276
|
+
const res = await call('wp_delete_revision', { post_id: 1, revision_id: 999 });
|
|
277
|
+
expect(res.isError).toBe(true);
|
|
278
|
+
expect(res.content[0].text).toContain('Revision not found');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('ERROR 403 — returns message about delete_posts capability', async () => {
|
|
282
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
283
|
+
|
|
284
|
+
const res = await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
285
|
+
expect(res.isError).toBe(true);
|
|
286
|
+
expect(res.content[0].text).toContain('delete_posts');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('AUDIT — logs permanent_delete action on success', async () => {
|
|
290
|
+
mockSuccess({ deleted: true });
|
|
291
|
+
|
|
292
|
+
await call('wp_delete_revision', { post_id: 1, revision_id: 101 });
|
|
293
|
+
|
|
294
|
+
const logs = getAuditLogs();
|
|
295
|
+
const entry = logs.find(l => l.tool === 'wp_delete_revision');
|
|
296
|
+
expect(entry).toBeDefined();
|
|
297
|
+
expect(entry.status).toBe('success');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
// wp_search
|
|
19
|
+
// =========================================================================
|
|
20
|
+
|
|
21
|
+
describe('wp_search', () => {
|
|
22
|
+
const searchResults = [
|
|
23
|
+
{ id: 1, title: 'Post 1', url: 'https://test.example.com/post-1', type: 'post', subtype: 'post' },
|
|
24
|
+
{ id: 10, title: 'About', url: 'https://test.example.com/about', type: 'post', subtype: 'page' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
it('SUCCESS — returns query, total, and results array', async () => {
|
|
28
|
+
mockSuccess(searchResults);
|
|
29
|
+
|
|
30
|
+
const res = await call('wp_search', { search: 'test' });
|
|
31
|
+
const data = parseResult(res);
|
|
32
|
+
|
|
33
|
+
expect(data.query).toBe('test');
|
|
34
|
+
expect(data.total).toBe(2);
|
|
35
|
+
expect(data.results).toHaveLength(2);
|
|
36
|
+
|
|
37
|
+
const first = data.results[0];
|
|
38
|
+
expect(first).toEqual(
|
|
39
|
+
expect.objectContaining({ id: 1, title: 'Post 1', url: 'https://test.example.com/post-1', type: 'post', subtype: 'post' })
|
|
40
|
+
);
|
|
41
|
+
const second = data.results[1];
|
|
42
|
+
expect(second).toEqual(
|
|
43
|
+
expect.objectContaining({ id: 10, title: 'About', url: 'https://test.example.com/about', type: 'post', subtype: 'page' })
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('ERROR 403 — returns error message', async () => {
|
|
48
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
49
|
+
|
|
50
|
+
const res = await call('wp_search', { search: 'secret' });
|
|
51
|
+
expect(res.isError).toBe(true);
|
|
52
|
+
expect(res.content[0].text).toContain('403');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('AUDIT — logs search action on success', async () => {
|
|
56
|
+
mockSuccess(searchResults);
|
|
57
|
+
|
|
58
|
+
await call('wp_search', { search: 'audit-test' });
|
|
59
|
+
|
|
60
|
+
const logs = getAuditLogs();
|
|
61
|
+
const entry = logs.find(l => l.tool === 'wp_search');
|
|
62
|
+
expect(entry).toBeDefined();
|
|
63
|
+
expect(entry.status).toBe('success');
|
|
64
|
+
expect(entry.action).toBe('search');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// =========================================================================
|
|
69
|
+
// wp_list_post_types
|
|
70
|
+
// =========================================================================
|
|
71
|
+
|
|
72
|
+
describe('wp_list_post_types', () => {
|
|
73
|
+
const typesResponse = {
|
|
74
|
+
post: { slug: 'post', name: 'Posts', description: '', hierarchical: false, rest_base: 'posts' },
|
|
75
|
+
page: { slug: 'page', name: 'Pages', description: '', hierarchical: true, rest_base: 'pages' },
|
|
76
|
+
product: { slug: 'product', name: 'Products', description: 'Custom products', hierarchical: false, rest_base: 'products' },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
it('SUCCESS — returns total and post_types array', async () => {
|
|
80
|
+
mockSuccess(typesResponse);
|
|
81
|
+
|
|
82
|
+
const res = await call('wp_list_post_types');
|
|
83
|
+
const data = parseResult(res);
|
|
84
|
+
|
|
85
|
+
expect(data.total).toBe(3);
|
|
86
|
+
expect(data.post_types).toHaveLength(3);
|
|
87
|
+
|
|
88
|
+
const product = data.post_types.find(t => t.slug === 'product');
|
|
89
|
+
expect(product).toEqual(
|
|
90
|
+
expect.objectContaining({ slug: 'product', name: 'Products', description: 'Custom products', hierarchical: false, rest_base: 'products' })
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('ERROR 403 — returns error message', async () => {
|
|
95
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
96
|
+
|
|
97
|
+
const res = await call('wp_list_post_types');
|
|
98
|
+
expect(res.isError).toBe(true);
|
|
99
|
+
expect(res.content[0].text).toContain('403');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('AUDIT — logs list action on success', async () => {
|
|
103
|
+
mockSuccess(typesResponse);
|
|
104
|
+
|
|
105
|
+
await call('wp_list_post_types');
|
|
106
|
+
|
|
107
|
+
const logs = getAuditLogs();
|
|
108
|
+
const entry = logs.find(l => l.tool === 'wp_list_post_types');
|
|
109
|
+
expect(entry).toBeDefined();
|
|
110
|
+
expect(entry.status).toBe('success');
|
|
111
|
+
expect(entry.action).toBe('list');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// =========================================================================
|
|
116
|
+
// wp_list_custom_posts
|
|
117
|
+
// =========================================================================
|
|
118
|
+
|
|
119
|
+
describe('wp_list_custom_posts', () => {
|
|
120
|
+
const typesResponse = {
|
|
121
|
+
post: { slug: 'post', name: 'Posts', description: '', hierarchical: false, rest_base: 'posts' },
|
|
122
|
+
page: { slug: 'page', name: 'Pages', description: '', hierarchical: true, rest_base: 'pages' },
|
|
123
|
+
product: { slug: 'product', name: 'Products', description: 'Custom products', hierarchical: false, rest_base: 'products' },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const productPosts = [
|
|
127
|
+
{
|
|
128
|
+
id: 50,
|
|
129
|
+
title: { rendered: 'Widget' },
|
|
130
|
+
status: 'publish',
|
|
131
|
+
date: '2024-01-01',
|
|
132
|
+
link: 'https://test.example.com/product/widget',
|
|
133
|
+
slug: 'widget',
|
|
134
|
+
type: 'product',
|
|
135
|
+
meta: {},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
it('SUCCESS — fetches types then custom posts', async () => {
|
|
140
|
+
// First call: GET /types
|
|
141
|
+
mockSuccess(typesResponse);
|
|
142
|
+
// Second call: GET /products?...
|
|
143
|
+
mockSuccess(productPosts);
|
|
144
|
+
|
|
145
|
+
const res = await call('wp_list_custom_posts', { post_type: 'product' });
|
|
146
|
+
const data = parseResult(res);
|
|
147
|
+
|
|
148
|
+
expect(data.post_type).toBe('product');
|
|
149
|
+
expect(data.total).toBe(1);
|
|
150
|
+
expect(data.posts).toHaveLength(1);
|
|
151
|
+
expect(data.posts[0]).toEqual(
|
|
152
|
+
expect.objectContaining({ id: 50, title: 'Widget', status: 'publish', slug: 'widget', type: 'product' })
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('ERROR — post type not found', async () => {
|
|
157
|
+
// Return types without the requested type
|
|
158
|
+
const limitedTypes = {
|
|
159
|
+
post: { slug: 'post', name: 'Posts', description: '', hierarchical: false, rest_base: 'posts' },
|
|
160
|
+
page: { slug: 'page', name: 'Pages', description: '', hierarchical: true, rest_base: 'pages' },
|
|
161
|
+
};
|
|
162
|
+
mockSuccess(limitedTypes);
|
|
163
|
+
|
|
164
|
+
const res = await call('wp_list_custom_posts', { post_type: 'nonexistent' });
|
|
165
|
+
expect(res.isError).toBe(true);
|
|
166
|
+
expect(res.content[0].text).toContain('not found');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('ERROR 403 — returns error', async () => {
|
|
170
|
+
mockError(403, '{"code":"rest_forbidden"}');
|
|
171
|
+
|
|
172
|
+
const res = await call('wp_list_custom_posts', { post_type: 'product' });
|
|
173
|
+
expect(res.isError).toBe(true);
|
|
174
|
+
expect(res.content[0].text).toContain('403');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('AUDIT — logs list action on success', async () => {
|
|
178
|
+
mockSuccess(typesResponse);
|
|
179
|
+
mockSuccess(productPosts);
|
|
180
|
+
|
|
181
|
+
await call('wp_list_custom_posts', { post_type: 'product' });
|
|
182
|
+
|
|
183
|
+
const logs = getAuditLogs();
|
|
184
|
+
const entry = logs.find(l => l.tool === 'wp_list_custom_posts');
|
|
185
|
+
expect(entry).toBeDefined();
|
|
186
|
+
expect(entry.status).toBe('success');
|
|
187
|
+
expect(entry.action).toBe('list');
|
|
188
|
+
expect(entry.params.post_type).toBe('product');
|
|
189
|
+
});
|
|
190
|
+
});
|