Licenses MCP stuff

This commit is contained in:
snipe
2026-05-08 10:37:25 +01:00
parent dc9f0104f6
commit 08b2d0c85d
15 changed files with 1688 additions and 0 deletions
+7
View File
@@ -6,23 +6,30 @@ use App\Mcp\Tools\AuditAssetTool;
use App\Mcp\Tools\CheckinAccessoryTool;
use App\Mcp\Tools\CheckinAssetTool;
use App\Mcp\Tools\CheckinComponentTool;
use App\Mcp\Tools\CheckinLicenseTool;
use App\Mcp\Tools\CheckoutAccessoryTool;
use App\Mcp\Tools\CheckoutAssetTool;
use App\Mcp\Tools\CheckoutComponentTool;
use App\Mcp\Tools\CheckoutLicenseTool;
use App\Mcp\Tools\CreateAccessoryTool;
use App\Mcp\Tools\CreateComponentTool;
use App\Mcp\Tools\CreateLicenseTool;
use App\Mcp\Tools\CreateUserTool;
use App\Mcp\Tools\DeleteAccessoryTool;
use App\Mcp\Tools\DeleteAssetTool;
use App\Mcp\Tools\DeleteComponentTool;
use App\Mcp\Tools\DeleteLicenseTool;
use App\Mcp\Tools\DeleteUserTool;
use App\Mcp\Tools\ListAssetsTool;
use App\Mcp\Tools\ListLicensesTool;
use App\Mcp\Tools\ListUsersTool;
use App\Mcp\Tools\ShowAssetTool;
use App\Mcp\Tools\ShowLicenseTool;
use App\Mcp\Tools\ShowUserTool;
use App\Mcp\Tools\UpdateAccessoryTool;
use App\Mcp\Tools\UpdateAssetTool;
use App\Mcp\Tools\UpdateComponentTool;
use App\Mcp\Tools\UpdateLicenseTool;
use App\Mcp\Tools\UpdateUserTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace App\Mcp\Tools;
use App\Events\CheckoutableCheckedIn;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('checkin_license')]
#[Title('Checkin License')]
#[Description('Check in a Snipe-IT license seat by its seat ID, returning it to the available pool')]
class CheckinLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'seat_id' => 'required|integer',
'note' => 'nullable|string|max:65535',
]);
$seat = LicenseSeat::with('license')->find($request->get('seat_id'));
if (! $seat) {
return Response::make(Response::error('License seat not found'));
}
if (is_null($seat->assigned_to) && is_null($seat->asset_id)) {
return Response::make(Response::error('This seat is not currently checked out'));
}
$license = $seat->license;
if (! $license) {
return Response::make(Response::error('License not found'));
}
// License checkin uses the checkout gate (matching application behavior)
if (! Gate::allows('checkout', $license)) {
return Response::make(Response::error('Unauthorized'));
}
$returnTo = null;
if ($seat->assigned_to) {
$returnTo = User::withTrashed()->find($seat->assigned_to);
} elseif ($seat->asset_id) {
$returnTo = Asset::find($seat->asset_id);
}
$note = $request->get('note');
$seat->assigned_to = null;
$seat->asset_id = null;
$seat->notes = $note;
if (! $license->reassignable) {
$seat->unreassignable_seat = true;
}
if ($seat->save()) {
event(new CheckoutableCheckedIn($seat, $returnTo, auth()->user(), $note));
return Response::make(
Response::text('License seat '.$seat->id.' checked in successfully')
)->withStructuredContent([
'success' => true,
'message' => 'License seat checked in successfully',
'seat_id' => $seat->id,
'license_id' => $license->id,
'license_name' => $license->name,
]);
}
return Response::make(Response::error('Checkin failed'));
}
public function schema(JsonSchema $schema): array
{
return [
'seat_id' => $schema->number()->description('ID of the license seat to check in (returned by checkout_license)'),
'note' => $schema->string()->description('Optional checkin note'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the checkin succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'seat_id' => $schema->number()->description('ID of the seat that was checked in'),
'license_id' => $schema->number()->description('Numeric ID of the license'),
'license_name' => $schema->string()->description('Name of the license'),
];
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace App\Mcp\Tools;
use App\Events\CheckoutableCheckedOut;
use App\Models\Asset;
use App\Models\License;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('checkout_license')]
#[Title('Checkout License')]
#[Description('Check out an available license seat to a user or asset')]
class CheckoutLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'assigned_to' => 'nullable|integer',
'asset_id' => 'nullable|integer',
'note' => 'nullable|string|max:65535',
]);
$license = $this->resolveLicense($request);
if (! $license) {
return Response::make(Response::error('License not found'));
}
if (! Gate::allows('checkout', $license)) {
return Response::make(Response::error('Unauthorized'));
}
if ($license->numRemaining() < 1) {
return Response::make(Response::error('No available seats for this license'));
}
if (! $request->filled('assigned_to') && ! $request->filled('asset_id')) {
return Response::make(Response::error('Please provide either assigned_to (user ID) or asset_id'));
}
$seat = $license->freeSeat();
if (! $seat) {
return Response::make(Response::error('No free seat found for this license'));
}
$note = $request->get('note');
if ($request->filled('assigned_to')) {
$target = User::find($request->get('assigned_to'));
if (! $target) {
return Response::make(Response::error('User not found'));
}
$seat->assigned_to = $target->id;
$seat->notes = $note;
if ($seat->save()) {
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
return Response::make(
Response::text('License seat checked out to user '.$target->username)
)->withStructuredContent([
'success' => true,
'message' => 'License seat checked out successfully',
'license_id' => $license->id,
'license_name' => $license->name,
'seat_id' => $seat->id,
'assigned_to_type' => 'user',
'assigned_to_id' => $target->id,
]);
}
} elseif ($request->filled('asset_id')) {
$target = Asset::find($request->get('asset_id'));
if (! $target) {
return Response::make(Response::error('Asset not found'));
}
$seat->asset_id = $target->id;
if ($target->checkedOutToUser()) {
$seat->assigned_to = $target->assigned_to;
}
$seat->notes = $note;
if ($seat->save()) {
event(new CheckoutableCheckedOut($seat, $target, auth()->user(), $note, [], 1));
return Response::make(
Response::text('License seat checked out to asset '.$target->asset_tag)
)->withStructuredContent([
'success' => true,
'message' => 'License seat checked out successfully',
'license_id' => $license->id,
'license_name' => $license->name,
'seat_id' => $seat->id,
'assigned_to_type' => 'asset',
'assigned_to_id' => $target->id,
]);
}
}
return Response::make(Response::error('Checkout failed'));
}
private function resolveLicense(Request $request): ?License
{
if ($request->filled('id')) {
return License::find($request->get('id'));
}
if ($request->filled('name')) {
return License::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the license to check out'),
'name' => $schema->string()->description('Name of the license to check out'),
'assigned_to' => $schema->number()->description('User ID to assign the seat to'),
'asset_id' => $schema->number()->description('Asset ID to assign the seat to'),
'note' => $schema->string()->description('Optional checkout note'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the checkout succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'license_id' => $schema->number()->description('Numeric ID of the license'),
'license_name' => $schema->string()->description('Name of the license'),
'seat_id' => $schema->number()->description('ID of the seat record (use this for checkin)'),
'assigned_to_type' => $schema->string()->description('Type of entity checked out to: user or asset'),
'assigned_to_id' => $schema->number()->description('ID of the entity checked out to'),
];
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Company;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('create_license')]
#[Title('Create License')]
#[Description('Create a new Snipe-IT software license')]
class CreateLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('create', License::class)) {
return Response::make(Response::error('Unauthorized'));
}
try {
$request->validate([
'name' => 'required|string|max:255',
'seats' => 'required|integer|min:1',
'category_id' => 'required|integer|exists:categories,id',
'serial' => 'nullable|string|max:255',
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
'supplier_id' => 'nullable|integer|exists:suppliers,id',
'company_id' => 'nullable|integer|exists:companies,id',
'purchase_date' => 'nullable|date_format:Y-m-d',
'purchase_cost' => 'nullable|numeric|min:0',
'purchase_order' => 'nullable|string|max:255',
'order_number' => 'nullable|string|max:255',
'expiration_date' => 'nullable|date_format:Y-m-d',
'termination_date' => 'nullable|date_format:Y-m-d',
'license_name' => 'nullable|string|max:255',
'license_email' => 'nullable|email|max:255',
'maintained' => 'nullable|boolean',
'reassignable' => 'nullable|boolean',
'notes' => 'nullable|string',
'min_amt' => 'nullable|integer|min:0',
]);
} catch (ValidationException $e) {
return Response::make(Response::error($e->validator->errors()->first()));
}
$license = new License;
$license->fill($request->only([
'name', 'seats', 'category_id', 'serial', 'manufacturer_id',
'supplier_id', 'purchase_date', 'purchase_cost', 'purchase_order',
'order_number', 'expiration_date', 'termination_date',
'license_name', 'license_email', 'maintained', 'reassignable',
'notes', 'min_amt',
]));
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
$license->created_by = auth()->id();
if ($license->save()) {
return Response::make(
Response::text('License '.$license->name.' created successfully')
)->withStructuredContent([
'success' => true,
'message' => 'License created successfully',
'id' => $license->id,
'name' => $license->name,
'seats' => $license->seats,
'category_id' => $license->category_id,
]);
}
return Response::make(Response::error('Create failed: '.$license->getErrors()->first()));
}
public function schema(JsonSchema $schema): array
{
return [
'name' => $schema->string()->description('License name (required)'),
'seats' => $schema->number()->description('Number of seats (required, min 1)'),
'category_id' => $schema->number()->description('Category ID — must be a license category (required)'),
'serial' => $schema->string()->description('Product key / serial number'),
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
'supplier_id' => $schema->number()->description('Supplier ID'),
'company_id' => $schema->number()->description('Company ID (defaults to the authenticated user\'s company)'),
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
'purchase_cost' => $schema->number()->description('Purchase cost'),
'purchase_order' => $schema->string()->description('Purchase order number'),
'order_number' => $schema->string()->description('Order number'),
'expiration_date' => $schema->string()->description('License expiration date (YYYY-MM-DD)'),
'termination_date' => $schema->string()->description('License termination date (YYYY-MM-DD)'),
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
'notes' => $schema->string()->description('Notes'),
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the license was created'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the new license'),
'name' => $schema->string()->description('Name of the new license'),
'seats' => $schema->number()->description('Total seat count'),
'category_id' => $schema->number()->description('Category ID'),
];
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Mcp\Tools;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('delete_license')]
#[Title('Delete License')]
#[Description('Soft-delete a Snipe-IT license. The license must have no seats currently assigned to users or assets.')]
class DeleteLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
$license = $this->resolveLicense($request);
if (! $license) {
return Response::make(Response::error('License not found'));
}
if (! Gate::allows('delete', $license)) {
return Response::make(Response::error('Unauthorized'));
}
if ($license->assignedCount()->count() > 0) {
return Response::make(Response::error('License has seats currently assigned and cannot be deleted. Check in all seats first.'));
}
$name = $license->name;
DB::table('license_seats')
->where('license_id', $license->id)
->update(['assigned_to' => null, 'asset_id' => null]);
$license->licenseseats()->delete();
$license->delete();
return Response::make(
Response::text('License '.$name.' deleted successfully')
)->withStructuredContent([
'success' => true,
'message' => 'License deleted successfully',
'name' => $name,
]);
}
private function resolveLicense(Request $request): ?License
{
if ($request->filled('id')) {
return License::find($request->get('id'));
}
if ($request->filled('name')) {
return License::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID of the license to delete'),
'name' => $schema->string()->description('Name of the license to delete'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the deletion succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'name' => $schema->string()->description('Name of the deleted license'),
];
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace App\Mcp\Tools;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('list_licenses')]
#[Title('List Licenses')]
#[Description('Search and list Snipe-IT licenses with optional filtering by keyword, company, category, and manufacturer')]
class ListLicensesTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
if (! Gate::allows('index', License::class)) {
return Response::make(Response::error('Unauthorized'));
}
$request->validate([
'search' => 'nullable|string|max:255',
'company_id' => 'nullable|integer',
'category_id' => 'nullable|integer',
'manufacturer_id' => 'nullable|integer',
'supplier_id' => 'nullable|integer',
'limit' => 'nullable|integer|min:1|max:500',
'offset' => 'nullable|integer|min:0',
]);
$licenses = License::with('company', 'manufacturer', 'supplier', 'category')
->withCount('freeSeats as free_seats_count');
if ($request->filled('search')) {
$licenses->TextSearch($request->get('search'));
}
if ($request->filled('company_id')) {
$licenses->where('licenses.company_id', '=', $request->get('company_id'));
}
if ($request->filled('category_id')) {
$licenses->where('category_id', '=', $request->get('category_id'));
}
if ($request->filled('manufacturer_id')) {
$licenses->where('manufacturer_id', '=', $request->get('manufacturer_id'));
}
if ($request->filled('supplier_id')) {
$licenses->where('supplier_id', '=', $request->get('supplier_id'));
}
$licenses->orderBy('licenses.created_at', 'desc');
$total = $licenses->count();
$limit = $request->filled('limit') ? (int) $request->get('limit') : 25;
$offset = $request->filled('offset') ? (int) $request->get('offset') : 0;
$results = $licenses->skip($offset)->take($limit)->get();
$licensesData = $results->map(fn (License $license) => [
'id' => $license->id,
'name' => $license->name,
'serial' => $license->serial,
'seats' => $license->seats,
'free_seats' => $license->free_seats_count,
'category' => $license->category?->name,
'manufacturer' => $license->manufacturer?->name,
'company' => $license->company?->name,
'supplier' => $license->supplier?->name,
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
])->values()->all();
return Response::make(
Response::text("Found {$total} licenses, returning ".count($licensesData))
)->withStructuredContent([
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'licenses' => $licensesData,
]);
}
public function schema(JsonSchema $schema): array
{
return [
'search' => $schema->string()->description('Keyword to search across name, serial, notes, and order number'),
'company_id' => $schema->number()->description('Filter by company ID'),
'category_id' => $schema->number()->description('Filter by category ID'),
'manufacturer_id' => $schema->number()->description('Filter by manufacturer ID'),
'supplier_id' => $schema->number()->description('Filter by supplier ID'),
'limit' => $schema->number()->description('Number of results to return (default: 25, max: 500)'),
'offset' => $schema->number()->description('Number of results to skip for pagination (default: 0)'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'total' => $schema->number()->description('Total number of matching licenses')->required(),
'offset' => $schema->number()->description('Current pagination offset')->required(),
'limit' => $schema->number()->description('Results per page')->required(),
];
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Mcp\Tools;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('show_license')]
#[Title('Show License Details')]
#[Description('Look up a single Snipe-IT license by numeric ID or name and return its full details including seat counts')]
class ShowLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
]);
if ($request->filled('id')) {
$license = License::with('company', 'manufacturer', 'supplier', 'category')
->withCount('freeSeats as free_seats_count')
->find($request->get('id'));
} elseif ($request->filled('name')) {
$license = License::with('company', 'manufacturer', 'supplier', 'category')
->withCount('freeSeats as free_seats_count')
->where('name', $request->get('name'))
->first();
} else {
return Response::make(Response::error('Please provide an id or name'));
}
if (! $license) {
return Response::make(Response::error('License not found'));
}
if (! Gate::allows('view', $license)) {
return Response::make(Response::error('Unauthorized'));
}
$assignedCount = $license->assignedCount()->count();
return Response::make(
Response::text('License '.$license->name.' found')
)->withStructuredContent([
'id' => $license->id,
'name' => $license->name,
'serial' => $license->serial,
'seats' => $license->seats,
'free_seats' => $license->free_seats_count,
'assigned_seats' => $assignedCount,
'category' => $license->category?->name,
'category_id' => $license->category_id,
'manufacturer' => $license->manufacturer?->name,
'manufacturer_id' => $license->manufacturer_id,
'company' => $license->company?->name,
'company_id' => $license->company_id,
'supplier' => $license->supplier?->name,
'supplier_id' => $license->supplier_id,
'license_name' => $license->license_name,
'license_email' => $license->license_email,
'maintained' => (bool) $license->maintained,
'reassignable' => (bool) $license->reassignable,
'purchase_date' => $license->purchase_date?->format('Y-m-d'),
'purchase_cost' => $license->purchase_cost,
'purchase_order' => $license->purchase_order,
'order_number' => $license->order_number,
'expiration_date' => $license->expiration_date?->format('Y-m-d'),
'termination_date' => $license->termination_date?->format('Y-m-d'),
'notes' => $license->notes,
'created_at' => $license->created_at?->format('Y-m-d H:i:s'),
'updated_at' => $license->updated_at?->format('Y-m-d H:i:s'),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric license ID'),
'name' => $schema->string()->description('License name to look up'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric license ID')->required(),
'name' => $schema->string()->description('License name')->required(),
'seats' => $schema->number()->description('Total seat count'),
'free_seats' => $schema->number()->description('Number of available (unassigned) seats'),
'assigned_seats' => $schema->number()->description('Number of currently assigned seats'),
];
}
}
+143
View File
@@ -0,0 +1,143 @@
<?php
namespace App\Mcp\Tools;
use App\Models\Company;
use App\Models\License;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Gate;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;
use Laravel\Mcp\Server\Tool;
#[Name('update_license')]
#[Title('Update License')]
#[Description('Update fields on a Snipe-IT license identified by numeric ID or name')]
class UpdateLicenseTool extends Tool
{
public function handle(Request $request): ResponseFactory
{
$request->validate([
'id' => 'nullable|integer',
'name' => 'nullable|string|max:255',
'new_name' => 'nullable|string|max:255',
'seats' => 'nullable|integer|min:1',
'serial' => 'nullable|string|max:255',
'category_id' => 'nullable|integer|exists:categories,id',
'manufacturer_id' => 'nullable|integer|exists:manufacturers,id',
'supplier_id' => 'nullable|integer|exists:suppliers,id',
'company_id' => 'nullable|integer|exists:companies,id',
'purchase_date' => 'nullable|date_format:Y-m-d',
'purchase_cost' => 'nullable|numeric|min:0',
'purchase_order' => 'nullable|string|max:255',
'order_number' => 'nullable|string|max:255',
'expiration_date' => 'nullable|date_format:Y-m-d',
'termination_date' => 'nullable|date_format:Y-m-d',
'license_name' => 'nullable|string|max:255',
'license_email' => 'nullable|email|max:255',
'maintained' => 'nullable|boolean',
'reassignable' => 'nullable|boolean',
'notes' => 'nullable|string',
'min_amt' => 'nullable|integer|min:0',
]);
$license = $this->resolveLicense($request);
if (! $license) {
return Response::make(Response::error('License not found'));
}
if (! Gate::allows('update', $license)) {
return Response::make(Response::error('Unauthorized'));
}
$updatable = [
'seats', 'serial', 'category_id', 'manufacturer_id', 'supplier_id',
'purchase_date', 'purchase_cost', 'purchase_order', 'order_number',
'expiration_date', 'termination_date', 'license_name', 'license_email',
'maintained', 'reassignable', 'notes', 'min_amt',
];
foreach ($updatable as $field) {
if ($request->filled($field)) {
$license->{$field} = $request->get($field);
}
}
if ($request->filled('new_name')) {
$license->name = $request->get('new_name');
}
if ($request->filled('company_id')) {
$license->company_id = Company::getIdForCurrentUser($request->get('company_id'));
}
if ($license->save()) {
return Response::make(
Response::text('License '.$license->name.' updated successfully')
)->withStructuredContent([
'success' => true,
'message' => 'License updated successfully',
'id' => $license->id,
'name' => $license->name,
'seats' => $license->seats,
]);
}
return Response::make(Response::error('Update failed: '.$license->getErrors()->first()));
}
private function resolveLicense(Request $request): ?License
{
if ($request->filled('id')) {
return License::find($request->get('id'));
}
if ($request->filled('name')) {
return License::where('name', $request->get('name'))->first();
}
return null;
}
public function schema(JsonSchema $schema): array
{
return [
'id' => $schema->number()->description('Numeric ID to identify the license'),
'name' => $schema->string()->description('Name to identify the license'),
'new_name' => $schema->string()->description('New name (renames the license)'),
'seats' => $schema->number()->description('Total seat count'),
'serial' => $schema->string()->description('Product key / serial number'),
'category_id' => $schema->number()->description('Category ID'),
'manufacturer_id' => $schema->number()->description('Manufacturer ID'),
'supplier_id' => $schema->number()->description('Supplier ID'),
'company_id' => $schema->number()->description('Company ID'),
'purchase_date' => $schema->string()->description('Purchase date (YYYY-MM-DD)'),
'purchase_cost' => $schema->number()->description('Purchase cost'),
'purchase_order' => $schema->string()->description('Purchase order number'),
'order_number' => $schema->string()->description('Order number'),
'expiration_date' => $schema->string()->description('Expiration date (YYYY-MM-DD)'),
'termination_date' => $schema->string()->description('Termination date (YYYY-MM-DD)'),
'license_name' => $schema->string()->description('Name of the licensed user/organization'),
'license_email' => $schema->string()->description('Email of the licensed user/organization'),
'maintained' => $schema->boolean()->description('Whether the license is under maintenance'),
'reassignable' => $schema->boolean()->description('Whether seats can be reassigned after checkin'),
'notes' => $schema->string()->description('Notes'),
'min_amt' => $schema->number()->description('Minimum seat threshold for alerts'),
];
}
public function outputSchema(JsonSchema $schema): array
{
return [
'success' => $schema->boolean()->description('True if the update succeeded'),
'message' => $schema->string()->description('Human-readable result message')->required(),
'id' => $schema->number()->description('Numeric ID of the license'),
'name' => $schema->string()->description('Name of the license'),
'seats' => $schema->number()->description('Total seat count'),
];
}
}
@@ -0,0 +1,139 @@
<?php
namespace Tests\Feature\Mcp;
use App\Events\CheckoutableCheckedIn;
use App\Mcp\Tools\CheckinLicenseTool;
use App\Mcp\Tools\CheckoutLicenseTool;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class CheckinLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->checkoutLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new CheckinLicenseTool)->handle(new Request($args));
}
private function checkoutToUser(License $license, User $user): LicenseSeat
{
$response = (new CheckoutLicenseTool)->handle(new Request([
'id' => $license->id,
'assigned_to' => $user->id,
]));
$seatId = $response->getStructuredContent()['seat_id'];
return LicenseSeat::find($seatId);
}
public function test_checks_in_seat_by_seat_id()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $this->checkoutToUser($license, $user);
$content = $this->handle(['seat_id' => $seat->id])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('license_seats', [
'id' => $seat->id,
'assigned_to' => null,
'asset_id' => null,
]);
}
public function test_response_includes_license_info()
{
$license = License::factory()->create(['name' => 'Checkin License', 'seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $this->checkoutToUser($license, $user);
$content = $this->handle(['seat_id' => $seat->id])->getStructuredContent();
$this->assertEquals($seat->id, $content['seat_id']);
$this->assertEquals($license->id, $content['license_id']);
$this->assertEquals('Checkin License', $content['license_name']);
}
public function test_fires_checkin_event()
{
Event::fake([CheckoutableCheckedIn::class]);
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $this->checkoutToUser($license, $user);
$this->handle(['seat_id' => $seat->id]);
Event::assertDispatched(CheckoutableCheckedIn::class);
}
public function test_returns_error_when_seat_not_found()
{
$this->assertTrue($this->handle(['seat_id' => 999999])->responses()->first()->isError());
}
public function test_returns_error_when_seat_is_not_checked_out()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$seat = $license->freeSeat();
$this->assertTrue($this->handle(['seat_id' => $seat->id])->responses()->first()->isError());
}
public function test_sets_unreassignable_flag_when_license_not_reassignable()
{
$license = License::factory()->create(['seats' => 1, 'reassignable' => false]);
$user = User::factory()->create();
$seat = $license->freeSeat();
$seat->assigned_to = $user->id;
$seat->save();
$this->handle(['seat_id' => $seat->id]);
$this->assertDatabaseHas('license_seats', [
'id' => $seat->id,
'unreassignable_seat' => true,
]);
}
public function test_does_not_set_unreassignable_flag_when_license_is_reassignable()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $this->checkoutToUser($license, $user);
$this->handle(['seat_id' => $seat->id]);
$refreshed = LicenseSeat::find($seat->id);
$this->assertFalse((bool) $refreshed->unreassignable_seat);
}
public function test_returns_error_when_user_lacks_permission()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $this->checkoutToUser($license, $user);
$this->actingAs(User::factory()->create());
$this->assertTrue($this->handle(['seat_id' => $seat->id])->responses()->first()->isError());
$this->assertDatabaseHas('license_seats', [
'id' => $seat->id,
'assigned_to' => $user->id,
]);
}
}
@@ -0,0 +1,164 @@
<?php
namespace Tests\Feature\Mcp;
use App\Events\CheckoutableCheckedOut;
use App\Mcp\Tools\CheckoutLicenseTool;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class CheckoutLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->checkoutLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new CheckoutLicenseTool)->handle(new Request($args));
}
public function test_checks_out_license_to_user_by_id()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$content = $this->handle([
'id' => $license->id,
'assigned_to' => $user->id,
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertEquals('user', $content['assigned_to_type']);
$this->assertEquals($user->id, $content['assigned_to_id']);
$this->assertDatabaseHas('license_seats', [
'license_id' => $license->id,
'assigned_to' => $user->id,
]);
}
public function test_checks_out_license_to_user_by_name()
{
$license = License::factory()->create(['name' => 'Named Checkout License', 'seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$content = $this->handle([
'name' => 'Named Checkout License',
'assigned_to' => $user->id,
])->getStructuredContent();
$this->assertTrue($content['success']);
}
public function test_checks_out_license_to_asset()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$asset = Asset::factory()->create();
$content = $this->handle([
'id' => $license->id,
'asset_id' => $asset->id,
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertEquals('asset', $content['assigned_to_type']);
$this->assertEquals($asset->id, $content['assigned_to_id']);
$this->assertDatabaseHas('license_seats', [
'license_id' => $license->id,
'asset_id' => $asset->id,
]);
}
public function test_response_includes_seat_id_for_checkin()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$content = $this->handle([
'id' => $license->id,
'assigned_to' => $user->id,
])->getStructuredContent();
$this->assertArrayHasKey('seat_id', $content);
$this->assertIsInt($content['seat_id']);
$this->assertNotNull(LicenseSeat::find($content['seat_id']));
}
public function test_fires_checkout_event()
{
Event::fake([CheckoutableCheckedOut::class]);
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$this->handle([
'id' => $license->id,
'assigned_to' => $user->id,
]);
Event::assertDispatched(CheckoutableCheckedOut::class);
}
public function test_returns_error_when_no_seats_remaining()
{
$license = License::factory()->create(['seats' => 1, 'reassignable' => true]);
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$seat = $license->freeSeat();
$seat->assigned_to = $user1->id;
$seat->save();
$response = $this->handle([
'id' => $license->id,
'assigned_to' => $user2->id,
]);
$this->assertTrue($response->responses()->first()->isError());
}
public function test_returns_error_when_neither_assignee_provided()
{
$license = License::factory()->create(['seats' => 3]);
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
}
public function test_returns_error_when_license_not_found()
{
$this->assertTrue($this->handle([
'id' => 999999,
'assigned_to' => User::factory()->create()->id,
])->responses()->first()->isError());
}
public function test_returns_error_when_user_not_found()
{
$license = License::factory()->create(['seats' => 3]);
$this->assertTrue($this->handle([
'id' => $license->id,
'assigned_to' => 999999,
])->responses()->first()->isError());
}
public function test_returns_error_when_user_lacks_permission()
{
$this->actingAs(User::factory()->create());
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$this->assertTrue($this->handle([
'id' => $license->id,
'assigned_to' => $user->id,
])->responses()->first()->isError());
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\CreateLicenseTool;
use App\Models\Category;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class CreateLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->createLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new CreateLicenseTool)->handle(new Request($args));
}
public function test_creates_license_with_required_fields()
{
$category = Category::factory()->create(['category_type' => 'license']);
$content = $this->handle([
'name' => 'Test License',
'seats' => 5,
'category_id' => $category->id,
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('licenses', ['name' => 'Test License', 'seats' => 5]);
}
public function test_response_includes_id_name_seats_and_category()
{
$category = Category::factory()->create(['category_type' => 'license']);
$content = $this->handle([
'name' => 'Response License',
'seats' => 10,
'category_id' => $category->id,
])->getStructuredContent();
$this->assertArrayHasKey('id', $content);
$this->assertEquals('Response License', $content['name']);
$this->assertEquals(10, $content['seats']);
$this->assertEquals($category->id, $content['category_id']);
}
public function test_creates_license_seats_automatically()
{
$category = Category::factory()->create(['category_type' => 'license']);
$content = $this->handle([
'name' => 'Seat Auto License',
'seats' => 3,
'category_id' => $category->id,
])->getStructuredContent();
$licenseId = $content['id'];
$this->assertEquals(3, LicenseSeat::where('license_id', $licenseId)->count());
}
public function test_creates_license_with_optional_fields()
{
$category = Category::factory()->create(['category_type' => 'license']);
$this->handle([
'name' => 'Full License',
'seats' => 2,
'category_id' => $category->id,
'serial' => 'SN-FULL-001',
'license_name' => 'Acme Corp',
'license_email' => 'admin@acme.com',
'purchase_date' => '2024-01-15',
'purchase_cost' => 299.99,
'expiration_date' => '2025-01-15',
'maintained' => true,
'reassignable' => false,
'notes' => 'Important license',
]);
$this->assertDatabaseHas('licenses', [
'name' => 'Full License',
'serial' => 'SN-FULL-001',
'license_name' => 'Acme Corp',
'license_email' => 'admin@acme.com',
]);
}
public function test_returns_error_when_name_missing()
{
$category = Category::factory()->create(['category_type' => 'license']);
$this->assertTrue($this->handle([
'seats' => 5,
'category_id' => $category->id,
])->responses()->first()->isError());
}
public function test_returns_error_when_seats_missing()
{
$category = Category::factory()->create(['category_type' => 'license']);
$this->assertTrue($this->handle([
'name' => 'No Seats',
'category_id' => $category->id,
])->responses()->first()->isError());
}
public function test_returns_error_when_category_missing()
{
$this->assertTrue($this->handle([
'name' => 'No Category',
'seats' => 5,
])->responses()->first()->isError());
}
public function test_returns_error_when_category_does_not_exist()
{
$this->assertTrue($this->handle([
'name' => 'Bad Category',
'seats' => 5,
'category_id' => 999999,
])->responses()->first()->isError());
}
public function test_returns_error_when_user_lacks_permission()
{
$this->actingAs(User::factory()->create());
$category = Category::factory()->create(['category_type' => 'license']);
$this->assertTrue($this->handle([
'name' => 'Blocked License',
'seats' => 5,
'category_id' => $category->id,
])->responses()->first()->isError());
$this->assertDatabaseMissing('licenses', ['name' => 'Blocked License']);
}
}
@@ -0,0 +1,82 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\DeleteLicenseTool;
use App\Models\License;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class DeleteLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->deleteLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new DeleteLicenseTool)->handle(new Request($args));
}
public function test_deletes_license_by_id()
{
$license = License::factory()->create();
$content = $this->handle(['id' => $license->id])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertSoftDeleted('licenses', ['id' => $license->id]);
}
public function test_deletes_license_by_name()
{
$license = License::factory()->create(['name' => 'Delete By Name License']);
$content = $this->handle(['name' => 'Delete By Name License'])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertSoftDeleted('licenses', ['id' => $license->id]);
}
public function test_response_includes_name()
{
$license = License::factory()->create(['name' => 'Named License']);
$content = $this->handle(['id' => $license->id])->getStructuredContent();
$this->assertEquals('Named License', $content['name']);
}
public function test_returns_error_when_not_found()
{
$this->assertTrue($this->handle(['id' => 999999])->responses()->first()->isError());
}
public function test_returns_error_when_seats_are_assigned()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $license->freeSeat();
$seat->assigned_to = $user->id;
$seat->save();
$response = $this->handle(['id' => $license->id]);
$this->assertTrue($response->responses()->first()->isError());
$this->assertNotSoftDeleted('licenses', ['id' => $license->id]);
}
public function test_returns_error_when_user_lacks_permission()
{
$license = License::factory()->create();
$this->actingAs(User::factory()->create());
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
$this->assertNotSoftDeleted('licenses', ['id' => $license->id]);
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\ListLicensesTool;
use App\Models\Category;
use App\Models\License;
use App\Models\Manufacturer;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class ListLicensesToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->viewLicenses()->create());
}
private function handle(array $args = []): ResponseFactory
{
return (new ListLicensesTool)->handle(new Request($args));
}
public function test_returns_total_licenses_and_pagination_metadata()
{
License::factory()->count(3)->create();
$content = $this->handle()->getStructuredContent();
$this->assertArrayHasKey('total', $content);
$this->assertArrayHasKey('licenses', $content);
$this->assertArrayHasKey('limit', $content);
$this->assertArrayHasKey('offset', $content);
$this->assertGreaterThanOrEqual(3, $content['total']);
}
public function test_each_license_includes_key_fields()
{
License::factory()->create();
$content = $this->handle()->getStructuredContent();
$license = $content['licenses'][0];
$this->assertArrayHasKey('id', $license);
$this->assertArrayHasKey('name', $license);
$this->assertArrayHasKey('seats', $license);
$this->assertArrayHasKey('free_seats', $license);
$this->assertArrayHasKey('category', $license);
}
public function test_limit_controls_number_of_results_returned()
{
License::factory()->count(10)->create();
$content = $this->handle(['limit' => 3])->getStructuredContent();
$this->assertCount(3, $content['licenses']);
$this->assertGreaterThan(3, $content['total']);
}
public function test_offset_skips_results_for_pagination()
{
License::factory()->count(10)->create();
$page1 = $this->handle(['limit' => 4, 'offset' => 0])->getStructuredContent();
$page2 = $this->handle(['limit' => 4, 'offset' => 4])->getStructuredContent();
$ids1 = array_column($page1['licenses'], 'id');
$ids2 = array_column($page2['licenses'], 'id');
$this->assertEmpty(array_intersect($ids1, $ids2));
}
public function test_search_matches_on_name()
{
$target = License::factory()->create(['name' => 'UniqueLicenseXYZ']);
License::factory()->create(['name' => 'Something Else']);
$content = $this->handle(['search' => 'UniqueLicenseXYZ'])->getStructuredContent();
$ids = array_column($content['licenses'], 'id');
$this->assertContains($target->id, $ids);
}
public function test_filters_by_category_id()
{
$category = Category::factory()->create(['category_type' => 'license']);
$matching = License::factory()->create(['category_id' => $category->id]);
$other = License::factory()->create();
$content = $this->handle(['category_id' => $category->id])->getStructuredContent();
$ids = array_column($content['licenses'], 'id');
$this->assertContains($matching->id, $ids);
$this->assertNotContains($other->id, $ids);
}
public function test_filters_by_manufacturer_id()
{
$manufacturer = Manufacturer::factory()->create();
$matching = License::factory()->create(['manufacturer_id' => $manufacturer->id]);
$other = License::factory()->create(['manufacturer_id' => null]);
$content = $this->handle(['manufacturer_id' => $manufacturer->id])->getStructuredContent();
$ids = array_column($content['licenses'], 'id');
$this->assertContains($matching->id, $ids);
$this->assertNotContains($other->id, $ids);
}
public function test_returns_error_when_user_lacks_permission()
{
$this->actingAs(User::factory()->create());
$this->assertTrue($this->handle()->responses()->first()->isError());
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\ShowLicenseTool;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class ShowLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->viewLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new ShowLicenseTool)->handle(new Request($args));
}
public function test_returns_license_by_id()
{
$license = License::factory()->create(['name' => 'Test License', 'seats' => 5]);
$content = $this->handle(['id' => $license->id])->getStructuredContent();
$this->assertEquals($license->id, $content['id']);
$this->assertEquals('Test License', $content['name']);
$this->assertEquals(5, $content['seats']);
}
public function test_returns_license_by_name()
{
$license = License::factory()->create(['name' => 'FindByName License']);
$content = $this->handle(['name' => 'FindByName License'])->getStructuredContent();
$this->assertEquals($license->id, $content['id']);
$this->assertEquals('FindByName License', $content['name']);
}
public function test_response_includes_seat_counts()
{
$license = License::factory()->create(['seats' => 3, 'reassignable' => true]);
$user = User::factory()->create();
$seat = $license->freeSeat();
$seat->assigned_to = $user->id;
$seat->save();
$content = $this->handle(['id' => $license->id])->getStructuredContent();
$this->assertEquals(3, $content['seats']);
$this->assertEquals(1, $content['assigned_seats']);
$this->assertEquals(2, $content['free_seats']);
}
public function test_response_includes_key_fields()
{
$license = License::factory()->create([
'name' => 'Full Details License',
'serial' => 'SN-12345',
'license_name' => 'Acme Corp',
'license_email' => 'legal@acme.com',
]);
$content = $this->handle(['id' => $license->id])->getStructuredContent();
$this->assertArrayHasKey('serial', $content);
$this->assertArrayHasKey('license_name', $content);
$this->assertArrayHasKey('license_email', $content);
$this->assertArrayHasKey('maintained', $content);
$this->assertArrayHasKey('reassignable', $content);
$this->assertArrayHasKey('category', $content);
$this->assertArrayHasKey('category_id', $content);
$this->assertEquals('SN-12345', $content['serial']);
$this->assertEquals('Acme Corp', $content['license_name']);
}
public function test_returns_error_when_no_identifier_provided()
{
$this->assertTrue($this->handle([])->responses()->first()->isError());
}
public function test_returns_error_when_not_found()
{
$this->assertTrue($this->handle(['id' => 999999])->responses()->first()->isError());
}
public function test_returns_error_when_user_lacks_permission()
{
$license = License::factory()->create();
$this->actingAs(User::factory()->create());
$this->assertTrue($this->handle(['id' => $license->id])->responses()->first()->isError());
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace Tests\Feature\Mcp;
use App\Mcp\Tools\UpdateLicenseTool;
use App\Models\Category;
use App\Models\License;
use App\Models\User;
use Laravel\Mcp\Request;
use Laravel\Mcp\ResponseFactory;
use Tests\TestCase;
class UpdateLicenseToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::factory()->editLicenses()->create());
}
private function handle(array $args): ResponseFactory
{
return (new UpdateLicenseTool)->handle(new Request($args));
}
public function test_updates_license_by_id()
{
$license = License::factory()->create(['name' => 'Original Name']);
$content = $this->handle([
'id' => $license->id,
'new_name' => 'Updated Name',
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Updated Name']);
}
public function test_updates_license_by_name()
{
$license = License::factory()->create(['name' => 'Find By Name License']);
$content = $this->handle([
'name' => 'Find By Name License',
'new_name' => 'Renamed License',
])->getStructuredContent();
$this->assertTrue($content['success']);
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Renamed License']);
}
public function test_response_includes_id_name_and_seats()
{
$license = License::factory()->create(['seats' => 5]);
$content = $this->handle([
'id' => $license->id,
'new_name' => 'Response Check License',
])->getStructuredContent();
$this->assertArrayHasKey('id', $content);
$this->assertEquals('Response Check License', $content['name']);
$this->assertEquals(5, $content['seats']);
}
public function test_updates_multiple_fields()
{
$license = License::factory()->create();
$category = Category::factory()->create(['category_type' => 'license']);
$this->handle([
'id' => $license->id,
'category_id' => $category->id,
'serial' => 'SN-NEW-001',
'purchase_cost' => 199.99,
'expiration_date' => '2026-12-31',
'maintained' => true,
'reassignable' => true,
'notes' => 'Updated notes',
]);
$this->assertDatabaseHas('licenses', [
'id' => $license->id,
'category_id' => $category->id,
'serial' => 'SN-NEW-001',
]);
}
public function test_returns_error_when_not_found()
{
$this->assertTrue($this->handle(['id' => 999999, 'new_name' => 'Ghost'])->responses()->first()->isError());
}
public function test_returns_error_when_user_lacks_permission()
{
$license = License::factory()->create(['name' => 'Unchanged License']);
$this->actingAs(User::factory()->create());
$this->assertTrue($this->handle([
'id' => $license->id,
'new_name' => 'Should Not Change',
])->responses()->first()->isError());
$this->assertDatabaseHas('licenses', ['id' => $license->id, 'name' => 'Unchanged License']);
}
}