From 4ce55db65887007486bc741f8bff02128d001d57 Mon Sep 17 00:00:00 2001 From: Aleksey Filippov Date: Thu, 26 Feb 2026 18:47:41 +0300 Subject: [PATCH] fix(ERP-241): use InvalidArgumentException in TimetableService for proper error display MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit TimetableService threw generic \Exception which EventBehavior couldn't handle, causing "Произошла неизвестная ошибка" in mobile app instead of actual validation messages. Changed to InvalidArgumentException (matching pattern used by all other api3 services) and switched from Json::encode to firstErrors[0] for human-readable messages. Co-Authored-By: Claude Opus 4.6 --- erp24/api3/core/services/TimetableService.php | 11 +- .../TimetableServiceExceptionTest.php | 135 ++++++++++++++++++ 2 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 erp24/tests/unit/services/TimetableServiceExceptionTest.php diff --git a/erp24/api3/core/services/TimetableService.php b/erp24/api3/core/services/TimetableService.php index 48d4167c..6449af93 100644 --- a/erp24/api3/core/services/TimetableService.php +++ b/erp24/api3/core/services/TimetableService.php @@ -4,7 +4,6 @@ namespace yii_app\api3\core\services; use yii\base\InvalidArgumentException; use yii\db\Exception; -use yii\helpers\Json; use yii\web\NotFoundHttpException; use yii_app\api3\modules\v1\models\Admin; use yii_app\api3\modules\v1\models\timetable\Timetable; @@ -39,7 +38,7 @@ class TimetableService //убрать после согласования оплаты подработчиков if (Admin::findOne($admin_id)->group_id === AdminGroup::GROUP_WORKERS && !$data->plan_id) { - throw new \Exception('Подработчики не могут открыть смены без плана!'); + throw new InvalidArgumentException('Подработчики не могут открыть смены без плана!'); } $transaction = \Yii::$app->db->beginTransaction(); @@ -55,7 +54,7 @@ class TimetableService $fact->tabel = 1; $fact->save(); if ($fact->getErrors()) { - throw new \Exception(Json::encode($fact->getErrors())); + throw new InvalidArgumentException(array_values($fact->firstErrors)[0] ?? ""); } } @@ -98,7 +97,7 @@ class TimetableService } if ($checkIn->getErrors()) { - throw new \Exception(Json::encode($checkIn->getErrors())); + throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? ""); } $transaction->commit(); @@ -169,7 +168,7 @@ class TimetableService } if ($checkIn->getErrors()) { - throw new \Exception(Json::encode($checkIn->getErrors())); + throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? ""); } return true; @@ -219,7 +218,7 @@ class TimetableService $checkIn->status = 0; $checkIn->save(); if ($checkIn->getErrors()) { - throw new \Exception(Json::encode($checkIn->getErrors())); + throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? ""); } $transaction->commit(); diff --git a/erp24/tests/unit/services/TimetableServiceExceptionTest.php b/erp24/tests/unit/services/TimetableServiceExceptionTest.php new file mode 100644 index 00000000..48c9f2cf --- /dev/null +++ b/erp24/tests/unit/services/TimetableServiceExceptionTest.php @@ -0,0 +1,135 @@ +serviceFilePath = dirname(__DIR__, 3) . '/api3/core/services/TimetableService.php'; + } + + // ======================================================================== + // ГРУППА 1: Код TimetableService не должен бросать голый \Exception + // ======================================================================== + + /** + * TimetableService не должен содержать throw new \Exception + * + * EventBehavior обрабатывает только ErrorException, HttpException, + * InvalidArgumentException. Голый \Exception приводит к "Произошла + * неизвестная ошибка" вместо понятного сообщения. + * + * @see \yii_app\api3\core\behaviors\EventBehavior::beforeSend() + */ + public function testService_NoGenericExceptionThrows(): void + { + if (!file_exists($this->serviceFilePath)) { + $this->markTestSkipped('TimetableService.php not found'); + } + + $content = file_get_contents($this->serviceFilePath); + + // throw new \Exception — голый Exception без типизации + $pattern = '/throw\s+new\s+\\\\Exception\s*\(/'; + preg_match_all($pattern, $content, $matches); + + $this->assertEmpty( + $matches[0], + 'TimetableService не должен бросать голый \\Exception. ' + . 'Используйте InvalidArgumentException для ошибок валидации. ' + . 'Найдено ' . count($matches[0]) . ' вхождений.' + ); + } + + /** + * TimetableService должен использовать InvalidArgumentException для ошибок валидации + */ + public function testService_UsesInvalidArgumentException(): void + { + if (!file_exists($this->serviceFilePath)) { + $this->markTestSkipped('TimetableService.php not found'); + } + + $content = file_get_contents($this->serviceFilePath); + + $this->assertStringContainsString( + 'use yii\base\InvalidArgumentException', + $content, + 'TimetableService должен импортировать InvalidArgumentException' + ); + + $this->assertStringContainsString( + 'throw new InvalidArgumentException', + $content, + 'TimetableService должен использовать InvalidArgumentException для ошибок валидации' + ); + } + + /** + * TimetableService должен передавать firstErrors[0] при ошибках save(), + * а не Json::encode(getErrors()) — для читаемого сообщения в API + */ + public function testService_UsesFirstErrorsNotJsonEncode(): void + { + if (!file_exists($this->serviceFilePath)) { + $this->markTestSkipped('TimetableService.php not found'); + } + + $content = file_get_contents($this->serviceFilePath); + + // Не должно быть Json::encode($...->getErrors()) в throw + $pattern = '/throw\s+new\s+\w+Exception\s*\(\s*Json::encode\s*\(/'; + preg_match_all($pattern, $content, $matches); + + $this->assertEmpty( + $matches[0], + 'TimetableService не должен использовать Json::encode(getErrors()) в исключениях. ' + . 'Используйте array_values($model->firstErrors)[0] для читаемого сообщения.' + ); + } + + // ======================================================================== + // ГРУППА 2: EventBehavior корректно обрабатывает InvalidArgumentException + // ======================================================================== + + /** + * EventBehavior должен возвращать type=invalid_request_type_2 + * для InvalidArgumentException + */ + public function testEventBehavior_HandlesInvalidArgumentException(): void + { + $behaviorPath = dirname(__DIR__, 3) . '/api3/core/behaviors/EventBehavior.php'; + if (!file_exists($behaviorPath)) { + $this->markTestSkipped('EventBehavior.php not found'); + } + + $content = file_get_contents($behaviorPath); + + $this->assertStringContainsString( + 'InvalidArgumentException', + $content, + 'EventBehavior должен обрабатывать InvalidArgumentException' + ); + + $this->assertStringContainsString( + 'invalid_request_type_2', + $content, + 'EventBehavior должен возвращать тип "invalid_request_type_2" для InvalidArgumentException' + ); + } +} -- 2.39.5