client->disableReboot(); } private function mockApiIr(bool $shahkar = true, ?array $person = ['firstName' => 'تست', 'lastName' => 'ایمپورت', 'alive' => true]): void { $mock = $this->createMock(ApiIrService::class); $mock->method('isConfigured')->willReturn(true); $mock->method('shahkarMatch')->willReturn($shahkar); $mock->method('personInfo')->willReturn($person); static::getContainer()->set(ApiIrService::class, $mock); } private function importUnclaimedDoctor(): string { $admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']); $code = 'C' . random_int(100_000, 999_999) . random_int(100, 999); $data = $this->authJson('POST', '/api/v1/admin/doctors/import', $admin, [ 'name' => 'دکتر تست ایمپورت', 'medical_system_code' => $code, ]); return $data['data']['uuid']; } private function claimBody(): array { // db_test هرگز reset نمی‌شود و users.national_code یکتاست → کد ملی هر تست تصادفی return [ 'national_code' => '00' . str_pad((string) random_int(0, 99_999_999), 8, '0', STR_PAD_LEFT), 'birth_date' => '1371/1/1', 'first_name' => 'تست', 'last_name' => 'ایمپورت', ]; } public function testSuccessfulClaimTransfersOwnershipAndDeletesSurrogate(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(); $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]); $surrogateId = $doctor->getUser()->getId(); $claimer = $this->createUser(['ROLE_USER']); $res = $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $this->claimBody()); $this->assertSame(200, $this->responseCode()); $this->assertSame('claimed', $res['data']['status']); $this->em->clear(); $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]); $this->assertSame('claimed', $doctor->getOwnerStatus()); $this->assertSame($claimer->getId(), $doctor->getUser()->getId()); $this->assertNull($doctor->getManagedBy()); $claimer = $this->em->getRepository(User::class)->find($claimer->getId()); $this->assertTrue($claimer->hasRole('ROLE_DOCTOR')); $this->assertTrue($claimer->isNationalCodeVerified()); $this->assertNull($this->em->getRepository(User::class)->find($surrogateId), 'surrogate must be deleted'); $claim = $this->em->getRepository(DoctorClaimRequest::class)->findOneBy(['doctor' => $doctor]); $this->assertSame(DoctorClaimRequest::STATUS_COMPLETED, $claim->getStatus()); } public function testNameMismatchRevertsToUnclaimed(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(person: ['firstName' => 'کس', 'lastName' => 'دیگری', 'alive' => true]); $claimer = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $this->claimBody()); $this->assertSame(422, $this->responseCode()); $this->em->clear(); $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]); $this->assertSame('unclaimed', $doctor->getOwnerStatus(), 'must be free for the real doctor to retry'); $this->assertStringStartsWith('imp_', $doctor->getUser()->getMobileNumber(), 'surrogate must survive'); $claim = $this->em->getRepository(DoctorClaimRequest::class)->findOneBy(['doctor' => $doctor]); $this->assertSame(DoctorClaimRequest::STATUS_FAILED, $claim->getStatus()); $this->assertNotNull($claim->getFailureReason()); } public function testDeceasedPersonIsRejected(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(person: ['firstName' => 'تست', 'lastName' => 'ایمپورت', 'alive' => false]); $claimer = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $this->claimBody()); $this->assertSame(422, $this->responseCode()); } public function testShahkarMismatchIsRejected(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(shahkar: false); $claimer = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $this->claimBody()); $this->assertSame(422, $this->responseCode()); } public function testAlreadyClaimedIsConflict(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(); $first = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $first, $this->claimBody()); $this->assertSame(200, $this->responseCode()); $second = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $second, $this->claimBody()); $this->assertSame(409, $this->responseCode()); } public function testUserWhoAlreadyOwnsADoctorCannotClaim(): void { $uuidA = $this->importUnclaimedDoctor(); $uuidB = $this->importUnclaimedDoctor(); $this->mockApiIr(); $claimer = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuidA}/claim", $claimer, $this->claimBody()); $this->assertSame(200, $this->responseCode()); $this->authJson('POST', "/api/v1/doctor/{$uuidB}/claim", $claimer, $this->claimBody()); $this->assertSame(409, $this->responseCode()); $this->em->clear(); $doctorB = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuidB]); $this->assertSame('unclaimed', $doctorB->getOwnerStatus()); } public function testValidationErrors(): void { $uuid = $this->importUnclaimedDoctor(); $this->mockApiIr(); $claimer = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, ['national_code' => '123', 'birth_date' => '1371/1/1', 'first_name' => 'الف', 'last_name' => 'ب']); $this->assertSame(422, $this->responseCode()); $this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, ['national_code' => '0010007700', 'birth_date' => 'invalid', 'first_name' => 'الف', 'last_name' => 'ب']); $this->assertSame(422, $this->responseCode()); } public function testClaimInfoIsPublic(): void { $uuid = $this->importUnclaimedDoctor(); $this->client->request('GET', "/api/v1/doctor/{$uuid}/claim-info"); $this->assertSame(200, $this->responseCode()); $data = json_decode($this->client->getResponse()->getContent(), true); $this->assertTrue($data['data']['claimable']); $this->assertSame('unclaimed', $data['data']['owner_status']); } public function testAdminTransferHappyPath(): void { $uuid = $this->importUnclaimedDoctor(); $admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']); $mobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT); $res = $this->authJson('POST', "/api/v1/admin/doctors/{$uuid}/transfer", $admin, ['mobile' => $mobile]); $this->assertSame(200, $this->responseCode()); $this->assertSame('claimed', $res['data']['owner_status']); $this->em->clear(); $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]); $this->assertSame('claimed', $doctor->getOwnerStatus()); $this->assertSame($mobile, $doctor->getUser()->getMobileNumber()); $this->assertTrue($doctor->getUser()->hasRole('ROLE_DOCTOR')); // transfer دوباره → 409 $this->authJson('POST', "/api/v1/admin/doctors/{$uuid}/transfer", $admin, ['mobile' => $mobile]); $this->assertSame(409, $this->responseCode()); } public function testAdminTransferRequiresAdmin(): void { $uuid = $this->importUnclaimedDoctor(); $user = $this->createUser(['ROLE_USER']); $this->authJson('POST', "/api/v1/admin/doctors/{$uuid}/transfer", $user, ['mobile' => '09121234567']); $this->assertSame(403, $this->responseCode()); } public function testClaimRequiresAuthentication(): void { $uuid = $this->importUnclaimedDoctor(); $this->client->request('POST', "/api/v1/doctor/{$uuid}/claim", server: ['CONTENT_TYPE' => 'application/json'], content: json_encode($this->claimBody())); $this->assertSame(401, $this->responseCode()); } }