> $days */ private function put(\App\Auth\Entity\User $user, string $addressUuid, array $days): array { return $this->authJson('PUT', "/api/v1/branch/$addressUuid/working-hours", $user, ['days' => $days]); } public function testEmptyBranchReportsSevenEmptyDaysAndUndefined(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user); self::assertSame(200, $this->responseCode()); self::assertFalse($body['data']['defined'], 'شعبهٔ بدون ساعت باید «تعریف‌نشده» باشد، نه همیشه‌باز'); self::assertSame(range(0, 6), array_map('intval', array_keys($body['data']['days']))); foreach ($body['data']['days'] as $ranges) { self::assertSame([], $ranges); } } public function testFullWeekIsStoredAndReadBackIdentically(): void { [$user, , $address] = $this->doctorWithAddress(); $days = []; foreach (range(0, 6) as $day) { $days[$day] = [ ['start_minute' => 540, 'end_minute' => 780], // 09:00-13:00 ['start_minute' => 960, 'end_minute' => 1200], // 16:00-20:00 ]; } $written = $this->put($user, $address->getUuid(), $days); self::assertSame(200, $this->responseCode(), json_encode($written, JSON_UNESCAPED_UNICODE)); self::assertTrue($written['data']['defined']); $read = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user); self::assertSame($written['data']['days'], $read['data']['days']); self::assertSame('09:00', $read['data']['days'][0][0]['start_time']); self::assertSame('20:00', $read['data']['days'][0][1]['end_time']); self::assertSame([0, 1], array_column($read['data']['days'][0], 'sequence')); } /** PUT قرارداد جایگزینی کامل دارد: آرایهٔ خالی یعنی شعبه بسته، نه «تغییری نده». */ public function testEmptyPayloadClosesTheBranch(): void { [$user, , $address] = $this->doctorWithAddress(); $this->put($user, $address->getUuid(), [3 => [['start_minute' => 600, 'end_minute' => 700]]]); $body = $this->put($user, $address->getUuid(), []); self::assertSame(200, $this->responseCode()); self::assertFalse($body['data']['defined']); self::assertSame([], $body['data']['days'][3]); } public function testEndBeforeStartIsRejected(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->put($user, $address->getUuid(), [0 => [['start_minute' => 800, 'end_minute' => 800]]]); self::assertSame(422, $this->responseCode()); self::assertSame('end_minute', $body['errors'][0]['field']); } public function testOverlappingRangesInOneDayAreRejected(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->put($user, $address->getUuid(), [2 => [ ['start_minute' => 540, 'end_minute' => 780], ['start_minute' => 700, 'end_minute' => 900], ]]); self::assertSame(422, $this->responseCode()); self::assertStringContainsString('هم‌پوشانی', $body['errors'][0]['message']); } /** بازهٔ چسبیده مجاز است: پایان یکی = شروع بعدی، هم‌پوشانی نیست. */ public function testTouchingRangesAreAccepted(): void { [$user, , $address] = $this->doctorWithAddress(); $this->put($user, $address->getUuid(), [2 => [ ['start_minute' => 540, 'end_minute' => 780], ['start_minute' => 780, 'end_minute' => 900], ]]); self::assertSame(200, $this->responseCode()); } public function testAllDayRangeIsOneRow(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->put($user, $address->getUuid(), [ 5 => [['start_minute' => 0, 'end_minute' => BranchWorkingHours::MINUTES_IN_DAY]], ]); self::assertSame(200, $this->responseCode()); self::assertCount(1, $body['data']['days'][5]); self::assertSame('24:00', $body['data']['days'][5][0]['end_time']); } public function testMinuteBeyondOneDayIsRejected(): void { [$user, , $address] = $this->doctorWithAddress(); $this->put($user, $address->getUuid(), [1 => [['start_minute' => 0, 'end_minute' => 1441]]]); self::assertSame(422, $this->responseCode()); } public function testInvalidDayKeyIsRejected(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->put($user, $address->getUuid(), [7 => [['start_minute' => 0, 'end_minute' => 60]]]); self::assertSame(422, $this->responseCode()); self::assertSame('day_of_week', $body['errors'][0]['field']); } /** * اتمی بودن: بازهٔ نامعتبر در روز ششم نباید روزهای درستِ قبل را پاک کند. * بدون اعتبارسنجیِ کاملِ پیش از DELETE، این تست هفتهٔ ذخیره‌شده را خالی می‌بیند. */ public function testInvalidLaterDayLeavesTheStoredWeekUntouched(): void { [$user, , $address] = $this->doctorWithAddress(); $valid = []; foreach (range(0, 6) as $day) { $valid[$day] = [['start_minute' => 540, 'end_minute' => 780]]; } $this->put($user, $address->getUuid(), $valid); $broken = $valid; $broken[5] = [['start_minute' => 900, 'end_minute' => 100]]; $this->put($user, $address->getUuid(), $broken); self::assertSame(422, $this->responseCode()); $read = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user); self::assertTrue($read['data']['defined']); foreach (range(0, 6) as $day) { self::assertCount(1, $read['data']['days'][$day], "روز $day نباید پاک شده باشد"); } } public function testMissingDaysFieldIsRejected(): void { [$user, , $address] = $this->doctorWithAddress(); $body = $this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, ['x' => 1]); self::assertSame(422, $this->responseCode()); self::assertSame('days', $body['errors'][0]['field']); } /** آدرس محیط دیگر: ۴۰۴ نه ۴۰۳ — وجود دادهٔ محیط بیگانه لو نمی‌رود. */ public function testForeignBranchIsNotFound(): void { [$doctorUser] = $this->doctorWithAddress(); [, , $foreignAddress] = $this->clinicWithAddress(); $this->authJson('GET', "/api/v1/branch/{$foreignAddress->getUuid()}/working-hours", $doctorUser); self::assertSame(404, $this->responseCode()); } public function testForeignBranchCannotBeWritten(): void { [$doctorUser] = $this->doctorWithAddress(); [, , $foreignAddress] = $this->clinicWithAddress(); $this->put($doctorUser, $foreignAddress->getUuid(), [0 => [['start_minute' => 0, 'end_minute' => 60]]]); self::assertSame(404, $this->responseCode()); } /** * `days` باید **شیء** JSON با کلیدهای "0".."6" باشد، نه آرایه. * کلیدهای ۰..۶ پشت‌سرهم‌اند و json_encode بی‌مراقبت آرایه می‌ساخت؛ کلاینت * `days["0"]` هر دو را می‌خواند، ولی شکل پاسخ با جا افتادن یک روز عوض می‌شد. */ public function testDaysIsAJsonObjectNotAnArray(): void { [$user, , $address] = $this->doctorWithAddress(); $this->put($user, $address->getUuid(), [0 => [['start_minute' => 540, 'end_minute' => 780]]]); foreach (['PUT', 'GET'] as $method) { if ($method === 'GET') { $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user); } $raw = json_decode($this->client->getResponse()->getContent(), false); self::assertInstanceOf(\stdClass::class, $raw->data->days, "$method: days باید شیء باشد"); // get_object_vars نامِ عددیِ ویژگی‌ها را به int برمی‌گرداند؛ آنچه مهم است // stdClass بودن بالا سنجیده شد. اینجا فقط کامل بودن هفت روز. self::assertSame(range(0, 6), array_keys(get_object_vars($raw->data->days))); } } public function testClinicOwnerManagesItsOwnBranch(): void { [$clinicUser, , $address] = $this->clinicWithAddress(); $body = $this->put($clinicUser, $address->getUuid(), [ 0 => [['start_minute' => 480, 'end_minute' => 1020]], ]); self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE)); self::assertSame('08:00', $body['data']['days'][0][0]['start_time']); } }