feat: add RichTextEditor component for rich text editing in articles

feat: create SanitizeBlogBodiesCommand to clean existing blog bodies according to current HTML sanitization policies

test: add AppointmentTreatmentSessionLinkTest to ensure appointment booking functionality works correctly with treatment session links
This commit is contained in:
hamed
2026-08-08 11:40:17 +03:30
parent 934405c42d
commit 47323daa27
36 changed files with 3461 additions and 12686 deletions
+67 -5
View File
@@ -160,14 +160,44 @@ class ApiLeastPrivilegeTest extends ApiTestCase
'app_clinicservice_clinicservice_deletesection' => 'حذف ممنوع — همیشه ۴۰۹',
// ── گِیت دارند ولی هدفشان از بدنه می‌آید، نه از path ──────────────────
// با بدنهٔ خالی روی uuidِ ناموجودِ داخلِ بدنه ۴۰۴ می‌دهند. مثل روت‌های
// پارامتردار، ولی چون path parameter ندارند سطح اولِ قاعده شاملشان می‌شد.
'app_appointment_appointmentsettings_createschedule' => 'پزشکِ هدف از بدنه؛ گِیت در denyDoctorAccess',
'app_appointment_appointmentsettings_createoverride' => 'پزشکِ هدف از بدنه؛ گِیت در denyDoctorAccess',
'app_appointment_appointmentsettings_createholiday' => 'پزشکِ هدف از بدنه؛ گِیت در denyDoctorAccess',
'app_secretary_secretary_create' => 'پزشکِ هدف از بدنه؛ مالکیت در canManage سنجیده می‌شود',
];
/**
* روت‌هایی که پیش‌چکِ منشی **پیش از واکشی** دارند، پس حتی با uuidِ ناموجود هم
* باید `403` بدهند نه `404`.
*
* این فهرست پیشرفتِ یافتهٔ ۱۰ آدیت ۲۰۲۶-۰۸-۰۷ را قفل می‌کند: چکِ اصلیِ این روت‌ها
* شیء‌محور است و بالا نمی‌رود، ولی سهمِ منشی از آن بالا برده شد. اگر کسی آن خط را
* بردارد، پاسخ به `404` برمی‌گردد و همین تست قرمز می‌شود.
*
* @var list<string>
*/
private const GATE_BEFORE_LOOKUP = [
'app_appointment_appointment_updatestatus',
'app_appointment_appointment_confirm',
'app_appointment_appointment_update',
'app_appointment_appointment_servicereschedule',
'app_appointment_appointmentsettings_createschedule',
'app_appointment_appointmentsettings_updateschedule',
'app_appointment_appointmentsettings_deleteschedule',
'app_appointment_appointmentsettings_createoverride',
'app_appointment_appointmentsettings_updateoverride',
'app_appointment_appointmentsettings_deleteoverride',
'app_appointment_appointmentsettings_createholiday',
'app_appointment_appointmentsettings_updateholiday',
'app_appointment_appointmentsettings_deleteholiday',
'app_clinic_clinic_update',
'app_clinic_clinic_detachdoctor',
'app_clinic_clinicdoctorpermission_updatepermissions',
'app_clinicinvitation_clinicinvitation_invitedoctor',
'app_clinicinvitation_clinicinvitation_resendinvitation',
'app_clinicinvitation_clinicinvitation_changeinvitationstatus',
'app_clinicinvitation_clinicinvitation_deleteinvitation',
'resource_block_create',
'resource_block_delete',
];
/**
* این تست ~۱۳۰ درخواست پشت‌سرهم می‌زند و هر درخواست کرنل را دوباره بالا
* می‌آورد، پس `$this->em` تا انتهای تست به یک نمونهٔ کهنه اشاره می‌کند. بدون
@@ -344,6 +374,38 @@ class ApiLeastPrivilegeTest extends ApiTestCase
));
}
/**
* قفلِ پیشرفت: هر روتِ `GATE_BEFORE_LOOKUP` با uuidِ ناموجود باید `403` بدهد.
*
* `404` یعنی گِیت دوباره پایین‌تر از واکشی رفته و enumeration oracle برگشته.
*/
public function testHoistedGatesAnswer403BeforeTheLookup(): void
{
$secretary = $this->makePowerlessSecretary();
$router = self::getContainer()->get('router');
$regressed = [];
foreach (self::GATE_BEFORE_LOOKUP as $name) {
$route = $router->getRouteCollection()->get($name);
$this->assertNotNull($route, "روت {$name} دیگر وجود ندارد — فهرست را به‌روز کن");
$method = array_values(array_intersect(
$route->getMethods(),
['POST', 'PUT', 'PATCH', 'DELETE'],
))[0];
$this->authJson($method, self::probePath($route), $secretary);
if ($this->responseCode() !== 403) {
$regressed[] = sprintf('%s → %d', $name, $this->responseCode());
}
}
$this->assertSame([], $regressed, sprintf(
"این روت‌ها دیگر پیش از واکشی گِیت نمی‌خورند:\n%s",
implode("\n", $regressed),
));
}
/**
* بدهی نباید بی‌صدا بماند: به‌محض اینکه گِیتِ یکی از KNOWN_GAPS اضافه شد، این
* تست قرمز می‌شود تا آن ردیف از فهرست حذف شود. بدون این، فهرست برای همیشه