Skip to content

Commit f732e96

Browse files
committed
tests: add initial set of tests for most tools
1 parent b442078 commit f732e96

File tree

7 files changed

+649
-0
lines changed

7 files changed

+649
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
use Laravel\Boost\Mcp\Tools\DatabaseConnections;
4+
use Laravel\Mcp\Server\Tools\ToolResult;
5+
6+
beforeEach(function () {
7+
config()->set('database.default', 'mysql');
8+
config()->set('database.connections', [
9+
'mysql' => ['driver' => 'mysql'],
10+
'pgsql' => ['driver' => 'pgsql'],
11+
'sqlite' => ['driver' => 'sqlite'],
12+
]);
13+
});
14+
15+
test('it returns database connections', function () {
16+
$tool = new DatabaseConnections;
17+
$result = $tool->handle([]);
18+
19+
expect($result)->toBeInstanceOf(ToolResult::class);
20+
$data = $result->toArray();
21+
expect($data['isError'])->toBe(false);
22+
23+
$content = json_decode($data['content'][0]['text'], true);
24+
expect($content['default_connection'])->toBe('mysql');
25+
expect($content['connections'])->toHaveCount(3);
26+
expect($content['connections'])->toContain('mysql');
27+
expect($content['connections'])->toContain('pgsql');
28+
expect($content['connections'])->toContain('sqlite');
29+
});
30+
31+
test('it returns empty connections when none configured', function () {
32+
config()->set('database.connections', []);
33+
34+
$tool = new DatabaseConnections;
35+
$result = $tool->handle([]);
36+
37+
expect($result)->toBeInstanceOf(ToolResult::class);
38+
$data = $result->toArray();
39+
expect($data['isError'])->toBe(false);
40+
41+
$content = json_decode($data['content'][0]['text'], true);
42+
expect($content['default_connection'])->toBe('mysql');
43+
expect($content['connections'])->toHaveCount(0);
44+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Route;
4+
use Laravel\Boost\Mcp\Tools\GetAbsoluteUrl;
5+
use Laravel\Mcp\Server\Tools\ToolResult;
6+
7+
beforeEach(function () {
8+
config()->set('app.url', 'http://localhost');
9+
Route::get('/test', function () {
10+
return 'test';
11+
})->name('test.route');
12+
});
13+
14+
test('it returns absolute url for root path by default', function () {
15+
$tool = new GetAbsoluteUrl;
16+
$result = $tool->handle([]);
17+
18+
expect($result)->toBeInstanceOf(ToolResult::class);
19+
$data = $result->toArray();
20+
expect($data['isError'])->toBe(false);
21+
expect($data['content'][0]['text'])->toBe('http://localhost');
22+
});
23+
24+
test('it returns absolute url for given path', function () {
25+
$tool = new GetAbsoluteUrl;
26+
$result = $tool->handle(['path' => '/dashboard']);
27+
28+
expect($result)->toBeInstanceOf(ToolResult::class);
29+
$data = $result->toArray();
30+
expect($data['isError'])->toBe(false);
31+
expect($data['content'][0]['text'])->toBe('http://localhost/dashboard');
32+
});
33+
34+
test('it returns absolute url for named route', function () {
35+
$tool = new GetAbsoluteUrl;
36+
$result = $tool->handle(['route' => 'test.route']);
37+
38+
expect($result)->toBeInstanceOf(ToolResult::class);
39+
$data = $result->toArray();
40+
expect($data['isError'])->toBe(false);
41+
expect($data['content'][0]['text'])->toBe('http://localhost/test');
42+
});
43+
44+
test('it prioritizes path over route when both are provided', function () {
45+
$tool = new GetAbsoluteUrl;
46+
$result = $tool->handle(['path' => '/dashboard', 'route' => 'test.route']);
47+
48+
expect($result)->toBeInstanceOf(ToolResult::class);
49+
$data = $result->toArray();
50+
expect($data['isError'])->toBe(false);
51+
expect($data['content'][0]['text'])->toBe('http://localhost/dashboard');
52+
});
53+
54+
test('it handles empty path', function () {
55+
$tool = new GetAbsoluteUrl;
56+
$result = $tool->handle(['path' => '']);
57+
58+
expect($result)->toBeInstanceOf(ToolResult::class);
59+
$data = $result->toArray();
60+
expect($data['isError'])->toBe(false);
61+
expect($data['content'][0]['text'])->toBe('http://localhost');
62+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
use Laravel\Boost\Mcp\Tools\GetConfig;
4+
use Laravel\Mcp\Server\Tools\ToolResult;
5+
6+
beforeEach(function () {
7+
config()->set('test.key', 'test_value');
8+
config()->set('nested.config.key', 'nested_value');
9+
config()->set('app.name', 'Test App');
10+
});
11+
12+
test('it returns config value when key exists', function () {
13+
$tool = new GetConfig;
14+
$result = $tool->handle(['key' => 'test.key']);
15+
16+
expect($result)->toBeInstanceOf(ToolResult::class);
17+
18+
$data = $result->toArray();
19+
expect($data['content'][0]['text'])->toContain('"key": "test.key"');
20+
expect($data['content'][0]['text'])->toContain('"value": "test_value"');
21+
});
22+
23+
test('it returns nested config value', function () {
24+
$tool = new GetConfig;
25+
$result = $tool->handle(['key' => 'nested.config.key']);
26+
27+
expect($result)->toBeInstanceOf(ToolResult::class);
28+
29+
$data = $result->toArray();
30+
expect($data['content'][0]['text'])->toContain('"key": "nested.config.key"');
31+
expect($data['content'][0]['text'])->toContain('"value": "nested_value"');
32+
});
33+
34+
test('it returns error when config key does not exist', function () {
35+
$tool = new GetConfig;
36+
$result = $tool->handle(['key' => 'nonexistent.key']);
37+
38+
expect($result)->toBeInstanceOf(ToolResult::class);
39+
40+
$data = $result->toArray();
41+
expect($data['isError'])->toBe(true);
42+
expect($data['content'][0]['text'])->toContain("Config key 'nonexistent.key' not found.");
43+
});
44+
45+
test('it works with built-in Laravel config keys', function () {
46+
$tool = new GetConfig;
47+
$result = $tool->handle(['key' => 'app.name']);
48+
49+
expect($result)->toBeInstanceOf(ToolResult::class);
50+
51+
$data = $result->toArray();
52+
expect($data['content'][0]['text'])->toContain('"key": "app.name"');
53+
expect($data['content'][0]['text'])->toContain('"value": "Test App"');
54+
});
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Cache;
4+
use Illuminate\Support\Facades\Http;
5+
use Laravel\Boost\Mcp\Tools\GetInertiaDoc;
6+
use Laravel\Mcp\Server\Tools\ToolResult;
7+
use Laravel\Roster\Enums\Packages;
8+
use Laravel\Roster\Package;
9+
use Laravel\Roster\Roster;
10+
11+
beforeEach(function () {
12+
Cache::flush();
13+
});
14+
15+
test('it returns error when filename is missing', function () {
16+
$roster = Mockery::mock(Roster::class);
17+
$tool = new GetInertiaDoc($roster);
18+
19+
$result = $tool->handle([]);
20+
21+
expect($result)->toBeInstanceOf(ToolResult::class);
22+
$data = $result->toArray();
23+
expect($data['isError'])->toBe(true);
24+
expect($data['content'][0]['text'])->toContain('The "filename" argument is required.');
25+
});
26+
27+
test('it returns error when filename is empty', function () {
28+
$roster = Mockery::mock(Roster::class);
29+
$tool = new GetInertiaDoc($roster);
30+
31+
$result = $tool->handle(['filename' => '']);
32+
33+
expect($result)->toBeInstanceOf(ToolResult::class);
34+
$data = $result->toArray();
35+
expect($data['isError'])->toBe(true);
36+
expect($data['content'][0]['text'])->toContain('The "filename" argument is required.');
37+
});
38+
39+
test('it returns error when filename does not end with jsx', function () {
40+
$roster = Mockery::mock(Roster::class);
41+
$tool = new GetInertiaDoc($roster);
42+
43+
$result = $tool->handle(['filename' => 'installation.md']);
44+
45+
expect($result)->toBeInstanceOf(ToolResult::class);
46+
$data = $result->toArray();
47+
expect($data['isError'])->toBe(true);
48+
expect($data['content'][0]['text'])->toContain('The "filename" argument must end with ".jsx".');
49+
});
50+
51+
test('it returns error when filename has invalid characters', function () {
52+
$roster = Mockery::mock(Roster::class);
53+
$tool = new GetInertiaDoc($roster);
54+
55+
$result = $tool->handle(['filename' => 'installation_INVALID.jsx']);
56+
57+
expect($result)->toBeInstanceOf(ToolResult::class);
58+
$data = $result->toArray();
59+
expect($data['isError'])->toBe(true);
60+
expect($data['content'][0]['text'])->toContain('The "filename" argument must be a valid filename');
61+
});
62+
63+
test('it returns error when inertia is not installed', function () {
64+
$roster = Mockery::mock(Roster::class);
65+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn(null);
66+
67+
$tool = new GetInertiaDoc($roster);
68+
$result = $tool->handle(['filename' => 'installation.jsx']);
69+
70+
expect($result)->toBeInstanceOf(ToolResult::class);
71+
$data = $result->toArray();
72+
expect($data['isError'])->toBe(true);
73+
expect($data['content'][0]['text'])->toContain('Inertia is not installed in this project.');
74+
});
75+
76+
test('it fetches documentation successfully', function () {
77+
$package = new Package(Packages::INERTIA_LARAVEL, '2.1.0');
78+
$roster = Mockery::mock(Roster::class);
79+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn($package);
80+
81+
$docContent = 'This is the installation documentation';
82+
$base64Content = base64_encode($docContent);
83+
84+
Http::fake([
85+
'https://api.github.com/repos/inertiajs/inertiajs.com/contents/resources/js/Pages/installation.jsx?ref=v2' => Http::response(json_encode([
86+
'type' => 'file',
87+
'content' => $base64Content,
88+
])),
89+
]);
90+
91+
$tool = new GetInertiaDoc($roster);
92+
$result = $tool->handle(['filename' => 'installation.jsx']);
93+
94+
expect($result)->toBeInstanceOf(ToolResult::class);
95+
$data = $result->toArray();
96+
expect($data['isError'])->toBe(false);
97+
expect($data['content'][0]['text'])->toBe($docContent);
98+
});
99+
100+
test('it returns error when github api fails', function () {
101+
$package = new Package(Packages::INERTIA_LARAVEL, '2.1.0');
102+
$roster = Mockery::mock(Roster::class);
103+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn($package);
104+
105+
Http::fake([
106+
'https://api.github.com/repos/inertiajs/inertiajs.com/contents/resources/js/Pages/installation.jsx?ref=v2' => Http::response(json_encode(['message' => 'Not found']), 404),
107+
]);
108+
109+
$tool = new GetInertiaDoc($roster);
110+
$result = $tool->handle(['filename' => 'installation.jsx']);
111+
112+
expect($result)->toBeInstanceOf(ToolResult::class);
113+
$data = $result->toArray();
114+
expect($data['isError'])->toBe(true);
115+
expect($data['content'][0]['text'])->toContain('Failed to fetch Inertia doc');
116+
});
117+
118+
test('it returns error when response structure is unexpected', function () {
119+
$package = new Package(Packages::INERTIA_LARAVEL, '2.1.0');
120+
$roster = Mockery::mock(Roster::class);
121+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn($package);
122+
123+
Http::fake([
124+
'https://api.github.com/repos/inertiajs/inertiajs.com/contents/resources/js/Pages/installation.jsx?ref=v2' => Http::response(json_encode([
125+
'type' => 'dir', // Should be 'file'
126+
])),
127+
]);
128+
129+
$tool = new GetInertiaDoc($roster);
130+
$result = $tool->handle(['filename' => 'installation.jsx']);
131+
132+
expect($result)->toBeInstanceOf(ToolResult::class);
133+
$data = $result->toArray();
134+
expect($data['isError'])->toBe(true);
135+
expect($data['content'][0]['text'])->toContain('Unexpected response structure');
136+
});
137+
138+
test('it returns error when base64 decode fails', function () {
139+
$package = new Package(Packages::INERTIA_LARAVEL, '2.1.0');
140+
$roster = Mockery::mock(Roster::class);
141+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn($package);
142+
143+
Http::fake([
144+
'https://api.github.com/repos/inertiajs/inertiajs.com/contents/resources/js/Pages/installation.jsx?ref=v2' => Http::response(json_encode([
145+
'type' => 'file',
146+
'content' => 'invalid-base64!!!',
147+
])),
148+
]);
149+
150+
$tool = new GetInertiaDoc($roster);
151+
$result = $tool->handle(['filename' => 'installation.jsx']);
152+
153+
expect($result)->toBeInstanceOf(ToolResult::class);
154+
$data = $result->toArray();
155+
expect($data['isError'])->toBe(true);
156+
expect($data['content'][0]['text'])->toContain('Failed to decode Inertia doc content');
157+
});
158+
159+
test('it uses correct version ref based on major version', function () {
160+
$package = new Package(Packages::INERTIA_LARAVEL, '1.2.0');
161+
$roster = Mockery::mock(Roster::class);
162+
$roster->shouldReceive('package')->with(Packages::INERTIA_LARAVEL)->andReturn($package);
163+
164+
$docContent = 'Version 1 documentation';
165+
$base64Content = base64_encode($docContent);
166+
167+
Http::fake([
168+
'https://api.github.com/repos/inertiajs/inertiajs.com/contents/resources/js/Pages/installation.jsx?ref=v1' => Http::response(json_encode([
169+
'type' => 'file',
170+
'content' => $base64Content,
171+
])),
172+
]);
173+
174+
$tool = new GetInertiaDoc($roster);
175+
$result = $tool->handle(['filename' => 'installation.jsx']);
176+
177+
expect($result)->toBeInstanceOf(ToolResult::class);
178+
$data = $result->toArray();
179+
expect($data['isError'])->toBe(false);
180+
expect($data['content'][0]['text'])->toBe($docContent);
181+
});
182+
183+
test('shouldRegister returns true when inertia is installed', function () {
184+
$roster = Mockery::mock(Roster::class);
185+
$roster->shouldReceive('uses')->with(Packages::INERTIA_LARAVEL)->andReturn(true);
186+
187+
$tool = new GetInertiaDoc($roster);
188+
189+
expect($tool->shouldRegister())->toBe(true);
190+
});
191+
192+
test('shouldRegister returns false when inertia is not installed', function () {
193+
$roster = Mockery::mock(Roster::class);
194+
$roster->shouldReceive('uses')->with(Packages::INERTIA_LARAVEL)->andReturn(false);
195+
196+
$tool = new GetInertiaDoc($roster);
197+
198+
expect($tool->shouldRegister())->toBe(false);
199+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
use Laravel\Boost\Mcp\Tools\ListArtisanCommands;
4+
use Laravel\Mcp\Server\Tools\ToolResult;
5+
6+
test('it returns list of artisan commands', function () {
7+
$tool = new ListArtisanCommands;
8+
$result = $tool->handle([]);
9+
10+
expect($result)->toBeInstanceOf(ToolResult::class);
11+
$data = $result->toArray();
12+
expect($data['isError'])->toBe(false);
13+
14+
$content = json_decode($data['content'][0]['text'], true);
15+
expect($content)->toBeArray();
16+
expect($content)->not->toBeEmpty();
17+
18+
// Check that it contains some basic Laravel commands
19+
$commandNames = array_column($content, 'name');
20+
expect($commandNames)->toContain('migrate');
21+
expect($commandNames)->toContain('make:model');
22+
expect($commandNames)->toContain('route:list');
23+
24+
// Check the structure of each command
25+
foreach ($content as $command) {
26+
expect($command)->toHaveKey('name');
27+
expect($command)->toHaveKey('description');
28+
expect($command['name'])->toBeString();
29+
expect($command['description'])->toBeString();
30+
}
31+
32+
// Check that commands are sorted alphabetically
33+
$sortedNames = $commandNames;
34+
sort($sortedNames);
35+
expect($commandNames)->toBe($sortedNames);
36+
});

0 commit comments

Comments
 (0)