Files
zyt/server/tests/DiagnosisDepartmentFilterPermissionTest.php
2026-11-06 12:01:31 +08:00

253 lines
12 KiB
PHP

<?php
declare(strict_types=1);
// Execute the real middleware, department controller and tree filtering logic.
// Only authentication storage, department rows and scope lookup are in memory.
namespace app\adminapi\logic {
final class LoginLogic
{
public static function adminMustBindWorkWechat(array $adminInfo): bool { return false; }
}
}
namespace app\common\cache {
final class AdminAuthCache
{
public static array $permissions = [];
public static array $allUris = [];
public static array $actors = [];
public static int $allUriReads = 0;
public function __construct(int $adminId) { self::$actors[] = $adminId; }
public function getAdminUri(): array { return self::$permissions; }
public function getAllUri(): array { self::$allUriReads++; return self::$allUris; }
}
}
namespace app\common\service {
final class JsonService
{
public static function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1): array
{
return compact('code', 'show', 'msg', 'data');
}
public static function data(array $data): array
{
return ['code' => 1, 'show' => 0, 'msg' => '', 'data' => $data];
}
}
}
namespace app\common\model\dept {
final class Dept
{
public static array $rows = [];
public static int $reads = 0;
public static function order(array $order): self { self::$reads++; return new self(); }
public function select(): self { return $this; }
public function toArray(): array { return self::$rows; }
}
}
namespace app\common\service\DataScope {
final class DataScopeService
{
public static ?array $allowed = [12 => true];
public static array $actors = [];
public static function isEnabled(): bool { return true; }
public static function getAllowedDeptIdSet(int $adminId, array $adminInfo): ?array
{
self::$actors[] = [$adminId, $adminInfo];
return self::$allowed;
}
}
}
namespace app\adminapi\http\middleware {
function request(): object { return $GLOBALS['diagnosis_department_request']; }
function env(...$arguments): never { throw new \RuntimeException('This test must not read environment configuration'); }
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\controller\dept\DeptController;
use app\adminapi\http\middleware\AuthMiddleware;
use app\common\cache\AdminAuthCache;
use app\common\model\dept\Dept;
use app\common\service\DataScope\DataScopeService;
use app\common\service\JsonService;
final class DiagnosisDepartmentRequest
{
public array $adminInfo;
public object $controllerObject;
public string $controllerName = 'dept.Dept';
public string $actionName = 'all';
public function __construct(public array $query = [], public string $method = 'GET',
public array $body = [], bool $root = false)
{
$this->adminInfo = ['admin_id' => 91, 'root' => $root ? 1 : 0, 'role_id' => [7],
'login_ip' => '192.0.2.91', 'work_wechat_userid' => 'synthetic-doctor-91'];
}
public function get(?string $name = null, $default = null)
{
return $name === null ? $this->query : ($this->query[$name] ?? $default);
}
public function param(string $name, $default = null)
{
return $this->body[$name] ?? $this->query[$name] ?? $default;
}
public function isGet(): bool { return $this->method === 'GET'; }
public function controller(): string { return $this->controllerName; }
public function action(): string { return $this->actionName; }
public function ip(): string { return '192.0.2.91'; }
}
final class DiagnosisDepartmentController extends DeptController
{
public function __construct(DiagnosisDepartmentRequest $request)
{
$this->request = $request;
$this->adminId = $request->adminInfo['admin_id'];
$this->adminInfo = $request->adminInfo;
}
protected function data($data) { return JsonService::data($data); }
}
$assertions = 0;
function departmentPermissionExpect(bool $ok, string $label): void
{
global $assertions;
if (!$ok) { throw new RuntimeException('FAIL: ' . $label); }
$assertions++;
}
function departmentPermissionInvoke(DiagnosisDepartmentRequest $request, array $permissions,
?array $registered = null): array
{
AdminAuthCache::$permissions = $permissions;
AdminAuthCache::$allUris = $registered ?? [
'dept.dept/all', 'dept.dept/lists', 'dept.dept/leaderDept', 'dept.dept/detail',
'dept.dept/add', 'dept.dept/edit', 'dept.dept/delete', 'dept.jobs/all',
];
AdminAuthCache::$actors = [];
AdminAuthCache::$allUriReads = 0;
DataScopeService::$actors = [];
Dept::$reads = 0;
$GLOBALS['diagnosis_department_request'] = $request;
$controller = new DiagnosisDepartmentController($request);
$request->controllerObject = $controller;
return (new AuthMiddleware())->handle($request, static function () use ($request, $controller): array {
if (strtolower($request->controller() . '/' . $request->action()) === 'dept.dept/all') {
return $controller->all();
}
// A sentinel verifies function-level grants without performing CRUD writes.
return ['code' => 1, 'continued' => true];
});
}
Dept::$rows = [
['id' => 10, 'pid' => 0, 'name' => '共享祖先'],
['id' => 11, 'pid' => 10, 'name' => '未授权同级'],
['id' => 12, 'pid' => 10, 'name' => '授权部门'],
['id' => 20, 'pid' => 0, 'name' => '其他部门'],
];
$scopedTree = [[
'id' => 10, 'pid' => 0, 'name' => '共享祖先', 'level' => 0,
'children' => [['id' => 12, 'pid' => 10, 'name' => '授权部门', 'level' => 1, 'children' => []]],
]];
foreach ([1, '1'] as $scope) {
foreach (['tcm.diagnosis/lists', 'TCM.DIAGNOSIS/LISTS'] as $permission) {
$request = new DiagnosisDepartmentRequest(['apply_data_scope' => $scope]);
$request->controllerName = 'DePt.DePt';
$request->actionName = 'AlL';
$result = departmentPermissionInvoke($request, [$permission]);
departmentPermissionExpect($result === ['code' => 1, 'show' => 0, 'msg' => '', 'data' => $scopedTree],
'exact patient-list grant returns the real scoped tree and original envelope');
departmentPermissionExpect(DataScopeService::$actors === [[91, $request->adminInfo]],
'controller passes the authenticated identity to data scope');
departmentPermissionExpect(Dept::$reads === 1 && AdminAuthCache::$allUriReads === 0,
'read-only alias does not need a department management grant');
}
}
$invalidQueries = [[], ['apply_data_scope' => null]];
foreach ([0, '0', '', '01', '1x', '1.0', ' 1', '1 ', true, false, 1.0, 2, -1,
[], ['1'], new stdClass()] as $invalid) {
$invalidQueries[] = ['apply_data_scope' => $invalid];
}
foreach ($invalidQueries as $query) {
$result = departmentPermissionInvoke(new DiagnosisDepartmentRequest($query), ['tcm.diagnosis/lists']);
departmentPermissionExpect($result['code'] === 0, 'missing or malformed scope cannot acquire the alias ' . json_encode($query));
departmentPermissionExpect(Dept::$reads === 0 && DataScopeService::$actors === [],
'invalid scope is rejected before any department read');
}
foreach ([[], ['tcm.diagnosis/detail'], ['tcm.diagnosis/edit'], ['tcm.diagnosis/listsOther'],
['dept.dept/lists']] as $permissions) {
$result = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1']), $permissions);
departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0,
'unrelated permissions do not acquire the patient-list alias ' . json_encode($permissions));
}
foreach (['POST', 'PUT', 'DELETE', 'HEAD'] as $method) {
$result = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1'], $method),
['tcm.diagnosis/lists']);
departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0, 'alias denies HTTP method ' . $method);
}
foreach (['GET', 'POST'] as $method) {
$result = departmentPermissionInvoke(new DiagnosisDepartmentRequest([], $method, ['apply_data_scope' => '1']),
['tcm.diagnosis/lists']);
departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0,
'request body cannot enable GET-only scope on ' . $method);
}
foreach (['lists', 'leaderDept', 'detail', 'add', 'edit', 'delete'] as $action) {
foreach (['GET', 'POST'] as $method) {
$request = new DiagnosisDepartmentRequest(['apply_data_scope' => '1'], $method);
$request->actionName = $action;
$result = departmentPermissionInvoke($request, ['tcm.diagnosis/lists']);
departmentPermissionExpect($result['code'] === 0 && Dept::$reads === 0,
'patient-list grant never authorizes department action ' . $method . ' ' . $action);
}
$request = new DiagnosisDepartmentRequest([], 'POST');
$request->actionName = $action;
$result = departmentPermissionInvoke($request, ['dept.dept/' . $action]);
departmentPermissionExpect(($result['continued'] ?? false) === true,
'original exact department permission still authorizes ' . $action);
}
$otherController = new DiagnosisDepartmentRequest(['apply_data_scope' => '1']);
$otherController->controllerName = 'dept.Jobs';
$result = departmentPermissionInvoke($otherController, ['tcm.diagnosis/lists']);
departmentPermissionExpect($result['code'] === 0, 'alias is limited to the exact department controller');
$originalGrant = departmentPermissionInvoke(new DiagnosisDepartmentRequest(), ['dept.dept/all']);
departmentPermissionExpect($originalGrant['code'] === 1 && count($originalGrant['data']) === 2
&& count($originalGrant['data'][0]['children']) === 2 && DataScopeService::$actors === [],
'original all-departments permission retains its unscoped response');
$originalScoped = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => 1]), ['dept.dept/all']);
departmentPermissionExpect($originalScoped['data'] === $scopedTree && count(DataScopeService::$actors) === 1,
'original department grant can still request a scoped tree');
$root = departmentPermissionInvoke(new DiagnosisDepartmentRequest([], 'GET', [], true), []);
departmentPermissionExpect($root['code'] === 1 && count($root['data']) === 2 && AdminAuthCache::$actors === [],
'root retains the existing middleware bypass and full tree');
DataScopeService::$allowed = null;
$rootScoped = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => 1], 'GET', [], true), []);
departmentPermissionExpect($rootScoped['data'] === $root['data'] && count(DataScopeService::$actors) === 1,
'root scoped requests still execute the controller data-scope contract');
DataScopeService::$allowed = [];
$empty = departmentPermissionInvoke(new DiagnosisDepartmentRequest(['apply_data_scope' => '1']), ['tcm.diagnosis/lists']);
departmentPermissionExpect($empty === ['code' => 1, 'show' => 0, 'msg' => '', 'data' => []],
'authorized empty scope remains a successful empty tree, never a full-data fallback');
$legacy = departmentPermissionInvoke(new DiagnosisDepartmentRequest(), [], []);
departmentPermissionExpect($legacy['code'] === 1 && count($legacy['data']) === 2 && AdminAuthCache::$allUriReads === 1,
'unmatched requests continue through the existing unregistered-route policy unchanged');
echo "Diagnosis department filter permission: {$assertions} assertions passed (no DB or network)\n";
}