Skip to main content
Glama
configure.test.ts13.8 kB
/** * Tests for configure command utilities * * @package WP_Navigator_MCP */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; // ============================================================================= // Helper Functions (extracted for testing) // ============================================================================= /** * Parse existing .wpnav.env file */ function parseWpnavEnv(content: string): Record<string, string> { const result: Record<string, string> = {}; const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith('#')) continue; const eqIndex = trimmed.indexOf('='); if (eqIndex > 0) { const key = trimmed.slice(0, eqIndex).trim(); let value = trimmed.slice(eqIndex + 1).trim(); // Remove surrounding quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } result[key] = value; } } return result; } /** * Generate .wpnav.env content */ function generateWpnavEnvContent( siteUrl: string, username: string, password: string ): string { const timestamp = new Date().toISOString(); return `# WP Navigator Connection Settings # Generated by wpnav configure on ${timestamp} # # WARNING: This file contains sensitive credentials. # Add .wpnav.env to your .gitignore file! # WordPress Site URL (without trailing slash) WP_BASE_URL=${siteUrl} # REST API endpoint (usually <site>/wp-json) WP_REST_API=${siteUrl}/wp-json # WP Navigator API base WPNAV_BASE=${siteUrl}/wp-json/wpnav/v1 # Introspect endpoint for plugin discovery WPNAV_INTROSPECT=${siteUrl}/wp-json/wpnav/v1/introspect # Application Password credentials # Generate at: ${siteUrl}/wp-admin/profile.php#application-passwords WP_APP_USER=${username} WP_APP_PASS=${password} `; } /** * Validate WordPress URL format */ function validateWordPressUrl(url: string): string | null { if (!url) return 'URL is required'; // Must start with http:// or https:// if (!url.startsWith('http://') && !url.startsWith('https://')) { return 'URL must start with http:// or https://'; } // Basic URL validation try { const parsed = new URL(url); if (!parsed.hostname) return 'Invalid URL format'; } catch { return 'Invalid URL format'; } // No trailing slash if (url.endsWith('/')) { return 'URL should not end with a slash'; } return null; } /** * Validate username format */ function validateUsername(username: string): string | null { if (!username) return 'Username is required'; if (username.length < 2) return 'Username must be at least 2 characters'; return null; } /** * Validate Application Password format */ function validatePassword(password: string): string | null { if (!password) return 'Password is required'; // WordPress app passwords are typically 24 characters with spaces (4 groups of 6) // But they can also be entered without spaces (24 chars) const cleaned = password.replace(/\s/g, ''); if (cleaned.length < 16) { return 'Application password seems too short. Generate one at WordPress Admin → Users → Profile → Application Passwords'; } return null; } /** * Mask password for display (show first 4 chars, rest as dots) */ function maskPassword(password: string): string { if (password.length <= 4) return '****'; return password.slice(0, 4) + '•'.repeat(Math.min(password.length - 4, 16)); } /** * Write .wpnav.env file with atomic write (temp file + rename) */ function writeWpnavEnvAtomic(filePath: string, content: string): void { const tempPath = `${filePath}.${process.pid}.tmp`; try { // Write to temp file first fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600 }); // Atomic rename fs.renameSync(tempPath, filePath); } catch (err) { // Clean up temp file if it exists try { if (fs.existsSync(tempPath)) { fs.unlinkSync(tempPath); } } catch { // Ignore cleanup errors } throw err; } } // ============================================================================= // Tests // ============================================================================= describe('parseWpnavEnv', () => { it('should parse simple key=value pairs', () => { const content = `WP_BASE_URL=https://example.com WP_APP_USER=admin`; const result = parseWpnavEnv(content); expect(result.WP_BASE_URL).toBe('https://example.com'); expect(result.WP_APP_USER).toBe('admin'); }); it('should skip comments', () => { const content = `# This is a comment WP_BASE_URL=https://example.com # Another comment WP_APP_USER=admin`; const result = parseWpnavEnv(content); expect(Object.keys(result)).toHaveLength(2); expect(result.WP_BASE_URL).toBe('https://example.com'); }); it('should skip empty lines', () => { const content = `WP_BASE_URL=https://example.com WP_APP_USER=admin `; const result = parseWpnavEnv(content); expect(Object.keys(result)).toHaveLength(2); }); it('should remove surrounding quotes from values', () => { const content = `WP_BASE_URL="https://example.com" WP_APP_PASS='xxxx xxxx xxxx xxxx'`; const result = parseWpnavEnv(content); expect(result.WP_BASE_URL).toBe('https://example.com'); expect(result.WP_APP_PASS).toBe('xxxx xxxx xxxx xxxx'); }); it('should handle values with equals signs', () => { const content = `SOME_VAR=value=with=equals`; const result = parseWpnavEnv(content); expect(result.SOME_VAR).toBe('value=with=equals'); }); it('should handle empty values', () => { const content = `EMPTY_VAR= ANOTHER_VAR=value`; const result = parseWpnavEnv(content); expect(result.EMPTY_VAR).toBe(''); expect(result.ANOTHER_VAR).toBe('value'); }); }); describe('generateWpnavEnvContent', () => { it('should generate valid env file content', () => { const content = generateWpnavEnvContent( 'https://example.com', 'admin', 'xxxx xxxx xxxx xxxx' ); expect(content).toContain('WP_BASE_URL=https://example.com'); expect(content).toContain('WP_REST_API=https://example.com/wp-json'); expect(content).toContain('WPNAV_BASE=https://example.com/wp-json/wpnav/v1'); expect(content).toContain('WPNAV_INTROSPECT=https://example.com/wp-json/wpnav/v1/introspect'); expect(content).toContain('WP_APP_USER=admin'); expect(content).toContain('WP_APP_PASS=xxxx xxxx xxxx xxxx'); }); it('should include warning about gitignore', () => { const content = generateWpnavEnvContent('https://example.com', 'admin', 'pass'); expect(content).toContain('.gitignore'); expect(content).toContain('WARNING'); }); it('should include timestamp', () => { const content = generateWpnavEnvContent('https://example.com', 'admin', 'pass'); expect(content).toContain('Generated by wpnav configure on'); }); it('should include application password help URL', () => { const content = generateWpnavEnvContent('https://example.com', 'admin', 'pass'); expect(content).toContain('https://example.com/wp-admin/profile.php#application-passwords'); }); it('generated content should be parseable', () => { const content = generateWpnavEnvContent( 'https://example.com', 'testuser', 'test password here' ); const parsed = parseWpnavEnv(content); expect(parsed.WP_BASE_URL).toBe('https://example.com'); expect(parsed.WP_APP_USER).toBe('testuser'); expect(parsed.WP_APP_PASS).toBe('test password here'); }); }); describe('validateWordPressUrl', () => { it('should accept valid https URLs', () => { expect(validateWordPressUrl('https://example.com')).toBeNull(); expect(validateWordPressUrl('https://sub.example.com')).toBeNull(); expect(validateWordPressUrl('https://example.com:8080')).toBeNull(); expect(validateWordPressUrl('https://example.com/wordpress')).toBeNull(); }); it('should accept valid http URLs', () => { expect(validateWordPressUrl('http://localhost')).toBeNull(); expect(validateWordPressUrl('http://localhost:8080')).toBeNull(); expect(validateWordPressUrl('http://192.168.1.100')).toBeNull(); }); it('should reject empty URL', () => { expect(validateWordPressUrl('')).toBe('URL is required'); }); it('should reject URLs without protocol', () => { expect(validateWordPressUrl('example.com')).toBe('URL must start with http:// or https://'); }); it('should reject URLs with trailing slash', () => { expect(validateWordPressUrl('https://example.com/')).toBe('URL should not end with a slash'); }); it('should reject invalid URL formats', () => { expect(validateWordPressUrl('https://')).toBe('Invalid URL format'); expect(validateWordPressUrl('not a url')).toBe('URL must start with http:// or https://'); }); }); describe('validateUsername', () => { it('should accept valid usernames', () => { expect(validateUsername('admin')).toBeNull(); expect(validateUsername('john_doe')).toBeNull(); expect(validateUsername('ab')).toBeNull(); // minimum 2 chars }); it('should reject empty username', () => { expect(validateUsername('')).toBe('Username is required'); }); it('should reject too short username', () => { expect(validateUsername('a')).toBe('Username must be at least 2 characters'); }); }); describe('validatePassword', () => { it('should accept valid application passwords with spaces', () => { // 24 characters with spaces (4 groups of 6 chars) expect(validatePassword('abcdef ghijkl mnopqr stuvwx')).toBeNull(); }); it('should accept valid application passwords without spaces', () => { // 24 characters without spaces expect(validatePassword('abcdefghijklmnopqrstuvwx')).toBeNull(); }); it('should accept passwords 16 chars or longer', () => { expect(validatePassword('1234567890123456')).toBeNull(); }); it('should reject empty password', () => { expect(validatePassword('')).toBe('Password is required'); }); it('should reject too short passwords', () => { const result = validatePassword('short'); expect(result).toContain('too short'); }); }); describe('maskPassword', () => { it('should show first 4 chars and mask the rest', () => { const result = maskPassword('abcdefghijkl'); expect(result.startsWith('abcd')).toBe(true); expect(result).toContain('•'); }); it('should handle short passwords', () => { expect(maskPassword('abc')).toBe('****'); expect(maskPassword('abcd')).toBe('****'); }); it('should limit mask length to 16 dots', () => { const result = maskPassword('a'.repeat(100)); // First 4 chars + 16 dots expect(result.length).toBe(20); }); }); describe('writeWpnavEnvAtomic', () => { let testDir: string; let testFile: string; beforeEach(() => { testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-test-')); testFile = path.join(testDir, '.wpnav.env'); }); afterEach(() => { // Clean up try { fs.rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); it('should write file with correct content', () => { const content = 'WP_BASE_URL=https://example.com\n'; writeWpnavEnvAtomic(testFile, content); const written = fs.readFileSync(testFile, 'utf8'); expect(written).toBe(content); }); it('should not leave temp files on success', () => { const content = 'WP_BASE_URL=https://example.com\n'; writeWpnavEnvAtomic(testFile, content); const files = fs.readdirSync(testDir); expect(files).toHaveLength(1); expect(files[0]).toBe('.wpnav.env'); }); it('should overwrite existing file', () => { // Write initial content fs.writeFileSync(testFile, 'OLD_CONTENT=value\n'); // Overwrite const newContent = 'NEW_CONTENT=value\n'; writeWpnavEnvAtomic(testFile, newContent); const written = fs.readFileSync(testFile, 'utf8'); expect(written).toBe(newContent); }); it('should set restrictive file permissions', () => { const content = 'WP_BASE_URL=https://example.com\n'; writeWpnavEnvAtomic(testFile, content); const stats = fs.statSync(testFile); // Check that file is readable/writable by owner only (0o600 = 384 in decimal) // On some systems, umask affects this, so just check it's not world-readable const mode = stats.mode & 0o777; expect((mode & 0o077)).toBe(0); // No group or other permissions }); }); describe('roundtrip integration', () => { let testDir: string; let testFile: string; beforeEach(() => { testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wpnav-test-')); testFile = path.join(testDir, '.wpnav.env'); }); afterEach(() => { try { fs.rmSync(testDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); it('should roundtrip generate -> write -> parse', () => { const siteUrl = 'https://mysite.example.com'; const username = 'testadmin'; const password = 'xxxx yyyy zzzz wwww'; // Generate content const content = generateWpnavEnvContent(siteUrl, username, password); // Write to file writeWpnavEnvAtomic(testFile, content); // Read and parse const readContent = fs.readFileSync(testFile, 'utf8'); const parsed = parseWpnavEnv(readContent); // Verify expect(parsed.WP_BASE_URL).toBe(siteUrl); expect(parsed.WP_APP_USER).toBe(username); expect(parsed.WP_APP_PASS).toBe(password); expect(parsed.WP_REST_API).toBe(`${siteUrl}/wp-json`); }); });

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/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server