feat: add BlogBodySanitizer for HTML sanitization on article save

- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks.
- Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content.
- Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
This commit is contained in:
hamed
2026-08-07 21:13:38 +03:30
parent a4a24c51af
commit 6876135a53
114 changed files with 2067 additions and 269 deletions
@@ -415,4 +415,65 @@ class SecretaryResourceEnforcementTest extends ApiTestCase
$this->assertSame(403, $this->responseCode());
}
// ── پروتکل درمان ─────────────────────────────────────────────────────────
//
// آدیت ۲۰۲۶-۰۸-۰۷: TreatmentProtocolController هیچ گِیت مجوزی نداشت و فقط
// مالکیتِ tenant را می‌سنجید، پس منشیِ `services:false` می‌توانست پروتکل را
// بخواند، بازنویسی کند و حذف کند. پروتکل خاصیتِ سرویس است، پس مجوزش `services`
// است. uuidِ ناموجود عمدی است: گیت پیش از واکشیِ سرویس اجرا می‌شود، پس ۴۰۳
// در برابر ۴۰۴ دقیقاً همان چیزی را جدا می‌کند که این تست‌ها می‌سنجند.
private const ABSENT_SERVICE = '00000000-0000-0000-0000-000000000000';
public function testTreatmentProtocolReadDeniedByDefault(): void
{
// DEFAULT_PERMISSIONS: services.* = false
[$secretary] = $this->makeClinicSecretary();
$this->em->flush();
$this->authJson('GET', '/api/v1/service-item/' . self::ABSENT_SERVICE . '/treatment-protocol', $secretary);
$this->assertSame(403, $this->responseCode());
}
public function testTreatmentProtocolReadAllowedWhenServicesGranted(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => ['services' => ['view' => true]]]);
$this->em->flush();
// گیت عبور می‌کند و به «سرویس یافت نشد» می‌رسد — نه ۴۰۳.
$this->authJson('GET', '/api/v1/service-item/' . self::ABSENT_SERVICE . '/treatment-protocol', $secretary);
$this->assertSame(404, $this->responseCode());
}
public function testTreatmentProtocolWriteNeedsServicesUpdate(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => ['services' => ['view' => true, 'update' => false]]]);
$this->em->flush();
$path = '/api/v1/service-item/' . self::ABSENT_SERVICE . '/treatment-protocol';
// خواندن مجاز است…
$this->authJson('GET', $path, $secretary);
$this->assertSame(404, $this->responseCode());
// …ولی بازنویسی و خاموش‌کردنِ سوییچ نه.
$this->authJson('PUT', $path, $secretary, ['steps' => []]);
$this->assertSame(403, $this->responseCode(), 'بازنویسی پروتکل باید services.update بخواهد');
$this->authJson('DELETE', $path, $secretary);
$this->assertSame(403, $this->responseCode(), 'حذف پروتکل باید services.update بخواهد');
}
public function testTreatmentProtocolWriteAllowedWhenServicesUpdateGranted(): void
{
[$secretary, $rel] = $this->makeClinicSecretary();
$rel->mergePermissions(['resources' => ['services' => ['view' => true, 'update' => true]]]);
$this->em->flush();
$this->authJson('DELETE', '/api/v1/service-item/' . self::ABSENT_SERVICE . '/treatment-protocol', $secretary);
$this->assertSame(404, $this->responseCode());
}
}