From ba778bdddff9eecc46f11087968f96155c3abb30 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Mon, 31 Aug 2026 11:14:23 +0200 Subject: [PATCH] Suppress reporting for expected API exceptions --- app/Exceptions/Api/ApiException.php | 1 - ...OwnerOfOrganizationWithMultipleMembers.php | 5 ++ .../Api/CanNotRemoveOwnerFromOrganization.php | 5 ++ .../ChangingRoleOfPlaceholderIsNotAllowed.php | 5 ++ .../ChangingRoleToPlaceholderIsNotAllowed.php | 5 ++ .../Api/EntityStillInUseApiException.php | 10 +++ ...reIsNotAvailableInFreePlanApiException.php | 5 ++ .../InactiveUserCanNotBeUsedApiException.php | 5 ++ ...onForTheEmailAlreadyExistsApiException.php | 5 ++ .../Api/OnlyOwnerCanChangeOwnership.php | 5 ++ ...aceholdersCanBeMergedIntoAnotherMember.php | 5 ++ ...ubscriptionButMultipleMembersException.php | 5 ++ .../Api/OrganizationNeedsAtLeastOneOwner.php | 5 ++ .../Api/OverlappingTimeEntryApiException.php | 5 ++ ...InvitedUseTheMergeToolInsteadException.php | 5 ++ ...TimeEntryCanNotBeRestartedApiException.php | 5 ++ .../Api/TimeEntryStillRunningApiException.php | 10 +++ ...lreadyMemberOfOrganizationApiException.php | 5 ++ ...erIsAlreadyMemberOfProjectApiException.php | 5 ++ .../Api/UserNotPlaceholderApiException.php | 5 ++ ...VerificationNoPendingEmailApiException.php | 5 ++ .../Unit/Exceptions/Api/ApiExceptionTest.php | 84 +++++++++++++++++++ 22 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Exceptions/Api/ApiExceptionTest.php diff --git a/app/Exceptions/Api/ApiException.php b/app/Exceptions/Api/ApiException.php index 34adf136..ad1cdffc 100644 --- a/app/Exceptions/Api/ApiException.php +++ b/app/Exceptions/Api/ApiException.php @@ -60,7 +60,6 @@ abstract class ApiException extends Exception */ public function report(): bool { - // TODO: temporary activated return false; } } diff --git a/app/Exceptions/Api/CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers.php b/app/Exceptions/Api/CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers.php index 507babe5..6e8f3a46 100644 --- a/app/Exceptions/Api/CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers.php +++ b/app/Exceptions/Api/CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException { public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/CanNotRemoveOwnerFromOrganization.php b/app/Exceptions/Api/CanNotRemoveOwnerFromOrganization.php index 6582e41f..ce59bd2c 100644 --- a/app/Exceptions/Api/CanNotRemoveOwnerFromOrganization.php +++ b/app/Exceptions/Api/CanNotRemoveOwnerFromOrganization.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class CanNotRemoveOwnerFromOrganization extends ApiException { public const string KEY = 'can_not_remove_owner_from_organization'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/ChangingRoleOfPlaceholderIsNotAllowed.php b/app/Exceptions/Api/ChangingRoleOfPlaceholderIsNotAllowed.php index 4f18c5a9..24792f37 100644 --- a/app/Exceptions/Api/ChangingRoleOfPlaceholderIsNotAllowed.php +++ b/app/Exceptions/Api/ChangingRoleOfPlaceholderIsNotAllowed.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class ChangingRoleOfPlaceholderIsNotAllowed extends ApiException { public const string KEY = 'changing_role_of_placeholder_is_not_allowed'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/ChangingRoleToPlaceholderIsNotAllowed.php b/app/Exceptions/Api/ChangingRoleToPlaceholderIsNotAllowed.php index be8d5f16..ccf484c7 100644 --- a/app/Exceptions/Api/ChangingRoleToPlaceholderIsNotAllowed.php +++ b/app/Exceptions/Api/ChangingRoleToPlaceholderIsNotAllowed.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class ChangingRoleToPlaceholderIsNotAllowed extends ApiException { public const string KEY = 'changing_role_to_placeholder_is_not_allowed'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/EntityStillInUseApiException.php b/app/Exceptions/Api/EntityStillInUseApiException.php index 56fcea94..9b5cabef 100644 --- a/app/Exceptions/Api/EntityStillInUseApiException.php +++ b/app/Exceptions/Api/EntityStillInUseApiException.php @@ -19,6 +19,16 @@ class EntityStillInUseApiException extends ApiException public const string KEY = 'entity_still_in_use'; + /** + * Report the exception. + * + * @return bool true means the exception handler will not report it again + */ + public function report(): bool + { + return true; + } + /** * Get the translated message for the exception. */ diff --git a/app/Exceptions/Api/FeatureIsNotAvailableInFreePlanApiException.php b/app/Exceptions/Api/FeatureIsNotAvailableInFreePlanApiException.php index 890ed646..3dca56cc 100644 --- a/app/Exceptions/Api/FeatureIsNotAvailableInFreePlanApiException.php +++ b/app/Exceptions/Api/FeatureIsNotAvailableInFreePlanApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class FeatureIsNotAvailableInFreePlanApiException extends ApiException { public const string KEY = 'feature_is_not_available_in_free_plan'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/InactiveUserCanNotBeUsedApiException.php b/app/Exceptions/Api/InactiveUserCanNotBeUsedApiException.php index dd59f783..f2828bfb 100644 --- a/app/Exceptions/Api/InactiveUserCanNotBeUsedApiException.php +++ b/app/Exceptions/Api/InactiveUserCanNotBeUsedApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class InactiveUserCanNotBeUsedApiException extends ApiException { public const string KEY = 'inactive_user_can_not_be_used'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/InvitationForTheEmailAlreadyExistsApiException.php b/app/Exceptions/Api/InvitationForTheEmailAlreadyExistsApiException.php index 8e6de813..2cee135c 100644 --- a/app/Exceptions/Api/InvitationForTheEmailAlreadyExistsApiException.php +++ b/app/Exceptions/Api/InvitationForTheEmailAlreadyExistsApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class InvitationForTheEmailAlreadyExistsApiException extends ApiException { public const string KEY = 'invitation_for_the_email_already_exists'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/OnlyOwnerCanChangeOwnership.php b/app/Exceptions/Api/OnlyOwnerCanChangeOwnership.php index 5d64ccee..1339a6e0 100644 --- a/app/Exceptions/Api/OnlyOwnerCanChangeOwnership.php +++ b/app/Exceptions/Api/OnlyOwnerCanChangeOwnership.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class OnlyOwnerCanChangeOwnership extends ApiException { public const string KEY = 'only_owner_can_change_ownership'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/OnlyPlaceholdersCanBeMergedIntoAnotherMember.php b/app/Exceptions/Api/OnlyPlaceholdersCanBeMergedIntoAnotherMember.php index 9f14c975..93627eda 100644 --- a/app/Exceptions/Api/OnlyPlaceholdersCanBeMergedIntoAnotherMember.php +++ b/app/Exceptions/Api/OnlyPlaceholdersCanBeMergedIntoAnotherMember.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class OnlyPlaceholdersCanBeMergedIntoAnotherMember extends ApiException { public const string KEY = 'only_placeholders_can_be_merged_into_another_member'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/OrganizationHasNoSubscriptionButMultipleMembersException.php b/app/Exceptions/Api/OrganizationHasNoSubscriptionButMultipleMembersException.php index 58cf371a..280c6f45 100644 --- a/app/Exceptions/Api/OrganizationHasNoSubscriptionButMultipleMembersException.php +++ b/app/Exceptions/Api/OrganizationHasNoSubscriptionButMultipleMembersException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class OrganizationHasNoSubscriptionButMultipleMembersException extends ApiException { public const string KEY = 'organization_has_no_subscription_but_multiple_members'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/OrganizationNeedsAtLeastOneOwner.php b/app/Exceptions/Api/OrganizationNeedsAtLeastOneOwner.php index 758f4fa1..ade13068 100644 --- a/app/Exceptions/Api/OrganizationNeedsAtLeastOneOwner.php +++ b/app/Exceptions/Api/OrganizationNeedsAtLeastOneOwner.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class OrganizationNeedsAtLeastOneOwner extends ApiException { public const string KEY = 'organization_needs_at_least_one_owner'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/OverlappingTimeEntryApiException.php b/app/Exceptions/Api/OverlappingTimeEntryApiException.php index 7c488955..d20252ca 100644 --- a/app/Exceptions/Api/OverlappingTimeEntryApiException.php +++ b/app/Exceptions/Api/OverlappingTimeEntryApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class OverlappingTimeEntryApiException extends ApiException { public const string KEY = 'overlapping_time_entry'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException.php b/app/Exceptions/Api/ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException.php index b2d13790..22f21011 100644 --- a/app/Exceptions/Api/ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException.php +++ b/app/Exceptions/Api/ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException extends ApiException { public const string KEY = 'this_placeholder_can_not_be_invited_use_the_merge_tool_instead_api_exception'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php b/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php index 34e73fb5..d3a2c5a3 100644 --- a/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php +++ b/app/Exceptions/Api/TimeEntryCanNotBeRestartedApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class TimeEntryCanNotBeRestartedApiException extends ApiException { public const string KEY = 'time_entry_can_not_be_restarted'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/TimeEntryStillRunningApiException.php b/app/Exceptions/Api/TimeEntryStillRunningApiException.php index c153534c..defb6f03 100644 --- a/app/Exceptions/Api/TimeEntryStillRunningApiException.php +++ b/app/Exceptions/Api/TimeEntryStillRunningApiException.php @@ -7,4 +7,14 @@ namespace App\Exceptions\Api; class TimeEntryStillRunningApiException extends ApiException { public const string KEY = 'time_entry_still_running'; + + /** + * Report the exception. + * + * @return bool true means the exception handler will not report it again + */ + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/UserIsAlreadyMemberOfOrganizationApiException.php b/app/Exceptions/Api/UserIsAlreadyMemberOfOrganizationApiException.php index fa925919..acf17c56 100644 --- a/app/Exceptions/Api/UserIsAlreadyMemberOfOrganizationApiException.php +++ b/app/Exceptions/Api/UserIsAlreadyMemberOfOrganizationApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class UserIsAlreadyMemberOfOrganizationApiException extends ApiException { public const string KEY = 'user_is_already_member_of_organization'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/UserIsAlreadyMemberOfProjectApiException.php b/app/Exceptions/Api/UserIsAlreadyMemberOfProjectApiException.php index 1c60785a..55600cdc 100644 --- a/app/Exceptions/Api/UserIsAlreadyMemberOfProjectApiException.php +++ b/app/Exceptions/Api/UserIsAlreadyMemberOfProjectApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class UserIsAlreadyMemberOfProjectApiException extends ApiException { public const string KEY = 'user_is_already_member_of_project'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/UserNotPlaceholderApiException.php b/app/Exceptions/Api/UserNotPlaceholderApiException.php index 92af4739..3007a8e6 100644 --- a/app/Exceptions/Api/UserNotPlaceholderApiException.php +++ b/app/Exceptions/Api/UserNotPlaceholderApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class UserNotPlaceholderApiException extends ApiException { public const string KEY = 'user_not_placeholder'; + + public function report(): bool + { + return true; + } } diff --git a/app/Exceptions/Api/UserResendEmailVerificationNoPendingEmailApiException.php b/app/Exceptions/Api/UserResendEmailVerificationNoPendingEmailApiException.php index ef8d442a..6e77d44e 100644 --- a/app/Exceptions/Api/UserResendEmailVerificationNoPendingEmailApiException.php +++ b/app/Exceptions/Api/UserResendEmailVerificationNoPendingEmailApiException.php @@ -7,4 +7,9 @@ namespace App\Exceptions\Api; class UserResendEmailVerificationNoPendingEmailApiException extends ApiException { public const string KEY = 'user_resend_email_verification_no_pending_email'; + + public function report(): bool + { + return true; + } } diff --git a/tests/Unit/Exceptions/Api/ApiExceptionTest.php b/tests/Unit/Exceptions/Api/ApiExceptionTest.php new file mode 100644 index 00000000..7fca6d79 --- /dev/null +++ b/tests/Unit/Exceptions/Api/ApiExceptionTest.php @@ -0,0 +1,84 @@ + + */ + public static function expectedApiExceptionProvider(): iterable + { + yield CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers::class => [new CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers]; + yield CanNotRemoveOwnerFromOrganization::class => [new CanNotRemoveOwnerFromOrganization]; + yield ChangingRoleOfPlaceholderIsNotAllowed::class => [new ChangingRoleOfPlaceholderIsNotAllowed]; + yield ChangingRoleToPlaceholderIsNotAllowed::class => [new ChangingRoleToPlaceholderIsNotAllowed]; + yield EntityStillInUseApiException::class => [new EntityStillInUseApiException('member', 'time_entry')]; + yield FeatureIsNotAvailableInFreePlanApiException::class => [new FeatureIsNotAvailableInFreePlanApiException]; + yield InactiveUserCanNotBeUsedApiException::class => [new InactiveUserCanNotBeUsedApiException]; + yield InvitationForTheEmailAlreadyExistsApiException::class => [new InvitationForTheEmailAlreadyExistsApiException]; + yield OnlyOwnerCanChangeOwnership::class => [new OnlyOwnerCanChangeOwnership]; + yield OnlyPlaceholdersCanBeMergedIntoAnotherMember::class => [new OnlyPlaceholdersCanBeMergedIntoAnotherMember]; + yield OrganizationHasNoSubscriptionButMultipleMembersException::class => [new OrganizationHasNoSubscriptionButMultipleMembersException]; + yield OrganizationNeedsAtLeastOneOwner::class => [new OrganizationNeedsAtLeastOneOwner]; + yield OverlappingTimeEntryApiException::class => [new OverlappingTimeEntryApiException]; + yield ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException::class => [new ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException]; + yield TimeEntryCanNotBeRestartedApiException::class => [new TimeEntryCanNotBeRestartedApiException]; + yield TimeEntryStillRunningApiException::class => [new TimeEntryStillRunningApiException]; + yield UserIsAlreadyMemberOfOrganizationApiException::class => [new UserIsAlreadyMemberOfOrganizationApiException]; + yield UserIsAlreadyMemberOfProjectApiException::class => [new UserIsAlreadyMemberOfProjectApiException]; + yield UserNotPlaceholderApiException::class => [new UserNotPlaceholderApiException]; + yield UserResendEmailVerificationNoPendingEmailApiException::class => [new UserResendEmailVerificationNoPendingEmailApiException]; + } + + #[DataProvider('expectedApiExceptionProvider')] + public function test_expected_api_exceptions_are_not_reported(ApiException $exception): void + { + $this->assertTrue($exception->report()); + } + + /** + * @return iterable + */ + public static function operationalApiExceptionProvider(): iterable + { + yield PdfRendererIsNotConfiguredException::class => [new PdfRendererIsNotConfiguredException]; + yield PersonalAccessClientIsNotConfiguredException::class => [new PersonalAccessClientIsNotConfiguredException]; + yield ExportException::class => [new ExportException]; + } + + #[DataProvider('operationalApiExceptionProvider')] + public function test_operational_api_exceptions_are_reported(ApiException $exception): void + { + $this->assertFalse($exception->report()); + } +}