import { WordPressClient } from "@/client/api.js";
/**
* WordPress API Live Contract Tests
* Tests against real WordPress when available, uses mocks otherwise
*/
describe("WordPress API Live Contract Tests", () => {
let wordpressClient;
const hasLiveWordPress = !!(process.env.WORDPRESS_TEST_URL || process.env.FORCE_LIVE_WP);
const useLive = hasLiveWordPress && !process.env.SKIP_LIVE_TESTS;
beforeAll(async () => {
if (useLive) {
console.log("🌐 Running live contract tests against WordPress instance");
const authConfig = {
method: process.env.WORDPRESS_AUTH_METHOD || "app-password",
username: process.env.WORDPRESS_USERNAME || "testuser",
};
if (authConfig.method === "basic") {
authConfig.password = process.env.WORDPRESS_APP_PASSWORD || "test-password";
} else {
authConfig.appPassword = process.env.WORDPRESS_APP_PASSWORD || "xxxx xxxx xxxx xxxx xxxx xxxx";
}
wordpressClient = new WordPressClient({
baseUrl: process.env.WORDPRESS_TEST_URL || process.env.WORDPRESS_SITE_URL || "http://localhost:8081",
auth: authConfig,
});
try {
const response = await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/`);
if (!response.ok) {
throw new Error(`WordPress not accessible: ${response.status}`);
}
console.log("✅ Live WordPress instance is accessible");
} catch (testError) {
throw new Error(`Failed to connect to live WordPress: ${testError.message}`);
}
} else {
console.log("🔧 Using mock client for contract tests - no live WordPress configured");
// Mock client with stateful contract-compliant responses
const mockPosts = new Map();
let nextId = 1;
wordpressClient = {
config: { baseUrl: "http://mock-wordpress.local" },
createPost: async (data) => {
const id = nextId++;
const post = {
id,
title: { rendered: data.title || "Mock Post" },
content: { rendered: data.content || "Mock content" },
status: data.status || "draft",
author: 1,
date: new Date().toISOString(),
modified: new Date().toISOString(),
};
mockPosts.set(id, post);
return post;
},
getPosts: async () => [
{
id: 1,
title: { rendered: "Mock Post" },
content: { rendered: "Mock content" },
status: "publish",
author: 1,
},
],
getPost: async (id) => {
if (id === 999999) throw new Error("Not Found");
return (
mockPosts.get(id) || {
id,
title: { rendered: "Mock Post" },
content: { rendered: "Mock content" },
status: "publish",
author: 1,
}
);
},
updatePost: async ({ id, ...data }) => {
const existing = mockPosts.get(id) || { id, author: 1, date: new Date().toISOString() };
const updated = {
...existing,
id,
title: { rendered: data.title || existing.title?.rendered || "Updated Mock Post" },
content: { rendered: data.content || existing.content?.rendered || "Updated content" },
status: data.status || existing.status || "publish",
modified: new Date().toISOString(),
};
mockPosts.set(id, updated);
return updated;
},
deletePost: async (id) => {
const post = mockPosts.get(id);
if (post) mockPosts.delete(id);
return { deleted: true, previous: post || { id } };
},
};
}
});
describe("Posts API Contract", () => {
it(`should create a post with valid response format (live=${useLive})`, async () => {
const postData = {
title: "Contract Test Post",
content: "This is a test post for contract validation",
status: "publish",
};
const result = await wordpressClient.createPost(postData);
// Verify the response structure matches our contract
expect(result).toMatchObject({
id: expect.any(Number),
title: {
rendered: expect.any(String),
},
content: {
rendered: expect.any(String),
},
status: expect.any(String),
author: expect.any(Number),
});
// Verify the data we sent is reflected
expect(result.title.rendered).toContain("Contract Test Post");
expect(result.status).toBe("publish");
});
it(`should retrieve posts with correct pagination format (live=${useLive})`, async () => {
const result = await wordpressClient.getPosts({ page: 1, per_page: 10 });
expect(Array.isArray(result)).toBe(true);
// Should always have at least one post (created during setup)
expect(result.length).toBeGreaterThan(0);
expect(result[0]).toMatchObject({
id: expect.any(Number),
title: {
rendered: expect.any(String),
},
content: {
rendered: expect.any(String),
},
status: expect.any(String),
});
});
it(`should handle post not found error correctly (live=${useLive})`, async () => {
await expect(wordpressClient.getPost(999999)).rejects.toThrow();
});
});
describe("REST API Discovery", () => {
it(`should return valid API index (live=${useLive})`, async () => {
let apiIndex;
if (useLive) {
const response = await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/`);
expect(response.ok).toBe(true);
apiIndex = await response.json();
} else {
// Mock API index response
apiIndex = {
namespace: "wp/v2",
routes: {
"/wp/v2/posts": {},
"/wp/v2/pages": {},
"/wp/v2/users": {},
},
};
}
expect(apiIndex).toHaveProperty("namespace");
expect(apiIndex).toHaveProperty("routes");
});
it(`should have required endpoints available (live=${useLive})`, async () => {
let endpoints;
if (useLive) {
const response = await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/`);
const apiIndex = await response.json();
endpoints = apiIndex.routes;
expect(endpoints).toHaveProperty("/wp/v2/posts");
expect(endpoints).toHaveProperty("/wp/v2/pages");
expect(endpoints).toHaveProperty("/wp/v2/users");
} else {
// Mock validation of required endpoints
endpoints = ["/wp/v2/posts", "/wp/v2/pages", "/wp/v2/users"];
expect(endpoints).toHaveLength(3);
expect(endpoints).toContain("/wp/v2/posts");
}
});
});
describe("Authentication Contract", () => {
it(`should authenticate successfully with valid credentials (live=${useLive})`, async () => {
let authResult;
if (useLive) {
const authHeader = `Basic ${Buffer.from(
`${process.env.WORDPRESS_USERNAME}:${process.env.WORDPRESS_APP_PASSWORD}`,
).toString("base64")}`;
const testPostData = {
title: "Auth Test Post",
content: "Testing authentication",
status: "draft",
};
const response = await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/posts`, {
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(testPostData),
});
expect(response.ok).toBe(true);
expect(response.status).toBe(201);
authResult = await response.json();
// Clean up: delete the test post
await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/posts/${authResult.id}?force=true`, {
method: "DELETE",
headers: {
Authorization: authHeader,
},
});
} else {
// Mock authentication success
authResult = {
id: 123,
title: { rendered: "Auth Test Post" },
status: "draft",
};
}
expect(authResult).toHaveProperty("id");
expect(authResult).toHaveProperty("title");
expect(authResult.title.rendered).toContain("Auth Test Post");
});
it(`should reject invalid credentials (live=${useLive})`, async () => {
if (useLive) {
const response = await fetch(`${wordpressClient.config.baseUrl}/wp-json/wp/v2/users/me`, {
headers: {
Authorization: "Basic aW52YWxpZDppbnZhbGlk", // invalid:invalid
},
});
expect(response.ok).toBe(false);
expect(response.status).toBe(401);
} else {
// Mock authentication failure - ensure it throws
expect(() => {
throw new Error("Unauthorized");
}).toThrow("Unauthorized");
}
});
});
describe("Content Management Contract", () => {
let createdPostId;
it(`should create, read, update, and delete posts (live=${useLive})`, async () => {
// Create
const createData = {
title: "CRUD Test Post",
content: "Original content",
status: "draft",
};
const created = await wordpressClient.createPost(createData);
expect(created.id).toBeDefined();
createdPostId = created.id;
// Read
const read = await wordpressClient.getPost(createdPostId);
expect(read.id).toBe(createdPostId);
expect(read.title.rendered).toContain("CRUD Test Post");
// Update
const updated = await wordpressClient.updatePost({
id: createdPostId,
content: "Updated content",
status: "publish",
});
expect(updated.content.rendered).toContain("Updated content");
expect(updated.status).toBe("publish");
// Delete (test deletion in both modes)
const deleted = await wordpressClient.deletePost(createdPostId, true);
expect(deleted.deleted).toBe(true);
});
});
});