UNPKG

@gorizond/mcp-rancher-multi

Version:

MCP server for multiple Rancher Manager backends with Fleet GitOps support

557 lines (463 loc) 17.9 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { resolveToken, loadStore, saveStore, obfuscateConfig, loadConfigFromEnv, stripMetadataManagedFields, stripKeys, RancherServerConfig } from '../../src/utils.js'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; describe('Utils', () => { let tempFile: string; beforeEach(() => { tempFile = path.join(os.tmpdir(), `test-servers-${Date.now()}.json`); }); afterEach(() => { if (fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); } }); describe('resolveToken', () => { it('should return token as-is when no environment variable pattern', () => { expect(resolveToken('simple-token')).toBe('simple-token'); expect(resolveToken('')).toBe(''); }); it('should resolve environment variable pattern', () => { process.env.TEST_TOKEN = 'env-token-value'; expect(resolveToken('${ENV:TEST_TOKEN}')).toBe('env-token-value'); }); it('should return empty string when environment variable not found', () => { expect(resolveToken('${ENV:NONEXISTENT_TOKEN}')).toBe(''); }); it('should handle invalid environment variable names', () => { expect(resolveToken('${ENV:123invalid}')).toBe('${ENV:123invalid}'); expect(resolveToken('${ENV:}')).toBe('${ENV:}'); }); it('should handle malformed environment variable pattern', () => { expect(resolveToken('${ENV}')).toBe('${ENV}'); expect(resolveToken('${ENV:}')).toBe('${ENV:}'); expect(resolveToken('${ENV:test')).toBe('${ENV:test'); expect(resolveToken('ENV:test}')).toBe('ENV:test}'); }); it('should handle special characters in environment variable names', () => { process.env.TEST_TOKEN_SPECIAL = 'special-value'; expect(resolveToken('${ENV:TEST_TOKEN_SPECIAL}')).toBe('special-value'); }); }); describe('obfuscateConfig', () => { it('should obfuscate tokens in config', () => { const config = { 'server1': { id: 'server1', name: 'Test Server 1', baseUrl: 'https://test1.local', token: 'token-abc123def456' }, 'server2': { id: 'server2', name: 'Test Server 2', baseUrl: 'https://test2.local', token: 'token-xyz789' } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBe('***f456'); expect(obfuscated.server2.token).toBe('***z789'); expect(obfuscated.server1.name).toBe('Test Server 1'); expect(obfuscated.server2.baseUrl).toBe('https://test2.local'); }); it('should handle empty token', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: '' } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBeUndefined(); }); it('should handle undefined token', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: undefined } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBeUndefined(); }); it('should handle null token', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: null as any } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBeUndefined(); }); it('should handle short token', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: 'abc' } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBe('***abc'); }); it('should handle empty config object', () => { const config = {}; const obfuscated = obfuscateConfig(config); expect(obfuscated).toEqual({}); }); it('should handle token with exactly 4 characters', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: 'abcd' } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBe('***abcd'); }); it('should handle token with less than 4 characters', () => { const config = { 'server1': { id: 'server1', name: 'Test Server', baseUrl: 'https://test.local', token: 'abc' } }; const obfuscated = obfuscateConfig(config); expect(obfuscated.server1.token).toBe('***abc'); }); }); describe('stripMetadataManagedFields', () => { it('should remove managedFields from root metadata objects', () => { const obj = { metadata: { name: 'example', managedFields: [{ manager: 'controller' }], labels: { app: 'demo' } }, spec: { replicas: 2 } }; const result = stripMetadataManagedFields(obj); expect(result).toBe(obj); expect(result.metadata).toEqual({ name: 'example', labels: { app: 'demo' } }); }); it('should remove managedFields deeply inside arrays and nested objects', () => { const obj: any = { items: [ { metadata: { name: 'a', managedFields: [{ manager: 'm1' }], annotations: { k: 'v' } } }, { metadata: { name: 'b', labels: { env: 'prod' } } } ], extra: { metadata: { managedFields: [{ manager: 'm2' }], annotations: { other: 'value' } } } }; const result = stripMetadataManagedFields(obj); expect(result.items[0].metadata).toEqual({ name: 'a', annotations: { k: 'v' } }); expect(result.items[1].metadata).toEqual({ name: 'b', labels: { env: 'prod' } }); expect(result.extra.metadata).toEqual({ annotations: { other: 'value' } }); }); it('should handle circular references without errors', () => { const obj: any = { metadata: { managedFields: [{ manager: 'loop' }] } }; obj.self = obj; expect(() => stripMetadataManagedFields(obj)).not.toThrow(); expect(obj.metadata).toEqual({}); }); }); describe('stripKeys', () => { it('should remove specified keys at any depth', () => { const obj: any = { apiVersion: 'v1', data: { a: '1', b: '2' }, items: [ { metadata: { name: 'one' }, data: { secret: 'x' }, nested: { data: 'y' } }, { metadata: { name: 'two', data: 'keep-meta' } } ], metadata: { name: 'root', data: 'should-go' } }; const result = stripKeys(obj, ['data']); expect(result.data).toBeUndefined(); expect(result.items[0].data).toBeUndefined(); expect(result.items[0].nested.data).toBeUndefined(); expect(result.items[1].metadata.data).toBeUndefined(); expect(result.metadata.data).toBeUndefined(); }); it('should ignore empty key list and handle circular references', () => { const obj: any = { keep: true }; obj.self = obj; expect(stripKeys(obj, [])).toBe(obj); expect(obj.keep).toBe(true); }); }); describe('loadConfigFromEnv', () => { beforeEach(() => { // Clear environment variables before each test delete process.env.RANCHER_SERVERS; delete process.env.RANCHER_SERVER_test_BASEURL; delete process.env.RANCHER_SERVER_test_TOKEN; delete process.env.RANCHER_SERVER_test_NAME; delete process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY; delete process.env.RANCHER_SERVER_test_CACERTPEMBASE64; }); it('should load configuration from RANCHER_SERVERS environment variable', () => { const serversConfig = { 'server1': { id: 'server1', name: 'Test Server 1', baseUrl: 'https://test1.local', token: 'token1' }, 'server2': { id: 'server2', name: 'Test Server 2', baseUrl: 'https://test2.local', token: 'token2' } }; process.env.RANCHER_SERVERS = JSON.stringify(serversConfig); const result = loadConfigFromEnv(); expect(result).toEqual(serversConfig); }); it('should load configuration from individual environment variables', () => { process.env.RANCHER_SERVER_test_BASEURL = 'https://test.local'; process.env.RANCHER_SERVER_test_TOKEN = 'test-token'; process.env.RANCHER_SERVER_test_NAME = 'Test Server'; process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY = 'true'; process.env.RANCHER_SERVER_test_CACERTPEMBASE64 = 'base64-cert'; const result = loadConfigFromEnv(); expect(result.test).toEqual({ id: 'test', baseUrl: 'https://test.local', token: 'test-token', name: 'Test Server', insecureSkipTlsVerify: true, caCertPemBase64: 'base64-cert' }); }); it('should handle case-insensitive property names', () => { process.env.RANCHER_SERVER_test_BASEURL = 'https://test.local'; process.env.RANCHER_SERVER_test_TOKEN = 'test-token'; process.env.RANCHER_SERVER_test_NAME = 'Test Server'; process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY = 'false'; process.env.RANCHER_SERVER_test_CACERTPEMBASE64 = 'base64-cert'; const result = loadConfigFromEnv(); expect(result.test.insecureSkipTlsVerify).toBe(false); }); it('should handle boolean values for insecureSkipTlsVerify', () => { process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY = 'true'; const result1 = loadConfigFromEnv(); expect(result1.test.insecureSkipTlsVerify).toBe(true); delete process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY; process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY = 'false'; const result2 = loadConfigFromEnv(); expect(result2.test.insecureSkipTlsVerify).toBe(false); delete process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY; process.env.RANCHER_SERVER_test_INSECURESKIPTLSVERIFY = 'TRUE'; const result3 = loadConfigFromEnv(); expect(result3.test.insecureSkipTlsVerify).toBe(true); }); it('should handle invalid JSON in RANCHER_SERVERS', () => { process.env.RANCHER_SERVERS = 'invalid json'; const result = loadConfigFromEnv(); expect(result).toEqual({}); }); it('should handle empty RANCHER_SERVERS', () => { process.env.RANCHER_SERVERS = ''; const result = loadConfigFromEnv(); expect(result).toEqual({}); }); it('should handle undefined environment variable values', () => { // Set environment variables to undefined by deleting them delete process.env.RANCHER_SERVER_test_BASEURL; delete process.env.RANCHER_SERVER_test_TOKEN; const result = loadConfigFromEnv(); // When no environment variables are set, no config should be created expect(result.test).toBeUndefined(); }); it('should handle server IDs with underscores', () => { process.env.RANCHER_SERVER_test_server_BASEURL = 'https://test.local'; process.env.RANCHER_SERVER_test_server_TOKEN = 'test-token'; const result = loadConfigFromEnv(); expect(result['test_server']).toBeDefined(); expect(result['test_server'].baseUrl).toBe('https://test.local'); expect(result['test_server'].token).toBe('test-token'); }); it('should handle unknown property names', () => { process.env.RANCHER_SERVER_test_UNKNOWN = 'unknown-value'; process.env.RANCHER_SERVER_test_BASEURL = 'https://test.local'; const result = loadConfigFromEnv(); expect(result.test.baseUrl).toBe('https://test.local'); // Unknown properties should be ignored expect(result.test).not.toHaveProperty('unknown'); }); }); describe('loadStore', () => { it('should return empty object for non-existent file', () => { const result = loadStore('/non/existent/file.json'); expect(result).toEqual({}); }); it('should load valid JSON file', () => { const testData = { 'test-server': { id: 'test-server', name: 'Test Server', baseUrl: 'https://test.local', token: 'test-token' } }; fs.writeFileSync(tempFile, JSON.stringify(testData)); const result = loadStore(tempFile); expect(result).toEqual(testData); }); it('should handle invalid JSON file', () => { fs.writeFileSync(tempFile, 'invalid json content'); const result = loadStore(tempFile); expect(result).toEqual({}); }); it('should handle empty file', () => { fs.writeFileSync(tempFile, ''); const result = loadStore(tempFile); expect(result).toEqual({}); }); it('should handle null JSON content', () => { fs.writeFileSync(tempFile, 'null'); const result = loadStore(tempFile); expect(result).toEqual({}); }); it('should handle JSON with null value', () => { fs.writeFileSync(tempFile, JSON.stringify(null)); const result = loadStore(tempFile); expect(result).toEqual({}); }); it('should handle file read errors', () => { // Mock fs to throw an error const originalRequire = require; const mockFs = { existsSync: () => true, readFileSync: () => { throw new Error('Read error'); } }; vi.doMock('node:fs', () => mockFs); const result = loadStore(tempFile); expect(result).toEqual({}); vi.doMock('node:fs', () => originalRequire('node:fs')); }); it('should handle file that does not exist', () => { const result = loadStore('/path/to/nonexistent/file.json'); expect(result).toEqual({}); }); }); describe('saveStore', () => { it('should save data to file', () => { const testData: Record<string, RancherServerConfig> = { 'test-server': { id: 'test-server', name: 'Test Server', baseUrl: 'https://test.local', token: 'test-token' } }; saveStore(testData, tempFile); expect(fs.existsSync(tempFile)).toBe(true); const savedData = JSON.parse(fs.readFileSync(tempFile, 'utf-8')); expect(savedData).toEqual(testData); }); it('should create directory if it does not exist', () => { const testDir = path.join(os.tmpdir(), `test-dir-${Date.now()}`); const testFile = path.join(testDir, 'servers.json'); const testData: Record<string, RancherServerConfig> = { 'test-server': { id: 'test-server', baseUrl: 'https://test.local', token: 'test-token' } }; saveStore(testData, testFile); expect(fs.existsSync(testFile)).toBe(true); const savedData = JSON.parse(fs.readFileSync(testFile, 'utf-8')); expect(savedData).toEqual(testData); // Cleanup fs.unlinkSync(testFile); fs.rmdirSync(testDir); }); it('should handle nested directory creation', () => { const testDir = path.join(os.tmpdir(), `test-dir-${Date.now()}`, 'nested', 'deep'); const testFile = path.join(testDir, 'servers.json'); const testData: Record<string, RancherServerConfig> = { 'test-server': { id: 'test-server', baseUrl: 'https://test.local', token: 'test-token' } }; saveStore(testData, testFile); expect(fs.existsSync(testFile)).toBe(true); const savedData = JSON.parse(fs.readFileSync(testFile, 'utf-8')); expect(savedData).toEqual(testData); // Cleanup - remove files and directories in reverse order fs.unlinkSync(testFile); fs.rmdirSync(testDir); fs.rmdirSync(path.dirname(testDir)); fs.rmdirSync(path.dirname(path.dirname(testDir))); }); it('should format JSON with proper indentation', () => { const testData: Record<string, RancherServerConfig> = { 'test-server': { id: 'test-server', name: 'Test Server', baseUrl: 'https://test.local', token: 'test-token' } }; saveStore(testData, tempFile); const savedContent = fs.readFileSync(tempFile, 'utf-8'); const lines = savedContent.split('\n'); // Should have proper indentation (2 spaces) expect(lines[1]).toMatch(/^\s{2}/); expect(lines[2]).toMatch(/^\s{4}/); }); it('should handle empty data object', () => { const testData: Record<string, RancherServerConfig> = {}; saveStore(testData, tempFile); expect(fs.existsSync(tempFile)).toBe(true); const savedData = JSON.parse(fs.readFileSync(tempFile, 'utf-8')); expect(savedData).toEqual({}); }); it('should handle complex nested data', () => { const testData: Record<string, RancherServerConfig> = { 'server1': { id: 'server1', name: 'Server 1', baseUrl: 'https://server1.local', token: 'token1', insecureSkipTlsVerify: true, caCertPemBase64: 'cert1' }, 'server2': { id: 'server2', name: 'Server 2', baseUrl: 'https://server2.local', token: 'token2', insecureSkipTlsVerify: false } }; saveStore(testData, tempFile); const savedData = JSON.parse(fs.readFileSync(tempFile, 'utf-8')); expect(savedData).toEqual(testData); }); }); });