From bcd42a5c399904ec3f9454c6fe1a74bde511ca2c Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Mon, 12 Oct 2026 16:36:47 +0800 Subject: [PATCH] feat(followup-audio): separate uploader-only recording delete permission --- .../http/middleware/AuthMiddleware.php | 3 +- .../adminapi/logic/tcm/FollowupAudioLogic.php | 5 +- .../followupaudio/FollowupAudioAccess.php | 13 ++ .../followupaudio/FollowupAudioStore.php | 5 +- ...10_12_followup_audio_delete_permission.sql | 15 ++ server/tests/FollowupAudioCoreTest.php | 4 +- .../FollowupAudioDeletePermissionTest.php | 220 ++++++++++++++++++ .../FollowupAudioEndpointContractTest.php | 2 + server/tests/FollowupAudioPermissionTest.php | 5 +- .../fixtures/followup_audio/database.php | 4 +- 10 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 server/sql/2026_10_12_followup_audio_delete_permission.sql create mode 100644 server/tests/FollowupAudioDeletePermissionTest.php diff --git a/server/app/adminapi/http/middleware/AuthMiddleware.php b/server/app/adminapi/http/middleware/AuthMiddleware.php index 75f414df2..9a48a2ae5 100755 --- a/server/app/adminapi/http/middleware/AuthMiddleware.php +++ b/server/app/adminapi/http/middleware/AuthMiddleware.php @@ -87,7 +87,8 @@ class AuthMiddleware $action = substr($accessUri, strlen('tcm.followupaudio/')); $uris = $this->formatUrl($adminAuthCache->getAdminUri() ?? []); if (in_array($action, $allowed, true) && in_array('tcm.diagnosis/edit', $uris, true) - && in_array('tcm.diagnosis/followupaudio', $uris, true)) { + && in_array('tcm.diagnosis/followupaudio', $uris, true) + && ($action !== 'delete' || in_array('tcm.followupaudio/delete', $uris, true))) { return $next($request); // The service also checks the current patient scope and daily permissions. } return JsonService::fail('权限不足,无法访问或操作'); diff --git a/server/app/adminapi/logic/tcm/FollowupAudioLogic.php b/server/app/adminapi/logic/tcm/FollowupAudioLogic.php index 5cbba5bb0..c78fe3df6 100644 --- a/server/app/adminapi/logic/tcm/FollowupAudioLogic.php +++ b/server/app/adminapi/logic/tcm/FollowupAudioLogic.php @@ -108,9 +108,8 @@ final class FollowupAudioLogic { $task = Store::task((int) $summary['id']); $info = Access::actor($actor); - $reason = !Store::enabled() ? '回访录音功能尚未启用' - : ((int) $info['root'] !== 1 && (int) $task['actor_id'] !== $actor - ? '仅上传者或超级管理员可删除录音' : Store::deleteBlockReason($task)); + $reason = !Store::enabled() ? '回访录音功能尚未启用' : Access::deleteReason($task, $actor); + if ($reason === '') { $reason = Store::deleteBlockReason($task); } $reextractReason = (int) $info['root'] !== 1 && (int) $task['actor_id'] !== $actor ? '仅上传者或超级管理员可重新提炼' : Reextract::blockReason($task); return $summary + ['can_delete' => $reason === '', 'delete_block_reason' => $reason, diff --git a/server/app/common/service/followupaudio/FollowupAudioAccess.php b/server/app/common/service/followupaudio/FollowupAudioAccess.php index 0f0989d9b..a4cb62668 100644 --- a/server/app/common/service/followupaudio/FollowupAudioAccess.php +++ b/server/app/common/service/followupaudio/FollowupAudioAccess.php @@ -74,6 +74,19 @@ final class FollowupAudioAccess return $task; } + /** One fresh authorization rule for both delete capability and the locked delete transaction. */ + public static function deleteReason(array $task, int $actor): string + { + $info = self::actor($actor); + if (!self::allowed($actor, $info, 'tcm.followupAudio/delete')) { + return '缺少删除回访录音权限'; + } + if ((int) $info['root'] !== 1 && (int) ($task['actor_id'] ?? 0) !== $actor) { + return '仅上传者或超级管理员可删除录音'; + } + return ''; + } + public static function canDaily(int $actor, array $info): bool { $info = self::actor($actor); diff --git a/server/app/common/service/followupaudio/FollowupAudioStore.php b/server/app/common/service/followupaudio/FollowupAudioStore.php index 4132646ff..0cbbe8948 100644 --- a/server/app/common/service/followupaudio/FollowupAudioStore.php +++ b/server/app/common/service/followupaudio/FollowupAudioStore.php @@ -273,9 +273,8 @@ final class FollowupAudioStore $task = self::lockTask($taskId); $info = FollowupAudioAccess::actor($actor); FollowupAudioAccess::task($taskId, $actor, $info); - if ((int) $info['root'] !== 1 && (int) $task['actor_id'] !== $actor) { - throw new DomainException('仅上传者或超级管理员可删除录音'); - } + $permissionReason = FollowupAudioAccess::deleteReason($task, $actor); + if ($permissionReason !== '') { throw new DomainException($permissionReason); } if ((int) $task['version'] !== $version) { throw new DomainException('FOLLOWUP_AUDIO_VERSION_CONFLICT'); } $reason = self::deleteBlockReason($task); if ($reason !== '') { throw new DomainException($reason); } diff --git a/server/sql/2026_10_12_followup_audio_delete_permission.sql b/server/sql/2026_10_12_followup_audio_delete_permission.sql new file mode 100644 index 000000000..13b89fe28 --- /dev/null +++ b/server/sql/2026_10_12_followup_audio_delete_permission.sql @@ -0,0 +1,15 @@ +-- 删除回访录音:按钮权限,与各入口的“回访录音”同层级。 +-- 不复制任何角色授权;非超级管理员仍限本人上传且未采用的可删除任务。 +INSERT INTO zyt_system_menu ( + pid, type, name, icon, sort, perms, paths, component, + selected, params, is_cache, is_show, is_disable, create_time, update_time +) +SELECT r.pid, 'A', '删除回访录音', '', GREATEST(CAST(MAX(r.sort) AS SIGNED) - 1, 0), + 'tcm.followupAudio/delete', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +FROM zyt_system_menu AS r +WHERE r.perms = 'tcm.diagnosis/followupAudio' AND r.type = 'A' + AND NOT EXISTS ( + SELECT 1 FROM zyt_system_menu AS d + WHERE d.pid = r.pid AND d.perms = 'tcm.followupAudio/delete' + ) +GROUP BY r.pid; diff --git a/server/tests/FollowupAudioCoreTest.php b/server/tests/FollowupAudioCoreTest.php index 2ddbd59bd..37c073de5 100644 --- a/server/tests/FollowupAudioCoreTest.php +++ b/server/tests/FollowupAudioCoreTest.php @@ -99,9 +99,9 @@ try { $pdo->exec('CREATE TABLE zyt_admin_dept(admin_id INT,dept_id INT)'); $pdo->exec('CREATE TABLE zyt_admin_jobs(admin_id INT,jobs_id INT)'); $pdo->exec('CREATE TABLE zyt_system_menu(id INT PRIMARY KEY,perms VARCHAR(100),is_disable INT)'); - $pdo->exec("INSERT INTO zyt_system_menu VALUES(1,'tcm.diagnosis/edit',0),(2,'tcm.diagnosis/dailyRecord',0),(3,'firstvisit.wecomPromotion/overview',0),(4,'tcm.diagnosis/followupAudio',0)"); + $pdo->exec("INSERT INTO zyt_system_menu VALUES(1,'tcm.diagnosis/edit',0),(2,'tcm.diagnosis/dailyRecord',0),(3,'firstvisit.wecomPromotion/overview',0),(4,'tcm.diagnosis/followupAudio',0),(5,'tcm.followupAudio/delete',0)"); $pdo->exec('CREATE TABLE zyt_system_role_menu(role_id INT,menu_id INT)'); - $pdo->exec('INSERT INTO zyt_system_role_menu VALUES(2,1),(2,2),(2,3),(2,4),(5,1),(5,3),(5,4)'); + $pdo->exec('INSERT INTO zyt_system_role_menu VALUES(2,1),(2,2),(2,3),(2,4),(2,5),(5,1),(5,3),(5,4),(5,5)'); $pdo->exec('CREATE TABLE zyt_doctor_appointment(id INT PRIMARY KEY,patient_id INT,doctor_id INT,status INT)'); $pdo->exec('CREATE TABLE zyt_dict_data(id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50),value VARCHAR(50),type_value VARCHAR(50),status INT,sort INT)'); $pdo->exec("INSERT INTO zyt_dict_data(name,value,type_value,status,sort) VALUES('Synthetic Enabled','dry','appetite',1,1),('Synthetic Disabled','bad','appetite',0,2)"); diff --git a/server/tests/FollowupAudioDeletePermissionTest.php b/server/tests/FollowupAudioDeletePermissionTest.php new file mode 100644 index 000000000..224da9dee --- /dev/null +++ b/server/tests/FollowupAudioDeletePermissionTest.php @@ -0,0 +1,220 @@ +withServer(['REQUEST_METHOD' => $method]); + $request = $method === 'GET' ? $request->withGet($body) : $request->withPost($body); + $controller = (new ReflectionClass(FollowupAudioController::class))->newInstanceWithoutConstructor(); + foreach (['request' => $request, 'adminId' => $actor, 'adminInfo' => ['root' => 1]] as $name => $value) { + $property = new ReflectionProperty($controller, $name); + $property->setAccessible(true); $property->setValue($controller, $value); + } + return $controller->$action()->getData(); +} +$f = followupAudioTestDatabase(['preview_only' => false]); +$checks = 0; $failures = []; +$expect = static function (bool $ok, string $label) use (&$checks, &$failures): void { + $checks++; + if (!$ok) { $failures[] = $label; fwrite(STDERR, 'FAIL: ' . $label . PHP_EOL); } +}; +$success = static function (array $response): array { + if ($response['code'] !== 1) { throw new RuntimeException(json_encode($response, JSON_UNESCAPED_UNICODE)); } + return $response['data']; +}; +$body = static fn (array $task): array => ['id' => (int) $task['id'], 'version' => (int) $task['version']]; +$make = static function (int $owner = 2) use ($f): array { + static $serial = 0; $serial++; + $pcm = str_repeat(pack('v', 2000 + $serial), 16000); + $path = $f['private'] . '/permission-' . $serial . '.wav'; + file_put_contents($path, 'RIFF' . pack('V', 36 + strlen($pcm)) . 'WAVEfmt ' + . pack('VvvVVvv', 16, 1, 1, 16000, 32000, 2, 16) . 'data' . pack('V', strlen($pcm)) . $pcm); + chmod($path, 0600); + $upload = followupAudioTestUpload($f, $path, 'synthetic.wav'); + $created = Store::create($upload, '2026-09-29 10:00:00', 'qwen', 1, $f['actor']); + $claim = Store::claim(); + if ((int) $claim['id'] !== (int) $created['id'] || !Store::complete((int) $created['id'], $claim['lease_token'], [ + 'summary' => 'Synthetic', 'transcript' => '今天合成症状', 'uncertainties' => [], + 'items' => [['kind' => 'diagnosis', 'values' => ['symptoms' => '合成症状'], 'record_date' => '2026-09-29', + 'date_text' => '今天', 'evidence' => [['text' => '今天合成症状']]]], + ])) { throw new RuntimeException('Synthetic completion failed'); } + // Explicit synthetic ownership, with patient/diagnosis unchanged; never call an upstream model. + Db::name('followup_audio_task')->where('id', $created['id'])->update(['actor_id' => $owner]); + Db::name('followup_audio_upload')->where('id', $upload['id'])->update(['actor_id' => $owner]); + return Store::task((int) $created['id']); +}; +$grant = static function (bool $enabled): void { + Db::name('system_role_menu')->where('role_id', 2)->where('menu_id', 5)->delete(); + if ($enabled) { Db::name('system_role_menu')->insert(['role_id' => 2, 'menu_id' => 5]); } +}; +$assertDenied = static function (array $task, string $label) use ($expect, $success, $body): void { + $detail = $success(followupDeletePermissionRequest('detail', ['id' => $task['id']])); + $expect($detail['can_delete'] === false && $detail['delete_block_reason'] !== '', $label . ': detail button disabled'); + $list = $success(followupDeletePermissionRequest('lists', ['diagnosis_id' => 1])); + $rows = array_values(array_filter($list['items'], static fn (array $row): bool => (int) $row['id'] === (int) $task['id'])); + $expect(count($rows) === 1 && $rows[0]['can_delete'] === false, $label . ': list button disabled'); + $response = followupDeletePermissionRequest('delete', $body($task), 2, 'POST'); + $expect($response['code'] === 0 && $response['msg'] === $detail['delete_block_reason'], $label . ': actual controller rejects with same reason'); + $row = Db::name('followup_audio_task')->where('id', $task['id'])->find(); + $expect($row['status'] === $task['status'] && (int) $row['active_slot'] === 0 + && (int) $row['version'] === (int) $task['version'], $label . ': rejected task unchanged'); +}; +try { + if (!Db::name('system_menu')->where('id', 5)->find()) { + Db::name('system_menu')->insert(['id' => 5, 'perms' => 'tcm.followupAudio/delete', 'is_disable' => 0]); + } + $grant(false); + $assertDenied($make(), 'own upload without delete permission'); + $grant(true); + $own = $make(); + $detail = $success(followupDeletePermissionRequest('detail', ['id' => $own['id']])); + $expect($detail['can_delete'] === true && $detail['delete_block_reason'] === '', 'grant enables own completed task'); + $deleted = $success(followupDeletePermissionRequest('delete', $body($own), 2, 'POST')); + $expect($deleted['status'] === 'deleted', 'grant allows own actual delete'); + $assertDenied($make(1), 'delete grant does not allow another uploader on own patient'); + + // Load the grant in a repeatable-read snapshot, then revoke from a separate connection. + $revoked = $make(); + $expect($success(followupDeletePermissionRequest('detail', ['id' => $revoked['id']]))['can_delete'] === true, 'grant visible before revocation'); + Db::startTrans(); Db::name('system_role_menu')->select(); + $f['pdo']->exec('DELETE FROM zyt_system_role_menu WHERE role_id=2 AND menu_id=5'); + $fresh = $success(followupDeletePermissionRequest('detail', ['id' => $revoked['id']])); + $expect($fresh['can_delete'] === false, 'revocation beats repeatable-read snapshot and stale client'); + Db::rollback(); + FollowupDeletePermissionCacheDouble::$uris = ['tcm.diagnosis/edit', 'tcm.diagnosis/followupAudio', 'tcm.followupAudio/delete']; + $assertDenied($revoked, 'revoked grant despite positive cached URI'); + $grant(true); + $disabled = $make(); + Db::startTrans(); Db::name('system_menu')->select(); + $f['pdo']->exec('UPDATE zyt_system_menu SET is_disable=1 WHERE id=5'); + $fresh = $success(followupDeletePermissionRequest('detail', ['id' => $disabled['id']])); + $expect($fresh['can_delete'] === false, 'disabled menu beats repeatable-read snapshot'); + Db::rollback(); + $assertDenied($disabled, 'disabled delete menu despite role grant'); + Db::name('system_menu')->where('id', 5)->update(['is_disable' => 0]); + if ((int) Db::name('followup_audio_task')->where('id', $disabled['id'])->value('active_slot') === 0) { + $expect($success(followupDeletePermissionRequest('detail', ['id' => $disabled['id']]))['can_delete'] === true, 'reenabling menu restores current permission'); + $success(followupDeletePermissionRequest('delete', $body($disabled), 2, 'POST')); + } + + foreach (['applied', 'queued', 'running', 'needs_reconciliation'] as $status) { + $task = $make(); + Db::name('followup_audio_task')->where('id', $task['id'])->update(['status' => $status]); + $task['status'] = $status; + $assertDenied($task, 'grant does not bypass status ' . $status); + Db::name('followup_audio_task')->where('id', $task['id'])->update(['status' => 'failed']); + } + $root = $make(2); $grant(false); + $rootDetail = $success(followupDeletePermissionRequest('detail', ['id' => $root['id']], 1)); + $expect($rootDetail['can_delete'] === true, 'current root retains existing exception without role grant'); + $expect($success(followupDeletePermissionRequest('delete', $body($root), 1, 'POST'))['status'] === 'deleted', 'root actual delete retained'); + $blockedActor = $make(); $grant(true); + Db::name('admin')->where('id', 2)->update(['disable' => 1]); + $result = followupDeletePermissionRequest('delete', $body($blockedActor), 2, 'POST'); + $expect($result['code'] === 0 && $result['msg'] === '账号已停用或无权访问', 'disabled actor rejected despite injected root and cached grants'); + Db::name('admin')->where('id', 2)->update(['disable' => 0]); + + $request = (new app\Request())->withServer(['REMOTE_ADDR' => '127.0.0.1', 'REQUEST_METHOD' => 'GET']); + Container::getInstance()->instance('request', $request); + Container::getInstance()->instance('think\\Request', $request); + $middleware = new AuthMiddleware(); + $otherActions = ['capabilities', 'uploadSession', 'uploadChunk', 'uploadComplete', 'create', 'lists', + 'detail', 'saveDraft', 'apply', 'retry', 'audio', 'reextract']; + foreach (array_merge($otherActions, ['delete']) as $action) { + for ($mask = 0; $mask < 8; $mask++) { + FollowupDeletePermissionCacheDouble::$uris = []; + foreach (['tcm.diagnosis/edit', 'tcm.diagnosis/followupAudio', 'tcm.followupAudio/delete'] as $bit => $uri) { + if (($mask & (1 << $bit)) !== 0) { FollowupDeletePermissionCacheDouble::$uris[] = $uri; } + } + $route = new class($action, $request->ip()) { + public array $adminInfo; + public object $controllerObject; + public function __construct(private string $verb, string $ip) { + $this->adminInfo = ['admin_id' => 2, 'root' => 0, 'login_ip' => $ip, 'work_wechat_userid' => 'synthetic']; + $this->controllerObject = new class { public function isNotNeedLogin(): bool { return false; } }; + } + public function controller(): string { return 'tcm.FollowupAudio'; } + public function action(): string { return $this->verb; } + public function param($name, $default = null) { return $default; } + }; + $actual = $middleware->handle($route, static fn () => 'ALLOWED'); + $allowed = ($mask & 3) === 3 && ($action !== 'delete' || ($mask & 4) !== 0); + $expect(($actual === 'ALLOWED') === $allowed, 'middleware ' . $action . ' permission bitmask ' . $mask); + } + } + + // Real menu migration: mirror every existing recording parent, remain idempotent, never grant roles. + $pdo = $f['pdo']; + $pdo->exec("ALTER TABLE zyt_system_menu MODIFY id INT NOT NULL AUTO_INCREMENT, + ADD pid INT NOT NULL DEFAULT 0, ADD type CHAR(1) NOT NULL DEFAULT 'A', ADD name VARCHAR(100) NOT NULL DEFAULT '', + ADD icon VARCHAR(100) NOT NULL DEFAULT '', ADD sort INT UNSIGNED NOT NULL DEFAULT 0, + ADD paths VARCHAR(200) NOT NULL DEFAULT '', ADD component VARCHAR(200) NOT NULL DEFAULT '', + ADD selected VARCHAR(200) NOT NULL DEFAULT '', ADD params VARCHAR(200) NOT NULL DEFAULT '', + ADD is_cache INT NOT NULL DEFAULT 0, ADD is_show INT NOT NULL DEFAULT 1, + ADD create_time INT NOT NULL DEFAULT 0, ADD update_time INT NOT NULL DEFAULT 0"); + $pdo->exec("UPDATE zyt_system_menu SET pid=182,name='回访录音',sort=58 WHERE id=4"); + $pdo->exec("UPDATE zyt_system_menu SET pid=182,name='删除回访录音',sort=57 WHERE id=5"); + $pdo->exec("INSERT INTO zyt_system_menu(id,pid,type,name,sort,perms,is_disable) VALUES + (191,0,'C','Synthetic second page',0,'firstvisit.myPatient/lists',0), + (192,191,'A','回访录音',0,'tcm.diagnosis/followupAudio',0)"); + $grants = $pdo->query('SELECT role_id,menu_id FROM zyt_system_role_menu ORDER BY role_id,menu_id')->fetchAll(PDO::FETCH_ASSOC); + $migrationFile = getenv('FOLLOWUP_AUDIO_TEST_DELETE_MENU_MIGRATION') ?: dirname(__DIR__) . '/sql/2026_10_12_followup_audio_delete_permission.sql'; + if (!is_file($migrationFile)) { throw new RuntimeException('Delete permission migration missing'); } + $migration = preg_replace('/^\s*(?:--|#).*$/m', '', file_get_contents($migrationFile)); + $first = null; + for ($run = 0; $run < 2; $run++) { + foreach (explode(';', $migration) as $sql) { + if (trim($sql) !== '') { $statement = $pdo->query($sql); $statement->closeCursor(); } + } + $menus = $pdo->query("SELECT pid,type,name,sort,perms,is_disable,is_show FROM zyt_system_menu + WHERE LOWER(perms)='tcm.followupaudio/delete' ORDER BY pid,id")->fetchAll(PDO::FETCH_ASSOC); + $expect(count($menus) === 2 && array_map('intval', array_column($menus, 'pid')) === [182, 191], 'migration mirrors recording menu parent set without duplicates'); + foreach ($menus as $menu) { + $expect($menu['type'] === 'A' && $menu['name'] === '删除回访录音' && (int) $menu['is_disable'] === 0 + && (int) $menu['is_show'] === 1, 'delete node assignable and correctly labeled'); + } + if ($first !== null) { $expect($menus === $first, 'second migration is idempotent'); } + $first = $menus; + $expect($pdo->query('SELECT role_id,menu_id FROM zyt_system_role_menu ORDER BY role_id,menu_id')->fetchAll(PDO::FETCH_ASSOC) === $grants, + 'migration changes no role grants'); + } +} finally { + try { Db::rollback(); } catch (Throwable $e) {} + followupAudioTestDatabaseCleanup($f); +} +echo 'FOLLOWUP_AUDIO_DELETE_PERMISSION assertions=' . $checks . ' failures=' . count($failures) + . ' ' . ($failures === [] ? 'PASS' : 'FAIL') + . ' mysql23319=1 controller=1 middleware_actions=13 nondelete_actions=12 fresh_rbac=1 owner_scope=1 state_guards=1 migration_idempotent=1 role_grants_unchanged=1 upstream_calls=0' . PHP_EOL; +exit($failures === [] ? 0 : 1); diff --git a/server/tests/FollowupAudioEndpointContractTest.php b/server/tests/FollowupAudioEndpointContractTest.php index 98656ab57..4341803b2 100644 --- a/server/tests/FollowupAudioEndpointContractTest.php +++ b/server/tests/FollowupAudioEndpointContractTest.php @@ -23,6 +23,8 @@ expectEndpoint(str_contains($middleware, "in_array('tcm.diagnosis/edit', \$uris, expectEndpoint(str_contains($access, "Db::name('admin')->where('id', \$actor)->whereNull('delete_time')->where('disable', 0)->lock(true)"), 'active actor refresh'); expectEndpoint(str_contains($access, 'MyPatientLogic::applyScope($query, $actor, $info)') && str_contains($access, '$query->lock(true)->find()'), 'row management guard'); expectEndpoint(str_contains($access, 'tcm.diagnosis/dailyRecord'), 'daily permission guard'); +expectEndpoint(str_contains($middleware, "in_array('tcm.followupaudio/delete', \$uris, true)"), 'delete has an independent route permission'); +expectEndpoint(str_contains($access, "self::allowed(\$actor, \$info, 'tcm.followupAudio/delete')"), 'delete permission also checked against current DB'); expectEndpoint(str_contains($middleware, "in_array('tcm.diagnosis/followupaudio', \$uris, true)"), 'independent audio route permission'); expectEndpoint(str_contains($access, "self::allowed(\$actor, \$info, 'tcm.diagnosis/followupAudio')"), 'fresh independent audio permission at every service entry'); expectEndpoint(str_contains($access, "\$task['patient_id'] !== (int) (\$diagnosis['patient_id']"), 'historical patient rebind guard'); diff --git a/server/tests/FollowupAudioPermissionTest.php b/server/tests/FollowupAudioPermissionTest.php index 9885734e3..1575d1386 100644 --- a/server/tests/FollowupAudioPermissionTest.php +++ b/server/tests/FollowupAudioPermissionTest.php @@ -79,7 +79,8 @@ try { 'detail', 'saveDraft', 'apply', 'retry', 'audio', 'delete', 'reextract']; foreach ($actions as $action) { foreach ([[], ['tcm.diagnosis/edit'], ['tcm.diagnosis/followupAudio'], - ['tcm.diagnosis/edit', 'tcm.diagnosis/followupAudio']] as $uris) { + ['tcm.diagnosis/edit', 'tcm.diagnosis/followupAudio'], + ['tcm.diagnosis/edit', 'tcm.diagnosis/followupAudio', 'tcm.followupAudio/delete']] as $uris) { FollowupPermissionAuthCacheDouble::$uris = $uris; $route = new class($action, $request->ip()) { public array $adminInfo; @@ -93,7 +94,7 @@ try { public function param($name, $default = null) { return $default; } }; $result = $middleware->handle($route, static fn () => 'ALLOWED'); - $expected = count($uris) === 2; + $expected = count($uris) >= 2 && ($action !== 'delete' || in_array('tcm.followupAudio/delete', $uris, true)); $expect(($result === 'ALLOWED') === $expected, 'middleware ' . $action . ' permission set ' . json_encode($uris)); if (!$expected) { $expect($result->getData()['code'] === 0, 'middleware failure envelope'); } } diff --git a/server/tests/fixtures/followup_audio/database.php b/server/tests/fixtures/followup_audio/database.php index 175eb785e..acea28e1a 100644 --- a/server/tests/fixtures/followup_audio/database.php +++ b/server/tests/fixtures/followup_audio/database.php @@ -47,9 +47,9 @@ function followupAudioTestDatabase(array $settings = []): array $pdo->exec('CREATE TABLE zyt_admin_dept(admin_id INT,dept_id INT)'); $pdo->exec('CREATE TABLE zyt_admin_jobs(admin_id INT,jobs_id INT)'); $pdo->exec('CREATE TABLE zyt_system_menu(id INT PRIMARY KEY,perms VARCHAR(100),is_disable INT)'); - $pdo->exec("INSERT INTO zyt_system_menu VALUES(1,'tcm.diagnosis/edit',0),(2,'tcm.diagnosis/dailyRecord',0),(3,'firstvisit.wecomPromotion/overview',0),(4,'tcm.diagnosis/followupAudio',0)"); + $pdo->exec("INSERT INTO zyt_system_menu VALUES(1,'tcm.diagnosis/edit',0),(2,'tcm.diagnosis/dailyRecord',0),(3,'firstvisit.wecomPromotion/overview',0),(4,'tcm.diagnosis/followupAudio',0),(5,'tcm.followupAudio/delete',0)"); $pdo->exec('CREATE TABLE zyt_system_role_menu(role_id INT,menu_id INT)'); - $pdo->exec('INSERT INTO zyt_system_role_menu VALUES(2,1),(2,2),(2,3),(2,4),(5,1),(5,3),(5,4)'); + $pdo->exec('INSERT INTO zyt_system_role_menu VALUES(2,1),(2,2),(2,3),(2,4),(2,5),(5,1),(5,3),(5,4),(5,5)'); $pdo->exec('CREATE TABLE zyt_doctor_appointment(id INT PRIMARY KEY,patient_id INT,doctor_id INT,status INT)'); $pdo->exec('CREATE TABLE zyt_dict_data(id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50),value VARCHAR(50),type_value VARCHAR(50),status INT,sort INT)'); $pdo->exec("INSERT INTO zyt_dict_data(name,value,type_value,status,sort) VALUES('Synthetic Enabled','dry','appetite',1,1),('Synthetic Disabled','bad','appetite',0,2)");