Add MCP tool to list project ticket types
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Mcp;
|
||||
|
||||
use App\Mcp\Servers\KanbanGitServer;
|
||||
use App\Mcp\Tools\ListTicketTypes;
|
||||
use App\Models\Project;
|
||||
use App\Models\TicketType;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\Fluent\AssertableJson;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ListTicketTypesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_application_key_can_discover_the_read_only_tool_and_required_project_input(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->withApplicationKey($user)->postJson('/mcp/kanbangit', [
|
||||
'id' => 1,
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => 'tools/list',
|
||||
'params' => [],
|
||||
])->assertOk();
|
||||
|
||||
$tool = collect($response->json('result.tools'))->firstWhere('name', 'list_ticket_types');
|
||||
|
||||
$this->assertNotNull($tool);
|
||||
$this->assertTrue($tool['annotations']['readOnlyHint']);
|
||||
$this->assertSame(['project_id'], $tool['inputSchema']['required']);
|
||||
$this->assertSame('integer', $tool['inputSchema']['properties']['project_id']['type']);
|
||||
}
|
||||
|
||||
public function test_project_viewer_can_list_all_project_types_in_configured_order_over_http(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$project = Project::factory()->create();
|
||||
$otherProject = Project::factory()->create();
|
||||
$taskType = TicketType::factory()->for($project)->create([
|
||||
'name' => 'Task',
|
||||
'is_parent' => false,
|
||||
'position' => 2,
|
||||
]);
|
||||
$parentType = TicketType::factory()->for($project)->create([
|
||||
'name' => 'Release',
|
||||
'is_parent' => true,
|
||||
'position' => 1,
|
||||
]);
|
||||
TicketType::factory()->for($otherProject)->create(['name' => 'Foreign type']);
|
||||
$this->syncProjectPermissions($project, $user, ['view']);
|
||||
$this->syncProjectPermissions($otherProject, $user, ['view']);
|
||||
|
||||
$this->withApplicationKey($user)->postJson('/mcp/kanbangit', [
|
||||
'id' => 2,
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => 'tools/call',
|
||||
'params' => [
|
||||
'name' => 'list_ticket_types',
|
||||
'arguments' => ['project_id' => $project->id],
|
||||
],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('result.isError', false)
|
||||
->assertJsonPath('result.structuredContent.data.project.id', $project->id)
|
||||
->assertJsonCount(2, 'result.structuredContent.data.ticket_types')
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.0.id', $parentType->id)
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.0.name', 'Release')
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.0.is_parent', true)
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.0.position', 1)
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.0.project_id', $project->id)
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.1.id', $taskType->id)
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.1.name', 'Task')
|
||||
->assertJsonPath('result.structuredContent.data.ticket_types.1.is_parent', false);
|
||||
|
||||
$this->assertDatabaseCount('ticket_types', 3);
|
||||
$this->assertDatabaseCount('tickets', 0);
|
||||
}
|
||||
|
||||
public function test_invisible_project_types_are_not_accessible(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$project = Project::factory()->create();
|
||||
TicketType::factory()->for($project)->create();
|
||||
|
||||
KanbanGitServer::actingAs($user)
|
||||
->tool(ListTicketTypes::class, ['project_id' => $project->id])
|
||||
->assertHasErrors(['You cannot view this project.']);
|
||||
}
|
||||
|
||||
public function test_missing_project_returns_an_actionable_error(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
KanbanGitServer::actingAs($user)
|
||||
->tool(ListTicketTypes::class, ['project_id' => 999999])
|
||||
->assertHasErrors(['Project was not found.']);
|
||||
}
|
||||
|
||||
public function test_listing_requires_authentication(): void
|
||||
{
|
||||
$project = Project::factory()->create();
|
||||
|
||||
KanbanGitServer::tool(ListTicketTypes::class, ['project_id' => $project->id])
|
||||
->assertHasErrors(['Authentication required.']);
|
||||
|
||||
$this->postJson('/mcp/kanbangit', [
|
||||
'id' => 1,
|
||||
'jsonrpc' => '2.0',
|
||||
'method' => 'tools/call',
|
||||
'params' => [
|
||||
'name' => 'list_ticket_types',
|
||||
'arguments' => ['project_id' => $project->id],
|
||||
],
|
||||
])->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_project_without_types_returns_an_empty_list(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$project = Project::factory()->create();
|
||||
$this->syncProjectPermissions($project, $user, ['view']);
|
||||
|
||||
KanbanGitServer::actingAs($user)
|
||||
->tool(ListTicketTypes::class, ['project_id' => $project->id])
|
||||
->assertOk()
|
||||
->assertStructuredContent(fn (AssertableJson $json) => $json
|
||||
->where('data.project.id', $project->id)
|
||||
->where('data.ticket_types', [])
|
||||
->etc());
|
||||
|
||||
$this->assertDatabaseCount('ticket_types', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
#[DataProvider('invalidProjectArguments')]
|
||||
public function test_invalid_project_arguments_are_rejected(array $arguments): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
KanbanGitServer::actingAs($user)
|
||||
->tool(ListTicketTypes::class, $arguments)
|
||||
->assertHasErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{array<string, mixed>}>
|
||||
*/
|
||||
public static function invalidProjectArguments(): array
|
||||
{
|
||||
return [
|
||||
'missing project' => [[]],
|
||||
'null project' => [['project_id' => null]],
|
||||
'project name instead of ID' => [['project_id' => 'CORE']],
|
||||
'fractional ID' => [['project_id' => 1.5]],
|
||||
'array instead of ID' => [['project_id' => [1]]],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user