Skip to main content
Glama
WordPressClient.test.js15.3 kB
/** * Tests for WordPressClient * * Basic test coverage for the main WordPress REST API client. * Tests core functionality, authentication, and error handling. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { WordPressClient } from "@/client/api.js"; import { WordPressAPIError, AuthenticationError, RateLimitError } from "@/client/api.js"; // Mock fetch const mockFetch = vi.fn(); global.fetch = mockFetch; // Mock logger to avoid console output vi.mock("../../dist/utils/logger.js", () => ({ LoggerFactory: { api: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), time: vi.fn().mockImplementation((name, fn) => fn()), }), }, })); // Mock config vi.mock("../../dist/config/Config.js", () => ({ config: () => ({ api: { timeout: 30000, retries: 3, rateLimitDelay: 1000, }, cache: { enabled: false, }, wordpress: { siteUrl: "", timeout: 30000, maxRetries: 3, }, security: { rateLimit: 60, }, app: { isProduction: false, isDevelopment: true, isTest: true, }, error: { legacyLogsEnabled: false, }, debug: { enabled: false, }, }), ConfigHelpers: { getTimeout: vi.fn(() => 30000), shouldUseCache: vi.fn(() => false), isTest: vi.fn(() => true), shouldDebug: vi.fn(() => false), }, })); describe("WordPressClient", () => { let client; let mockResponse; beforeEach(() => { // Default successful response mockResponse = { ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ id: 1, title: "Test" }), text: vi.fn().mockResolvedValue('{"id": 1, "title": "Test"}'), }; mockFetch.mockResolvedValue(mockResponse); const config = { baseUrl: "https://test.example.com", auth: { method: "app-password", username: "testuser", appPassword: "test pass word", }, }; client = new WordPressClient(config); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Constructor", () => { it("should initialize with basic configuration", () => { expect(client).toBeDefined(); expect(client.config.baseUrl).toBe("https://test.example.com"); expect(client.config.auth.method).toBe("app-password"); }); it("should handle default authentication method", () => { const minimalConfig = { baseUrl: "https://example.com", auth: { username: "user", appPassword: "pass", }, }; const minimalClient = new WordPressClient(minimalConfig); expect(minimalClient.config.auth.method).toBe("app-password"); }); it("should validate required configuration", () => { expect(() => new WordPressClient({})).toThrow(); }); }); describe("Authentication", () => { it("should set app-password authentication headers", async () => { await client.get("posts"); expect(mockFetch).toHaveBeenCalled(); const [_url, options] = mockFetch.mock.calls[0]; expect(options.headers.Authorization).toMatch(/^Basic /); }); it("should handle JWT authentication", async () => { const jwtConfig = { baseUrl: "https://test.example.com", auth: { method: "jwt", username: "testuser", password: "testpass", secret: "test-jwt-secret", }, }; const jwtClient = new WordPressClient(jwtConfig); // Mock JWT token response mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ token: "jwt-token-123" }), text: vi.fn().mockResolvedValue('{"token": "jwt-token-123"}'), }); await jwtClient.authenticate(); expect(mockFetch).toHaveBeenCalled(); }); it("should handle basic authentication", async () => { const basicConfig = { baseUrl: "https://test.example.com", auth: { method: "basic", username: "testuser", password: "testpass", }, }; const basicClient = new WordPressClient(basicConfig); await basicClient.get("posts"); const [_url, options] = mockFetch.mock.calls[0]; expect(options.headers.Authorization).toMatch(/^Basic /); }); }); describe("HTTP Methods", () => { it("should make GET requests", async () => { const result = await client.get("posts"); expect(mockFetch).toHaveBeenCalledWith( "https://test.example.com/wp-json/wp/v2/posts", expect.objectContaining({ method: "GET", headers: expect.any(Object), }), ); expect(result).toEqual({ id: 1, title: "Test" }); }); it("should make POST requests with data", async () => { const postData = { title: "New Post", content: "Content" }; await client.post("posts", postData); expect(mockFetch).toHaveBeenCalledWith( "https://test.example.com/wp-json/wp/v2/posts", expect.objectContaining({ method: "POST", body: JSON.stringify(postData), headers: expect.objectContaining({ "Content-Type": "application/json", }), }), ); }); it("should make PUT requests", async () => { const putData = { id: 1, title: "Updated Post" }; await client.put("posts/1", putData); expect(mockFetch).toHaveBeenCalledWith( "https://test.example.com/wp-json/wp/v2/posts/1", expect.objectContaining({ method: "PUT", body: JSON.stringify(putData), }), ); }); it("should make DELETE requests", async () => { await client.delete("posts/1"); expect(mockFetch).toHaveBeenCalledWith( "https://test.example.com/wp-json/wp/v2/posts/1", expect.objectContaining({ method: "DELETE", }), ); }); }); describe("Error Handling", () => { it("should handle 404 errors", async () => { mockFetch.mockResolvedValue({ ok: false, status: 404, statusText: "Not Found", headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ code: "rest_post_invalid_id", message: "Invalid post ID", }), }); await expect(client.get("posts/999")).rejects.toThrow(WordPressAPIError); }); it("should handle 401 authentication errors", async () => { mockFetch.mockResolvedValue({ ok: false, status: 401, statusText: "Unauthorized", headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ code: "rest_forbidden", message: "Sorry, you are not allowed to do that", }), }); await expect(client.get("posts")).rejects.toThrow(AuthenticationError); }); it("should handle 429 rate limit errors", async () => { mockFetch.mockResolvedValue({ ok: false, status: 429, statusText: "Too Many Requests", headers: new Map([ ["content-type", "application/json"], ["retry-after", "60"], ]), json: vi.fn().mockResolvedValue({ code: "rest_too_many_requests", message: "Too many requests", }), }); await expect(client.get("posts")).rejects.toThrow(RateLimitError); }); it("should handle network errors", async () => { mockFetch.mockRejectedValue(new Error("Network error")); await expect(client.get("posts")).rejects.toThrow("Network error"); }); it("should handle malformed JSON responses", async () => { mockFetch.mockResolvedValue({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), text: vi.fn().mockResolvedValue("invalid json"), }); const result = await client.get("posts"); expect(result).toBe("invalid json"); }); }); describe("URL Construction", () => { it("should build correct URLs with base path", () => { const url = client.buildUrl("posts"); expect(url).toBe("https://test.example.com/wp-json/wp/v2/posts"); }); it("should handle query parameters", () => { const url = client.buildUrl("posts", { per_page: 10, status: "publish" }); expect(url).toBe("https://test.example.com/wp-json/wp/v2/posts?per_page=10&status=publish"); }); it("should handle trailing slashes correctly", () => { const clientWithSlash = new WordPressClient({ baseUrl: "https://test.example.com/", auth: { method: "app-password", username: "user", appPassword: "pass" }, }); const url = clientWithSlash.buildUrl("posts"); expect(url).toBe("https://test.example.com/wp-json/wp/v2/posts"); }); }); describe("Request Options", () => { it("should set correct headers", async () => { await client.get("posts"); const [_url, options] = mockFetch.mock.calls[0]; expect(options.headers).toEqual( expect.objectContaining({ "User-Agent": expect.stringMatching(/mcp-wordpress/), "Content-Type": "application/json", Authorization: expect.any(String), }), ); }); it("should handle custom headers", async () => { const customHeaders = { "X-Custom-Header": "test-value" }; await client.request("GET", "posts", null, { headers: customHeaders, }); const [_url, options] = mockFetch.mock.calls[0]; expect(options.headers["X-Custom-Header"]).toBe("test-value"); }); it("should set timeout", async () => { await client.get("posts"); const [_url, options] = mockFetch.mock.calls[0]; expect(options.signal).toBeDefined(); }); }); describe("WordPress-specific Methods", () => { it("should get posts", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue([ { id: 1, title: { rendered: "Post 1" } }, { id: 2, title: { rendered: "Post 2" } }, ]), text: vi.fn().mockResolvedValue( JSON.stringify([ { id: 1, title: { rendered: "Post 1" } }, { id: 2, title: { rendered: "Post 2" } }, ]), ), }); const posts = await client.getPosts(); expect(posts).toHaveLength(2); expect(posts[0].title.rendered).toBe("Post 1"); }); it("should get posts with parameters", async () => { await client.getPosts({ per_page: 5, status: "publish" }); const [url] = mockFetch.mock.calls[0]; expect(url).toContain("per_page=5"); expect(url).toContain("status=publish"); }); it("should get single post", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ id: 1, title: { rendered: "Single Post" } }), text: vi.fn().mockResolvedValue('{"id": 1, "title": {"rendered": "Single Post"}}'), }); const post = await client.getPost(1); expect(post.id).toBe(1); expect(post.title.rendered).toBe("Single Post"); }); it("should create posts", async () => { const newPost = { title: "New Post", content: "Post content", status: "draft", }; mockFetch.mockResolvedValueOnce({ ok: true, status: 201, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ id: 3, ...newPost }), text: vi.fn().mockResolvedValue(JSON.stringify({ id: 3, ...newPost })), }); const result = await client.createPost(newPost); expect(result.id).toBe(3); expect(result.title).toBe("New Post"); }); it("should update posts", async () => { const updatedPost = { id: 1, title: "Updated Title" }; mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue(updatedPost), text: vi.fn().mockResolvedValue(JSON.stringify(updatedPost)), }); const result = await client.updatePost(updatedPost); expect(result.title).toBe("Updated Title"); }); it("should delete posts", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ deleted: true, previous: { id: 1 } }), text: vi.fn().mockResolvedValue('{"deleted": true, "previous": {"id": 1}}'), }); const result = await client.deletePost(1); expect(result.deleted).toBe(true); }); }); describe("Statistics and Monitoring", () => { it("should track request statistics", async () => { await client.get("posts"); await client.get("pages"); const stats = client.stats; expect(stats.totalRequests).toBe(2); expect(stats.successfulRequests).toBe(2); }); it("should track error statistics", async () => { mockFetch.mockRejectedValue(new Error("Network error")); try { await client.get("posts"); } catch (_e) { // Expected error } const stats = client.stats; expect(stats.totalRequests).toBe(1); expect(stats.failedRequests).toBe(1); }); }); describe("Edge Cases", () => { it("should handle empty responses", async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue(null), text: vi.fn().mockResolvedValue("null"), }); const result = await client.get("posts"); expect(result).toBeNull(); }); it("should handle non-JSON responses", async () => { mockFetch.mockResolvedValue({ ok: true, status: 200, headers: new Map([["content-type", "text/html"]]), json: vi.fn().mockRejectedValue(new Error("Not JSON")), text: vi.fn().mockResolvedValue("<html>Not JSON</html>"), }); const result = await client.get("posts"); expect(result).toBe("<html>Not JSON</html>"); }); it("should handle very large responses", async () => { const largeArray = Array.from({ length: 1000 }, (_, i) => ({ id: i, title: `Post ${i}` })); mockFetch.mockResolvedValueOnce({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue(largeArray), text: vi.fn().mockResolvedValue(JSON.stringify(largeArray)), }); const result = await client.get("posts"); expect(result).toHaveLength(1000); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server