diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 6ca6308..8ae42ff 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -2,7 +2,6 @@ name: Quality Assurance on: push: - branches: [ "main" ] pull_request: branches: [ "main" ] @@ -12,6 +11,15 @@ jobs: steps: - uses: actions/checkout@master + - name: Create Database Directory + run: | + mkdir -p var + touch var/test.db + chmod -R 777 var + + - name: Copy .env.test + run: cp .env.test .env.test.local + - name: PHP CS Fixer uses: docker://jakzal/phpqa:php8.3 with: @@ -42,7 +50,26 @@ jobs: with: args: bin/console lint:container + - name: Run Migrations + uses: docker://jakzal/phpqa:php8.3 + env: + DATABASE_URL: "sqlite:///%kernel.project_dir%/var/test.db" + APP_ENV: test + with: + args: bin/console doctrine:migrations:migrate --no-interaction --env=test + + - name: Load Fixtures + uses: docker://jakzal/phpqa:php8.3 + env: + DATABASE_URL: "sqlite:///%kernel.project_dir%/var/test.db" + APP_ENV: test + with: + args: bin/console doctrine:fixtures:load --no-interaction --env=test + - name: Run Tests uses: docker://jakzal/phpqa:php8.3 + env: + DATABASE_URL: "sqlite:///%kernel.project_dir%/var/test.db" + APP_ENV: test with: args: bin/phpunit --testdox \ No newline at end of file diff --git a/composer.json b/composer.json index ebe425c..acf63b0 100644 --- a/composer.json +++ b/composer.json @@ -98,6 +98,7 @@ } }, "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^4.0", "phpunit/phpunit": "^9.5", "symfony/browser-kit": "7.2.*", "symfony/css-selector": "7.2.*", diff --git a/composer.lock b/composer.lock index 9a1afc3..f706cf9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "40823ac0a24a9f1dc85cbd2c8075caed", + "content-hash": "1d9c000fec52ad9466aba3b339f2845b", "packages": [ { "name": "composer/semver", @@ -7577,6 +7577,175 @@ } ], "packages-dev": [ + { + "name": "doctrine/data-fixtures", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "2ae45139f148c9272c49a521d82cc50491355a99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/2ae45139f148c9272c49a521d82cc50491355a99", + "reference": "2ae45139f148c9272c49a521d82cc50491355a99", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^3.1", + "php": "^8.1", + "psr/log": "^1.1 || ^2 || ^3" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5.3", + "symfony/cache": "^6.4 || ^7", + "symfony/var-exporter": "^6.4 || ^7" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2024-12-10T07:03:23+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "90185317e6bb3d845667c5ebd444d9c83ae19a01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/90185317e6bb3d845667c5ebd444d9c83ae19a01", + "reference": "90185317e6bb3d845667c5ebd444d9c83ae19a01", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^2.0", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4 || ^3.0", + "php": "^8.1", + "psr/log": "^2 || ^3", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/doctrine-bridge": "^5.4.48 || ^6.4.16 || ^7.1.9", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.38 || ^11" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2024-12-05T18:35:55+00:00" + }, { "name": "masterminds/html5", "version": "2.9.0", diff --git a/config/bundles.php b/config/bundles.php index 165f54d..10b2d6d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -12,4 +12,5 @@ Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/config/packages/test/doctrine.yaml b/config/packages/test/doctrine.yaml new file mode 100644 index 0000000..ea97be8 --- /dev/null +++ b/config/packages/test/doctrine.yaml @@ -0,0 +1,5 @@ + +doctrine: + dbal: + driver: 'pdo_sqlite' + path: '%kernel.project_dir%/var/test.db' \ No newline at end of file diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..966d269 --- /dev/null +++ b/src/DataFixtures/AppFixtures.php @@ -0,0 +1,104 @@ +loadUsers($manager); + $this->loadPolls($manager); + $manager->flush(); + } + + private function loadUsers(ObjectManager $manager): void + { + $admin = $this->createUser('admin', 'adminpass', ['ROLE_ADMIN']); + $manager->persist($admin); + + $user = $this->createUser('user', 'userpass', ['ROLE_USER']); + $manager->persist($user); + } + + /** + * @param list $roles + */ + private function createUser(string $username, string $password, array $roles): User + { + $user = new User(); + $user->setUsername($username); + $user->setPassword($this->passwordHasher->hashPassword($user, $password)); + $user->setRoles($roles); + + return $user; + } + + private function loadPolls(ObjectManager $manager): void + { + $activePoll = $this->createActivePoll(); + $manager->persist($activePoll); + + $expiredPoll = $this->createExpiredPoll(); + $manager->persist($expiredPoll); + + // Créer plus de votes pour le sondage actif + $this->createVotesForPoll($manager, $activePoll, 10); + // Quelques votes pour le sondage expiré + $this->createVotesForPoll($manager, $expiredPoll, 5); + } + + private function createActivePoll(): Poll + { + $poll = new Poll(); + $poll + ->setTitle('Active Poll') + ->setDescription('This is an active poll') + ->setShortCode('abc123') + ->setStartAt(new \DateTimeImmutable('now')) + ->setEndAt(new \DateTimeImmutable('+1 hour')) + ->setQuestion1('Choice 1') + ->setQuestion2('Choice 2') + ->setQuestion3('Choice 3'); + + return $poll; + } + + private function createExpiredPoll(): Poll + { + $poll = new Poll(); + $poll + ->setTitle('Expired Poll') + ->setDescription('This is an expired poll') + ->setShortCode('xyz789') + ->setStartAt(new \DateTimeImmutable('-2 hours')) + ->setEndAt(new \DateTimeImmutable('-1 hour')) + ->setQuestion1('Choice 1') + ->setQuestion2('Choice 2'); + + return $poll; + } + + private function createVotesForPoll(ObjectManager $manager, Poll $poll, int $numberOfVotes): void + { + for ($i = 1; $i <= $numberOfVotes; ++$i) { + $vote = new Vote(); + $vote->setPoll($poll); + $vote->setChoice(($i % 3) + 1); // Répartir les votes entre les choix 1, 2 et 3 + $vote->setVoterId('visitor_'.$i); + $vote->setCreatedAt(new \DateTimeImmutable()); + $manager->persist($vote); + } + } +} diff --git a/symfony.lock b/symfony.lock index e28f3bc..d83687d 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,18 @@ "src/Repository/.gitignore" ] }, + "doctrine/doctrine-fixtures-bundle": { + "version": "4.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, "doctrine/doctrine-migrations-bundle": { "version": "3.3", "recipe": { diff --git a/tests/Command/CreateUserCommandTest.php b/tests/Command/CreateUserCommandTest.php new file mode 100644 index 0000000..67ea930 --- /dev/null +++ b/tests/Command/CreateUserCommandTest.php @@ -0,0 +1,69 @@ +userService = $this->createMock(UserService::class); + + $command = new CreateUserCommand($this->userService); + + $application = new Application(); + $application->add($command); + + $this->commandTester = new CommandTester($command); + } + + public function testExecuteSuccessfully(): void + { + $this->userService + ->expects($this->once()) + ->method('createUser') + ->with('testuser', 'testpass'); + + $this->commandTester->execute([ + 'username' => 'testuser', + 'password' => 'testpass', + ]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + $this->assertStringContainsString('User created', $this->commandTester->getDisplay()); + } + + public function testExecuteWithError(): void + { + $this->userService + ->expects($this->once()) + ->method('createUser') + ->willThrowException(new \Exception('User already exists')); + + $this->commandTester->execute([ + 'username' => 'existing_user', + 'password' => 'testpass', + ]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + $this->assertStringContainsString('User already exists', $this->commandTester->getDisplay()); + } + + public function testExecuteWithMissingArguments(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Not enough arguments (missing: "username, password")'); + + $this->commandTester->execute([]); + } +} diff --git a/tests/Entity/PollTest.php b/tests/Entity/PollTest.php new file mode 100644 index 0000000..8e6ee79 --- /dev/null +++ b/tests/Entity/PollTest.php @@ -0,0 +1,117 @@ +poll = new Poll(); + $this->startAt = new \DateTimeImmutable('2024-01-01 10:00:00'); + $this->endAt = new \DateTimeImmutable('2024-01-01 11:00:00'); + } + + public function testPollBasicAttributes(): void + { + $this->poll + ->setTitle('Test Poll') + ->setDescription('Test Description') + ->setShortCode('abc123') + ->setStartAt($this->startAt) + ->setEndAt($this->endAt); + + $this->assertEquals('Test Poll', $this->poll->getTitle()); + $this->assertEquals('Test Description', $this->poll->getDescription()); + $this->assertEquals('abc123', $this->poll->getShortCode()); + $this->assertSame($this->startAt, $this->poll->getStartAt()); + $this->assertSame($this->endAt, $this->poll->getEndAt()); + } + + public function testPollQuestions(): void + { + $this->poll + ->setQuestion1('Question 1') + ->setQuestion2('Question 2') + ->setQuestion3('Question 3') + ->setQuestion4('Question 4') + ->setQuestion5('Question 5'); + + $this->assertEquals('Question 1', $this->poll->getQuestion1()); + $this->assertEquals('Question 2', $this->poll->getQuestion2()); + $this->assertEquals('Question 3', $this->poll->getQuestion3()); + $this->assertEquals('Question 4', $this->poll->getQuestion4()); + $this->assertEquals('Question 5', $this->poll->getQuestion5()); + } + + public function testVoteManagement(): void + { + $vote = new Vote(); + $this->poll->addVote($vote); + + $this->assertCount(1, $this->poll->getVotes()); + $this->assertSame($this->poll, $vote->getPoll()); + + $this->poll->removeVote($vote); + $this->assertCount(0, $this->poll->getVotes()); + $this->assertNull($vote->getPoll()); + } + + public function testGetVotesForChoice(): void + { + $vote1 = new Vote(); + $vote1->setChoice(1); + + $vote2 = new Vote(); + $vote2->setChoice(1); + + $vote3 = new Vote(); + $vote3->setChoice(2); + + $this->poll->addVote($vote1); + $this->poll->addVote($vote2); + $this->poll->addVote($vote3); + + $this->assertEquals(2, $this->poll->getVotesForChoice(1)); + $this->assertEquals(1, $this->poll->getVotesForChoice(2)); + $this->assertEquals(0, $this->poll->getVotesForChoice(3)); + } + + public function testGetTotalVotes(): void + { + $vote1 = new Vote(); + $vote2 = new Vote(); + + $this->poll->addVote($vote1); + $this->poll->addVote($vote2); + + $this->assertEquals(2, $this->poll->getTotalVotes()); + } + + public function testRemoveVote(): void + { + $vote = new Vote(); + $this->poll->addVote($vote); + $this->assertEquals(1, $this->poll->getTotalVotes()); + + $this->poll->removeVote($vote); + $this->assertEquals(0, $this->poll->getTotalVotes()); + } + + public function testNullableFields(): void + { + $this->assertNull($this->poll->getDescription()); + $this->assertNull($this->poll->getQuestion1()); + $this->assertNull($this->poll->getQuestion2()); + $this->assertNull($this->poll->getQuestion3()); + $this->assertNull($this->poll->getQuestion4()); + $this->assertNull($this->poll->getQuestion5()); + } +} diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php new file mode 100644 index 0000000..1fc5ef7 --- /dev/null +++ b/tests/Entity/UserTest.php @@ -0,0 +1,42 @@ +user = new User(); + } + + public function testGetUserIdentifier(): void + { + $this->user->setUsername('testuser'); + $this->assertEquals('testuser', $this->user->getUserIdentifier()); + } + + public function testDefaultRoles(): void + { + $this->assertEquals(['ROLE_USER'], $this->user->getRoles()); + } + + public function testCustomRoles(): void + { + $this->user->setRoles(['ROLE_ADMIN']); + $roles = $this->user->getRoles(); + + $this->assertContains('ROLE_USER', $roles); + $this->assertContains('ROLE_ADMIN', $roles); + $this->assertCount(2, $roles); + } + + public function testEmptyUsernameReturnsAnonymous(): void + { + $this->assertEquals('anonymous', $this->user->getUserIdentifier()); + } +} diff --git a/tests/Entity/VoteTest.php b/tests/Entity/VoteTest.php new file mode 100644 index 0000000..d56aa29 --- /dev/null +++ b/tests/Entity/VoteTest.php @@ -0,0 +1,33 @@ +vote = new Vote(); + } + + public function testVoteAttributes(): void + { + $poll = new Poll(); + $createdAt = new \DateTimeImmutable(); + + $this->vote->setPoll($poll); + $this->vote->setVoterId('visitor123'); + $this->vote->setCreatedAt($createdAt); + $this->vote->setChoice(1); + + $this->assertSame($poll, $this->vote->getPoll()); + $this->assertEquals('visitor123', $this->vote->getVoterId()); + $this->assertSame($createdAt, $this->vote->getCreatedAt()); + $this->assertEquals(1, $this->vote->getChoice()); + } +} diff --git a/tests/Enum/FlashTypeEnumTest.php b/tests/Enum/FlashTypeEnumTest.php new file mode 100644 index 0000000..0e33fb3 --- /dev/null +++ b/tests/Enum/FlashTypeEnumTest.php @@ -0,0 +1,35 @@ +assertEquals($expectedClass, $type->getAlertClass()); + } + + public function provideFlashTypes(): array + { + return [ + 'success type' => [FlashTypeEnum::SUCCESS, 'alert-success'], + 'error type' => [FlashTypeEnum::ERROR, 'alert-error'], + 'warning type' => [FlashTypeEnum::WARNING, 'alert-warning'], + 'info type' => [FlashTypeEnum::INFO, 'alert-info'], + ]; + } + + public function testEnumValues(): void + { + $this->assertEquals('success', FlashTypeEnum::SUCCESS->value); + $this->assertEquals('error', FlashTypeEnum::ERROR->value); + $this->assertEquals('warning', FlashTypeEnum::WARNING->value); + $this->assertEquals('info', FlashTypeEnum::INFO->value); + } +} diff --git a/tests/Service/Poll/PollServiceTest.php b/tests/Service/Poll/PollServiceTest.php new file mode 100644 index 0000000..072f2ad --- /dev/null +++ b/tests/Service/Poll/PollServiceTest.php @@ -0,0 +1,62 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->pollRepository = $this->createMock(PollRepository::class); + $this->pollService = new PollService($this->entityManager, $this->pollRepository); + } + + public function testCreatePoll(): void + { + $poll = $this->pollService->createPoll(); + + $this->assertInstanceOf(Poll::class, $poll); + $this->assertNotNull($poll->getStartAt()); + $this->assertNotNull($poll->getEndAt()); + $this->assertNotNull($poll->getShortCode()); + } + + public function testCheckIfPollIsActive(): void + { + $this->pollRepository + ->expects($this->once()) + ->method('findOneActive') + ->willReturn(new Poll()); + + $this->assertTrue($this->pollService->checkIfPollIsActive()); + } + + public function testCheckIfPollHasVotes(): void + { + $poll = new Poll(); + $this->assertFalse($this->pollService->checkIfPollHasVotes($poll)); + } + + public function testCheckIfPollIsExpired(): void + { + $poll = new Poll(); + $poll->setEndAt(new \DateTimeImmutable('-1 hour')); + + $this->assertTrue($this->pollService->checkIfPollIsExpired($poll)); + } +} diff --git a/tests/Service/User/UserServiceTest.php b/tests/Service/User/UserServiceTest.php new file mode 100644 index 0000000..a7ce77b --- /dev/null +++ b/tests/Service/User/UserServiceTest.php @@ -0,0 +1,86 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->passwordHasher = $this->createMock(UserPasswordHasherInterface::class); + $this->userRepository = $this->createMock(UserRepository::class); + + $this->userService = new UserService( + $this->entityManager, + $this->passwordHasher, + $this->userRepository + ); + } + + public function testCreateUser(): void + { + // Configure Repository mock + $this->userRepository + ->expects($this->once()) + ->method('findOneBy') + ->with(['username' => 'testuser']) + ->willReturn(null); + + // Configure PasswordHasher mock avec willReturnCallback + $this->passwordHasher + ->expects($this->once()) + ->method('hashPassword') + ->willReturnCallback(function($user, $password) { + return 'hashed_' . $password; + }); + + // Configure EntityManager mock + $this->entityManager + ->expects($this->once()) + ->method('persist') + ->with($this->isInstanceOf(User::class)); + + $this->entityManager + ->expects($this->once()) + ->method('flush'); + + // Test + $user = $this->userService->createUser('testuser', 'password123'); + + $this->assertInstanceOf(User::class, $user); + $this->assertEquals('testuser', $user->getUsername()); + $this->assertEquals('hashed_password123', $user->getPassword()); + } + + public function testCreateUserWithExistingUsername(): void + { + $this->userRepository + ->expects($this->once()) + ->method('findOneBy') + ->willReturn(new User()); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('User already exists'); + + $this->userService->createUser('existing_user', 'password123'); + } +} diff --git a/tests/Service/Visitor/VisitorServiceTest.php b/tests/Service/Visitor/VisitorServiceTest.php new file mode 100644 index 0000000..e921a24 --- /dev/null +++ b/tests/Service/Visitor/VisitorServiceTest.php @@ -0,0 +1,78 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->visitorService = new VisitorService($this->entityManager); + } + + public function testCreateVote(): void + { + $poll = new Poll(); + $voterId = 'test123'; + + $vote = $this->visitorService->createVote($poll, $voterId); + + $this->assertInstanceOf(Vote::class, $vote); + $this->assertEquals($voterId, $vote->getVoterId()); + $this->assertSame($poll, $vote->getPoll()); + $this->assertNotNull($vote->getCreatedAt()); + } + + public function testSaveVote(): void + { + $vote = new Vote(); + + $this->entityManager + ->expects($this->once()) + ->method('persist') + ->with($vote); + + $this->entityManager + ->expects($this->once()) + ->method('flush'); + + $this->visitorService->saveVote($vote); + } + + public function testCheckIfVisitorHasVoted(): void + { + $poll = new Poll(); + $voterId = 'visitor123'; + + $vote = new Vote(); + $vote->setVoterId($voterId); + $poll->addVote($vote); + + $this->assertTrue($this->visitorService->checkIfVisitorHasVoted($voterId, $poll)); + $this->assertFalse($this->visitorService->checkIfVisitorHasVoted('other_visitor', $poll)); + } + + public function testGetClientIdFromRequest(): void + { + $request = new Request(); + $request->server->set('REMOTE_ADDR', '127.0.0.1'); + $request->headers->set('User-Agent', 'PHPUnit Test Browser'); + + $clientId = $this->visitorService->getClientIdFromRequest($request); + + $this->assertIsString($clientId); + $this->assertNotEmpty($clientId); + } +}