diff --git a/.github/workflows/i18n-validate.yml b/.github/workflows/i18n-validate.yml new file mode 100644 index 00000000..4e49efa8 --- /dev/null +++ b/.github/workflows/i18n-validate.yml @@ -0,0 +1,69 @@ +name: I18n Validate + +on: + pull_request: + paths: + - 'resources/translations/**/*.xlf' + - 'composer.lock' + - 'composer.json' + +jobs: + validate-xliff: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + php: ['8.1'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: imap, zip + tools: composer:v2 + coverage: none + + - name: Cache Composer packages + uses: actions/cache@v4 + with: + path: | + ~/.composer/cache/files + key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer-${{ matrix.php }}- + + - name: Install dependencies (no dev autoloader scripts) + run: | + set -euo pipefail + composer install --no-interaction --no-progress --prefer-dist + + - name: Lint XLIFF with Symfony + run: | + set -euo pipefail + # Adjust the directory to match your repo layout + php bin/console lint:xliff resources/translations + + - name: Validate XLIFF XML with xmllint + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends libxml2-utils + # Adjust root dir; prune vendor; accept spaces/newlines safely + find resources/translations -type f -name '*.xlf' -not -path '*/vendor/*' -print0 \ + | xargs -0 -n1 xmllint --noout + + - name: Symfony translation sanity (extract dry-run) + run: | + set -euo pipefail + # Show what would be created/updated without writing files + php bin/console translation:extract en \ + --format=xlf \ + --domain=messages \ + --dump-messages \ + --no-interaction + # Note: omit --force to keep this a dry-run diff --git a/.weblate b/.weblate new file mode 100644 index 00000000..5917a8b8 --- /dev/null +++ b/.weblate @@ -0,0 +1,23 @@ +# .weblate +--- +projects: + - slug: phplist-core + name: phpList core + components: + - slug: messages + name: Messages + files: + # {language} is Weblate’s placeholder (e.g., fr, de, es) + - src: resources/translations/messages.en.xlf + template: true + # Where localized files live (mirrors Symfony layout) + target: resources/translations/messages.{language}.xlf + file_format: xliff + language_code_style: bcp + # Ensure placeholders like %name% are preserved + parse_file_headers: true + check_flags: + - xml-invalid + - placeholders + - urls + - accelerated diff --git a/config/config.yml b/config/config.yml index e235f999..7de6dca6 100644 --- a/config/config.yml +++ b/config/config.yml @@ -10,7 +10,10 @@ parameters: framework: #esi: ~ - #translator: { fallbacks: ['%locale%'] } + translator: + default_path: '%kernel.project_dir%/resources/translations' + fallbacks: ['%locale%'] + secret: '%secret%' router: resource: '%kernel.project_dir%/config/routing.yml' diff --git a/config/services/managers.yml b/config/services/managers.yml index 5ef215b3..22dbe066 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -4,6 +4,14 @@ services: autoconfigure: true public: false + PhpList\Core\Domain\Configuration\Service\Manager\ConfigManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager: + autowire: true + autoconfigure: true + PhpList\Core\Domain\Identity\Service\SessionManager: autowire: true autoconfigure: true @@ -80,10 +88,6 @@ services: autowire: true autoconfigure: true - PhpList\Core\Domain\Configuration\Service\Manager\ConfigManager: - autowire: true - autoconfigure: true - PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager: autowire: true autoconfigure: true diff --git a/config/services/repositories.yml b/config/services/repositories.yml index 82ae6a82..1289bea7 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -1,4 +1,14 @@ services: + PhpList\Core\Domain\Configuration\Repository\ConfigRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Configuration\Model\Config + + PhpList\Core\Domain\Configuration\Repository\EventLogRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Configuration\Model\EventLog + PhpList\Core\Domain\Identity\Repository\AdministratorRepository: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: @@ -66,11 +76,6 @@ services: arguments: - PhpList\Core\Domain\Messaging\Model\TemplateImage - PhpList\Core\Domain\Configuration\Repository\ConfigRepository: - parent: PhpList\Core\Domain\Common\Repository\AbstractRepository - arguments: - - PhpList\Core\Domain\Configuration\Model\Config - PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: diff --git a/config/services/services.yml b/config/services/services.yml index 19caddd8..1f509787 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -107,3 +107,10 @@ services: PhpList\Core\Domain\Messaging\Service\BounceActionResolver: arguments: - !tagged_iterator { tag: 'phplist.bounce_action_handler' } + + # I18n + PhpList\Core\Domain\Common\I18n\SimpleTranslator: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Common\I18n\TranslatorInterface: '@PhpList\Core\Domain\Common\I18n\SimpleTranslator' diff --git a/resources/translations/messages.en.xlf b/resources/translations/messages.en.xlf new file mode 100644 index 00000000..7e176e3e --- /dev/null +++ b/resources/translations/messages.en.xlf @@ -0,0 +1,44 @@ + + + + + + + + Not authorized + Not authorized + + + + Failed admin login attempt for '%login%' + Failed admin login attempt for '%login%' + + + + Login attempt for disabled admin '%login%' + Login attempt for disabled admin '%login%' + + + + + Administrator not found + Administrator not found + + + + + Subscriber list not found. + Subscriber list not found. + + + Subscriber does not exists. + Subscriber does not exists. + + + Subscription not found for this subscriber and list. + Subscription not found for this subscriber and list. + + + + + diff --git a/src/Domain/Common/I18n/Messages.php b/src/Domain/Common/I18n/Messages.php new file mode 100644 index 00000000..f9e8822f --- /dev/null +++ b/src/Domain/Common/I18n/Messages.php @@ -0,0 +1,29 @@ +page; + } + + public function getDateFrom(): ?DateTimeInterface + { + return $this->dateFrom; + } + + public function getDateTo(): ?DateTimeInterface + { + return $this->dateTo; + } +} diff --git a/src/Domain/Configuration/Model/I18n.php b/src/Domain/Configuration/Model/I18n.php index bffed897..b8eefd63 100644 --- a/src/Domain/Configuration/Model/I18n.php +++ b/src/Domain/Configuration/Model/I18n.php @@ -8,6 +8,11 @@ use PhpList\Core\Domain\Common\Model\Interfaces\DomainModel; use PhpList\Core\Domain\Configuration\Repository\I18nRepository; +/** + * @deprecated + * + * Symfony\Contracts\Translation will be used instead. + */ #[ORM\Entity(repositoryClass: I18nRepository::class)] #[ORM\Table(name: 'phplist_i18n')] #[ORM\UniqueConstraint(name: 'lanorigunq', columns: ['lan', 'original'])] diff --git a/src/Domain/Configuration/Repository/EventLogRepository.php b/src/Domain/Configuration/Repository/EventLogRepository.php index 7caf5462..47640007 100644 --- a/src/Domain/Configuration/Repository/EventLogRepository.php +++ b/src/Domain/Configuration/Repository/EventLogRepository.php @@ -4,11 +4,48 @@ namespace PhpList\Core\Domain\Configuration\Repository; +use InvalidArgumentException; +use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface; use PhpList\Core\Domain\Common\Repository\AbstractRepository; use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait; use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface; +use PhpList\Core\Domain\Configuration\Model\Filter\EventLogFilter; +use PhpList\Core\Domain\Configuration\Model\EventLog; class EventLogRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; + + /** + * @return EventLog[] + * @throws InvalidArgumentException + */ + public function getFilteredAfterId(int $lastId, int $limit, ?FilterRequestInterface $filter = null): array + { + $queryBuilder = $this->createQueryBuilder('e') + ->andWhere('e.id > :lastId') + ->setParameter('lastId', $lastId) + ->orderBy('e.id', 'ASC') + ->setMaxResults($limit); + + if ($filter === null) { + return $queryBuilder->getQuery()->getResult(); + } + + if (!$filter instanceof EventLogFilter) { + throw new InvalidArgumentException('Expected EventLogFilter.'); + } + + if ($filter->getPage() !== null) { + $queryBuilder->andWhere('e.page = :page')->setParameter('page', $filter->getPage()); + } + if ($filter->getDateFrom() !== null) { + $queryBuilder->andWhere('e.entered >= :dateFrom')->setParameter('dateFrom', $filter->getDateFrom()); + } + if ($filter->getDateTo() !== null) { + $queryBuilder->andWhere('e.entered <= :dateTo')->setParameter('dateTo', $filter->getDateTo()); + } + + return $queryBuilder->getQuery()->getResult(); + } } diff --git a/src/Domain/Configuration/Repository/I18nRepository.php b/src/Domain/Configuration/Repository/I18nRepository.php index f4465103..33fa599a 100644 --- a/src/Domain/Configuration/Repository/I18nRepository.php +++ b/src/Domain/Configuration/Repository/I18nRepository.php @@ -6,6 +6,7 @@ use PhpList\Core\Domain\Common\Repository\AbstractRepository; +/** @deprecated */ class I18nRepository extends AbstractRepository { } diff --git a/src/Domain/Configuration/Service/Manager/EventLogManager.php b/src/Domain/Configuration/Service/Manager/EventLogManager.php new file mode 100644 index 00000000..374db7ed --- /dev/null +++ b/src/Domain/Configuration/Service/Manager/EventLogManager.php @@ -0,0 +1,54 @@ +repository = $repository; + } + + public function log(string $page, string $entry): EventLog + { + $log = (new EventLog()) + ->setEntered(new DateTimeImmutable()) + ->setPage($page) + ->setEntry($entry); + + $this->repository->save($log); + + return $log; + } + + /** + * Get event logs with optional filters (page and date range) and cursor pagination. + * + * @return EventLog[] + */ + public function get( + int $lastId = 0, + int $limit = 50, + ?string $page = null, + ?DateTimeInterface $dateFrom = null, + ?DateTimeInterface $dateTo = null + ): array { + $filter = new EventLogFilter($page, $dateFrom, $dateTo); + return $this->repository->getFilteredAfterId($lastId, $limit, $filter); + } + + public function delete(EventLog $log): void + { + $this->repository->remove($log); + } +} diff --git a/src/Domain/Identity/Service/PasswordManager.php b/src/Domain/Identity/Service/PasswordManager.php index f6ad2a9e..2c7ebe1e 100644 --- a/src/Domain/Identity/Service/PasswordManager.php +++ b/src/Domain/Identity/Service/PasswordManager.php @@ -5,6 +5,7 @@ namespace PhpList\Core\Domain\Identity\Service; use DateTime; +use PhpList\Core\Domain\Common\I18n\Messages; use PhpList\Core\Domain\Identity\Model\AdminPasswordRequest; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Repository\AdminPasswordRequestRepository; @@ -13,6 +14,7 @@ use PhpList\Core\Security\HashGenerator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class PasswordManager { @@ -22,17 +24,20 @@ class PasswordManager private AdministratorRepository $administratorRepository; private HashGenerator $hashGenerator; private MessageBusInterface $messageBus; + private TranslatorInterface $translator; public function __construct( AdminPasswordRequestRepository $passwordRequestRepository, AdministratorRepository $administratorRepository, HashGenerator $hashGenerator, - MessageBusInterface $messageBus + MessageBusInterface $messageBus, + TranslatorInterface $translator ) { $this->passwordRequestRepository = $passwordRequestRepository; $this->administratorRepository = $administratorRepository; $this->hashGenerator = $hashGenerator; $this->messageBus = $messageBus; + $this->translator = $translator; } /** @@ -47,7 +52,8 @@ public function generatePasswordResetToken(string $email): string { $administrator = $this->administratorRepository->findOneBy(['email' => $email]); if ($administrator === null) { - throw new NotFoundHttpException('Administrator not found', null, 1500567100); + $message = $this->translator->trans(Messages::IDENTITY_ADMIN_NOT_FOUND); + throw new NotFoundHttpException($message, null, 1500567100); } $existingRequests = $this->passwordRequestRepository->findByAdmin($administrator); diff --git a/src/Domain/Identity/Service/SessionManager.php b/src/Domain/Identity/Service/SessionManager.php index 52daafa3..82f52af1 100644 --- a/src/Domain/Identity/Service/SessionManager.php +++ b/src/Domain/Identity/Service/SessionManager.php @@ -4,6 +4,9 @@ namespace PhpList\Core\Domain\Identity\Service; +use PhpList\Core\Domain\Common\I18n\Messages; +use Symfony\Contracts\Translation\TranslatorInterface; +use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager; use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; @@ -13,24 +16,36 @@ class SessionManager { private AdministratorTokenRepository $tokenRepository; private AdministratorRepository $administratorRepository; + private EventLogManager $eventLogManager; + private TranslatorInterface $translator; public function __construct( AdministratorTokenRepository $tokenRepository, - AdministratorRepository $administratorRepository + AdministratorRepository $administratorRepository, + EventLogManager $eventLogManager, + TranslatorInterface $translator ) { $this->tokenRepository = $tokenRepository; $this->administratorRepository = $administratorRepository; + $this->eventLogManager = $eventLogManager; + $this->translator = $translator; } public function createSession(string $loginName, string $password): AdministratorToken { $administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password); if ($administrator === null) { - throw new UnauthorizedHttpException('', 'Not authorized', null, 1500567098); + $entry = $this->translator->trans(Messages::AUTH_LOGIN_FAILED, ['login' => $loginName]); + $this->eventLogManager->log('login', $entry); + $message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED); + throw new UnauthorizedHttpException('', $message, null, 1500567098); } if ($administrator->isDisabled()) { - throw new UnauthorizedHttpException('', 'Not authorized', null, 1500567099); + $entry = $this->translator->trans(Messages::AUTH_LOGIN_DISABLED, ['login' => $loginName]); + $this->eventLogManager->log('login', $entry); + $message = $this->translator->trans(Messages::AUTH_NOT_AUTHORIZED); + throw new UnauthorizedHttpException('', $message, null, 1500567099); } $token = new AdministratorToken(); diff --git a/src/Domain/Subscription/Service/Manager/SubscriptionManager.php b/src/Domain/Subscription/Service/Manager/SubscriptionManager.php index bb3a0e14..764106ec 100644 --- a/src/Domain/Subscription/Service/Manager/SubscriptionManager.php +++ b/src/Domain/Subscription/Service/Manager/SubscriptionManager.php @@ -4,6 +4,7 @@ namespace PhpList\Core\Domain\Subscription\Service\Manager; +use PhpList\Core\Domain\Common\I18n\Messages; use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberList; @@ -11,21 +12,25 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Repository\SubscriptionRepository; +use Symfony\Contracts\Translation\TranslatorInterface; class SubscriptionManager { private SubscriptionRepository $subscriptionRepository; private SubscriberRepository $subscriberRepository; private SubscriberListRepository $subscriberListRepository; + private TranslatorInterface $translator; public function __construct( SubscriptionRepository $subscriptionRepository, SubscriberRepository $subscriberRepository, - SubscriberListRepository $subscriberListRepository + SubscriberListRepository $subscriberListRepository, + TranslatorInterface $translator ) { $this->subscriptionRepository = $subscriptionRepository; $this->subscriberRepository = $subscriberRepository; $this->subscriberListRepository = $subscriberListRepository; + $this->translator = $translator; } public function addSubscriberToAList(Subscriber $subscriber, int $listId): Subscription @@ -37,7 +42,8 @@ public function addSubscriberToAList(Subscriber $subscriber, int $listId): Subsc } $subscriberList = $this->subscriberListRepository->find($listId); if (!$subscriberList) { - throw new SubscriptionCreationException('Subscriber list not found.', 404); + $message = $this->translator->trans(Messages::SUBSCRIPTION_LIST_NOT_FOUND); + throw new SubscriptionCreationException($message, 404); } $subscription = new Subscription(); @@ -64,7 +70,8 @@ private function createSubscription(SubscriberList $subscriberList, string $emai { $subscriber = $this->subscriberRepository->findOneBy(['email' => $email]); if (!$subscriber) { - throw new SubscriptionCreationException('Subscriber does not exists.', 404); + $message = $this->translator->trans(Messages::SUBSCRIPTION_SUBSCRIBER_NOT_FOUND); + throw new SubscriptionCreationException($message, 404); } $existingSubscription = $this->subscriptionRepository @@ -101,7 +108,8 @@ private function deleteSubscription(SubscriberList $subscriberList, string $emai ->findOneBySubscriberEmailAndListId($subscriberList->getId(), $email); if (!$subscription) { - throw new SubscriptionCreationException('Subscription not found for this subscriber and list.', 404); + $message = $this->translator->trans(Messages::SUBSCRIPTION_NOT_FOUND_FOR_LIST_AND_SUBSCRIBER); + throw new SubscriptionCreationException($message, 404); } $this->subscriptionRepository->remove($subscription); diff --git a/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php b/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php new file mode 100644 index 00000000..818b8de0 --- /dev/null +++ b/tests/Unit/Domain/Configuration/Service/Manager/EventLogManagerTest.php @@ -0,0 +1,94 @@ +repository = $this->createMock(EventLogRepository::class); + $this->manager = new EventLogManager($this->repository); + } + + public function testLogCreatesAndPersists(): void + { + $this->repository->expects($this->once()) + ->method('save') + ->with($this->isInstanceOf(EventLog::class)); + + $log = $this->manager->log('dashboard', 'Viewed dashboard'); + + $this->assertInstanceOf(EventLog::class, $log); + $this->assertSame('dashboard', $log->getPage()); + $this->assertSame('Viewed dashboard', $log->getEntry()); + $this->assertNotNull($log->getEntered()); + $this->assertInstanceOf(DateTimeImmutable::class, $log->getEntered()); + } + + public function testDelete(): void + { + $log = new EventLog(); + $this->repository->expects($this->once()) + ->method('remove') + ->with($log); + + $this->manager->delete($log); + } + + public function testGetWithFiltersDelegatesToRepository(): void + { + $expected = [new EventLog(), new EventLog()]; + + $this->repository->expects($this->once()) + ->method('getFilteredAfterId') + ->with( + 100, + 25, + $this->callback(function (EventLogFilter $filter) { + // Use getters to validate + return method_exists($filter, 'getPage') + && $filter->getPage() === 'settings' + && $filter->getDateFrom() instanceof DateTimeImmutable + && $filter->getDateTo() instanceof DateTimeImmutable + && $filter->getDateFrom() <= $filter->getDateTo(); + }) + ) + ->willReturn($expected); + + $from = new DateTimeImmutable('-2 days'); + $to = new DateTimeImmutable('now'); + $result = $this->manager->get(lastId: 100, limit: 25, page: 'settings', dateFrom: $from, dateTo: $to); + + $this->assertSame($expected, $result); + } + + public function testGetWithoutFiltersDefaults(): void + { + $expected = []; + + $this->repository->expects($this->once()) + ->method('getFilteredAfterId') + ->with( + 0, + 50, + $this->anything() + ) + ->willReturn($expected); + + $result = $this->manager->get(); + $this->assertSame($expected, $result); + } +} diff --git a/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php b/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php index 85e02f81..59ace13d 100644 --- a/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/PasswordManagerTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Contracts\Translation\TranslatorInterface; class PasswordManagerTest extends TestCase { @@ -36,7 +37,8 @@ protected function setUp(): void passwordRequestRepository: $this->passwordRequestRepository, administratorRepository: $this->administratorRepository, hashGenerator: $this->hashGenerator, - messageBus: $this->messageBus + messageBus: $this->messageBus, + translator: $this->createMock(TranslatorInterface::class) ); } diff --git a/tests/Unit/Domain/Identity/Service/SessionManagerTest.php b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php index 44072452..14419b0e 100644 --- a/tests/Unit/Domain/Identity/Service/SessionManagerTest.php +++ b/tests/Unit/Domain/Identity/Service/SessionManagerTest.php @@ -4,16 +4,19 @@ namespace PhpList\Core\Tests\Unit\Domain\Identity\Service; +use PhpList\Core\Domain\Common\I18n\Messages; +use PhpList\Core\Domain\Configuration\Service\Manager\EventLogManager; use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; use PhpList\Core\Domain\Identity\Service\SessionManager; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Contracts\Translation\TranslatorInterface; class SessionManagerTest extends TestCase { - public function testCreateSessionWithInvalidCredentialsThrowsException(): void + public function testCreateSessionWithInvalidCredentialsThrowsExceptionAndLogs(): void { $adminRepo = $this->createMock(AdministratorRepository::class); $adminRepo->expects(self::once()) @@ -24,7 +27,24 @@ public function testCreateSessionWithInvalidCredentialsThrowsException(): void $tokenRepo = $this->createMock(AdministratorTokenRepository::class); $tokenRepo->expects(self::never())->method('save'); - $manager = new SessionManager($tokenRepo, $adminRepo); + $eventLogManager = $this->createMock(EventLogManager::class); + $eventLogManager->expects(self::once()) + ->method('log') + ->with('login', $this->stringContains('admin')); + + $translator = $this->createMock(TranslatorInterface::class); + $translator->expects(self::exactly(2)) + ->method('trans') + ->withConsecutive( + [Messages::AUTH_LOGIN_FAILED, ['login' => 'admin']], + [Messages::AUTH_NOT_AUTHORIZED, []] + ) + ->willReturnOnConsecutiveCalls( + "Failed admin login attempt for 'admin'", + 'Not authorized' + ); + + $manager = new SessionManager($tokenRepo, $adminRepo, $eventLogManager, $translator); $this->expectException(UnauthorizedHttpException::class); $this->expectExceptionMessage('Not authorized'); @@ -42,8 +62,10 @@ public function testDeleteSessionCallsRemove(): void ->with($token); $adminRepo = $this->createMock(AdministratorRepository::class); + $eventLogManager = $this->createMock(EventLogManager::class); + $translator = $this->createMock(TranslatorInterface::class); - $manager = new SessionManager($tokenRepo, $adminRepo); + $manager = new SessionManager($tokenRepo, $adminRepo, $eventLogManager, $translator); $manager->deleteSession($token); } } diff --git a/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php b/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php index e535a7fe..f0c1d3af 100644 --- a/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/Manager/SubscriptionManagerTest.php @@ -14,11 +14,13 @@ use PhpList\Core\Domain\Subscription\Service\Manager\SubscriptionManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Contracts\Translation\TranslatorInterface; class SubscriptionManagerTest extends TestCase { private SubscriptionRepository&MockObject $subscriptionRepository; private SubscriberRepository&MockObject $subscriberRepository; + private TranslatorInterface&MockObject $translator; private SubscriptionManager $manager; protected function setUp(): void @@ -26,10 +28,12 @@ protected function setUp(): void $this->subscriptionRepository = $this->createMock(SubscriptionRepository::class); $this->subscriberRepository = $this->createMock(SubscriberRepository::class); $subscriberListRepository = $this->createMock(SubscriberListRepository::class); + $this->translator = $this->createMock(TranslatorInterface::class); $this->manager = new SubscriptionManager( - $this->subscriptionRepository, - $this->subscriberRepository, - $subscriberListRepository + subscriptionRepository: $this->subscriptionRepository, + subscriberRepository: $this->subscriberRepository, + subscriberListRepository: $subscriberListRepository, + translator: $this->translator, ); } @@ -51,6 +55,7 @@ public function testCreateSubscriptionWhenSubscriberExists(): void public function testCreateSubscriptionThrowsWhenSubscriberMissing(): void { + $this->translator->method('trans')->willReturn('Subscriber does not exists.'); $this->expectException(SubscriptionCreationException::class); $this->expectExceptionMessage('Subscriber does not exists.');