Add MCP tool to list project ticket types
CI/CD / test_and_check_app (push) Canceled after 0s
CI/CD / Publish immutable image (push) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 6s

This commit is contained in:
2026-09-09 07:44:41 +00:00
parent bd774e20f6
commit ce01b8ec55
3 changed files with 223 additions and 1 deletions
+164
View File
@@ -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]]],
];
}
}