em->persist($province); $city = new City($cityName, $province); $this->em->persist($city); return $city; } private function makeDoctor(string $name): Doctor { $doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), $name); $this->em->persist($doctor); return $doctor; } private function giveOwnAddress(Doctor $doctor, City $city): void { $address = DoctorAddress::forDoctor($doctor); $address->setCity($city)->setProvince($city->getProvince()); $this->em->persist($address); } /** @return array map of doctor name => list payload */ private function fetchListByName(string $name): array { $this->client->request('GET', '/api/v1/doctors?limit=50&name=' . urlencode($name)); $this->assertSame(200, $this->responseCode()); $payload = json_decode($this->client->getResponse()->getContent(), true); $byName = []; foreach ($payload['data'] as $row) { $byName[$row['name']] = $row; } return $byName; } public function testDoctorWithOwnAddressReportsItsCity(): void { $city = $this->makeCity('یاسوج', 'کهگیلویه و بویراحمد'); $marker = 'loc-own-' . bin2hex(random_bytes(4)); $doctor = $this->makeDoctor($marker); $this->giveOwnAddress($doctor, $city); $this->em->flush(); $row = $this->fetchListByName($marker)[$marker] ?? null; $this->assertNotNull($row, 'doctor missing from list'); $this->assertCount(1, $row['city']); $this->assertSame('یاسوج', $row['city'][0]['name']); $this->assertSame((string) $city->getId(), $row['city'][0]['id']); $this->assertSame((string) $city->getProvince()->getId(), $row['city'][0]['parent']); $this->assertCount(1, $row['state']); $this->assertSame('کهگیلویه و بویراحمد', $row['state'][0]['name']); } public function testDoctorWithoutOwnAddressFallsBackToClinicCity(): void { $city = $this->makeCity('تبریز', 'آذربایجان شرقی'); $marker = 'loc-clinic-' . bin2hex(random_bytes(4)); $doctor = $this->makeDoctor($marker); $clinic = new Clinic($this->createUser(['ROLE_CLINIC'])); $this->em->persist($clinic); $clinic->getDoctors()->add($doctor); $this->em->flush(); // آدرس کلینیک ردیفی از DoctorAddress با doctor NULL و clinicId پرشده است. $clinicAddress = DoctorAddress::forClinic($clinic->getId()); $clinicAddress->setCity($city)->setProvince($city->getProvince()); $this->em->persist($clinicAddress); $this->em->flush(); $row = $this->fetchListByName($marker)[$marker] ?? null; $this->assertNotNull($row, 'doctor missing from list'); $this->assertCount(1, $row['city'], 'clinic-located doctor must still report a city'); $this->assertSame('تبریز', $row['city'][0]['name']); } public function testDoctorWithNoLocationReportsEmptyArrays(): void { $marker = 'loc-none-' . bin2hex(random_bytes(4)); $this->makeDoctor($marker); $this->em->flush(); $row = $this->fetchListByName($marker)[$marker] ?? null; $this->assertNotNull($row, 'doctor missing from list'); $this->assertSame([], $row['city']); $this->assertSame([], $row['state']); } public function testReportedCityMatchesCityIdFilter(): void { $city = $this->makeCity('یزد', 'یزد'); $marker = 'loc-filter-' . bin2hex(random_bytes(4)); $doctor = $this->makeDoctor($marker); $this->giveOwnAddress($doctor, $city); $this->em->flush(); $this->client->request('GET', '/api/v1/doctors?limit=50&city_id=' . $city->getId()); $this->assertSame(200, $this->responseCode()); $payload = json_decode($this->client->getResponse()->getContent(), true); $this->assertNotEmpty($payload['data']); foreach ($payload['data'] as $row) { $this->assertNotEmpty( $row['city'], "doctor {$row['name']} matched city_id filter but reports no city" ); $this->assertSame((string) $city->getId(), $row['city'][0]['id']); } } /** * سهمِ خودِ حل‌مکان اندازه‌گیری می‌شود، نه کل اندپوینت: پاسخ لیست lazy-loadهای * قدیمی (specialties) هم دارد که با تعداد پزشک رشد می‌کنند و ربطی به این تغییر * ندارند. پس مستقیم ریپازیتوری تست می‌شود — باید حداکثر ۲ کوئری باشد، ثابت. */ public function testLocationResolutionCostIsConstant(): void { $city = $this->makeCity('اصفهان', 'اصفهان'); $repo = static::getContainer()->get(DoctorRepository::class); $makeDoctors = function (int $count) use ($city): array { $doctors = []; for ($i = 0; $i < $count; $i++) { $doctor = $this->makeDoctor('loc-cost-' . bin2hex(random_bytes(4))); $this->giveOwnAddress($doctor, $city); $doctors[] = $doctor; } $this->em->flush(); return $doctors; }; $few = $makeDoctors(2); $many = $makeDoctors(12); $qFew = $this->countQueries(fn () => $repo->findLocationsByDoctors($few)); $qMany = $this->countQueries(fn () => $repo->findLocationsByDoctors($many)); $this->assertSame($qFew, $qMany, "location resolution scales with doctor count: $qFew -> $qMany"); $this->assertLessThanOrEqual(2, $qMany, 'location resolution must cost at most 2 queries'); // و همچنان درست کار کند $resolved = $repo->findLocationsByDoctors($many); $this->assertCount(12, $resolved); foreach ($many as $doctor) { $this->assertSame('اصفهان', $resolved[$doctor->getId()]['city']['name']); } } public function testPaginationMetaExposesAppliedLimit(): void { $this->client->request('GET', '/api/v1/doctors?limit=500'); $this->assertSame(200, $this->responseCode()); $payload = json_decode($this->client->getResponse()->getContent(), true); // سقف ریپازیتوری ۵۰ است؛ meta باید مقدار واقعاً اعمال‌شده را بگوید نه ۵۰۰. $this->assertSame(50, $payload['meta']['limit']); } }