forked from core/php-mcp
test: add comprehensive tests for AuditLogService
Cover record(), log entry creation, security audit fields (actor type, IP, sensitivity), sensitive tool registration, field redaction (default and tool-specific), hash chain integrity, chain verification, consent requirements, export/retrieval (JSON, CSV), and statistics. Fixes #5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa867cfa7d
commit
4f11d7d435
1 changed files with 727 additions and 0 deletions
727
src/Mcp/Tests/Unit/AuditLogServiceTest.php
Normal file
727
src/Mcp/Tests/Unit/AuditLogServiceTest.php
Normal file
|
|
@ -0,0 +1,727 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Unit: Audit Log Service
|
||||
*
|
||||
* Tests for MCP audit log service covering:
|
||||
* - record() method and log entry creation
|
||||
* - Security audit fields (actor, IP, sensitivity)
|
||||
* - Sensitive tool registration and field redaction
|
||||
* - Hash chain integrity and tamper detection
|
||||
* - Query/retrieval via export and stats
|
||||
* - Consent requirements
|
||||
*
|
||||
* @see https://forge.lthn.ai/core/php-mcp/issues/5
|
||||
*/
|
||||
|
||||
use Core\Mcp\Models\McpAuditLog;
|
||||
use Core\Mcp\Models\McpSensitiveTool;
|
||||
use Core\Mcp\Services\AuditLogService;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
// =============================================================================
|
||||
// record() — Basic Log Entry Creation
|
||||
// =============================================================================
|
||||
|
||||
describe('record() creates audit log entries', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('creates an audit log entry with required fields', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
);
|
||||
|
||||
expect($entry)->toBeInstanceOf(McpAuditLog::class);
|
||||
expect($entry->exists)->toBeTrue();
|
||||
expect($entry->server_id)->toBe('mcp-server-1');
|
||||
expect($entry->tool_name)->toBe('query_database');
|
||||
expect($entry->success)->toBeTrue();
|
||||
});
|
||||
|
||||
it('stores input parameters as JSON', function () {
|
||||
$params = ['query' => 'SELECT * FROM users', 'limit' => 10];
|
||||
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
inputParams: $params,
|
||||
);
|
||||
|
||||
expect($entry->input_params)->toBe($params);
|
||||
});
|
||||
|
||||
it('stores output summary', function () {
|
||||
$output = ['rows_returned' => 5, 'columns' => ['id', 'name']];
|
||||
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
outputSummary: $output,
|
||||
);
|
||||
|
||||
expect($entry->output_summary)->toBe($output);
|
||||
});
|
||||
|
||||
it('records a failed execution', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
success: false,
|
||||
errorCode: 'QUERY_BLOCKED',
|
||||
errorMessage: 'INSERT statements are not allowed',
|
||||
);
|
||||
|
||||
expect($entry->success)->toBeFalse();
|
||||
expect($entry->error_code)->toBe('QUERY_BLOCKED');
|
||||
expect($entry->error_message)->toBe('INSERT statements are not allowed');
|
||||
});
|
||||
|
||||
it('records execution duration', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
durationMs: 250,
|
||||
);
|
||||
|
||||
expect($entry->duration_ms)->toBe(250);
|
||||
});
|
||||
|
||||
it('records session and workspace context', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
sessionId: 'session-abc-123',
|
||||
workspaceId: null,
|
||||
);
|
||||
|
||||
expect($entry->session_id)->toBe('session-abc-123');
|
||||
});
|
||||
|
||||
it('records agent type and plan slug', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'deploy_service',
|
||||
agentType: 'charon',
|
||||
planSlug: 'deploy-api-v2',
|
||||
);
|
||||
|
||||
expect($entry->agent_type)->toBe('charon');
|
||||
expect($entry->plan_slug)->toBe('deploy-api-v2');
|
||||
});
|
||||
|
||||
it('persists entries to the database', function () {
|
||||
$this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'tool_a',
|
||||
);
|
||||
|
||||
$this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'tool_b',
|
||||
);
|
||||
|
||||
expect(McpAuditLog::count())->toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Security Audit Fields (Actor, IP, Sensitivity)
|
||||
// =============================================================================
|
||||
|
||||
describe('Security audit fields', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('records actor type and actor ID for user actors', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
actorType: McpAuditLog::ACTOR_USER,
|
||||
actorId: 42,
|
||||
);
|
||||
|
||||
expect($entry->actor_type)->toBe('user');
|
||||
expect($entry->actor_id)->toBe(42);
|
||||
});
|
||||
|
||||
it('records actor type for API key actors', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
actorType: McpAuditLog::ACTOR_API_KEY,
|
||||
actorId: 7,
|
||||
);
|
||||
|
||||
expect($entry->actor_type)->toBe('api_key');
|
||||
expect($entry->actor_id)->toBe(7);
|
||||
});
|
||||
|
||||
it('records actor type for system actors', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'system_cleanup',
|
||||
actorType: McpAuditLog::ACTOR_SYSTEM,
|
||||
);
|
||||
|
||||
expect($entry->actor_type)->toBe('system');
|
||||
});
|
||||
|
||||
it('records actor IP address', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
actorType: McpAuditLog::ACTOR_USER,
|
||||
actorId: 1,
|
||||
actorIp: '192.168.1.100',
|
||||
);
|
||||
|
||||
expect($entry->actor_ip)->toBe('192.168.1.100');
|
||||
});
|
||||
|
||||
it('records IPv6 actor IP address', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
actorIp: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
);
|
||||
|
||||
expect($entry->actor_ip)->toBe('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
|
||||
});
|
||||
|
||||
it('marks non-sensitive tools as not sensitive', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'list_files',
|
||||
);
|
||||
|
||||
expect($entry->is_sensitive)->toBeFalse();
|
||||
expect($entry->sensitivity_reason)->toBeNull();
|
||||
});
|
||||
|
||||
it('marks registered sensitive tools as sensitive', function () {
|
||||
McpSensitiveTool::register(
|
||||
'database_admin',
|
||||
'Administrative database access',
|
||||
['connection_string'],
|
||||
);
|
||||
|
||||
Cache::forget('mcp:audit:sensitive_tools');
|
||||
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'database_admin',
|
||||
);
|
||||
|
||||
expect($entry->is_sensitive)->toBeTrue();
|
||||
expect($entry->sensitivity_reason)->toBe('Administrative database access');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Sensitive Tool Registration and Field Redaction
|
||||
// =============================================================================
|
||||
|
||||
describe('Sensitive tool registration', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('registers a sensitive tool', function () {
|
||||
$this->service->registerSensitiveTool(
|
||||
toolName: 'payment_process',
|
||||
reason: 'Handles financial transactions',
|
||||
redactFields: ['card_number', 'cvv'],
|
||||
requireConsent: true,
|
||||
);
|
||||
|
||||
$tool = McpSensitiveTool::where('tool_name', 'payment_process')->first();
|
||||
|
||||
expect($tool)->not->toBeNull();
|
||||
expect($tool->reason)->toBe('Handles financial transactions');
|
||||
expect($tool->redact_fields)->toBe(['card_number', 'cvv']);
|
||||
expect($tool->require_explicit_consent)->toBeTrue();
|
||||
});
|
||||
|
||||
it('unregisters a sensitive tool', function () {
|
||||
$this->service->registerSensitiveTool(
|
||||
'temp_tool',
|
||||
'Temporary sensitive tool',
|
||||
);
|
||||
|
||||
$result = $this->service->unregisterSensitiveTool('temp_tool');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
expect(McpSensitiveTool::where('tool_name', 'temp_tool')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false when unregistering non-existent tool', function () {
|
||||
$result = $this->service->unregisterSensitiveTool('nonexistent_tool');
|
||||
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('lists all registered sensitive tools', function () {
|
||||
$this->service->registerSensitiveTool('tool_a', 'Reason A');
|
||||
$this->service->registerSensitiveTool('tool_b', 'Reason B');
|
||||
|
||||
$tools = $this->service->getSensitiveTools();
|
||||
|
||||
expect($tools)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('clears cache when registering a sensitive tool', function () {
|
||||
Cache::put('mcp:audit:sensitive_tools', ['stale' => 'data'], 300);
|
||||
|
||||
$this->service->registerSensitiveTool('new_tool', 'New reason');
|
||||
|
||||
expect(Cache::has('mcp:audit:sensitive_tools'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('clears cache when unregistering a sensitive tool', function () {
|
||||
$this->service->registerSensitiveTool('cached_tool', 'Cached reason');
|
||||
Cache::put('mcp:audit:sensitive_tools', ['stale' => 'data'], 300);
|
||||
|
||||
$this->service->unregisterSensitiveTool('cached_tool');
|
||||
|
||||
expect(Cache::has('mcp:audit:sensitive_tools'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Field redaction', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('redacts default sensitive fields from input params', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'api_call',
|
||||
inputParams: [
|
||||
'url' => 'https://api.example.com',
|
||||
'password' => 'super-secret-123',
|
||||
'api_key' => 'sk-abc123',
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->input_params['url'])->toBe('https://api.example.com');
|
||||
expect($entry->input_params['password'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['api_key'])->toBe('[REDACTED]');
|
||||
});
|
||||
|
||||
it('redacts default sensitive fields from output summary', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'auth_check',
|
||||
outputSummary: [
|
||||
'status' => 'authenticated',
|
||||
'access_token' => 'eyJhbG...',
|
||||
'refresh_token' => 'dGhpcyBp...',
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->output_summary['status'])->toBe('authenticated');
|
||||
expect($entry->output_summary['access_token'])->toBe('[REDACTED]');
|
||||
expect($entry->output_summary['refresh_token'])->toBe('[REDACTED]');
|
||||
});
|
||||
|
||||
it('redacts nested sensitive fields recursively', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'config_read',
|
||||
inputParams: [
|
||||
'config' => [
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'password' => 'db-secret',
|
||||
],
|
||||
'api' => [
|
||||
'token' => 'bearer-xyz',
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->input_params['config']['database']['host'])->toBe('localhost');
|
||||
expect($entry->input_params['config']['database']['password'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['config']['api']['token'])->toBe('[REDACTED]');
|
||||
});
|
||||
|
||||
it('redacts additional tool-specific fields for sensitive tools', function () {
|
||||
McpSensitiveTool::register(
|
||||
'payment_process',
|
||||
'Handles payments',
|
||||
['merchant_id', 'routing_number'],
|
||||
);
|
||||
|
||||
Cache::forget('mcp:audit:sensitive_tools');
|
||||
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'payment_process',
|
||||
inputParams: [
|
||||
'amount' => 99.99,
|
||||
'merchant_id' => 'MERCH-001',
|
||||
'routing_number' => '123456789',
|
||||
'password' => 'also-redacted',
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->input_params['amount'])->toBe(99.99);
|
||||
expect($entry->input_params['merchant_id'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['routing_number'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['password'])->toBe('[REDACTED]');
|
||||
});
|
||||
|
||||
it('redacts fields using case-insensitive matching', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'mixed_case_tool',
|
||||
inputParams: [
|
||||
'Password' => 'secret1',
|
||||
'API_KEY' => 'secret2',
|
||||
'apiKey' => 'secret3',
|
||||
'CreditCard' => '4111111111111111',
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->input_params['Password'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['API_KEY'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['apiKey'])->toBe('[REDACTED]');
|
||||
expect($entry->input_params['CreditCard'])->toBe('[REDACTED]');
|
||||
});
|
||||
|
||||
it('does not redact non-sensitive fields', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'safe_tool',
|
||||
inputParams: [
|
||||
'name' => 'John',
|
||||
'email' => 'john@example.com',
|
||||
'query' => 'SELECT 1',
|
||||
],
|
||||
);
|
||||
|
||||
expect($entry->input_params['name'])->toBe('John');
|
||||
expect($entry->input_params['email'])->toBe('john@example.com');
|
||||
expect($entry->input_params['query'])->toBe('SELECT 1');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Hash Chain Integrity
|
||||
// =============================================================================
|
||||
|
||||
describe('Hash chain integrity', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('generates an entry hash for each record', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
);
|
||||
|
||||
expect($entry->entry_hash)->not->toBeNull();
|
||||
expect($entry->entry_hash)->toHaveLength(64); // SHA-256 hex
|
||||
});
|
||||
|
||||
it('first entry has null previous_hash', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
);
|
||||
|
||||
expect($entry->previous_hash)->toBeNull();
|
||||
});
|
||||
|
||||
it('links entries via previous_hash chain', function () {
|
||||
$first = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'tool_a',
|
||||
);
|
||||
|
||||
$second = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'tool_b',
|
||||
);
|
||||
|
||||
expect($second->previous_hash)->toBe($first->entry_hash);
|
||||
});
|
||||
|
||||
it('builds a chain across multiple entries', function () {
|
||||
$entries = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$entries[] = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: "tool_{$i}",
|
||||
);
|
||||
}
|
||||
|
||||
for ($i = 1; $i < 5; $i++) {
|
||||
expect($entries[$i]->previous_hash)->toBe($entries[$i - 1]->entry_hash);
|
||||
}
|
||||
});
|
||||
|
||||
it('produces a valid hash that can be verified', function () {
|
||||
$entry = $this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
inputParams: ['query' => 'SELECT 1'],
|
||||
durationMs: 50,
|
||||
);
|
||||
|
||||
expect($entry->verifyHash())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Chain Verification
|
||||
// =============================================================================
|
||||
|
||||
describe('verifyChain()', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('reports valid chain for properly linked entries', function () {
|
||||
$this->service->record('mcp-server-1', 'tool_a');
|
||||
$this->service->record('mcp-server-1', 'tool_b');
|
||||
$this->service->record('mcp-server-1', 'tool_c');
|
||||
|
||||
$result = $this->service->verifyChain();
|
||||
|
||||
expect($result['valid'])->toBeTrue();
|
||||
expect($result['total'])->toBe(3);
|
||||
expect($result['verified'])->toBe(3);
|
||||
expect($result['issues'])->toBeEmpty();
|
||||
});
|
||||
|
||||
it('supports verifying a subset of the chain by ID range', function () {
|
||||
$first = $this->service->record('mcp-server-1', 'tool_a');
|
||||
$second = $this->service->record('mcp-server-1', 'tool_b');
|
||||
$third = $this->service->record('mcp-server-1', 'tool_c');
|
||||
|
||||
$result = $this->service->verifyChain(fromId: $second->id, toId: $third->id);
|
||||
|
||||
expect($result['valid'])->toBeTrue();
|
||||
expect($result['total'])->toBe(2);
|
||||
expect($result['verified'])->toBe(2);
|
||||
});
|
||||
|
||||
it('reports valid for empty audit log', function () {
|
||||
$result = $this->service->verifyChain();
|
||||
|
||||
expect($result['valid'])->toBeTrue();
|
||||
expect($result['total'])->toBe(0);
|
||||
expect($result['verified'])->toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Consent Requirements
|
||||
// =============================================================================
|
||||
|
||||
describe('Consent requirements', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('returns true for tools requiring consent', function () {
|
||||
$this->service->registerSensitiveTool(
|
||||
'dangerous_tool',
|
||||
'Can modify production data',
|
||||
[],
|
||||
requireConsent: true,
|
||||
);
|
||||
|
||||
expect($this->service->requiresConsent('dangerous_tool'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false for tools not requiring consent', function () {
|
||||
$this->service->registerSensitiveTool(
|
||||
'monitored_tool',
|
||||
'Just needs logging',
|
||||
[],
|
||||
requireConsent: false,
|
||||
);
|
||||
|
||||
expect($this->service->requiresConsent('monitored_tool'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false for unregistered tools', function () {
|
||||
expect($this->service->requiresConsent('unknown_tool'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Export and Retrieval
|
||||
// =============================================================================
|
||||
|
||||
describe('export()', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('exports all entries when no filters given', function () {
|
||||
$this->service->record('mcp-server-1', 'tool_a');
|
||||
$this->service->record('mcp-server-1', 'tool_b');
|
||||
$this->service->record('mcp-server-1', 'tool_c');
|
||||
|
||||
$exported = $this->service->export();
|
||||
|
||||
expect($exported)->toHaveCount(3);
|
||||
});
|
||||
|
||||
it('filters by tool name', function () {
|
||||
$this->service->record('mcp-server-1', 'query_database');
|
||||
$this->service->record('mcp-server-1', 'file_read');
|
||||
$this->service->record('mcp-server-1', 'query_database');
|
||||
|
||||
$exported = $this->service->export(toolName: 'query_database');
|
||||
|
||||
expect($exported)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filters sensitive-only entries', function () {
|
||||
McpSensitiveTool::register('secret_tool', 'Top secret');
|
||||
Cache::forget('mcp:audit:sensitive_tools');
|
||||
|
||||
$this->service->record('mcp-server-1', 'secret_tool');
|
||||
$this->service->record('mcp-server-1', 'normal_tool');
|
||||
|
||||
$exported = $this->service->export(sensitiveOnly: true);
|
||||
|
||||
expect($exported)->toHaveCount(1);
|
||||
expect($exported->first()['tool_name'])->toBe('secret_tool');
|
||||
});
|
||||
|
||||
it('returns entries in export array format', function () {
|
||||
$this->service->record(
|
||||
serverId: 'mcp-server-1',
|
||||
toolName: 'query_database',
|
||||
actorType: 'user',
|
||||
actorId: 5,
|
||||
);
|
||||
|
||||
$exported = $this->service->export();
|
||||
$entry = $exported->first();
|
||||
|
||||
expect($entry)->toHaveKeys([
|
||||
'id', 'timestamp', 'server_id', 'tool_name',
|
||||
'success', 'entry_hash', 'actor_type', 'actor_id',
|
||||
]);
|
||||
expect($entry['server_id'])->toBe('mcp-server-1');
|
||||
expect($entry['tool_name'])->toBe('query_database');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportToJson()', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('produces valid JSON with integrity metadata', function () {
|
||||
$this->service->record('mcp-server-1', 'tool_a');
|
||||
$this->service->record('mcp-server-1', 'tool_b');
|
||||
|
||||
$json = $this->service->exportToJson();
|
||||
$data = json_decode($json, true);
|
||||
|
||||
expect($data)->toHaveKeys(['exported_at', 'integrity', 'filters', 'entries']);
|
||||
expect($data['integrity']['valid'])->toBeTrue();
|
||||
expect($data['integrity']['total_entries'])->toBe(2);
|
||||
expect($data['entries'])->toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportToCsv()', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('produces CSV with headers', function () {
|
||||
$this->service->record('mcp-server-1', 'tool_a');
|
||||
|
||||
$csv = $this->service->exportToCsv();
|
||||
|
||||
expect($csv)->toContain('server_id');
|
||||
expect($csv)->toContain('tool_name');
|
||||
expect($csv)->toContain('mcp-server-1');
|
||||
expect($csv)->toContain('tool_a');
|
||||
});
|
||||
|
||||
it('returns empty string when no entries', function () {
|
||||
$csv = $this->service->exportToCsv();
|
||||
|
||||
expect($csv)->toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Statistics
|
||||
// =============================================================================
|
||||
|
||||
describe('getStats()', function () {
|
||||
beforeEach(function () {
|
||||
$this->service = new AuditLogService();
|
||||
});
|
||||
|
||||
it('returns correct totals', function () {
|
||||
$this->service->record('mcp-server-1', 'tool_a', success: true);
|
||||
$this->service->record('mcp-server-1', 'tool_a', success: true);
|
||||
$this->service->record('mcp-server-1', 'tool_a', success: false, errorCode: 'ERR');
|
||||
|
||||
$stats = $this->service->getStats();
|
||||
|
||||
expect($stats['total'])->toBe(3);
|
||||
expect($stats['successful'])->toBe(2);
|
||||
expect($stats['failed'])->toBe(1);
|
||||
expect($stats['success_rate'])->toBe(66.67);
|
||||
});
|
||||
|
||||
it('counts sensitive calls', function () {
|
||||
McpSensitiveTool::register('secret_tool', 'Classified');
|
||||
Cache::forget('mcp:audit:sensitive_tools');
|
||||
|
||||
$this->service->record('mcp-server-1', 'secret_tool');
|
||||
$this->service->record('mcp-server-1', 'normal_tool');
|
||||
|
||||
$stats = $this->service->getStats();
|
||||
|
||||
expect($stats['sensitive_calls'])->toBe(1);
|
||||
});
|
||||
|
||||
it('lists top tools by usage', function () {
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->service->record('mcp-server-1', 'popular_tool');
|
||||
}
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$this->service->record('mcp-server-1', 'rare_tool');
|
||||
}
|
||||
|
||||
$stats = $this->service->getStats();
|
||||
|
||||
expect($stats['top_tools'])->toHaveKey('popular_tool');
|
||||
expect($stats['top_tools']['popular_tool'])->toBe(5);
|
||||
expect($stats['top_tools']['rare_tool'])->toBe(2);
|
||||
});
|
||||
|
||||
it('returns zero stats for empty log', function () {
|
||||
$stats = $this->service->getStats();
|
||||
|
||||
expect($stats['total'])->toBe(0);
|
||||
expect($stats['successful'])->toBe(0);
|
||||
expect($stats['failed'])->toBe(0);
|
||||
expect($stats['success_rate'])->toBe(0);
|
||||
expect($stats['sensitive_calls'])->toBe(0);
|
||||
expect($stats['top_tools'])->toBeEmpty();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue