Files
zyt/server/tests/FollowupAudioAccessTest.php
T

101 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service\followupaudio {
final class FollowupAudioStore
{
public static function task(int $id): array
{
return ['id' => $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}`");
}
}