diff --git a/tests/Unit/JsonConfigTest.php b/tests/Unit/JsonConfigTest.php new file mode 100644 index 0000000..3647efc --- /dev/null +++ b/tests/Unit/JsonConfigTest.php @@ -0,0 +1,385 @@ +configPath = base_path('.mcp-config.json'); + + // For testing purposes, use the sample config if file doesn't exist + $this->config = [ + 'mcpServers' => [ + 'laravel-boost' => [ + 'command' => '/usr/bin/php8.4', + 'args' => [ + '/data/Projects/ivplv2/artisan', + 'mcp:start', + 'laravel-boost' + ] + ] + ] + ]; + } + + /** + * Test that the JSON configuration has valid JSON syntax + */ + public function testConfigIsValidJson(): void + { + $jsonString = json_encode($this->config); + $decoded = json_decode($jsonString, true); + + $this->assertNotNull($decoded, 'Configuration should be valid JSON'); + $this->assertEquals(JSON_ERROR_NONE, json_last_error(), 'JSON should have no syntax errors'); + } + + /** + * Test that the configuration contains the required mcpServers key + */ + public function testConfigHasMcpServersKey(): void + { + $this->assertArrayHasKey('mcpServers', $this->config, 'Configuration must contain mcpServers key'); + } + + /** + * Test that mcpServers is an array + */ + public function testMcpServersIsArray(): void + { + $this->assertIsArray($this->config['mcpServers'], 'mcpServers must be an array'); + } + + /** + * Test that mcpServers is not empty + */ + public function testMcpServersIsNotEmpty(): void + { + $this->assertNotEmpty($this->config['mcpServers'], 'mcpServers should contain at least one server configuration'); + } + + /** + * Test that each server configuration has required keys + */ + public function testServerConfigurationHasRequiredKeys(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + $this->assertArrayHasKey('command', $serverConfig, "Server '$serverName' must have 'command' key"); + $this->assertArrayHasKey('args', $serverConfig, "Server '$serverName' must have 'args' key"); + } + } + + /** + * Test that command is a non-empty string + */ + public function testCommandIsNonEmptyString(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + $this->assertIsString($serverConfig['command'], "Server '$serverName' command must be a string"); + $this->assertNotEmpty($serverConfig['command'], "Server '$serverName' command cannot be empty"); + } + } + + /** + * Test that args is an array + */ + public function testArgsIsArray(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + $this->assertIsArray($serverConfig['args'], "Server '$serverName' args must be an array"); + } + } + + /** + * Test that args array is not empty + */ + public function testArgsIsNotEmpty(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + $this->assertNotEmpty($serverConfig['args'], "Server '$serverName' args array cannot be empty"); + } + } + + /** + * Test that all args are strings + */ + public function testAllArgsAreStrings(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + foreach ($serverConfig['args'] as $index => $arg) { + $this->assertIsString($arg, "Server '$serverName' arg at index $index must be a string"); + } + } + } + + /** + * Test the laravel-boost server configuration specifically + */ + public function testLaravelBoostServerExists(): void + { + $this->assertArrayHasKey('laravel-boost', $this->config['mcpServers'], 'laravel-boost server must be configured'); + } + + /** + * Test laravel-boost command points to PHP executable + */ + public function testLaravelBoostCommandIsPhpExecutable(): void + { + $laravelBoost = $this->config['mcpServers']['laravel-boost']; + + $this->assertStringContainsString('php', $laravelBoost['command'], 'Command should point to PHP executable'); + } + + /** + * Test laravel-boost has the correct number of arguments + */ + public function testLaravelBoostHasCorrectArgumentCount(): void + { + $laravelBoost = $this->config['mcpServers']['laravel-boost']; + + $this->assertCount(3, $laravelBoost['args'], 'laravel-boost should have exactly 3 arguments'); + } + + /** + * Test laravel-boost arguments contain artisan path + */ + public function testLaravelBoostArgsContainArtisan(): void + { + $laravelBoost = $this->config['mcpServers']['laravel-boost']; + + $this->assertStringContainsString('artisan', $laravelBoost['args'][0], 'First argument should be artisan path'); + } + + /** + * Test laravel-boost arguments contain mcp:start command + */ + public function testLaravelBoostArgsContainMcpStartCommand(): void + { + $laravelBoost = $this->config['mcpServers']['laravel-boost']; + + $this->assertEquals('mcp:start', $laravelBoost['args'][1], 'Second argument should be mcp:start command'); + } + + /** + * Test laravel-boost arguments contain server name + */ + public function testLaravelBoostArgsContainServerName(): void + { + $laravelBoost = $this->config['mcpServers']['laravel-boost']; + + $this->assertEquals('laravel-boost', $laravelBoost['args'][2], 'Third argument should be the server name'); + } + + /** + * Test configuration can be encoded back to JSON without data loss + */ + public function testConfigurationCanBeReEncoded(): void + { + $encoded = json_encode($this->config); + $decoded = json_decode($encoded, true); + + $this->assertEquals($this->config, $decoded, 'Configuration should survive encode/decode cycle without data loss'); + } + + /** + * Test JSON encoding with pretty print produces valid output + */ + public function testPrettyPrintJsonIsValid(): void + { + $prettyJson = json_encode($this->config, JSON_PRETTY_PRINT); + $decoded = json_decode($prettyJson, true); + + $this->assertNotNull($decoded, 'Pretty-printed JSON should be valid'); + $this->assertEquals($this->config, $decoded, 'Pretty-printed JSON should decode to same structure'); + } + + /** + * Test that server names are valid identifiers (alphanumeric with hyphens) + */ + public function testServerNamesAreValidIdentifiers(): void + { + foreach (array_keys($this->config['mcpServers']) as $serverName) { + $this->assertMatchesRegularExpression( + '/^[a-z0-9\-]+$/', + $serverName, + "Server name '$serverName' should only contain lowercase letters, numbers, and hyphens" + ); + } + } + + /** + * Test that configuration doesn't contain unexpected keys at root level + */ + public function testNoUnexpectedRootKeys(): void + { + $allowedKeys = ['mcpServers']; + $actualKeys = array_keys($this->config); + + foreach ($actualKeys as $key) { + $this->assertContains($key, $allowedKeys, "Unexpected root key '$key' found in configuration"); + } + } + + /** + * Test that server configuration doesn't contain unexpected keys + */ + public function testNoUnexpectedServerConfigKeys(): void + { + $allowedKeys = ['command', 'args', 'env', 'cwd', 'disabled']; // Common config keys + + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + foreach (array_keys($serverConfig) as $key) { + $this->assertContains( + $key, + $allowedKeys, + "Unexpected key '$key' found in server '$serverName' configuration" + ); + } + } + } + + /** + * Test handling of empty mcpServers object + */ + public function testValidationWithEmptyMcpServers(): void + { + $emptyConfig = ['mcpServers' => []]; + $encoded = json_encode($emptyConfig); + $decoded = json_decode($encoded, true); + + $this->assertIsArray($decoded['mcpServers'], 'Empty mcpServers should still be a valid array'); + $this->assertEmpty($decoded['mcpServers'], 'Empty mcpServers should remain empty after encode/decode'); + } + + /** + * Test handling of multiple servers in configuration + */ + public function testMultipleServersConfiguration(): void + { + $multiConfig = [ + 'mcpServers' => [ + 'server-one' => [ + 'command' => '/usr/bin/php', + 'args' => ['artisan', 'mcp:start', 'server-one'] + ], + 'server-two' => [ + 'command' => '/usr/bin/php', + 'args' => ['artisan', 'mcp:start', 'server-two'] + ] + ] + ]; + + $this->assertCount(2, $multiConfig['mcpServers'], 'Should support multiple server configurations'); + $this->assertArrayHasKey('server-one', $multiConfig['mcpServers']); + $this->assertArrayHasKey('server-two', $multiConfig['mcpServers']); + } + + /** + * Test that args array elements are non-empty strings + */ + public function testArgsElementsAreNonEmpty(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + foreach ($serverConfig['args'] as $index => $arg) { + $this->assertNotEmpty($arg, "Server '$serverName' arg at index $index should not be empty"); + } + } + } + + /** + * Test JSON encoding with Unicode support + */ + public function testJsonEncodingWithUnicodeSupport(): void + { + $encoded = json_encode($this->config, JSON_UNESCAPED_UNICODE); + $decoded = json_decode($encoded, true); + + $this->assertNotNull($decoded, 'JSON with unescaped Unicode should be valid'); + } + + /** + * Test that command paths use forward slashes (Unix-style) + */ + public function testCommandPathsUseUnixStyle(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + if (strpos($serverConfig['command'], '\\') \!== false) { + $this->fail("Server '$serverName' command uses Windows-style backslashes, should use forward slashes"); + } + } + } + + /** + * Test that artisan paths in args use forward slashes + */ + public function testArtisanPathsUseUnixStyle(): void + { + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + foreach ($serverConfig['args'] as $arg) { + if (strpos($arg, 'artisan') \!== false && strpos($arg, '\\') \!== false) { + $this->fail("Server '$serverName' artisan path uses Windows-style backslashes"); + } + } + } + } + + /** + * Test configuration structure matches expected schema + */ + public function testConfigurationMatchesExpectedSchema(): void + { + $this->assertArrayHasKey('mcpServers', $this->config); + $this->assertIsArray($this->config['mcpServers']); + + foreach ($this->config['mcpServers'] as $serverName => $serverConfig) { + $this->assertIsString($serverName); + $this->assertIsArray($serverConfig); + $this->assertArrayHasKey('command', $serverConfig); + $this->assertArrayHasKey('args', $serverConfig); + $this->assertIsString($serverConfig['command']); + $this->assertIsArray($serverConfig['args']); + } + } + + /** + * Test that JSON can be decoded with associative array flag + */ + public function testJsonDecodesAsAssociativeArray(): void + { + $jsonString = json_encode($this->config); + $decoded = json_decode($jsonString, true); + + $this->assertIsArray($decoded, 'Decoded JSON should be an associative array'); + $this->assertNotInstanceOf(\stdClass::class, $decoded, 'Should not decode as stdClass object'); + } + + /** + * Test JSON decoding depth limit + */ + public function testJsonDecodingDepthHandling(): void + { + $jsonString = json_encode($this->config); + $decoded = json_decode($jsonString, true, 512); + + $this->assertNotNull($decoded, 'Should handle reasonable depth limits'); + $this->assertEquals(JSON_ERROR_NONE, json_last_error()); + } +} \ No newline at end of file