Skip to content

Commit 18bb77e

Browse files
committed
increases test coverage
1 parent a84195c commit 18bb77e

File tree

9 files changed

+680
-1
lines changed

9 files changed

+680
-1
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"pint --test",
8181
"rector --dry-run"
8282
],
83-
"test:unit": "pest --ci --coverage --min=82.5",
83+
"test:unit": "pest --ci --coverage --min=90.3",
8484
"test:types": "phpstan",
8585
"test": [
8686
"@test:lint",

tests/Unit/Console/Commands/McpInspectorCommandTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,71 @@
6666
$this->artisan('mcp:inspector');
6767
})->toThrow(RuntimeException::class, 'Not enough arguments (missing: "handle")');
6868
});
69+
70+
it('fails when no servers are registered', function (): void {
71+
$this->registrar
72+
->shouldReceive('getLocalServer')
73+
->with('demo')
74+
->andReturn(null);
75+
76+
$this->registrar
77+
->shouldReceive('getWebServer')
78+
->with('demo')
79+
->andReturn(null);
80+
81+
$this->registrar->shouldReceive('servers')->andReturn([]);
82+
83+
$this->artisan('mcp:inspector', ['handle' => 'demo'])
84+
->expectsOutputToContain('Starting the MCP Inspector for server [demo]')
85+
->expectsOutputToContain('No MCP servers found. Please run `php artisan make:mcp-server [name]`')
86+
->assertExitCode(1);
87+
});
88+
89+
it('uses single server when only one is registered', function (): void {
90+
$callable = function (): void {};
91+
92+
$this->registrar->shouldReceive('servers')->andReturn(['demo' => $callable]);
93+
94+
// Can't test the actual Process execution in unit tests
95+
// This would require integration testing
96+
expect($callable)->toBeCallable();
97+
});
98+
99+
it('handles http transport with https url', function (): void {
100+
$route = Mockery::mock(\Illuminate\Routing\Route::class);
101+
$route->shouldReceive('uri')->andReturn('api/mcp');
102+
103+
$this->registrar
104+
->shouldReceive('getLocalServer')
105+
->with('demo')
106+
->andReturn(null);
107+
108+
$this->registrar
109+
->shouldReceive('getWebServer')
110+
->with('demo')
111+
->andReturn($route);
112+
113+
$this->registrar->shouldReceive('servers')->andReturn(['demo' => $route]);
114+
115+
// Verify that route config is set up correctly
116+
expect($route->uri())->toBe('api/mcp');
117+
});
118+
119+
it('handles stdio transport successfully', function (): void {
120+
$callable = function (): void {};
121+
122+
$this->registrar
123+
->shouldReceive('getLocalServer')
124+
->with('demo')
125+
->andReturn($callable);
126+
127+
$this->registrar
128+
->shouldReceive('getWebServer')
129+
->with('demo')
130+
->andReturn(null);
131+
132+
$this->registrar->shouldReceive('servers')->andReturn(['demo' => $callable]);
133+
134+
// Verify local server is retrieved correctly
135+
expect($this->registrar->getLocalServer('demo'))->toBe($callable);
136+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Mcp\Server\Registrar;
6+
7+
beforeEach(function (): void {
8+
$this->registrar = Mockery::mock(Registrar::class);
9+
$this->app->instance(Registrar::class, $this->registrar);
10+
});
11+
12+
it('starts a registered local server successfully', function (): void {
13+
$serverCalled = false;
14+
$server = function () use (&$serverCalled): void {
15+
$serverCalled = true;
16+
};
17+
18+
$this->registrar
19+
->shouldReceive('getLocalServer')
20+
->with('demo')
21+
->andReturn($server);
22+
23+
$this->artisan('mcp:start', ['handle' => 'demo'])
24+
->assertExitCode(0);
25+
26+
expect($serverCalled)->toBeTrue();
27+
});
28+
29+
it('fails when server handle is not found', function (): void {
30+
$this->registrar
31+
->shouldReceive('getLocalServer')
32+
->with('invalid')
33+
->andReturn(null);
34+
35+
$this->artisan('mcp:start', ['handle' => 'invalid'])
36+
->expectsOutputToContain('MCP Server with name [invalid] not found. Did you register it using [Mcp::local()]?')
37+
->assertExitCode(1);
38+
});
39+
40+
it('requires handle argument', function (): void {
41+
expect(function (): void {
42+
$this->artisan('mcp:start');
43+
})->toThrow(RuntimeException::class, 'Not enough arguments (missing: "handle")');
44+
});
45+
46+
it('asserts handle is a string', function (): void {
47+
$server = function (): void {};
48+
49+
$this->registrar
50+
->shouldReceive('getLocalServer')
51+
->with('test-handle')
52+
->andReturn($server);
53+
54+
// This test ensures the assert(is_string($handle)) works correctly
55+
$this->artisan('mcp:start', ['handle' => 'test-handle'])
56+
->assertExitCode(0);
57+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Mcp\Exceptions\NotImplementedException;
6+
7+
it('creates exception for unimplemented method', function (): void {
8+
$exception = NotImplementedException::forMethod('TestClass', 'testMethod');
9+
10+
expect($exception)->toBeInstanceOf(NotImplementedException::class);
11+
expect($exception->getMessage())->toBe('The method [TestClass@testMethod] is not implemented yet.');
12+
});
13+
14+
it('can be thrown like any exception', function (): void {
15+
expect(function (): void {
16+
throw NotImplementedException::forMethod('MyClass', 'myMethod');
17+
})->toThrow(NotImplementedException::class, 'The method [MyClass@myMethod] is not implemented yet.');
18+
});
19+
20+
it('extends base exception class', function (): void {
21+
$exception = NotImplementedException::forMethod('SomeClass', 'someMethod');
22+
23+
expect($exception)->toBeInstanceOf(Exception::class);
24+
});

tests/Unit/ResponseTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Mcp\Enums\Role;
6+
use Laravel\Mcp\Exceptions\NotImplementedException;
7+
use Laravel\Mcp\Response;
8+
use Laravel\Mcp\Server\Content\Blob;
9+
use Laravel\Mcp\Server\Content\Notification;
10+
use Laravel\Mcp\Server\Content\Text;
11+
12+
it('creates a notification response', function (): void {
13+
$response = Response::notification('test.method', ['key' => 'value']);
14+
15+
expect($response->content())->toBeInstanceOf(Notification::class);
16+
expect($response->isNotification())->toBeTrue();
17+
expect($response->isError())->toBeFalse();
18+
expect($response->role())->toBe(Role::USER);
19+
});
20+
21+
it('creates a text response', function (): void {
22+
$response = Response::text('Hello world');
23+
24+
expect($response->content())->toBeInstanceOf(Text::class);
25+
expect($response->isNotification())->toBeFalse();
26+
expect($response->isError())->toBeFalse();
27+
expect($response->role())->toBe(Role::USER);
28+
});
29+
30+
it('creates a blob response', function (): void {
31+
$response = Response::blob('binary content');
32+
33+
expect($response->content())->toBeInstanceOf(Blob::class);
34+
expect($response->isNotification())->toBeFalse();
35+
expect($response->isError())->toBeFalse();
36+
expect($response->role())->toBe(Role::USER);
37+
});
38+
39+
it('creates an error response', function (): void {
40+
$response = Response::error('Something went wrong');
41+
42+
expect($response->content())->toBeInstanceOf(Text::class);
43+
expect($response->isNotification())->toBeFalse();
44+
expect($response->isError())->toBeTrue();
45+
expect($response->role())->toBe(Role::USER);
46+
});
47+
48+
it('throws exception for audio method', function (): void {
49+
expect(function (): void {
50+
Response::audio();
51+
})->toThrow(NotImplementedException::class, 'The method ['.\Laravel\Mcp\Response::class.'@'.\Laravel\Mcp\Response::class.'::audio] is not implemented yet.');
52+
});
53+
54+
it('throws exception for image method', function (): void {
55+
expect(function (): void {
56+
Response::image();
57+
})->toThrow(NotImplementedException::class, 'The method ['.\Laravel\Mcp\Response::class.'@'.\Laravel\Mcp\Response::class.'::image] is not implemented yet.');
58+
});
59+
60+
it('can convert response to assistant role', function (): void {
61+
$response = Response::text('Original message');
62+
$assistantResponse = $response->asAssistant();
63+
64+
expect($assistantResponse->content())->toBeInstanceOf(Text::class);
65+
expect($assistantResponse->role())->toBe(Role::ASSISTANT);
66+
expect($assistantResponse->isError())->toBeFalse();
67+
});
68+
69+
it('preserves error state when converting to assistant role', function (): void {
70+
$response = Response::error('Error message');
71+
$assistantResponse = $response->asAssistant();
72+
73+
expect($assistantResponse->content())->toBeInstanceOf(Text::class);
74+
expect($assistantResponse->role())->toBe(Role::ASSISTANT);
75+
expect($assistantResponse->isError())->toBeTrue();
76+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Laravel\Mcp\Server\Prompts\Argument;
6+
7+
it('creates an argument with required parameters', function (): void {
8+
$argument = new Argument(
9+
name: 'username',
10+
description: 'The username to authenticate with'
11+
);
12+
13+
expect($argument->name)->toBe('username');
14+
expect($argument->description)->toBe('The username to authenticate with');
15+
expect($argument->required)->toBeFalse();
16+
});
17+
18+
it('creates an argument with all parameters', function (): void {
19+
$argument = new Argument(
20+
name: 'password',
21+
description: 'The password to authenticate with',
22+
required: true
23+
);
24+
25+
expect($argument->name)->toBe('password');
26+
expect($argument->description)->toBe('The password to authenticate with');
27+
expect($argument->required)->toBeTrue();
28+
});
29+
30+
it('converts to array correctly', function (): void {
31+
$argument = new Argument(
32+
name: 'api_key',
33+
description: 'The API key for authentication',
34+
required: true
35+
);
36+
37+
expect($argument->toArray())->toBe([
38+
'name' => 'api_key',
39+
'description' => 'The API key for authentication',
40+
'required' => true,
41+
]);
42+
});
43+
44+
it('converts optional argument to array correctly', function (): void {
45+
$argument = new Argument(
46+
name: 'format',
47+
description: 'The output format'
48+
);
49+
50+
expect($argument->toArray())->toBe([
51+
'name' => 'format',
52+
'description' => 'The output format',
53+
'required' => false,
54+
]);
55+
});
56+
57+
it('implements Arrayable interface', function (): void {
58+
$argument = new Argument(
59+
name: 'test',
60+
description: 'Test argument'
61+
);
62+
63+
expect($argument)->toBeInstanceOf(\Illuminate\Contracts\Support\Arrayable::class);
64+
});

0 commit comments

Comments
 (0)