Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 385 additions & 0 deletions tests/Unit/JsonConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

/**
* Unit tests for MCP Server JSON Configuration
*
* Testing Framework: PHPUnit (Laravel default)
*
* These tests validate the structure, format, and content of the MCP server
* configuration JSON file to ensure it meets the expected schema and can be
* properly parsed and validated.
*/
class JsonConfigTest extends TestCase
{
private string $configPath;
private array $config;

protected function setUp(): void
{
parent::setUp();

// Locate the JSON config file - adjust path as needed
$this->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'
]
]
]
];
}
Comment on lines +21 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Tests use hardcoded config and never load the actual .mcp-config.json file.

The comment on line 28 suggests conditional logic ("use the sample config if file doesn't exist"), but the implementation always uses the hardcoded config. The $configPath property (line 26) is set but never used.

This means the tests validate a sample structure but not the actual configuration file in the repository. For unit tests, this is acceptable if the goal is to validate the test logic itself. However, if you want to validate the real .mcp-config.json file, you should add logic to load it.

Consider whether you want to:

  1. Keep as-is for pure unit testing (validates sample structure only)
  2. Load and validate the actual file (integration test approach)

If option 2, apply this diff:

 protected function setUp(): void
 {
     parent::setUp();
     
     // Locate the JSON config file - adjust path as needed
     $this->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'
-                ]
-            ]
-        ]
-    ];
+    // Load actual config or use sample if file doesn't exist
+    if (file_exists($this->configPath)) {
+        $jsonContent = file_get_contents($this->configPath);
+        $this->config = json_decode($jsonContent, true);
+        $this->assertNotNull($this->config, 'Failed to parse .mcp-config.json');
+    } else {
+        $this->config = [
+            'mcpServers' => [
+                'laravel-boost' => [
+                    'command' => '/usr/bin/php8.4',
+                    'args' => [
+                        '/data/Projects/ivplv2/artisan',
+                        'mcp:start',
+                        'laravel-boost'
+                    ]
+                ]
+            ]
+        ];
+    }
 }

Alternatively, verify whether .mcp-config.json exists in the repository:


🏁 Script executed:

#!/bin/bash
# Check if .mcp-config.json exists and show its content
if [ -f ".mcp-config.json" ]; then
    echo "=== .mcp-config.json exists ==="
    cat .mcp-config.json
else
    echo "=== .mcp-config.json does not exist ==="
fi

Length of output: 39


Clarify or implement config loading in setUp
In tests/Unit/JsonConfigTest.php setUp() (lines 21–41), you assign $this->configPath but never use it and always load a hardcoded sample config. Either remove the unused property and update the comment to reflect that tests only cover the sample, or add an if (file_exists(...)) block to load and parse .mcp-config.json as described in the comment.

🤖 Prompt for AI Agents
In tests/Unit/JsonConfigTest.php around lines 21 to 41, setUp() assigns
$this->configPath but never uses it and always uses a hardcoded sample config;
either remove $this->configPath and adjust the comment to state the test uses a
sample config only, or implement loading logic: check if
file_exists($this->configPath), read the file, json_decode it into $this->config
with error checking and fall back to the sample config if parsing fails, and
keep the comment in sync with the chosen approach.


/**
* 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Syntax issue: unnecessary backslash escape in line 337.

Line 337 has \!== which unnecessarily escapes the exclamation mark. This should be !==.

Apply this diff:

 public function testCommandPathsUseUnixStyle(): void
 {
     foreach ($this->config['mcpServers'] as $serverName => $serverConfig) {
-        if (strpos($serverConfig['command'], '\\') \!== false) {
+        if (strpos($serverConfig['command'], '\\') !== false) {
             $this->fail("Server '$serverName' command uses Windows-style backslashes, should 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) {
+            if (strpos($arg, 'artisan') !== false && strpos($arg, '\\') !== false) {
                 $this->fail("Server '$serverName' artisan path uses Windows-style backslashes");
             }
         }
     }
 }

Also applies to: 337-337

🤖 Prompt for AI Agents
In tests/Unit/JsonConfigTest.php around lines 324 and 337, the condition uses an
escaped exclamation mark `\!==` which is a syntax error; replace the escaped
form with the proper PHP strict-inequality operator `!==` so the line becomes
using `!==` (remove the backslash before `!==`) to correct the comparison and
fix the syntax error.

$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());
}
}