$id, 'diagnosis_id' => 91, 'patient_id' => 101]; } } } namespace { require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php'; // Explicit disposable local database only; no application initialization or .env loading. $port = (int) getenv('FOLLOWUP_AUDIO_TEST_MYSQL_PORT'); if ($port <= 0 || getenv('FOLLOWUP_AUDIO_TEST_ALLOW_DISPOSABLE') !== '1') { throw new \RuntimeException('Explicit local disposable MySQL port and FOLLOWUP_AUDIO_TEST_ALLOW_DISPOSABLE=1 required'); } $password = (string) getenv('FOLLOWUP_AUDIO_TEST_MYSQL_PASSWORD'); $database = 'fa_access_' . bin2hex(random_bytes(6)); $control = new \PDO("mysql:host=127.0.0.1;port={$port};charset=utf8mb4", 'root', $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); $control->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4"); new \think\App(); set_exception_handler(static function (\Throwable $e): void { fwrite(STDERR, $e->getMessage() . PHP_EOL . $e->getTraceAsString() . PHP_EOL); exit(1); }); $manager = new \think\DbManager(); $manager->setConfig(['default' => 'mysql', 'connections' => ['mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'hostport' => $port, 'database' => $database, 'username' => 'root', 'password' => $password, 'charset' => 'utf8mb4', 'prefix' => 'faa_', 'debug' => false, ]]]); \think\Container::getInstance()->instance('think\DbManager', $manager); \think\Container::getInstance()->instance('config', new \think\Config()); $db = \think\facade\Db::class; $tables = [ 'admin' => 'id BIGINT PRIMARY KEY, name VARCHAR(30), root INT, disable INT, delete_time BIGINT NULL', 'admin_role' => 'admin_id BIGINT, role_id BIGINT, PRIMARY KEY(admin_id,role_id)', 'admin_dept' => 'admin_id BIGINT, dept_id BIGINT, PRIMARY KEY(admin_id,dept_id)', 'system_role_menu' => 'role_id BIGINT, menu_id BIGINT, PRIMARY KEY(role_id,menu_id)', 'system_menu' => 'id BIGINT PRIMARY KEY, perms VARCHAR(100), is_disable INT', 'tcm_diagnosis' => 'id BIGINT PRIMARY KEY, patient_id BIGINT, assistant_id BIGINT, status INT, delete_time BIGINT NULL', ]; foreach ($tables as $name => $fields) { $db::execute("DROP TABLE IF EXISTS faa_{$name}"); $db::execute("CREATE TABLE faa_{$name} ({$fields}) ENGINE=InnoDB"); } $db::name('admin')->insertAll([ ['id' => 7, 'name' => 'root fixture', 'root' => 1, 'disable' => 0], ['id' => 8, 'name' => 'assistant fixture', 'root' => 0, 'disable' => 0], ['id' => 9, 'name' => 'other fixture', 'root' => 0, 'disable' => 0], ]); $db::name('admin_role')->insert(['admin_id' => 8, 'role_id' => 2]); $db::name('system_menu')->insertAll([ ['id' => 1, 'perms' => 'tcm.diagnosis/edit', 'is_disable' => 0], ['id' => 2, 'perms' => 'tcm.diagnosis/dailyRecord', 'is_disable' => 0], ]); $db::name('system_role_menu')->insert(['role_id' => 2, 'menu_id' => 1]); $db::name('tcm_diagnosis')->insertAll([ ['id' => 91, 'patient_id' => 101, 'assistant_id' => 8, 'status' => 1], ['id' => 92, 'patient_id' => 102, 'assistant_id' => 9, 'status' => 1], ]); $a = \app\common\service\followupaudio\FollowupAudioAccess::class; $checks = 0; $ok = function (bool $yes, string $label) use (&$checks): void { if (!$yes) { throw new \RuntimeException($label); } $checks++; }; $deny = function (callable $f, string $label) use ($ok): void { try { $f(); } catch (\DomainException $e) { $ok(true, $label); return; } $ok(false, $label); }; $other = new \PDO("mysql:host=127.0.0.1;port={$port};dbname={$database};charset=utf8mb4", 'root', $password, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); try { $ok((int) $a::diagnosis(91, 8, ['root' => 1])['id'] === 91, 'own patient'); $deny(fn () => $a::diagnosis(92, 8, ['root' => 1]), 'forged cached root must fail'); $ok(!$a::canDaily(8, ['root' => 1]), 'missing daily permission'); $deny(fn () => $a::diagnosis(91, 8, [], true), 'daily write denied'); $db::name('system_role_menu')->insert(['role_id' => 2, 'menu_id' => 2]); $ok($a::canDaily(8, []), 'fresh daily grant'); // A consistent read exists before another connection commits permission revocation. $db::startTrans(); $db::name('system_role_menu')->select(); $other->exec('DELETE FROM faa_system_role_menu WHERE role_id=2 AND menu_id=2'); $ok(!$a::canDaily(8, []), 'revocation must beat repeatable-read snapshot'); $db::rollback(); $db::startTrans(); $db::name('tcm_diagnosis')->where('id', 91)->find(); $other->exec('UPDATE faa_tcm_diagnosis SET assistant_id=9 WHERE id=91'); $deny(fn () => $a::diagnosis(91, 8, []), 'reassignment must beat repeatable-read snapshot'); $db::rollback(); $other->exec('UPDATE faa_tcm_diagnosis SET assistant_id=8 WHERE id=91'); $db::startTrans(); $db::name('admin')->where('id', 8)->find(); $other->exec('UPDATE faa_admin SET disable=1 WHERE id=8'); $deny(fn () => $a::diagnosis(91, 8, []), 'disabled actor must beat repeatable-read snapshot'); $db::rollback(); $other->exec('UPDATE faa_admin SET disable=0 WHERE id=8'); $ok((int) $a::task(1, 8, [])['id'] === 1, 'task binding before rebind'); $other->exec('UPDATE faa_tcm_diagnosis SET patient_id=999 WHERE id=91'); $deny(fn () => $a::task(1, 8, []), 'historical recording must not follow patient rebind'); $other->exec('UPDATE faa_tcm_diagnosis SET status=0 WHERE id=91'); $deny(fn () => $a::diagnosis(91, 7, ['root' => 1]), 'inactive diagnosis even root'); echo "Followup audio real authorization: {$checks} checks passed (isolated MySQL; current-read revocation/reassignment)\n"; } finally { try { $db::rollback(); } catch (\Throwable $e) {} $manager->connect()->close(); $control->exec("DROP DATABASE IF EXISTS `{$database}`"); } }