createUser(['ROLE_USER', 'ROLE_DOCTOR']); $doctor = new Doctor($user, $name); $doctor->setMobileNumber($user->getMobileNumber()); $this->em->persist($doctor); $this->em->flush(); return $doctor; } /** @return array{0: User, 1: Clinic} */ private function makeClinicWith(Doctor ...$doctors): array { $owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']); $clinic = new Clinic($owner); $clinic->setName('کلینیک تست'); foreach ($doctors as $d) { $clinic->getDoctors()->add($d); } $this->em->persist($clinic); $this->em->flush(); return [$owner, $clinic]; } private function makeAppointment(Doctor $doctor, ?Clinic $clinic, ?User $patient = null): Appointment { $patient ??= $this->createUser(); $start = strtotime('+3 days 10:00'); $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic); $this->em->persist($appointment); $this->em->flush(); return $appointment; } private function makeClinicSecretary(Clinic $clinic, Doctor $doctor, array $permissionPatch = []): User { $user = $this->createUser(['ROLE_USER', 'ROLE_SECRETARY']); $secretary = new DoctorSecretary($doctor, $user, $clinic); if ($permissionPatch !== []) { $secretary->mergePermissions(['resources' => ['appointments' => $permissionPatch]]); } $this->em->persist($secretary); $this->em->flush(); static::getContainer()->get(UserActiveContextRepository::class) ->upsert($user, $clinic->getUuid(), EntityContext::TYPE_CLINIC); return $user; } public function testClinicOwnerCanViewClinicAppointment(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $owner); self::assertSame(200, $this->responseCode()); } public function testClinicOwnerCanUpdateAndMoveClinicAppointment(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $newStart = strtotime('+4 days 11:00'); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $owner, [ 'slot_start' => $newStart, 'slot_end' => $newStart + 900, 'note' => 'جابه‌جا شد', 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); } public function testClinicOwnerCanChangeStatusOfClinicAppointment(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $owner, [ 'status' => Appointment::STATUS_CONFIRMED, 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); } public function testClinicOwnerCanTransferAppointmentToReserveAndBack(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $midnight = strtotime('+3 days 00:00'); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $owner, [ 'is_reserve' => true, 'slot_start' => $midnight, 'slot_end' => $midnight, 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode(), 'انتقال به لیست رزرو'); $this->em->clear(); $reloaded = $this->em->getRepository(Appointment::class)->find($appointment->getId()); self::assertTrue($reloaded->isReserve()); $back = strtotime('+5 days 09:00'); $this->authJson('PATCH', "/api/v1/appointment/{$reloaded->getUuid()}", $owner, [ 'is_reserve' => false, 'slot_start' => $back, 'slot_end' => $back + 900, 'version' => $reloaded->getVersion(), ]); self::assertSame(200, $this->responseCode(), 'بازگشت از لیست رزرو'); } public function testClinicOwnerCanReadAppointmentEvents(): void { $doctor = $this->makeDoctor(); [$owner, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/events", $owner); self::assertSame(200, $this->responseCode()); } public function testClinicOwnerCannotTouchDoctorPersonalAppointment(): void { $doctor = $this->makeDoctor(); [$owner] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, null); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $owner); self::assertSame(403, $this->responseCode(), 'مطب شخصی پزشک از دسترس کلینیک خارج است'); } public function testForeignClinicOwnerIsDenied(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); [$otherOwner] = $this->makeClinicWith($this->makeDoctor('دکتر دیگر')); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $otherOwner); self::assertSame(403, $this->responseCode()); } public function testMemberDoctorLosesAccessWhenDeactivated(): void { $member = $this->makeDoctor('دکتر عضو'); $colleague = $this->makeDoctor('همکار'); [, $clinic] = $this->makeClinicWith($member, $colleague); $appointment = $this->makeAppointment($colleague, $clinic); $permissions = static::getContainer()->get(ClinicDoctorPermissionRepository::class); $permissions->getOrCreate($clinic, $member); $this->em->flush(); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $member->getUser()); self::assertSame(200, $this->responseCode(), 'پزشک فعالِ کلینیک نوبت‌های همان کلینیک را می‌بیند'); $permissions->getOrCreate($clinic, $member)->setActive(false); $this->em->flush(); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $member->getUser()); self::assertSame(403, $this->responseCode(), 'پس از پایان همکاری دسترسی قطع می‌شود'); } public function testClinicSecretaryCanManageAssignedDoctorAppointment(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); $secretary = $this->makeClinicSecretary($clinic, $doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $secretary); self::assertSame(200, $this->responseCode()); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [ 'status' => Appointment::STATUS_CONFIRMED, 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); } public function testClinicSecretaryCannotTouchUnassignedDoctorAppointment(): void { $mine = $this->makeDoctor('پزشک من'); $theirs = $this->makeDoctor('پزشک دیگر'); [, $clinic] = $this->makeClinicWith($mine, $theirs); $secretary = $this->makeClinicSecretary($clinic, $mine); $appointment = $this->makeAppointment($theirs, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $secretary); self::assertSame(403, $this->responseCode(), 'منشی فقط پزشکان تخصیص‌یافتهٔ خودش را دارد'); } public function testSecretaryCancelRequiresCancelPermission(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); $secretary = $this->makeClinicSecretary($clinic, $doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [ 'status' => Appointment::STATUS_CANCELLED_BY_DOCTOR, 'version' => $appointment->getVersion(), ]); self::assertSame(403, $this->responseCode(), 'لغو به‌صورت پیش‌فرض برای منشی خاموش است'); } public function testSecretaryWithCancelPermissionCanCancel(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); $secretary = $this->makeClinicSecretary($clinic, $doctor, ['cancel' => true]); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [ 'status' => Appointment::STATUS_CANCELLED_BY_DOCTOR, 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); } public function testInlineStatusCannotBypassCancelGate(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); $secretary = $this->makeClinicSecretary($clinic, $doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $secretary, [ 'status' => Appointment::STATUS_CANCELLED_BY_DOCTOR, 'version' => $appointment->getVersion(), ]); self::assertSame(403, $this->responseCode(), 'status درون‌خطی همان گیت لغو را دارد'); } public function testOwnerDoctorKeepsFullAccessToOwnClinicAppointment(): void { $doctor = $this->makeDoctor(); [, $clinic] = $this->makeClinicWith($doctor); $appointment = $this->makeAppointment($doctor, $clinic); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $doctor->getUser()); self::assertSame(200, $this->responseCode()); } public function testPatientCanViewButNotRescheduleOwnAppointment(): void { $doctor = $this->makeDoctor(); $patient = $this->createUser(); $appointment = $this->makeAppointment($doctor, null, $patient); $newStart = strtotime('+6 days 10:00'); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $patient); self::assertSame(200, $this->responseCode()); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $patient, [ 'slot_start' => $newStart, 'slot_end' => $newStart + 900, 'version' => $appointment->getVersion(), ]); self::assertSame(403, $this->responseCode(), 'بیمار نوبت خودش را جابه‌جا نمی‌کند'); } public function testPatientCanCancelOwnAppointment(): void { $doctor = $this->makeDoctor(); $patient = $this->createUser(); $appointment = $this->makeAppointment($doctor, null, $patient); $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $patient, [ 'status' => Appointment::STATUS_CANCELLED_BY_USER, 'version' => $appointment->getVersion(), ]); self::assertSame(200, $this->responseCode()); } public function testStrangerIsDenied(): void { $doctor = $this->makeDoctor(); $appointment = $this->makeAppointment($doctor, null); $stranger = $this->createUser(); $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $stranger); self::assertSame(403, $this->responseCode()); } }