212 lines
11 KiB
PHP
212 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// All identities and records are synthetic. The real controller and middleware
|
|
// execute below, while their IO dependencies stay entirely 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 int $allUriReads = 0;
|
|
public static array $actors = [];
|
|
public function __construct(int $adminId) { self::$actors[] = $adminId; }
|
|
public function getAdminUri(): array { return self::$permissions; }
|
|
public function getAllUri(): array { self::$allUriReads++; return []; }
|
|
}
|
|
}
|
|
|
|
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\adminapi\logic\tcm {
|
|
final class DiagnosisLogic
|
|
{
|
|
public static bool $visible = true;
|
|
public static string $error = '';
|
|
public static array $events = [];
|
|
public static array $records = [];
|
|
public static function canViewReadonlyDiagnosis(int $id, int $adminId, array $adminInfo): bool
|
|
{
|
|
self::$events[] = ['authorize', $id, $adminId, $adminInfo];
|
|
return self::$visible;
|
|
}
|
|
public static function getCallRecords(array $params): array
|
|
{
|
|
if (!self::$visible) { throw new \RuntimeException('Forbidden records reached the read dependency'); }
|
|
self::$events[] = ['read', $params];
|
|
return self::$records;
|
|
}
|
|
public static function getError(): string { return self::$error; }
|
|
}
|
|
}
|
|
|
|
namespace app\adminapi\http\middleware {
|
|
function request(): object { return $GLOBALS['consultation_transcript_request']; }
|
|
function env(...$arguments): never { throw new \RuntimeException('This test must not access environment configuration'); }
|
|
}
|
|
|
|
namespace {
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\controller\tcm\DiagnosisController;
|
|
use app\adminapi\http\middleware\AuthMiddleware;
|
|
use app\adminapi\logic\tcm\DiagnosisLogic;
|
|
use app\common\cache\AdminAuthCache;
|
|
use app\common\service\JsonService;
|
|
|
|
final class ConsultationTranscriptRequest
|
|
{
|
|
public array $adminInfo;
|
|
public object $controllerObject;
|
|
public string $actionName = 'getCallRecords';
|
|
public string $controllerName = 'tcm.Diagnosis';
|
|
public function __construct(public array $query = [], bool $root = false)
|
|
{
|
|
$this->adminInfo = ['admin_id' => 91, 'root' => $root ? 1 : 0,
|
|
'login_ip' => '192.0.2.91', 'work_wechat_userid' => 'synthetic-doctor-91'];
|
|
$this->controllerObject = new class {
|
|
public function isNotNeedLogin(): bool { return false; }
|
|
};
|
|
}
|
|
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 $default; }
|
|
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 ConsultationTranscriptController extends DiagnosisController
|
|
{
|
|
public function __construct(ConsultationTranscriptRequest $request)
|
|
{
|
|
$this->request = $request;
|
|
$this->adminId = $request->adminInfo['admin_id'];
|
|
$this->adminInfo = $request->adminInfo;
|
|
}
|
|
protected function data($data) { return JsonService::data($data); }
|
|
protected function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1)
|
|
{
|
|
return JsonService::fail($msg, $data, $code, $show);
|
|
}
|
|
}
|
|
|
|
$assertions = 0;
|
|
function transcriptAuthorizationExpect(bool $ok, string $label): void
|
|
{
|
|
global $assertions;
|
|
if (!$ok) { throw new RuntimeException('FAIL: ' . $label); }
|
|
$assertions++;
|
|
}
|
|
|
|
function transcriptAuthorizationInvoke(array $query, array $permissions, bool $root = false): array
|
|
{
|
|
AdminAuthCache::$permissions = $permissions;
|
|
AdminAuthCache::$allUriReads = 0;
|
|
AdminAuthCache::$actors = [];
|
|
DiagnosisLogic::$events = [];
|
|
$request = new ConsultationTranscriptRequest($query, $root);
|
|
$GLOBALS['consultation_transcript_request'] = $request;
|
|
$controller = new ConsultationTranscriptController($request);
|
|
$request->controllerObject = $controller;
|
|
return (new AuthMiddleware())->handle($request, static fn () => $controller->getCallRecords());
|
|
}
|
|
|
|
DiagnosisLogic::$records = [
|
|
['id' => 1102, 'diagnosis_id' => 701, 'caller_id' => 202, 'caller_type' => 'doctor',
|
|
'transcription_session_id' => 'synthetic-session-1102', 'transcription_status' => 'partial',
|
|
'transcription_segment_count' => 2, 'transcript_text' => "医生:合成问题 <test>\n患者:合成回答 & 保留原文。"],
|
|
['id' => 1101, 'diagnosis_id' => 701, 'caller_id' => 201, 'caller_type' => 'doctor',
|
|
'transcription_session_id' => 'synthetic-session-1101', 'transcription_status' => 'completed',
|
|
'transcription_segment_count' => 1, 'transcript_text' => '未知说话人:另一通合成面诊。'],
|
|
];
|
|
|
|
foreach ([[], ['tcm.diagnosis/edit'], ['tcm.diagnosis/detail'], ['tcm.diagnosis/video-call'],
|
|
['tcm.diagnosis/huifangOther'], ['tcm.diagnosis/getCallRecordsOther']] as $permissions) {
|
|
$result = transcriptAuthorizationInvoke(['diagnosis_id' => 701], $permissions);
|
|
transcriptAuthorizationExpect($result['code'] === 0, 'unregistered read rejects unrelated grants ' . json_encode($permissions));
|
|
transcriptAuthorizationExpect(DiagnosisLogic::$events === [], 'function permission denial precedes row and transcript reads');
|
|
transcriptAuthorizationExpect(AdminAuthCache::$allUriReads === 0, 'transcript route never consults legacy unregistered-route bypass');
|
|
}
|
|
|
|
foreach ([['tcm.diagnosis/huifang'], ['tcm.diagnosis/getCallRecords'],
|
|
['TCM.DIAGNOSIS/GETCALLRECORDS'], ['tcm.diagnosis/huifang', 'tcm.diagnosis/getCallRecords']] as $permissions) {
|
|
DiagnosisLogic::$visible = true;
|
|
$result = transcriptAuthorizationInvoke(['diagnosis_id' => '701', 'call_record_id' => 9999,
|
|
'appointment_id' => 9999, 'caller_id' => 91], $permissions);
|
|
transcriptAuthorizationExpect($result['code'] === 1, 'either exact read grant allows the controller without edit ' . json_encode($permissions));
|
|
transcriptAuthorizationExpect($result['data'] === DiagnosisLogic::$records, 'full transcript and every call identity remain unchanged');
|
|
transcriptAuthorizationExpect(DiagnosisLogic::$events === [
|
|
['authorize', 701, 91, $GLOBALS['consultation_transcript_request']->adminInfo],
|
|
['read', ['diagnosis_id' => 701]],
|
|
], 'real controller authorizes trusted actor before a diagnosis-only read, without a caller filter');
|
|
transcriptAuthorizationExpect(AdminAuthCache::$allUriReads === 0, 'grant works even with no registered menu routes');
|
|
}
|
|
|
|
$root = transcriptAuthorizationInvoke(['diagnosis_id' => 701], [], true);
|
|
transcriptAuthorizationExpect($root['data'] === DiagnosisLogic::$records, 'root retains the existing middleware grant bypass');
|
|
transcriptAuthorizationExpect(AdminAuthCache::$actors === [], 'root does not need a permission cache or menu grant');
|
|
transcriptAuthorizationExpect(DiagnosisLogic::$events[0][0] === 'authorize', 'root still passes the controller through diagnosis row authorization');
|
|
|
|
DiagnosisLogic::$visible = false;
|
|
foreach (['诊单不存在或无权访问', ''] as $error) {
|
|
DiagnosisLogic::$error = $error;
|
|
$denied = transcriptAuthorizationInvoke(['diagnosis_id' => 702], ['tcm.diagnosis/huifang']);
|
|
transcriptAuthorizationExpect($denied['code'] === 0 && $denied['msg'] === '诊单不存在或无权访问', 'forbidden or missing diagnosis shares the same non-enumerating error');
|
|
transcriptAuthorizationExpect(count(DiagnosisLogic::$events) === 1
|
|
&& DiagnosisLogic::$events[0][0] === 'authorize'
|
|
&& DiagnosisLogic::$events[0][1] === 702, 'row denial happens before transcript records can be read');
|
|
}
|
|
|
|
DiagnosisLogic::$visible = true;
|
|
DiagnosisLogic::$error = '';
|
|
foreach ([[], ['diagnosis_id' => null], ['diagnosis_id' => 0], ['diagnosis_id' => -1],
|
|
['diagnosis_id' => '0'], ['diagnosis_id' => ''], ['diagnosis_id' => 'invalid'],
|
|
['diagnosis_id' => '701suffix'], ['diagnosis_id' => '701.5'], ['diagnosis_id' => 701.5],
|
|
['diagnosis_id' => true], ['diagnosis_id' => false], ['diagnosis_id' => []],
|
|
['diagnosis_id' => ['701']], ['diagnosis_id' => new stdClass()]] as $query) {
|
|
$invalid = transcriptAuthorizationInvoke($query, ['tcm.diagnosis/huifang']);
|
|
transcriptAuthorizationExpect($invalid['code'] === 0, 'missing or nonpositive diagnosis id is rejected ' . json_encode($query));
|
|
transcriptAuthorizationExpect(DiagnosisLogic::$events === [], 'invalid id never reaches row authorization or a record read');
|
|
}
|
|
|
|
$request = new ConsultationTranscriptRequest(['diagnosis_id' => 701]);
|
|
$request->controllerName = 'TcM.DiAgNoSiS';
|
|
$request->actionName = 'GeTcAlLrEcOrDs';
|
|
$GLOBALS['consultation_transcript_request'] = $request;
|
|
AdminAuthCache::$permissions = [];
|
|
$continued = false;
|
|
$caseDenied = (new AuthMiddleware())->handle($request, static function () use (&$continued): array {
|
|
$continued = true;
|
|
return ['code' => 1];
|
|
});
|
|
transcriptAuthorizationExpect($caseDenied['code'] === 0 && !$continued, 'route case variations cannot evade the exact protected action');
|
|
|
|
DiagnosisLogic::$records = [];
|
|
$empty = transcriptAuthorizationInvoke(['diagnosis_id' => 701], ['tcm.diagnosis/getCallRecords']);
|
|
transcriptAuthorizationExpect($empty['code'] === 1 && $empty['data'] === [], 'authorized empty history retains the original array response');
|
|
|
|
echo "Consultation transcript authorization: {$assertions} assertions passed (no DB or network)\n";
|
|
}
|