Skip to main content
Glama
ComposedManagers.test.js15.6 kB
/** * Composed Managers Test Suite * Tests for the new composition-based manager architecture */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { ComposedAuthenticationManager } from "../../dist/client/managers/ComposedAuthenticationManager.js"; import { ComposedRequestManager } from "../../dist/client/managers/ComposedRequestManager.js"; import { ComposedManagerFactory, createComposedWordPressClient, } from "../../dist/client/managers/ComposedManagerFactory.js"; import { ConfigurationProviderImpl } from "../../dist/client/managers/implementations/ConfigurationProviderImpl.js"; import { ErrorHandlerImpl } from "../../dist/client/managers/implementations/ErrorHandlerImpl.js"; import { ParameterValidatorImpl } from "../../dist/client/managers/implementations/ParameterValidatorImpl.js"; import { MigrationAdapter } from "../../dist/client/managers/composed/MigrationAdapter.js"; import { WordPressAPIError, AuthenticationError } from "../../dist/types/client.js"; // Mock the config module to enable debug logging for this test suite vi.mock("../../dist/config/Config.js", () => { return { config: vi.fn(() => ({ wordpress: {}, error: { legacyLogsEnabled: false }, debug: { enabled: true }, rateLimitEnabled: false, rateLimitRequests: 100, rateLimitWindow: 60000, rateLimit: 60, security: { rateLimit: 60, }, })), ConfigHelpers: { shouldDebug: vi.fn(() => true), }, }; }); // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; describe("Composed Managers", () => { let mockConfig; beforeEach(() => { mockConfig = { baseUrl: "https://test-site.com", timeout: 30000, auth: { method: "app-password", username: "testuser", appPassword: "test app password", }, }; // Reset fetch mock mockFetch.mockReset(); // Default successful response mockFetch.mockResolvedValue({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ id: 1, title: "Test Post" }), }); }); afterEach(() => { vi.restoreAllMocks(); }); describe("ConfigurationProviderImpl", () => { it("should provide configuration access", () => { const configProvider = new ConfigurationProviderImpl(mockConfig); expect(configProvider.config.baseUrl).toBe("https://test-site.com"); expect(configProvider.getTimeout()).toBe(30000); expect(configProvider.isDebugEnabled()).toBe(false); }); it("should validate required configuration", () => { const invalidConfig = { baseUrl: "https://test.com" }; // missing auth expect(() => { const configProvider = new ConfigurationProviderImpl(invalidConfig); configProvider.validateConfiguration(); }).toThrow("Missing required configuration: auth"); }); it("should provide configuration values by path", () => { const configProvider = new ConfigurationProviderImpl(mockConfig); expect(configProvider.getConfigValue("auth.method")).toBe("app-password"); expect(configProvider.getConfigValue("nonexistent", "default")).toBe("default"); }); }); describe("ErrorHandlerImpl", () => { let configProvider; let errorHandler; beforeEach(() => { configProvider = new ConfigurationProviderImpl(mockConfig); errorHandler = new ErrorHandlerImpl(configProvider); }); it("should handle WordPress API errors", () => { const apiError = new WordPressAPIError("Test error", 400, "test_error"); expect(() => { errorHandler.handleError(apiError, "test operation"); }).toThrow(WordPressAPIError); }); it("should handle timeout errors", () => { const timeoutError = { name: "AbortError" }; expect(() => { errorHandler.handleError(timeoutError, "test operation"); }).toThrow("Request timeout after 30000ms"); }); it("should handle connection errors", () => { const connectionError = { code: "ECONNREFUSED" }; expect(() => { errorHandler.handleError(connectionError, "test operation"); }).toThrow("Cannot connect to WordPress site"); }); it("should log successful operations", () => { // The logSuccess method should complete without errors // We can see from the stderr output that debug logging is working expect(() => { errorHandler.logSuccess("test operation", { test: true }); }).not.toThrow(); }); }); describe("ParameterValidatorImpl", () => { let validator; beforeEach(() => { validator = new ParameterValidatorImpl(); }); it("should validate required parameters", () => { const params = { name: "test", value: 123 }; expect(() => { validator.validateRequired(params, ["name", "value"]); }).not.toThrow(); expect(() => { validator.validateRequired(params, ["name", "missing"]); }).toThrow("Missing required parameter: missing"); }); it("should validate string parameters", () => { expect(validator.validateString("hello", "test")).toBe("hello"); expect(validator.validateString(null, "test")).toBe(""); expect(() => { validator.validateString(123, "test"); }).toThrow("test must be a string"); expect(() => { validator.validateString("", "test", { required: true }); }).toThrow("test is required"); }); it("should validate string length", () => { expect(() => { validator.validateString("hi", "test", { minLength: 5 }); }).toThrow("test must be at least 5 characters"); expect(() => { validator.validateString("this is too long", "test", { maxLength: 5 }); }).toThrow("test must be no more than 5 characters"); }); it("should validate number parameters", () => { expect(validator.validateNumber(42, "test")).toBe(42); expect(validator.validateNumber("42", "test")).toBe(42); expect(validator.validateNumber(null, "test")).toBe(0); expect(() => { validator.validateNumber("not a number", "test"); }).toThrow("test must be a valid number"); }); it("should validate WordPress IDs", () => { expect(validator.validateWordPressId(42)).toBe(42); expect(() => { validator.validateWordPressId(0); }).toThrow("id must be at least 1"); expect(() => { validator.validateWordPressId("not a number"); }).toThrow("id must be a valid number"); }); }); describe("ComposedAuthenticationManager", () => { let authManager; beforeEach(() => { authManager = ComposedAuthenticationManager.create(mockConfig); }); it("should create authentication manager", () => { expect(authManager).toBeInstanceOf(ComposedAuthenticationManager); }); it("should authenticate with app password", async () => { const result = await authManager.authenticate(); expect(result).toBe(true); expect(authManager.isAuthenticated()).toBe(true); }); it("should provide auth headers", async () => { await authManager.authenticate(); const headers = authManager.getAuthHeaders(); expect(headers.Authorization).toContain("Basic "); }); it("should handle authentication failures", async () => { const invalidConfig = { ...mockConfig, auth: { method: "app-password" }, // missing credentials }; expect(() => { ComposedAuthenticationManager.create(invalidConfig); }).toThrow(AuthenticationError); }); it("should provide authentication status", async () => { await authManager.authenticate(); const status = authManager.getAuthStatus(); expect(status.isAuthenticated).toBe(true); expect(status.method).toBe("app-password"); expect(status.lastAuthAttempt).toBeInstanceOf(Date); }); }); describe("ComposedRequestManager", () => { let requestManager; let authManager; beforeEach(async () => { authManager = ComposedAuthenticationManager.create(mockConfig); await authManager.authenticate(); requestManager = ComposedRequestManager.create(mockConfig, authManager); await requestManager.initialize(); }); it("should create request manager", () => { expect(requestManager).toBeInstanceOf(ComposedRequestManager); }); it("should make GET requests", async () => { const response = await requestManager.request("GET", "/wp/v2/posts"); expect(response).toEqual({ id: 1, title: "Test Post" }); expect(mockFetch).toHaveBeenCalledWith( "https://test-site.com/wp-json/wp/v2/posts", expect.objectContaining({ method: "GET", headers: expect.objectContaining({ "Content-Type": "application/json", Authorization: expect.stringContaining("Basic"), }), }), ); }); it("should make POST requests", async () => { const postData = { title: "New Post", content: "Post content" }; await requestManager.request("POST", "/wp/v2/posts", postData); expect(mockFetch).toHaveBeenCalledWith( "https://test-site.com/wp-json/wp/v2/posts", expect.objectContaining({ method: "POST", body: JSON.stringify(postData), }), ); }); it("should track request statistics", async () => { await requestManager.request("GET", "/wp/v2/posts"); const stats = requestManager.getStats(); expect(stats.totalRequests).toBe(1); expect(stats.successfulRequests).toBe(1); expect(stats.failedRequests).toBe(0); }); it("should reset statistics", async () => { await requestManager.request("GET", "/wp/v2/posts"); requestManager.resetStats(); const stats = requestManager.getStats(); expect(stats.totalRequests).toBe(0); expect(stats.successfulRequests).toBe(0); }); it("should handle HTTP errors", async () => { mockFetch.mockResolvedValue({ ok: false, status: 404, statusText: "Not Found", json: vi.fn().mockResolvedValue({ message: "Post not found", }), }); await expect(requestManager.request("GET", "/wp/v2/posts/999")).rejects.toThrow("Post not found"); }); it("should handle network errors", async () => { mockFetch.mockRejectedValue(new Error("Network error")); await expect(requestManager.request("GET", "/wp/v2/posts")).rejects.toThrow("Network error"); }); it("should require initialization", async () => { const uninitializedManager = ComposedRequestManager.create(mockConfig, authManager); await expect(uninitializedManager.request("GET", "/wp/v2/posts")).rejects.toThrow("not initialized"); }); }); describe("ComposedManagerFactory", () => { let factory; beforeEach(() => { factory = new ComposedManagerFactory(); }); it("should create configuration provider", () => { const configProvider = factory.createConfigurationProvider(mockConfig); expect(configProvider).toBeInstanceOf(ConfigurationProviderImpl); }); it("should create error handler", () => { const errorHandler = factory.createErrorHandler(mockConfig); expect(errorHandler).toBeInstanceOf(ErrorHandlerImpl); }); it("should create parameter validator", () => { const validator = factory.createParameterValidator(); expect(validator).toBeInstanceOf(ParameterValidatorImpl); }); it("should create authentication provider", () => { const authProvider = factory.createAuthenticationProvider(mockConfig); expect(authProvider).toBeInstanceOf(ComposedAuthenticationManager); }); it("should create complete composed client", async () => { const client = await factory.createComposedClient({ clientConfig: mockConfig }); expect(client.isAuthenticated()).toBe(true); expect(client.config.baseUrl).toBe("https://test-site.com"); }); }); describe("ComposedWordPressClient", () => { let client; beforeEach(async () => { client = await createComposedWordPressClient(mockConfig); }); it("should create and initialize client", () => { expect(client.isAuthenticated()).toBe(true); expect(client.config.baseUrl).toBe("https://test-site.com"); }); it("should provide WordPress convenience methods", async () => { // Test getPosts mockFetch.mockResolvedValue({ ok: true, status: 200, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue([ { id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }, ]), }); const posts = await client.getPosts(); expect(posts).toHaveLength(2); expect(posts[0].title).toBe("Post 1"); }); it("should provide getPost method", async () => { const post = await client.getPost(1); expect(post.id).toBe(1); expect(post.title).toBe("Test Post"); }); it("should validate post ID parameters", async () => { await expect(client.getPost("invalid")).rejects.toThrow("post ID must be a valid number"); await expect(client.getPost(0)).rejects.toThrow("post ID must be at least 1"); }); it("should create posts", async () => { const postData = { title: "New Post", content: "Content" }; mockFetch.mockResolvedValue({ ok: true, status: 201, headers: new Map([["content-type", "application/json"]]), json: vi.fn().mockResolvedValue({ id: 3, ...postData }), }); const result = await client.createPost(postData); expect(result.id).toBe(3); expect(result.title).toBe("New Post"); }); it("should validate required fields for post creation", async () => { await expect(client.createPost({})).rejects.toThrow("Missing required parameter: title"); await expect(client.createPost({ title: "Test" })).rejects.toThrow("Missing required parameter: content"); }); it("should dispose resources", () => { expect(() => client.dispose()).not.toThrow(); }); }); describe("MigrationAdapter", () => { it("should create compatible managers", async () => { const { authManager } = await MigrationAdapter.createCompatibleManagers(mockConfig); expect(authManager).toBeInstanceOf(ComposedAuthenticationManager); expect(authManager.isAuthenticated()).toBe(true); }); it("should detect composed managers", () => { const authManager = ComposedAuthenticationManager.create(mockConfig); expect(MigrationAdapter.isComposed(authManager)).toBe(true); expect(MigrationAdapter.isComposed({})).toBe(false); }); it("should provide migration status", () => { const authManager = ComposedAuthenticationManager.create(mockConfig); const plainObject = {}; const status = MigrationAdapter.getMigrationStatus([authManager, plainObject]); expect(status.total).toBe(2); expect(status.composed).toBe(1); expect(status.inheritance).toBe(1); expect(status.percentage).toBe(50); }); it("should generate migration guide", () => { const guide = MigrationAdapter.generateMigrationGuide(); expect(guide).toContain("Migration Guide"); expect(guide).toContain("Benefits of Composition"); expect(guide).toContain("Migration Steps"); }); }); });

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