Files
zyt/server/tests/PatientAiReportPermissionTest.php
T
2026-08-14 14:37:30 +08:00

142 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\PatientAiReportLogic;
final class PatientAiReportHistoryQueryDouble
{
/** @var array<int,array<string,mixed>> */
public static array $rows = [];
public static function where(string $field, $value): self
{
return new self();
}
public function field(array $fields): self
{
return $this;
}
public function order(string $field, string $direction): self
{
return $this;
}
public function select(): self
{
return $this;
}
/** @return array<int,array<string,mixed>> */
public function toArray(): array
{
return self::$rows;
}
}
patientPermissionExpect(
class_alias(PatientAiReportHistoryQueryDouble::class, 'app\\common\\model\\tcm\\PatientAiReport'),
'history model test double is installed before logic autoload'
);
function patientPermissionExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
$reflection = new ReflectionClass(PatientAiReportLogic::class);
$hasPermission = $reflection->getMethod('hasPermission');
patientPermissionExpect(
$hasPermission->invoke(null, 1, ['root' => 1], 'tcm.diagnosis/patientaireports') === true,
'root remains compatible without menu rows'
);
patientPermissionExpect(
$hasPermission->invoke(null, 0, ['root' => 0], 'tcm.diagnosis/patientaireports') === false,
'invalid unauthenticated admin fails closed'
);
$source = file_get_contents($reflection->getFileName());
patientPermissionExpect(is_string($source), 'logic source is readable');
patientPermissionExpect(
str_contains($source, 'MyPatientLogic::applyScope($query, $adminId, $adminInfo)'),
'patient access applies doctor/assistant/team department scope'
);
patientPermissionExpect(
str_contains($source, "->where('d.patient_id', \$patientId)")
&& str_contains($source, "->whereNull('d.delete_time')")
&& str_contains($source, "->where('d.status', 1)"),
'authorization derives visible diagnosis rows from the stable patient id'
);
patientPermissionExpect(
str_contains($source, "->whereIn('diagnosis_id', \$diagnosisIds)"),
'all subordinate sources are restricted to authorized diagnosis ids'
);
$historyMethod = $reflection->getMethod('buildHistoryPayload');
$sourceLines = file($reflection->getFileName());
$historySource = is_array($sourceLines) ? implode('', array_slice(
$sourceLines,
$historyMethod->getStartLine() - 1,
$historyMethod->getEndLine() - $historyMethod->getStartLine() + 1
)) : '';
patientPermissionExpect(
str_contains($historySource, "PatientAiReport::where('patient_id', \$patientId)")
&& str_contains($historySource, "self::decodeJsonArray(\$row['source_diagnosis_ids_json'] ?? '')")
&& str_contains($historySource, "array_filter(\$sourceDiagnosisIds")
&& str_contains($historySource, '!isset($authorized[$id])')
&& str_contains($historySource, "\$sourceDiagnosisIds = [(int) \$row['diagnosis_id']]")
&& !str_contains($historySource, "->whereIn('diagnosis_id', \$diagnosisIds)"),
'history requires every source diagnosis to remain authorized, with legacy diagnosis fallback only'
);
$baseRow = [
'patient_id' => 77,
'model_key' => 'qwen',
'model_name' => 'server-model',
'model_label' => 'Qwen',
'report_json' => '{"diagnosis":"诊断","risk_assessment":[],"treatment_advice":"建议"}',
'source_summary_json' => '{"diagnosis_count":2}',
'source_hash' => str_repeat('a', 64),
'prompt_version' => 'patient-longitudinal-report-v1',
'generated_at' => 1786665600,
'created_at' => 1786665600,
];
PatientAiReportHistoryQueryDouble::$rows = [
$baseRow + ['id' => 1, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => '[11,12]'],
$baseRow + ['id' => 2, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => '[11,99]'],
$baseRow + ['id' => 3, 'diagnosis_id' => 12, 'source_diagnosis_ids_json' => ''],
$baseRow + ['id' => 4, 'diagnosis_id' => null, 'source_diagnosis_ids_json' => '[]'],
];
$history = $historyMethod->invoke(null, 77, [11, 12], 1);
patientPermissionExpect(
array_column($history['reports'], 'id') === [1, 3],
'history executable filter keeps complete authorized and legacy rows but hides partial or missing source sets'
);
patientPermissionExpect(
$history['generated_report']['id'] === 1 && $history['report']['id'] === 1,
'generated report selection still works after complete-source authorization filtering'
);
patientPermissionExpect(
str_contains($source, "self::setError('患者不存在或无权访问')"),
'missing and unauthorized patients share a non-enumerating error'
);
$migration = file_get_contents(dirname(__DIR__) . '/database/migrations/2026_08_14_patient_ai_report.sql');
patientPermissionExpect(is_string($migration), 'permission migration is readable');
patientPermissionExpect(
substr_count($migration, "WHERE `perms` = 'tcm.diagnosis/patientAiReports'") >= 2,
'read permission registration is idempotent and addressable'
);
patientPermissionExpect(
substr_count($migration, "WHERE `perms` = 'tcm.diagnosis/generatePatientAiReport'") >= 2,
'generate permission registration is idempotent and addressable'
);
echo "Patient AI report permission scope: OK\n";