UNPKG

@gorizond/mcp-rancher-multi

Version:

MCP server for multiple Rancher Manager backends with Fleet GitOps support

373 lines (316 loc) 11.7 kB
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { RancherClient } from "../../src/rancher-client.js"; import { loadConfigFromEnv, obfuscateConfig, saveStore, loadStore, } from "../../src/utils.js"; import { RancherServerConfig } from "../../src/utils.js"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; // Mock fetch globally global.fetch = vi.fn(); describe("Integration Tests", () => { let tempFile: string; let mockFetch: any; beforeEach(() => { vi.clearAllMocks(); mockFetch = fetch as any; tempFile = path.join(os.tmpdir(), `integration-test-${Date.now()}.json`); }); afterEach(() => { if (fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); } vi.restoreAllMocks(); }); describe("Configuration Flow", () => { it("should load config from env and create client", () => { // Setup environment process.env.RANCHER_SERVER_test_BASEURL = "https://test.rancher.com"; process.env.RANCHER_SERVER_test_TOKEN = "test-token"; process.env.RANCHER_SERVER_test_NAME = "Test Server"; // Load configuration const config = loadConfigFromEnv(); expect(config.test).toBeDefined(); expect(config.test.baseUrl).toBe("https://test.rancher.com"); expect(config.test.token).toBe("test-token"); // Create client const client = new RancherClient(config.test); expect(client.baseUrl).toBe("https://test.rancher.com"); expect(client.token).toBe("test-token"); // Cleanup delete process.env.RANCHER_SERVER_test_BASEURL; delete process.env.RANCHER_SERVER_test_TOKEN; delete process.env.RANCHER_SERVER_test_NAME; }); it("should save and load configuration from file", () => { const testConfig: Record<string, RancherServerConfig> = { server1: { id: "server1", name: "Test Server 1", baseUrl: "https://server1.local", token: "token1", }, server2: { id: "server2", name: "Test Server 2", baseUrl: "https://server2.local", token: "token2", }, }; // Save configuration saveStore(testConfig, tempFile); expect(fs.existsSync(tempFile)).toBe(true); // Load configuration const loadedConfig = loadStore(tempFile); expect(loadedConfig).toEqual(testConfig); // Create clients from loaded config const client1 = new RancherClient(loadedConfig.server1); const client2 = new RancherClient(loadedConfig.server2); expect(client1.baseUrl).toBe("https://server1.local"); expect(client2.baseUrl).toBe("https://server2.local"); }); it("should obfuscate config for safe logging", () => { const testConfig: Record<string, RancherServerConfig> = { server1: { id: "server1", name: "Test Server 1", baseUrl: "https://server1.local", token: "secret-token-12345", }, }; const obfuscated = obfuscateConfig(testConfig); expect(obfuscated.server1.token).toBe("***2345"); expect(obfuscated.server1.name).toBe("Test Server 1"); expect(obfuscated.server1.baseUrl).toBe("https://server1.local"); }); }); describe("Client Operations", () => { let client: RancherClient; let mockConfig: RancherServerConfig; beforeEach(() => { mockConfig = { id: "test-server", baseUrl: "https://test.rancher.com", token: "test-token", name: "Test Server", }; client = new RancherClient(mockConfig); }); it("should handle complete cluster listing workflow", async () => { const mockClusters = { data: [ { id: "cluster1", name: "Test Cluster 1", state: "active" }, { id: "cluster2", name: "Test Cluster 2", state: "active" }, ], }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockClusters), }); const clusters = await client.listClusters(); expect(clusters).toEqual([ { id: "cluster1", name: "Test Cluster 1" }, { id: "cluster2", name: "Test Cluster 2" }, ]); expect(mockFetch).toHaveBeenCalledWith( "https://test.rancher.com/v3/clusters", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token", Accept: "application/json", }), }), ); }); it("should handle complete node listing workflow", async () => { const mockNodes = { data: [ { id: "node1", nodeName: "test-node-1", clusterId: "cluster1", state: "active", }, ], }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockNodes), }); const nodes = await client.listNodes("cluster1"); expect(nodes).toEqual(mockNodes.data); expect(mockFetch).toHaveBeenCalledWith( "https://test.rancher.com/v3/nodes?clusterId=cluster1", expect.any(Object), ); }); it("should handle complete kubeconfig generation workflow", async () => { const mockKubeconfig = { config: "apiVersion: v1\nkind: Config\nclusters:\n- name: test-cluster", }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockKubeconfig), }); const kubeconfig = await client.generateKubeconfig("cluster1"); expect(kubeconfig).toBe(mockKubeconfig.config); expect(mockFetch).toHaveBeenCalledWith( "https://test.rancher.com/v3/clusters/cluster1?action=generateKubeconfig", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ "content-type": "application/json", }), }), ); }); it("should handle complete namespace listing workflow", async () => { const mockNamespaces = { items: [ { metadata: { name: "default" } }, { metadata: { name: "kube-system" } }, ], }; mockFetch.mockResolvedValueOnce({ ok: true, headers: new Map([["content-type", "application/json"]]), json: () => Promise.resolve(mockNamespaces), }); const namespaces = await client.listNamespaces("cluster1"); expect(namespaces).toEqual(mockNamespaces.items); expect(mockFetch).toHaveBeenCalledWith( "https://test.rancher.com/k8s/clusters/cluster1/api/v1/namespaces", expect.any(Object), ); }); }); describe("Error Handling Integration", () => { let client: RancherClient; let mockConfig: RancherServerConfig; beforeEach(() => { mockConfig = { id: "test-server", baseUrl: "https://test.rancher.com", token: "test-token", name: "Test Server", }; client = new RancherClient(mockConfig); }); it("should handle network errors gracefully", async () => { mockFetch.mockRejectedValueOnce(new Error("Network error")); await expect(client.listClusters()).rejects.toThrow("Network error"); }); it("should handle HTTP errors gracefully", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: "Unauthorized", text: () => Promise.resolve("Unauthorized access"), }); await expect(client.listClusters()).rejects.toThrow( "HTTP 401 Unauthorized", ); }); it("should handle malformed responses gracefully", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.reject(new Error("Invalid JSON")), }); await expect(client.listClusters()).rejects.toThrow("Invalid JSON"); }); }); describe("Environment Variable Integration", () => { beforeEach(() => { // Clear environment variables delete process.env.RANCHER_SERVERS; delete process.env.RANCHER_SERVER_test_BASEURL; delete process.env.RANCHER_SERVER_test_TOKEN; }); afterEach(() => { // Cleanup delete process.env.RANCHER_SERVERS; delete process.env.RANCHER_SERVER_test_BASEURL; delete process.env.RANCHER_SERVER_test_TOKEN; }); it("should handle environment variable resolution in tokens", () => { process.env.SECRET_TOKEN = "secret-value"; process.env.RANCHER_SERVER_test_TOKEN = "${ENV:SECRET_TOKEN}"; process.env.RANCHER_SERVER_test_BASEURL = "https://test.local"; const config = loadConfigFromEnv(); const client = new RancherClient(config.test); expect(client.token).toBe("secret-value"); }); it("should handle missing environment variables gracefully", () => { process.env.RANCHER_SERVER_test_TOKEN = "${ENV:NONEXISTENT}"; process.env.RANCHER_SERVER_test_BASEURL = "https://test.local"; const config = loadConfigFromEnv(); const client = new RancherClient(config.test); expect(client.token).toBe(""); }); it("should handle mixed configuration sources", () => { // Set some values via individual env vars process.env.RANCHER_SERVER_server1_BASEURL = "https://server1.local"; process.env.RANCHER_SERVER_server1_TOKEN = "token1"; // Set some values via RANCHER_SERVERS const serversConfig = { server2: { id: "server2", name: "Server 2", baseUrl: "https://server2.local", token: "token2", }, }; process.env.RANCHER_SERVERS = JSON.stringify(serversConfig); const config = loadConfigFromEnv(); expect(config.server1).toBeDefined(); expect(config.server1.baseUrl).toBe("https://server1.local"); expect(config.server2).toBeDefined(); expect(config.server2.baseUrl).toBe("https://server2.local"); }); }); describe("File System Integration", () => { it("should handle complete save-load cycle with complex data", () => { const complexConfig: Record<string, RancherServerConfig> = { "prod-server": { id: "prod-server", name: "Production Server", baseUrl: "https://prod.rancher.com", token: "prod-token-12345", insecureSkipTlsVerify: false, caCertPemBase64: "prod-cert-base64", }, "dev-server": { id: "dev-server", name: "Development Server", baseUrl: "https://dev.rancher.com", token: "dev-token-67890", insecureSkipTlsVerify: true, }, }; // Save configuration saveStore(complexConfig, tempFile); // Load configuration const loadedConfig = loadStore(tempFile); // Verify all properties are preserved expect(loadedConfig["prod-server"]).toEqual(complexConfig["prod-server"]); expect(loadedConfig["dev-server"]).toEqual(complexConfig["dev-server"]); // Create clients and verify they work const prodClient = new RancherClient(loadedConfig["prod-server"]); const devClient = new RancherClient(loadedConfig["dev-server"]); expect(prodClient.baseUrl).toBe("https://prod.rancher.com"); expect(prodClient.insecure).toBe(false); expect(devClient.baseUrl).toBe("https://dev.rancher.com"); expect(devClient.insecure).toBe(true); }); it("should handle file system errors gracefully", () => { // Test with non-existent directory const nonExistentPath = "/non/existent/path/servers.json"; const result = loadStore(nonExistentPath); expect(result).toEqual({}); }); }); });