diff --git a/tests/Controller/BrowserPluginControllerTest.php b/tests/Controller/BrowserPluginControllerTest.php new file mode 100644 index 00000000..8af82ce9 --- /dev/null +++ b/tests/Controller/BrowserPluginControllerTest.php @@ -0,0 +1,247 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Controller; + +use App\Entity\UserSystem\User; +use App\Settings\InfoProviderSystem\BrowserPluginSettings; +use PHPUnit\Framework\Attributes\Group; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpFoundation\Response; + +#[Group("slow")] +#[Group("DB")] +final class BrowserPluginControllerTest extends WebTestCase +{ + // --- GET /browser_info --- + + public function testGetInfoReturns401WhenNotAuthenticated(): void + { + $client = static::createClient(); + $client->request('GET', '/en/tools/info_providers/browser_info'); + + self::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); + } + + public function testGetInfoReturnsForbiddenForUnprivilegedUser(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'noread'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('GET', '/en/tools/info_providers/browser_info'); + + $this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN); + } + + public function testGetInfoReturns451WhenPluginDisabled(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + // BrowserPluginSettings::$enabled defaults to false + + $client->request('GET', '/en/tools/info_providers/browser_info'); + + self::assertResponseStatusCodeSame(451); + } + + public function testGetInfoReturnsJsonWithExpectedKeys(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('GET', '/en/tools/info_providers/browser_info'); + + self::assertResponseStatusCodeSame(Response::HTTP_OK); + self::assertResponseHeaderSame('Content-Type', 'application/json'); + + $data = json_decode((string) $client->getResponse()->getContent(), true); + $this->assertArrayHasKey('username', $data); + $this->assertArrayHasKey('instance_name', $data); + $this->assertArrayHasKey('url_providers', $data); + $this->assertIsString($data['username']); + $this->assertIsString($data['instance_name']); + $this->assertIsArray($data['url_providers']); + $this->assertNotEmpty($data['username']); + $this->assertNotEmpty($data['instance_name']); + } + + public function testGetInfoUrlProvidersHaveIdAndLabel(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('GET', '/en/tools/info_providers/browser_info'); + + self::assertResponseStatusCodeSame(Response::HTTP_OK); + $data = json_decode((string) $client->getResponse()->getContent(), true); + + foreach ($data['url_providers'] as $provider) { + $this->assertArrayHasKey('id', $provider); + $this->assertArrayHasKey('label', $provider); + $this->assertIsString($provider['id']); + $this->assertIsString($provider['label']); + $this->assertNotEmpty($provider['id']); + $this->assertNotEmpty($provider['label']); + } + } + + // --- POST /browser_html --- + + public function testSubmitHtmlReturns401WhenNotAuthenticated(): void + { + $client = static::createClient(); + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['url' => 'https://example.com', 'html' => '', 'title' => 'Test'])); + + self::assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); + } + + public function testSubmitHtmlReturns451WhenPluginDisabled(): void + { + $client = static::createClient(); + $this->loginAsUser($client, 'admin'); + // BrowserPluginSettings::$enabled defaults to false + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['url' => 'https://example.com', 'html' => '', 'title' => 'Test'])); + + self::assertResponseStatusCodeSame(451); + } + + public function testSubmitHtmlWithValidDataAndProvider(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode([ + 'url' => 'https://example.com/product/123', + 'html' => 'Product page', + 'title' => 'Some Product', + 'provider' => 'generic_web', + ])); + + self::assertResponseStatusCodeSame(Response::HTTP_OK); + $data = json_decode((string) $client->getResponse()->getContent(), true); + $this->assertArrayHasKey('redirect_url', $data); + $this->assertNotNull($data['redirect_url']); + $this->assertStringContainsString('generic_web', (string) $data['redirect_url']); + } + + public function testSubmitHtmlWithoutProviderReturnsNullRedirectUrl(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode([ + 'url' => 'https://example.com/product/123', + 'html' => 'Product page', + 'title' => 'Some Product', + ])); + + self::assertResponseStatusCodeSame(Response::HTTP_OK); + $data = json_decode((string) $client->getResponse()->getContent(), true); + $this->assertArrayHasKey('redirect_url', $data); + $this->assertNull($data['redirect_url']); + } + + public function testSubmitHtmlWithInvalidJsonReturns400(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], 'this is not valid json {'); + + self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); + } + + public function testSubmitHtmlWithMissingUrlReturns422(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['html' => '', 'title' => 'Test'])); + + self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); + } + + public function testSubmitHtmlWithMissingHtmlReturns422(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['url' => 'https://example.com', 'title' => 'Test'])); + + self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); + } + + public function testSubmitHtmlWithInvalidUrlReturns422(): void + { + $client = static::createClient(); + $client->disableReboot(); + $this->loginAsUser($client, 'admin'); + static::getContainer()->get(BrowserPluginSettings::class)->enabled = true; + + $client->request('POST', '/en/tools/info_providers/browser_html', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['url' => 'not-a-url', 'html' => '', 'title' => 'Test'])); + + self::assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); + } + + private function loginAsUser(mixed $client, string $username): void + { + $entityManager = static::getContainer()->get('doctrine')->getManager(); + $user = $entityManager->getRepository(User::class)->findOneBy(['name' => $username]); + if (!$user) { + $this->markTestSkipped("User '{$username}' not found in fixtures"); + } + $client->loginUser($user); + } +} diff --git a/tests/Services/InfoProviderSystem/DTOs/BrowserSubmittedPageTest.php b/tests/Services/InfoProviderSystem/DTOs/BrowserSubmittedPageTest.php new file mode 100644 index 00000000..bafff477 --- /dev/null +++ b/tests/Services/InfoProviderSystem/DTOs/BrowserSubmittedPageTest.php @@ -0,0 +1,86 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Services\InfoProviderSystem\DTOs; + +use App\Services\InfoProviderSystem\DTOs\BrowserSubmittedPage; +use PHPUnit\Framework\TestCase; + +final class BrowserSubmittedPageTest extends TestCase +{ + public function testTokenIsNonEmpty(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $this->assertNotEmpty($page->token); + } + + public function testTokenIsDeterministic(): void + { + $page1 = new BrowserSubmittedPage('https://example.com', '', 'Title A'); + $page2 = new BrowserSubmittedPage('https://example.com', '', 'Title B'); + + // Token is derived from URL + HTML only, title does not affect it + $this->assertSame($page1->token, $page2->token); + } + + public function testDifferentUrlProducesDifferentToken(): void + { + $page1 = new BrowserSubmittedPage('https://example.com/1', '', 'Test'); + $page2 = new BrowserSubmittedPage('https://example.com/2', '', 'Test'); + + $this->assertNotSame($page1->token, $page2->token); + } + + public function testDifferentHtmlProducesDifferentToken(): void + { + $page1 = new BrowserSubmittedPage('https://example.com', 'A', 'Test'); + $page2 = new BrowserSubmittedPage('https://example.com', 'B', 'Test'); + + $this->assertNotSame($page1->token, $page2->token); + } + + public function testTokenMatchesPageTokenProperty(): void + { + $page = new BrowserSubmittedPage('https://example.com', 'content', 'Test'); + $expected = hash('xxh3', 'https://example.com|content'); + + $this->assertSame($expected, $page->token); + } + + public function testDefaultSubmittedAtIsNow(): void + { + $before = new \DateTimeImmutable(); + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $after = new \DateTimeImmutable(); + + $this->assertGreaterThanOrEqual($before->getTimestamp(), $page->submittedAt->getTimestamp()); + $this->assertLessThanOrEqual($after->getTimestamp(), $page->submittedAt->getTimestamp()); + } + + public function testCustomSubmittedAt(): void + { + $dt = new \DateTimeImmutable('2025-01-01 12:00:00'); + $page = new BrowserSubmittedPage('https://example.com', '', 'Test', $dt); + + $this->assertSame($dt, $page->submittedAt); + } +} diff --git a/tests/Services/InfoProviderSystem/SubmittedPageStorageTest.php b/tests/Services/InfoProviderSystem/SubmittedPageStorageTest.php new file mode 100644 index 00000000..d754b2e1 --- /dev/null +++ b/tests/Services/InfoProviderSystem/SubmittedPageStorageTest.php @@ -0,0 +1,181 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Services\InfoProviderSystem; + +use App\Services\InfoProviderSystem\DTOs\BrowserSubmittedPage; +use App\Services\InfoProviderSystem\SubmittedPageStorage; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; + +final class SubmittedPageStorageTest extends TestCase +{ + private SubmittedPageStorage $storage; + private Session $session; + + protected function setUp(): void + { + $this->session = new Session(new MockArraySessionStorage()); + $request = new Request(); + $request->setSession($this->session); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $this->storage = new SubmittedPageStorage($requestStack, new ArrayAdapter()); + } + + public function testStoreReturnsToken(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $token = $this->storage->store($page); + + $this->assertSame($page->token, $token); + } + + public function testStoreAndRetrieve(): void + { + $page = new BrowserSubmittedPage('https://example.com', 'content', 'Test Page'); + $token = $this->storage->store($page); + + $retrieved = $this->storage->retrieve($token); + + $this->assertNotNull($retrieved); + $this->assertSame($page->url, $retrieved->url); + $this->assertSame($page->html, $retrieved->html); + $this->assertSame($page->title, $retrieved->title); + $this->assertSame($page->token, $retrieved->token); + } + + public function testRetrieveReturnsNullForUnknownToken(): void + { + $this->assertNull($this->storage->retrieve('nonexistent_token_xyz')); + } + + public function testStoreReturnsSameTokenForSameUrlAndHtml(): void + { + $page1 = new BrowserSubmittedPage('https://example.com', '', 'Title One'); + $page2 = new BrowserSubmittedPage('https://example.com', '', 'Title Two'); + + $this->assertSame($this->storage->store($page1), $this->storage->store($page2)); + } + + public function testRemoveByTokenDeletesFromCache(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $token = $this->storage->store($page); + + $this->storage->remove($token); + + $this->assertNull($this->storage->retrieve($token)); + } + + public function testRemoveByPageObjectDeletesFromCache(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $this->storage->store($page); + + $this->storage->remove($page); + + $this->assertNull($this->storage->retrieve($page->token)); + } + + public function testRemoveDeletesFromSession(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $this->storage->store($page); + + $this->storage->remove($page); + + $this->assertEmpty($this->storage->getRecentPages()); + } + + public function testGetRecentPagesReturnsStoredPages(): void + { + $page1 = new BrowserSubmittedPage('https://example.com/1', '1', 'Page 1'); + $page2 = new BrowserSubmittedPage('https://example.com/2', '2', 'Page 2'); + $this->storage->store($page1); + $this->storage->store($page2); + + $recent = $this->storage->getRecentPages(); + + $this->assertCount(2, $recent); + } + + public function testGetRecentPagesReturnsNewestFirst(): void + { + $page1 = new BrowserSubmittedPage('https://example.com/1', '1', 'Page 1'); + $page2 = new BrowserSubmittedPage('https://example.com/2', '2', 'Page 2'); + $this->storage->store($page1); + $this->storage->store($page2); + + $recent = $this->storage->getRecentPages(); + + $this->assertSame($page2->url, $recent[0]->url); + $this->assertSame($page1->url, $recent[1]->url); + } + + public function testStoreDeduplicatesSamePageInSession(): void + { + $page = new BrowserSubmittedPage('https://example.com', '', 'Test'); + $this->storage->store($page); + $this->storage->store($page); + + $this->assertCount(1, $this->storage->getRecentPages()); + } + + public function testStoreMovesResubmittedPageToTop(): void + { + $page1 = new BrowserSubmittedPage('https://example.com/1', '1', 'Page 1'); + $page2 = new BrowserSubmittedPage('https://example.com/2', '2', 'Page 2'); + $this->storage->store($page1); + $this->storage->store($page2); + // Resubmit page1 — it should move back to the top + $this->storage->store($page1); + + $recent = $this->storage->getRecentPages(); + + $this->assertSame($page1->url, $recent[0]->url); + $this->assertSame($page2->url, $recent[1]->url); + } + + public function testGetRecentPagesSilentlyOmitsExpiredEntries(): void + { + // Put a token in the session that has no corresponding cache entry (simulates expiry) + $this->session->set('browser_plugin_recent_urls', ['expired_token_xyz']); + + $this->assertEmpty($this->storage->getRecentPages()); + } + + public function testSessionCappedAtTenEntries(): void + { + for ($i = 0; $i < 12; $i++) { + $page = new BrowserSubmittedPage("https://example.com/{$i}", "{$i}", "Page {$i}"); + $this->storage->store($page); + } + + $this->assertCount(10, $this->storage->getRecentPages()); + } +}