clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $laser = $this->createSkill($user, 'لیزر آلکساندرایت'); $botox = $this->createSkill($user, 'بوتاکس'); $withBoth = $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [ ['skill_uuid' => $laser['data']['uuid'], 'level' => 4], ['skill_uuid' => $botox['data']['uuid']], ], ]); self::assertSame(200, $this->responseCode(), json_encode($withBoth, JSON_UNESCAPED_UNICODE)); self::assertCount(2, $withBoth['data']['skills']); self::assertSame(1, $withBoth['data']['skills'][1]['level'], 'سطح پیش‌فرض ۱ است'); // PUT جایگزینی کامل است: مهارتی که نیامده، برداشته می‌شود. $onlyLaser = $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $laser['data']['uuid'], 'level' => 5]], ]); self::assertCount(1, $onlyLaser['data']['skills']); self::assertSame($laser['data']['uuid'], $onlyLaser['data']['skills'][0]['skill_uuid']); self::assertSame(5, $onlyLaser['data']['skills'][0]['level']); } public function testEmptyListClearsAllSkills(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $skill = $this->createSkill($user); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $skill['data']['uuid']]], ]); $body = $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, ['skills' => []]); self::assertSame(200, $this->responseCode()); self::assertSame([], $body['data']['skills']); } public function testLevelOutsideOneToFiveIsRejected(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $skill = $this->createSkill($user); foreach ([0, 6] as $level) { $body = $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $skill['data']['uuid'], 'level' => $level]], ]); self::assertSame(422, $this->responseCode(), "level=$level باید رد شود"); self::assertSame('level', $body['errors'][0]['field']); } } /** * اعتبارسنجی پیش از حذف: ورودی نامعتبر در انتهای فهرست نباید مهارت‌های درستِ * قبلی را پاک کند و بعد ۴۲۲ برگرداند. */ public function testInvalidRowLeavesTheStoredSkillsUntouched(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $laser = $this->createSkill($user, 'لیزر'); $botox = $this->createSkill($user, 'بوتاکس'); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $laser['data']['uuid'], 'level' => 3]], ]); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [ ['skill_uuid' => $botox['data']['uuid']], ['skill_uuid' => $laser['data']['uuid'], 'level' => 99], ], ]); self::assertSame(422, $this->responseCode()); $read = $this->authJson('GET', "/api/v1/resource/{$resource['data']['uuid']}", $user); self::assertCount(1, $read['data']['skills']); self::assertSame(3, $read['data']['skills'][0]['level']); } public function testDuplicateSkillInOnePayloadIsRejected(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $skill = $this->createSkill($user); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [ ['skill_uuid' => $skill['data']['uuid']], ['skill_uuid' => $skill['data']['uuid'], 'level' => 2], ], ]); self::assertSame(422, $this->responseCode()); } public function testForeignSkillIsNotFound(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); [$otherUser] = $this->clinicWithAddress('شعبهٔ دیگر'); $foreignSkill = $this->createSkill($otherUser, 'مهارت بیگانه'); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $foreignSkill['data']['uuid']]], ]); self::assertSame(404, $this->responseCode()); } /** مهارتی که روی منبعی نشسته حذف نمی‌شود؛ وگرنه FK خطای خام دیتابیس می‌داد. */ public function testSkillInUseCannotBeDeleted(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $skill = $this->createSkill($user); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $skill['data']['uuid']]], ]); $body = $this->authJson('DELETE', "/api/v1/skill/{$skill['data']['uuid']}", $user); self::assertSame(422, $this->responseCode()); // رقم لاتین عمدی است: قرارداد پیام‌های درون‌ریزیِ بک‌اند همین است // (BackfillServiceDurationCommand، WorkingHoursService و …) و قالب‌بندی فارسی // کارِ نمایش در کلاینت است. self::assertStringContainsString('1 منبع', $body['errors'][0]['message']); // بعد از برداشتن از منبع، حذف مجاز است. $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, ['skills' => []]); $this->authJson('DELETE', "/api/v1/skill/{$skill['data']['uuid']}", $user); self::assertSame(200, $this->responseCode()); } public function testSkillListReportsUsageCount(): void { [$user, , $address] = $this->clinicWithAddress(); $resource = $this->createResource($user, $address, $this->resourceType($address)); $skill = $this->createSkill($user); $before = $this->authJson('GET', '/api/v1/skills', $user); self::assertSame(0, $before['data'][0]['resources_count']); $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/skills", $user, [ 'skills' => [['skill_uuid' => $skill['data']['uuid']]], ]); $after = $this->authJson('GET', '/api/v1/skills', $user); self::assertSame(1, $after['data'][0]['resources_count']); } public function testBlankSkillNameIsRejected(): void { [$user] = $this->clinicWithAddress(); $body = $this->authJson('POST', '/api/v1/skills', $user, ['name' => ' ']); self::assertSame(422, $this->responseCode()); self::assertSame('name', $body['errors'][0]['field']); } }