/**
* Comprehensive tests for PostTools class
* Achieving ≥70% coverage for posts tool implementation
*/
import { vi } from "vitest";
import { PostTools } from "../../../dist/tools/posts/index.js";
import { WordPressClient } from "../../../dist/client/api.js"; // eslint-disable-line no-unused-vars
// Mock dependencies
vi.mock("../../../dist/client/api.js");
vi.mock("../../../dist/utils/streaming.js", () => ({
WordPressDataStreamer: {
streamPosts: vi.fn().mockImplementation(async function* (posts) {
for (const post of posts) {
yield { type: "post", data: post };
}
}),
},
StreamingUtils: {
formatStreamingResponse: vi.fn().mockReturnValue("Streamed response"),
},
}));
describe("PostTools", () => {
let postTools;
let mockClient;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Create mock client instance with all necessary methods
mockClient = {
config: {
siteUrl: "https://test.wordpress.com",
},
getSiteUrl: vi.fn().mockReturnValue("https://test.wordpress.com"),
getPosts: vi.fn(),
getPost: vi.fn(),
createPost: vi.fn(),
updatePost: vi.fn(),
deletePost: vi.fn(),
getPostRevisions: vi.fn(),
getUser: vi.fn(),
getCategory: vi.fn(),
getTag: vi.fn(),
};
// Create PostTools instance
postTools = new PostTools();
});
describe("getTools", () => {
it("should return all post tool definitions", () => {
const tools = postTools.getTools();
expect(tools).toHaveLength(6);
expect(tools.map((t) => t.name)).toEqual([
"wp_list_posts",
"wp_get_post",
"wp_create_post",
"wp_update_post",
"wp_delete_post",
"wp_get_post_revisions",
]);
});
it("should include handlers for each tool", () => {
const tools = postTools.getTools();
tools.forEach((tool) => {
expect(tool.handler).toBeDefined();
expect(typeof tool.handler).toBe("function");
});
});
it("should include proper tool metadata", () => {
const tools = postTools.getTools();
const listPostsTool = tools.find((t) => t.name === "wp_list_posts");
expect(listPostsTool.description).toContain("Lists posts from a WordPress site");
expect(listPostsTool.inputSchema).toBeDefined();
expect(listPostsTool.inputSchema.type).toBe("object");
expect(listPostsTool.inputSchema.properties).toBeDefined();
});
});
describe("handleListPosts", () => {
beforeEach(() => {
mockClient.getPosts.mockResolvedValue([
{
id: 1,
title: { rendered: "Test Post 1" },
content: { rendered: "<p>Content 1</p>" },
excerpt: { rendered: "Excerpt 1" },
date: "2024-01-01T00:00:00",
modified: "2024-01-02T00:00:00",
status: "publish",
link: "https://test.wordpress.com/test-post-1",
author: 1,
featured_media: 10,
categories: [1, 2],
tags: [3, 4],
_links: {
self: [{ href: "https://test.wordpress.com/wp-json/wp/v2/posts/1" }],
edit: [{ href: "https://test.wordpress.com/wp-admin/post.php?post=1&action=edit" }],
},
},
{
id: 2,
title: { rendered: "Test Post 2" },
content: { rendered: "<p>Content 2</p>" },
excerpt: { rendered: "Excerpt 2" },
date: "2024-01-03T00:00:00",
modified: "2024-01-04T00:00:00",
status: "draft",
link: "https://test.wordpress.com/?p=2",
author: 1,
featured_media: 0,
categories: [1],
tags: [],
_links: {
self: [{ href: "https://test.wordpress.com/wp-json/wp/v2/posts/2" }],
edit: [{ href: "https://test.wordpress.com/wp-admin/post.php?post=2&action=edit" }],
},
},
]);
// Mock additional data fetching
mockClient.getUser.mockResolvedValue({ name: "Test User" });
mockClient.getCategory.mockResolvedValue({ name: "Test Category" });
mockClient.getTag.mockResolvedValue({ name: "Test Tag" });
});
it("should list posts with default parameters", async () => {
const result = await postTools.handleListPosts(mockClient, {});
expect(mockClient.getPosts).toHaveBeenCalledWith({ per_page: 10 });
expect(typeof result).toBe("string");
expect(result).toContain("Test Post 1");
expect(result).toContain("Test Post 2");
});
it("should handle search parameter", async () => {
const result = await postTools.handleListPosts(mockClient, { search: "WordPress" });
expect(mockClient.getPosts).toHaveBeenCalledWith(expect.objectContaining({ search: "WordPress", per_page: 10 }));
expect(typeof result).toBe("string");
expect(result).toContain("Test Post 1");
});
it("should handle status filter", async () => {
await postTools.handleListPosts(mockClient, { status: "draft" });
// WordPress API accepts status as either string or array
expect(mockClient.getPosts).toHaveBeenCalledWith(
expect.objectContaining({
status: expect.anything(),
per_page: 10,
}),
);
const callArgs = mockClient.getPosts.mock.calls[0][0];
expect(callArgs.status === "draft" || (Array.isArray(callArgs.status) && callArgs.status[0] === "draft")).toBe(
true,
);
});
it("should handle categories filter", async () => {
await postTools.handleListPosts(mockClient, { categories: [1, 2] });
expect(mockClient.getPosts).toHaveBeenCalledWith(expect.objectContaining({ categories: [1, 2], per_page: 10 }));
});
it("should handle tags filter", async () => {
await postTools.handleListPosts(mockClient, { tags: [3, 4] });
expect(mockClient.getPosts).toHaveBeenCalledWith(expect.objectContaining({ tags: [3, 4], per_page: 10 }));
});
it("should handle pagination parameters", async () => {
await postTools.handleListPosts(mockClient, { per_page: 20, page: 2 });
expect(mockClient.getPosts).toHaveBeenCalledWith(
expect.objectContaining({
per_page: 20,
page: 2,
}),
);
});
it("should handle empty results", async () => {
mockClient.getPosts.mockResolvedValue([]);
const result = await postTools.handleListPosts(mockClient, {});
expect(typeof result).toBe("string");
expect(result).toContain("No posts found");
});
it("should format post metadata correctly", async () => {
// Mock additional data fetching
mockClient.getUser.mockResolvedValue({ name: "Test User" });
mockClient.getCategory.mockResolvedValue({ name: "Test Category" });
mockClient.getTag.mockResolvedValue({ name: "Test Tag" });
const result = await postTools.handleListPosts(mockClient, {});
expect(typeof result).toBe("string");
expect(result).toContain("Test Post 1");
expect(result).toContain("publish");
});
it("should handle API errors gracefully", async () => {
mockClient.getPosts.mockRejectedValue(new Error("API Error"));
await expect(postTools.handleListPosts(mockClient, {})).rejects.toThrow("Failed to list posts: API Error");
});
});
describe("handleGetPost", () => {
beforeEach(() => {
mockClient.getPost.mockResolvedValue({
id: 1,
title: { rendered: "Test Post" },
content: { rendered: "<p>Full post content here</p>" },
excerpt: { rendered: "Post excerpt" },
date: "2024-01-01T00:00:00",
modified: "2024-01-02T00:00:00",
status: "publish",
link: "https://test.wordpress.com/test-post",
author: 1,
featured_media: 10,
categories: [1, 2],
tags: [3, 4],
format: "standard",
meta: { custom_field: "value" },
_links: {
self: [{ href: "https://test.wordpress.com/wp-json/wp/v2/posts/1" }],
edit: [{ href: "https://test.wordpress.com/wp-admin/post.php?post=1&action=edit" }],
},
});
// Mock additional data fetching
mockClient.getUser.mockResolvedValue({ name: "Test User", username: "testuser" });
mockClient.getCategory.mockResolvedValue({ name: "Test Category" });
mockClient.getTag.mockResolvedValue({ name: "Test Tag" });
});
it("should get a post by ID", async () => {
const result = await postTools.handleGetPost(mockClient, { id: 1 });
expect(mockClient.getPost).toHaveBeenCalledWith(1);
expect(typeof result).toBe("string");
expect(result).toContain("Test Post");
expect(result).toContain("Full post content here");
});
it("should handle missing ID parameter", async () => {
await expect(postTools.handleGetPost(mockClient, {})).rejects.toThrow();
});
it("should handle non-existent post", async () => {
mockClient.getPost.mockRejectedValue(new Error("404 Post not found"));
const result = await postTools.handleGetPost(mockClient, { id: 999 });
expect(typeof result).toBe("string");
expect(result).toContain("Post with ID 999 not found");
});
it("should format post details correctly", async () => {
const result = await postTools.handleGetPost(mockClient, { id: 1 });
expect(typeof result).toBe("string");
expect(result).toContain("**Status**: publish");
expect(result).toContain("Test User");
expect(result).toContain("Test Category");
expect(result).toContain("Test Tag");
});
});
describe("handleCreatePost", () => {
beforeEach(() => {
mockClient.createPost.mockResolvedValue({
id: 100,
title: { rendered: "New Post" },
content: { rendered: "<p>New content</p>" },
status: "publish",
link: "https://test.wordpress.com/new-post",
_links: {
self: [{ href: "https://test.wordpress.com/wp-json/wp/v2/posts/100" }],
edit: [{ href: "https://test.wordpress.com/wp-admin/post.php?post=100&action=edit" }],
},
});
});
it("should create a post with title only", async () => {
const result = await postTools.handleCreatePost(mockClient, {
title: "New Post",
});
expect(mockClient.createPost).toHaveBeenCalledWith({
title: "New Post",
});
expect(typeof result).toBe("string");
expect(result).toContain("Post Created Successfully");
expect(result).toContain("**ID**: 100");
});
it("should create a post with full parameters", async () => {
const postData = {
title: "New Post",
content: "<p>New content</p>",
status: "publish",
excerpt: "Post excerpt",
categories: [1, 2],
tags: [3, 4],
featured_media: 10,
};
const result = await postTools.handleCreatePost(mockClient, postData);
expect(mockClient.createPost).toHaveBeenCalledWith(postData);
expect(typeof result).toBe("string");
expect(result).toContain("Post Created Successfully");
});
it("should handle missing title", async () => {
await expect(
postTools.handleCreatePost(mockClient, {
content: "Content without title",
}),
).rejects.toThrow();
});
it("should handle creation errors", async () => {
mockClient.createPost.mockRejectedValue(new Error("Permission denied"));
await expect(
postTools.handleCreatePost(mockClient, {
title: "New Post",
}),
).rejects.toThrow("Failed to create post: Permission denied");
});
it("should set featured media to 0 to remove it", async () => {
await postTools.handleCreatePost(mockClient, {
title: "Post without featured image",
featured_media: 0,
});
expect(mockClient.createPost).toHaveBeenCalledWith(
expect.objectContaining({
featured_media: 0,
}),
);
});
});
describe("handleUpdatePost", () => {
beforeEach(() => {
mockClient.getPost.mockResolvedValue({
id: 1,
title: { rendered: "Original Post" },
content: { rendered: "<p>Original content</p>" },
status: "draft",
excerpt: { rendered: "Original excerpt" },
});
mockClient.updatePost.mockResolvedValue({
id: 1,
title: { rendered: "Updated Post" },
content: { rendered: "<p>Updated content</p>" },
status: "publish",
link: "https://test.wordpress.com/updated-post",
modified: "2024-01-10T00:00:00",
_links: {
self: [{ href: "https://test.wordpress.com/wp-json/wp/v2/posts/1" }],
edit: [{ href: "https://test.wordpress.com/wp-admin/post.php?post=1&action=edit" }],
},
});
});
it("should update post title", async () => {
const result = await postTools.handleUpdatePost(mockClient, {
id: 1,
title: "Updated Post",
});
expect(mockClient.getPost).toHaveBeenCalledWith(1);
expect(mockClient.updatePost).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
title: "Updated Post",
}),
);
expect(typeof result).toBe("string");
expect(result).toContain("Post Updated Successfully");
});
it("should update multiple fields", async () => {
await postTools.handleUpdatePost(mockClient, {
id: 1,
title: "Updated Post",
content: "<p>Updated content</p>",
status: "publish",
categories: [5, 6],
});
expect(mockClient.updatePost).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
title: "Updated Post",
content: "<p>Updated content</p>",
status: "publish",
categories: [5, 6],
}),
);
});
it("should handle missing ID", async () => {
await expect(
postTools.handleUpdatePost(mockClient, {
title: "Updated Post",
}),
).rejects.toThrow();
});
it("should handle update errors", async () => {
mockClient.getPost.mockRejectedValue(new Error("404 Post not found"));
const result = await postTools.handleUpdatePost(mockClient, {
id: 999,
title: "Updated Post",
});
expect(typeof result).toBe("string");
expect(result).toContain("Post with ID 999 not found");
});
it("should remove featured media with 0", async () => {
await postTools.handleUpdatePost(mockClient, {
id: 1,
title: "Updated Post", // Include title to avoid validation error
featured_media: 0,
});
expect(mockClient.updatePost).toHaveBeenCalledWith(
expect.objectContaining({
id: 1,
title: "Updated Post",
featured_media: 0,
}),
);
});
});
describe("handleDeletePost", () => {
beforeEach(() => {
mockClient.deletePost.mockResolvedValue({
deleted: true,
previous: {
id: 1,
title: { rendered: "Deleted Post" },
status: "trash",
},
});
});
it("should delete post to trash by default", async () => {
const result = await postTools.handleDeletePost(mockClient, {
id: 1,
});
expect(mockClient.deletePost).toHaveBeenCalledWith(1, undefined);
expect(typeof result).toBe("string");
expect(result).toContain("moved to trash successfully");
});
it("should permanently delete with force=true", async () => {
mockClient.deletePost.mockResolvedValue({
deleted: true,
previous: {
id: 1,
title: { rendered: "Deleted Post" },
},
});
const result = await postTools.handleDeletePost(mockClient, {
id: 1,
force: true,
});
expect(mockClient.deletePost).toHaveBeenCalledWith(1, true);
expect(typeof result).toBe("string");
expect(result).toContain("permanently deleted successfully");
});
it("should handle missing ID", async () => {
await expect(postTools.handleDeletePost(mockClient, {})).rejects.toThrow();
});
it("should handle deletion errors", async () => {
mockClient.deletePost.mockRejectedValue(new Error("404 Permission denied"));
const result = await postTools.handleDeletePost(mockClient, {
id: 999,
});
expect(typeof result).toBe("string");
expect(result).toContain("Post with ID 999 not found");
});
});
describe("handleGetPostRevisions", () => {
beforeEach(() => {
mockClient.getPostRevisions.mockResolvedValue([
{
id: 101,
author: 1,
date: "2024-01-05T00:00:00",
modified: "2024-01-05T00:00:00",
parent: 1,
title: { rendered: "Revision 2" },
content: { rendered: "<p>Revision 2 content</p>" },
excerpt: { rendered: "Revision 2 excerpt" },
},
{
id: 100,
author: 1,
date: "2024-01-03T00:00:00",
modified: "2024-01-03T00:00:00",
parent: 1,
title: { rendered: "Revision 1" },
content: { rendered: "<p>Revision 1 content</p>" },
excerpt: { rendered: "Revision 1 excerpt" },
},
]);
});
it("should get post revisions", async () => {
const result = await postTools.handleGetPostRevisions(mockClient, {
id: 1,
});
expect(mockClient.getPostRevisions).toHaveBeenCalledWith(1);
expect(typeof result).toBe("string");
expect(result).toContain("Post Revisions");
expect(result).toContain("2 total");
expect(result).toContain("Revision 2");
expect(result).toContain("Revision 1");
});
it("should handle missing ID", async () => {
await expect(postTools.handleGetPostRevisions(mockClient, {})).rejects.toThrow();
});
it("should handle no revisions", async () => {
mockClient.getPostRevisions.mockResolvedValue([]);
const result = await postTools.handleGetPostRevisions(mockClient, {
id: 1,
});
expect(typeof result).toBe("string");
expect(result).toContain("No revisions found");
});
it("should handle revision retrieval errors", async () => {
mockClient.getPostRevisions.mockRejectedValue(new Error("404 Post not found"));
const result = await postTools.handleGetPostRevisions(mockClient, {
id: 999,
});
expect(typeof result).toBe("string");
expect(result).toContain("Post with ID 999 not found");
});
it("should format revision details correctly", async () => {
const result = await postTools.handleGetPostRevisions(mockClient, {
id: 1,
});
expect(typeof result).toBe("string");
expect(result).toContain("ID: 101");
expect(result).toContain("Date: ");
expect(result).toContain("Title: Revision 2");
});
});
describe("edge cases and error handling", () => {
it("should handle network errors", async () => {
mockClient.getPost.mockRejectedValue(new Error("ECONNREFUSED"));
await expect(postTools.handleGetPost(mockClient, { id: 1 })).rejects.toThrow("Failed to get post: ECONNREFUSED");
});
it("should handle malformed responses", async () => {
mockClient.getPost.mockResolvedValue(null);
await expect(postTools.handleGetPost(mockClient, { id: 1 })).rejects.toThrow();
});
it("should handle authentication errors", async () => {
mockClient.createPost.mockRejectedValue(new Error("401 Unauthorized"));
await expect(postTools.handleCreatePost(mockClient, { title: "Test" })).rejects.toThrow(
"Failed to create post: 401 Unauthorized",
);
});
it("should handle rate limiting", async () => {
mockClient.getPosts.mockRejectedValue(new Error("429 Too Many Requests"));
await expect(postTools.handleListPosts(mockClient, {})).rejects.toThrow(
"Failed to list posts: 429 Too Many Requests",
);
});
});
});