Skip to main content
Glama
BulkOperations.test.js12.2 kB
/** * BulkOperations Tests * * Tests for the SEO bulk operations functionality including * batch processing, retry logic, progress tracking, and error handling. * * @since 2.7.0 */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { BulkOperations } from "../../../dist/tools/seo/BulkOperations.js"; // Mock WordPress client const createMockClient = () => ({ getPost: vi.fn(), updatePost: vi.fn(), authenticate: vi.fn().mockResolvedValue(true), }); // Mock cache manager const createMockCache = () => ({ get: vi.fn(), set: vi.fn(), delete: vi.fn(), clear: vi.fn(), }); describe("BulkOperations", () => { let bulkOps; let mockClient; let mockCache; beforeEach(() => { mockClient = createMockClient(); mockCache = createMockCache(); bulkOps = new BulkOperations(mockClient, mockCache, { batchSize: 3, // Small batch size for testing maxRetries: 2, retryDelayMs: 10, // Fast retries for tests maxRetryDelayMs: 100, operationTimeoutMs: 5000, enableProgress: true, }); }); describe("Configuration", () => { it("should initialize with default configuration", () => { const defaultBulkOps = new BulkOperations(mockClient); const config = defaultBulkOps.getConfig(); expect(config.batchSize).toBe(10); expect(config.maxRetries).toBe(3); expect(config.enableProgress).toBe(true); }); it("should allow configuration updates", () => { bulkOps.updateConfig({ batchSize: 5, maxRetries: 5 }); const config = bulkOps.getConfig(); expect(config.batchSize).toBe(5); expect(config.maxRetries).toBe(5); }); }); describe("Bulk Metadata Updates", () => { const samplePost = { id: 1, title: { rendered: "Test Post" }, content: { rendered: "<p>Test content for SEO analysis.</p>" }, excerpt: { rendered: "Test excerpt" }, link: "https://example.com/test-post", status: "publish", type: "post", }; beforeEach(() => { mockClient.getPost.mockResolvedValue(samplePost); }); it("should process bulk metadata updates successfully", async () => { const params = { postIds: [1, 2, 3], site: "test", focusKeywords: ["SEO"], dryRun: false, }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result).toBeDefined(); expect(result.total).toBe(3); expect(result.success).toBe(3); expect(result.failed).toBe(0); expect(result.skipped).toBe(0); expect(result.dryRun).toBe(false); expect(result.processingTime).toBeGreaterThan(0); expect(mockClient.getPost).toHaveBeenCalledTimes(3); }); it("should handle dry run mode", async () => { const params = { postIds: [1, 2], site: "test", dryRun: true, }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result.dryRun).toBe(true); expect(result.success).toBe(2); }); it("should handle empty post IDs array", async () => { const params = { postIds: [], site: "test", }; await expect(bulkOps.bulkUpdateMetadata(params)).rejects.toThrow("No post IDs provided"); }); it("should handle missing post IDs", async () => { const params = { site: "test", }; await expect(bulkOps.bulkUpdateMetadata(params)).rejects.toThrow("No post IDs provided"); }); it("should track progress correctly", async () => { const progressCallbacks = []; const progressCallback = (progress) => { progressCallbacks.push({ ...progress }); }; const params = { postIds: [1, 2, 3, 4, 5], site: "test", }; await bulkOps.bulkUpdateMetadata(params, progressCallback); expect(progressCallbacks.length).toBeGreaterThan(0); const finalProgress = progressCallbacks[progressCallbacks.length - 1]; expect(finalProgress.total).toBe(5); expect(finalProgress.processed).toBe(5); expect(finalProgress.completed).toBe(5); }); it("should handle post not found errors", async () => { mockClient.getPost .mockResolvedValueOnce(samplePost) .mockResolvedValueOnce(null) .mockResolvedValueOnce(samplePost); const params = { postIds: [1, 2, 3], site: "test", }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result.success).toBe(2); expect(result.failed).toBe(1); expect(result.errors).toHaveLength(1); expect(result.errors[0].postId).toBe(2); expect(result.errors[0].error).toContain("not found"); }); it("should cache results to avoid duplicate processing", async () => { mockCache.get.mockReturnValueOnce(null).mockReturnValueOnce({ title: "Cached" }).mockReturnValueOnce(null); const params = { postIds: [1, 2, 3], site: "test", }; await bulkOps.bulkUpdateMetadata(params); // Should only fetch posts that weren't cached expect(mockClient.getPost).toHaveBeenCalledTimes(2); expect(mockCache.get).toHaveBeenCalledTimes(3); }); it("should respect force parameter to bypass cache", async () => { mockCache.get.mockReturnValue({ title: "Cached" }); const params = { postIds: [1, 2], site: "test", force: true, }; await bulkOps.bulkUpdateMetadata(params); // Should fetch all posts despite cache expect(mockClient.getPost).toHaveBeenCalledTimes(2); }); }); describe("Bulk Content Analysis", () => { const samplePost = { id: 1, title: { rendered: "Analysis Test Post" }, content: { rendered: "<h1>Main Title</h1><p>Content for analysis with good readability.</p>" }, excerpt: { rendered: "Analysis excerpt" }, link: "https://example.com/analysis-test", status: "publish", type: "post", }; beforeEach(() => { mockClient.getPost.mockResolvedValue(samplePost); }); it("should perform bulk content analysis", async () => { const params = { postIds: [1, 2, 3], site: "test", analysisType: "full", focusKeywords: ["SEO", "analysis"], }; const { results, summary } = await bulkOps.bulkAnalyzeContent(params); expect(results).toHaveLength(3); expect(summary.total).toBe(3); expect(summary.success).toBe(3); expect(summary.failed).toBe(0); // Check analysis result structure results.forEach((result) => { expect(result).toHaveProperty("score"); expect(result).toHaveProperty("status"); expect(result).toHaveProperty("metrics"); expect(result).toHaveProperty("recommendations"); expect(result.score).toBeGreaterThanOrEqual(0); expect(result.score).toBeLessThanOrEqual(100); }); }); it("should handle analysis errors with retries", async () => { mockClient.getPost .mockRejectedValueOnce(new Error("Network error")) .mockResolvedValueOnce(samplePost) .mockResolvedValueOnce(samplePost); const params = { postIds: [1, 2, 3], site: "test", analysisType: "readability", }; const { results, summary } = await bulkOps.bulkAnalyzeContent(params); expect(results.length).toBeGreaterThan(0); expect(summary.success).toBeGreaterThan(0); }); it("should cache analysis results", async () => { const params = { postIds: [1, 2], site: "test", analysisType: "keywords", }; await bulkOps.bulkAnalyzeContent(params); expect(mockCache.set).toHaveBeenCalledTimes(2); // Verify cache keys contain analysis type const setCalls = mockCache.set.mock.calls; setCalls.forEach((call) => { const cacheKey = call[0]; expect(cacheKey).toContain("bulk-analysis"); expect(cacheKey).toContain("keywords"); }); }); }); describe("Error Handling and Retries", () => { it("should retry retryable errors", async () => { mockClient.getPost .mockRejectedValueOnce(new Error("503 Service Unavailable")) .mockRejectedValueOnce(new Error("timeout")) .mockResolvedValueOnce({ id: 1, title: { rendered: "Test" }, content: { rendered: "Content" }, status: "publish", }); const params = { postIds: [1], site: "test", }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result.success).toBe(1); expect(result.failed).toBe(0); expect(mockClient.getPost).toHaveBeenCalledTimes(3); // Original + 2 retries }); it("should not retry non-retryable errors", async () => { mockClient.getPost.mockRejectedValue(new Error("404 Not Found")); const params = { postIds: [1], site: "test", }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result.success).toBe(0); expect(result.failed).toBe(1); expect(mockClient.getPost).toHaveBeenCalledTimes(1); // No retries for 404 }); it("should give up after max retries", async () => { mockClient.getPost.mockRejectedValue(new Error("502 Bad Gateway")); const params = { postIds: [1], site: "test", }; const result = await bulkOps.bulkUpdateMetadata(params); expect(result.failed).toBe(1); expect(mockClient.getPost).toHaveBeenCalledTimes(3); // Original + 2 retries (maxRetries = 2) }); }); describe("Batch Processing", () => { it("should process items in correct batch sizes", async () => { const progressUpdates = []; const progressCallback = (progress) => { progressUpdates.push({ currentBatch: progress.currentBatch, totalBatches: progress.totalBatches, processed: progress.processed, }); }; mockClient.getPost.mockResolvedValue({ id: 1, title: { rendered: "Test" }, content: { rendered: "Content" }, status: "publish", }); const params = { postIds: [1, 2, 3, 4, 5, 6, 7], // 7 items with batchSize 3 = 3 batches site: "test", }; await bulkOps.bulkUpdateMetadata(params, progressCallback); const finalUpdate = progressUpdates[progressUpdates.length - 1]; expect(finalUpdate.totalBatches).toBe(3); // Math.ceil(7/3) expect(finalUpdate.processed).toBe(7); }); it("should calculate estimated completion time", async () => { let hasEta = false; const progressCallback = (progress) => { if (progress.eta && progress.processed < progress.total) { hasEta = true; expect(progress.eta).toBeInstanceOf(Date); expect(progress.eta.getTime()).toBeGreaterThan(Date.now()); } }; mockClient.getPost.mockResolvedValue({ id: 1, title: { rendered: "Test" }, content: { rendered: "Content" }, status: "publish", }); const params = { postIds: [1, 2, 3, 4, 5], site: "test", }; await bulkOps.bulkUpdateMetadata(params, progressCallback); // ETA should be calculated during processing expect(hasEta).toBe(true); }); }); describe("Performance", () => { it("should complete bulk operations within reasonable time", async () => { mockClient.getPost.mockResolvedValue({ id: 1, title: { rendered: "Performance Test" }, content: { rendered: "Content for performance testing" }, status: "publish", }); const params = { postIds: Array.from({ length: 20 }, (_, i) => i + 1), site: "test", }; const startTime = Date.now(); const result = await bulkOps.bulkUpdateMetadata(params); const endTime = Date.now(); expect(result.success).toBe(20); expect(endTime - startTime).toBeLessThan(10000); // Should complete within 10 seconds expect(result.processingTime).toBeGreaterThan(0); }); }); });

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