feat: add read-only System Info admin page with health checks and module inventory
New page at /admin/system-info with three tabs: - Overview: PHP version, SAPI, environment, 6 health checks (DB, schema, storage, RBAC baseline, admin role, scheduler heartbeat) - Modules: table of active modules with version, dependencies, permission count - Permissions: active/inactive counts and per-source breakdown Gated behind new system_info.view permission assigned to Admin role. No mutations — purely diagnostic/observability. Includes SystemHealthService, SystemInfoService, SystemHealthRepository with interface, DI registration, i18n keys (de+en), idempotent DB update script, and 17 new PHPUnit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
195
tests/Service/System/SystemHealthServiceTest.php
Normal file
195
tests/Service/System/SystemHealthServiceTest.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\System;
|
||||
|
||||
use MintyPHP\Repository\System\SystemHealthRepositoryInterface;
|
||||
use MintyPHP\Service\System\SystemHealthService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SystemHealthServiceTest extends TestCase
|
||||
{
|
||||
private SystemHealthRepositoryInterface&MockObject $repository;
|
||||
private SystemHealthService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(SystemHealthRepositoryInterface::class);
|
||||
$this->service = new SystemHealthService($this->repository);
|
||||
}
|
||||
|
||||
public function testCheckDatabaseReturnsOkWhenConnected(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('checkDatabaseConnectivity')
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->checkDatabase();
|
||||
|
||||
self::assertSame('ok', $result['status']);
|
||||
self::assertSame('Database connectivity', $result['name']);
|
||||
}
|
||||
|
||||
public function testCheckDatabaseReturnsFailWhenDisconnected(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('checkDatabaseConnectivity')
|
||||
->willReturn(false);
|
||||
|
||||
$result = $this->service->checkDatabase();
|
||||
|
||||
self::assertSame('fail', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckDatabaseReturnsFailOnException(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('checkDatabaseConnectivity')
|
||||
->willThrowException(new \RuntimeException('Connection refused'));
|
||||
|
||||
$result = $this->service->checkDatabase();
|
||||
|
||||
self::assertSame('fail', $result['status']);
|
||||
self::assertSame('check failed', $result['message']);
|
||||
}
|
||||
|
||||
public function testCheckDatabaseSchemaReturnsOkWhenAllTablesPresent(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('listPresentTables')
|
||||
->willReturn([
|
||||
'users', 'roles', 'permissions', 'user_roles', 'role_permissions',
|
||||
'tenants', 'departments', 'settings', 'scheduler_runtime_status',
|
||||
]);
|
||||
|
||||
$result = $this->service->checkDatabaseSchema();
|
||||
|
||||
self::assertSame('ok', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckDatabaseSchemaReturnsFailWhenTablesMissing(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('listPresentTables')
|
||||
->willReturn(['users', 'roles']);
|
||||
|
||||
$result = $this->service->checkDatabaseSchema();
|
||||
|
||||
self::assertSame('fail', $result['status']);
|
||||
self::assertStringContainsString('missing tables', $result['message']);
|
||||
}
|
||||
|
||||
public function testCheckRbacBaselineReturnsOkWhenAllPermissionsActive(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('listActivePermissionKeys')
|
||||
->willReturn([
|
||||
'users.view', 'tenants.view', 'departments.view',
|
||||
'roles.view', 'permissions.view', 'settings.view',
|
||||
]);
|
||||
|
||||
$result = $this->service->checkRbacBaseline();
|
||||
|
||||
self::assertSame('ok', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckRbacBaselineReturnsFailWhenPermissionsMissing(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('listActivePermissionKeys')
|
||||
->willReturn(['users.view']);
|
||||
|
||||
$result = $this->service->checkRbacBaseline();
|
||||
|
||||
self::assertSame('fail', $result['status']);
|
||||
self::assertStringContainsString('missing active permissions', $result['message']);
|
||||
}
|
||||
|
||||
public function testCheckAdminRoleAssignmentReturnsOkWhenAdminsExist(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('countAdminUsers')
|
||||
->willReturn(2);
|
||||
|
||||
$result = $this->service->checkAdminRoleAssignment();
|
||||
|
||||
self::assertSame('ok', $result['status']);
|
||||
self::assertStringContainsString('2 admin user(s)', $result['message']);
|
||||
}
|
||||
|
||||
public function testCheckAdminRoleAssignmentReturnsFailWhenNoAdmins(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('countAdminUsers')
|
||||
->willReturn(0);
|
||||
|
||||
$result = $this->service->checkAdminRoleAssignment();
|
||||
|
||||
self::assertSame('fail', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckSchedulerHeartbeatReturnsWarnWhenNoStatusRow(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getSchedulerStatus')
|
||||
->willReturn(null);
|
||||
|
||||
$result = $this->service->checkSchedulerHeartbeat();
|
||||
|
||||
self::assertSame('warn', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckSchedulerHeartbeatReturnsOkWhenRecent(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getSchedulerStatus')
|
||||
->willReturn([
|
||||
'last_heartbeat_at' => gmdate('Y-m-d H:i:s', time() - 10),
|
||||
'last_result' => 'ok',
|
||||
'last_error_code' => '',
|
||||
]);
|
||||
|
||||
$result = $this->service->checkSchedulerHeartbeat();
|
||||
|
||||
self::assertSame('ok', $result['status']);
|
||||
}
|
||||
|
||||
public function testCheckSchedulerHeartbeatReturnsWarnWhenStale(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('getSchedulerStatus')
|
||||
->willReturn([
|
||||
'last_heartbeat_at' => gmdate('Y-m-d H:i:s', time() - 600),
|
||||
'last_result' => 'ok',
|
||||
'last_error_code' => '',
|
||||
]);
|
||||
|
||||
$result = $this->service->checkSchedulerHeartbeat();
|
||||
|
||||
self::assertSame('warn', $result['status']);
|
||||
}
|
||||
|
||||
public function testRunAllReturnsAllSixChecks(): void
|
||||
{
|
||||
$this->repository->method('checkDatabaseConnectivity')->willReturn(true);
|
||||
$this->repository->method('listPresentTables')->willReturn([
|
||||
'users', 'roles', 'permissions', 'user_roles', 'role_permissions',
|
||||
'tenants', 'departments', 'settings', 'scheduler_runtime_status',
|
||||
]);
|
||||
$this->repository->method('listActivePermissionKeys')->willReturn([
|
||||
'users.view', 'tenants.view', 'departments.view',
|
||||
'roles.view', 'permissions.view', 'settings.view',
|
||||
]);
|
||||
$this->repository->method('countAdminUsers')->willReturn(1);
|
||||
$this->repository->method('getSchedulerStatus')->willReturn(null);
|
||||
|
||||
$results = $this->service->runAll();
|
||||
|
||||
self::assertCount(6, $results);
|
||||
foreach ($results as $check) {
|
||||
self::assertArrayHasKey('status', $check);
|
||||
self::assertArrayHasKey('name', $check);
|
||||
self::assertArrayHasKey('message', $check);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user