Done!

Integration Test


First: Mock the Actual Controller to return Exception

namespace App\Tests\Integration\Sentry\Mock;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;

class UnauthorizedHttpExceptionController
{
    public function index(): JsonResponse
    {
        throw new UnauthorizedHttpException('Unauthorized Http Exception!');
    }
}

Then, Actual Integration Test


namespace App\Tests\Integration\Sentry;

use App\Controller\PingController;
use App\Entity\UserDB\User;
use App\Tests\Integration\Controller\ControllerIntegrationTestCase;
use App\Tests\Integration\Sentry\Mock\AccessDeniedExceptionController;
use App\Tests\Integration\Sentry\Mock\DruidExceptionController;
use App\Tests\Integration\Sentry\Mock\NotFoundHttpExceptionController;
use App\Tests\Integration\Sentry\Mock\TooManyRequestsHttpExceptionController;
use App\Tests\Integration\Sentry\Mock\UnauthorizedHttpExceptionController;
use App\Tests\Integration\Sentry\Mock\UnknownUserExceptionController;
use Generator;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class LoggerExceptionTest extends ControllerIntegrationTestCase
{
    /**
     * @var MockResponse
     */
    private $mockResponse;

    public function setUp(): void
    {
        parent::setUp();

        $this->mockResponse = new MockResponse();
    }

    public function testOnDruidExceptionSendLogTagIsFoundOnSentryRequest(): void
    {
        $this->mockHttpClient();
        self::$container->set(PingController::class, new DruidExceptionController());
        $this->jsonRequest('GET', '/v3/ping');

        $result = json_decode($this->mockResponse->getRequestOptions()['body']);

        self::assertObjectHasAttribute('tags', $result);
        self::assertObjectHasAttribute('log', $result->tags);
        self::assertObjectHasAttribute('system', $result->tags);
        self::assertSame('sentry', $result->tags->log);
        self::assertSame('druid', $result->tags->system);
    }

    /**
     * @dataProvider sentryIgnoreErrorExceptionsDataProvider
     */
    public function testSentryIgnoreErrorsExceptionDoesNotSendRequestToSentry($sentryIgnoreErrorException): void
    {
        $this->mockHttpClient();
        self::$container->set(PingController::class, $sentryIgnoreErrorException);

        $this->jsonRequest('GET', '/v3/ping');
        $requestBody = $this->mockResponse->getRequestOptions();

        self::assertEmpty($requestBody);
    }

    /**
     * @dataProvider logExceptionEqualOrHigherThanErrorDataProvider
     */
    public function testOnLogExceptionEqualOrHigherThanErrorExceptionSentryLogTagIsFoundOnSentryRequest(string $exceptionType): void
    {
        $this->mockHttpClient();

        $logger = self::$container->get(LoggerInterface::class);
        $logger->{$exceptionType}('Some exception Message');

        $requestBody = $this->mockResponse->getRequestOptions()['body'];

        $result = json_decode($requestBody);

        self::assertObjectHasAttribute('tags', $result);
        self::assertObjectHasAttribute('log', $result->tags);
        self::assertSame('sentry', $result->tags->log);
    }

    /**
     * @dataProvider logExceptionLowerThanErrorDataProvider
     */
    public function testOnLogExceptionLowerThanErrorExceptionNoSentryRequestIsSent(string $exceptionType): void
    {
        $this->mockHttpClient();

        $logger = self::$container->get(LoggerInterface::class);
        $logger->{$exceptionType}('Some Exception Message');

        $requestBody = $this->mockResponse->getRequestOptions();

        self::assertEmpty($requestBody);
    }

    public function testOnLogErrorExceptionUserInformationIsFound(): void
    {
        $this->setIpAddressOnRequest();
        $this->setTokenStorage();
        $this->mockHttpClient();

        $logger = self::$container->get(LoggerInterface::class);
        $logger->error('Some Error Message');

        $requestBody = $this->mockResponse->getRequestOptions()['body'];

        $result = json_decode($requestBody);

        self::assertObjectHasAttribute('user', $result);
        self::assertObjectHasAttribute('id', $result->user);
        self::assertSame('123', $result->user->id);
        self::assertSame('127.0.0.1', $result->user->ip_address);
    }

    public function testOnLogErrorExceptionExtraDataIsFoundIfRequestIdIsSetOnRequest(): void
    {
        $this->setRequestIdOnRequest();
        $this->mockHttpClient();

        $logger = self::$container->get(LoggerInterface::class);
        $logger->error('Some Error Message');

        $requestBody = $this->mockResponse->getRequestOptions()['body'];

        $result = json_decode($requestBody);

        self::assertObjectHasAttribute('extra', $result);
        self::assertSame('234', $result->extra->request_id);
    }

    public function logExceptionEqualOrHigherThanErrorDataProvider(): Generator
    {
        yield ['error'];
        yield ['critical'];
        yield ['alert'];
        yield ['emergency'];
    }

    public function logExceptionLowerThanErrorDataProvider(): Generator
    {
        yield ['warning'];
        yield ['notice'];
        yield ['info'];
        yield ['debug'];
    }

    public function sentryIgnoreErrorExceptionsDataProvider(): Generator
    {
        yield [new UnknownUserExceptionController()];
        yield [new AccessDeniedExceptionController()];
        yield [new UnauthorizedHttpExceptionController()];
        yield [new TooManyRequestsHttpExceptionController()];
        yield [new NotFoundHttpExceptionController()];
    }

    private function setTokenStorage(): void
    {
        $user = new User();
        $user->setId('123');
        $token = new UsernamePasswordToken(
            $user,
            null,
            'secure_area',
            ['ROLE_ADMIN']
        );

        static::$kernel
            ->getContainer()
            ->get('security.token_storage')
            ->setToken($token);
    }

    private function setIpAddressOnRequest(): void
    {
        $requestStack = self::$container->get('request_stack');
        $requestStack->push(
            new Request(
                [],
                [],
                [],
                [],
                [],
                ['REMOTE_ADDR' => '127.0.0.1']
            )
        );
    }

    private function setRequestIdOnRequest(): void
    {
        $requestStack = self::$container->get('request_stack');
        $requestStack->push(
            new Request(
                [],
                [],
                [],
                [],
                [],
                ['REQUEST_ID' => '234']
            )
        );
    }

    private function mockHttpClient(): void
    {
        $mockClient = new MockHttpClient($this->mockResponse);
        $psr18client = new Psr18Client($mockClient);

        $roundRobinClient = self::$container->get('http.async.test.client');
        $roundRobinClient->addHttpClient($psr18client);
    }
}

Also, need to configure services_yaml on test to configure mock http dependencies

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: true

  App\Security\UserProvider:
    public: true

  Symfony\Component\Security\Core\Security:
    public: true
#
  App\EventListener\ExceptionListener:
    tags:
      - { name: kernel.event_listener, event: kernel.exception }

  security.authorization_checker:
    class: Symfony\Component\Security\Core\Authorization\AuthorizationChecker
    public: true

  App\Service\AnalyticsService:
    public: true

  ph.rate_limiting.test.calculation.analytics.api:
      alias: ph.rate_limiting.calculation.analytics.api
      public: true

  ph.rate_limiting.test.calculation.souq.analytics.api:
      alias: ph.rate_limiting.calculation.souq.analytics.api
      public: true

  # Public service definitions - required in order to replace services in tests
  # If the service identifier is an interface, you'll have to manually map it back to
  # its implementation
  App\Repository\UserDB\AnalyticsConsoleUserStateRepository:
  App\Repository\UserDB\CampaignRepository:
  App\Repository\UserDB\CampaignTermsRepository:
  App\Repository\UserDB\CampaignTermsAcknowledgementRepository:
  App\Repository\UserDB\CommissionGroupRepositoryInterface:
    class: App\Repository\UserDB\CommissionGroupRepository
  App\Repository\UserDB\CreativeTagRepository:
  App\Repository\UserDB\NetworkRepositoryInterface:
    class: App\Repository\UserDB\NetworkRepository
  App\Repository\UserDB\NetworkTermsRepositoryInterface:
    class: App\Repository\UserDB\NetworkTermsRepository
  App\Repository\UserDB\PartnerRepository:
  App\Repository\UserDB\PartnerGroupRepository:
  App\Repository\UserDB\ParticipationRepository:
  App\Repository\UserDB\UserRepository:
  App\Repository\UserDB\NetworkRepository:
  App\Repository\UserDB\FeatureRepositoryInterface:
    class: App\Repository\UserDB\FeatureRepository
  App\Repository\UserDB\UserResourceActionRepositoryInterface:
    class: App\Repository\UserDB\UserResourceActionRepository
  App\Service\PartnerizeTagService:
  App\Service\NetworkService:
  App\Service\FeatureService:
  App\Service\PartnerProspects\ProspectInvitationService:
    arguments: {
      $gatewaySourceName: 'my_test_gateway'
    }
  App\Service\Terms\NetworkTermsService:
  PH\UarBundle\Service\UserService:
  App\Repository\InputDB\AutogeneratedInvoiceRepositoryInterface:
  App\Service\AutogeneratedInvoiceReportService:
  PH\RateLimitingBundle\Services\RateLimitingServiceManager:
  psr18.druid:
    class: App\Tests\Integration\Mocks\PsrHttpClientProxyMock

  http.async.test.client:
    class: Http\Client\Common\HttpClientPool\RoundRobinClientPool
 
 Sentry\Transport\TransportFactoryInterface:
    class: Sentry\SentryBundle\Transport\TransportFactory
    arguments:
      $httpClient: '@http.async.test.client'
      $logger: null