createUser(['ROLE_USER', 'ROLE_REPRESENTATION']); $rep = new Representation($user, 'نمایندهٔ ' . uniqid()); $this->em->persist($rep); $this->em->flush(); return [$user, $rep]; } /** پزشکی که همان نماینده ثبتش کرده؛ uuid برمی‌گردد. */ private function doctorCreatedBy(User $repUser): string { $body = $this->authJson('POST', '/api/v1/representation/doctor', $repUser, [ 'mobile' => '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT), 'name' => 'دکتر آزمون ' . uniqid(), ]); self::assertSame(201, $this->responseCode(), 'ساخت پزشک توسط نماینده باید ۲۰۱ بدهد'); return $body['data']['uuid']; } private function editLogCount(): int { return (int) static::getContainer()->get(Connection::class) ->fetchOne("SELECT COUNT(*) FROM app_log WHERE channel = 'representation_edit'"); } private function reloadDoctor(string $uuid): Doctor { $this->em->clear(); return $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]); } // ── مسیر موفق ───────────────────────────────────────────────────────────── public function testOwningRepresentativeCanEditContentFields(): void { [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $body = $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $repUser, [ 'info' => 'متن معرفی تازه', 'degree' => 'متخصص پوست', ]); self::assertSame(200, $this->responseCode()); self::assertTrue($body['success']); self::assertSame('متن معرفی تازه', $this->reloadDoctor($uuid)->getInfo()); } public function testTheWholePayloadTheEditFormSendsIsAccepted(): void { // همان کلیدهایی که DoctorDetailPage در حالت نماینده می‌فرستد؛ اگر یکی // بیرون از whitelist بماند، هر ذخیره‌ای ۴۰۳ می‌شود. [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $repUser, [ 'title' => 'دکتر نام تازه', 'gender' => 'man', 'degree' => 'specialist', 'mobile_number' => '09121111111', 'info' => 'توضیحات', 'activity_time' => 1600000000, 'specialties' => [], 'doctor_services' => [], 'social_media' => [ 'instagram' => 'https://instagram.com/test', 'telegram' => null, 'aparat' => null, 'youtube' => null, 'linkedin' => null, ], ]); self::assertSame(200, $this->responseCode()); self::assertSame('نام تازه', $this->reloadDoctor($uuid)->getName()); } public function testASuccessfulEditWritesExactlyOneLogRow(): void { [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $before = $this->editLogCount(); $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $repUser, ['info' => 'x']); self::assertSame(200, $this->responseCode()); self::assertSame($before + 1, $this->editLogCount()); } // ── مسیر خطا ────────────────────────────────────────────────────────────── public function testAnotherRepresentativeIsForbidden(): void { [$ownerUser] = $this->newRepresentative(); [$strangerUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($ownerUser); $body = $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $strangerUser, ['info' => 'نباید ذخیره شود']); self::assertSame(403, $this->responseCode()); self::assertSame('ERR_AUTH_006', $body['errors'][0]['code']); self::assertNotSame('نباید ذخیره شود', $this->reloadDoctor($uuid)->getInfo()); } public function testForbiddenFieldIsRejectedAndNothingIsSaved(): void { [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $before = $this->reloadDoctor($uuid)->getMedicalSystemCode(); $body = $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $repUser, [ 'info' => 'این هم نباید ذخیره شود', 'medical_system_code' => '999999', ]); self::assertSame(403, $this->responseCode()); self::assertSame('medical_system_code', $body['errors'][0]['field']); $doctor = $this->reloadDoctor($uuid); self::assertSame($before, $doctor->getMedicalSystemCode()); self::assertNotSame('این هم نباید ذخیره شود', $doctor->getInfo()); } public function testTogglingActiveThroughPatchIsForbiddenForRepresentative(): void { [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $body = $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $repUser, ['active' => true]); self::assertSame(403, $this->responseCode()); self::assertSame('active', $body['errors'][0]['field']); } // ── مرزی ────────────────────────────────────────────────────────────────── public function testDoctorWithoutARepresentationIsNotEditableByAnyRepresentative(): void { [$repUser] = $this->newRepresentative(); $orphanUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']); $orphan = new Doctor($orphanUser, 'پزشک بی‌نماینده'); $this->em->persist($orphan); $this->em->flush(); $this->authJson('PATCH', '/api/v1/doctor/' . $orphan->getUuid(), $repUser, ['info' => 'x']); self::assertSame(403, $this->responseCode()); } public function testRoleWithoutARepresentationRowIsForbiddenNotFatal(): void { [$ownerUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($ownerUser); $rowless = $this->createUser(['ROLE_USER', 'ROLE_REPRESENTATION']); $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $rowless, ['info' => 'x']); self::assertSame(403, $this->responseCode()); } public function testDoctorEditingOwnProfileIsUnaffectedByTheWhitelist(): void { $doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($doctorUser, 'پزشک خودگردان'); $this->em->persist($doctor); $this->em->flush(); $uuid = $doctor->getUuid(); $before = $this->editLogCount(); // یکتا per-run: doctors.source_code ایندکس یکتا دارد و db_test هرگز پاک نمی‌شود. $code = 'mc' . substr(uniqid(), -8); // کد نظام پزشکی برای نماینده ممنوع است اما برای خودِ پزشک نه. $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $doctorUser, ['medical_system_code' => $code]); self::assertSame(200, $this->responseCode()); self::assertSame($code, $this->reloadDoctor($uuid)->getMedicalSystemCode()); self::assertSame($before, $this->editLogCount(), 'ویرایش خودِ پزشک نباید لاگ نماینده بسازد'); } public function testAdminIsUnaffectedByTheWhitelist(): void { [$repUser] = $this->newRepresentative(); $uuid = $this->doctorCreatedBy($repUser); $admin = $this->createUser(['ROLE_ADMIN']); $before = $this->editLogCount(); $code = 'ac' . substr(uniqid(), -8); $this->authJson('PATCH', '/api/v1/doctor/' . $uuid, $admin, ['medical_system_code' => $code]); self::assertSame(200, $this->responseCode()); self::assertSame($code, $this->reloadDoctor($uuid)->getMedicalSystemCode()); self::assertSame($before, $this->editLogCount(), 'ویرایش ادمین نباید لاگ نماینده بسازد'); } }