service = new IntendedUrlService(); $this->session = $this->createMock(SessionStoreInterface::class); } // ── sanitize ────────────────────────────────────────────── public function testSanitizeAcceptsValidInternalPath(): void { $this->assertSame('/admin/users/edit/5', $this->service->sanitize('/admin/users/edit/5')); } public function testSanitizeAcceptsPathWithQueryString(): void { $this->assertSame('/admin/users?page=2&sort=name', $this->service->sanitize('/admin/users?page=2&sort=name')); } public function testSanitizeAcceptsLocalePrefixedPath(): void { $this->assertSame('/de/admin/users', $this->service->sanitize('/de/admin/users')); } public function testSanitizeRejectsEmptyString(): void { $this->assertSame('', $this->service->sanitize('')); } public function testSanitizeRejectsWhitespaceOnly(): void { $this->assertSame('', $this->service->sanitize(' ')); } public function testSanitizeRejectsRelativePath(): void { $this->assertSame('', $this->service->sanitize('admin/users')); } public function testSanitizeRejectsExternalUrlWithProtocol(): void { $this->assertSame('', $this->service->sanitize('https://evil.com/admin')); } public function testSanitizeRejectsProtocolRelativeUrl(): void { $this->assertSame('', $this->service->sanitize('//evil.com/admin')); } public function testSanitizeRejectsUrlWithEmbeddedProtocol(): void { $this->assertSame('', $this->service->sanitize('/admin?redirect=http://evil.com')); } public function testSanitizeRejectsAuthPrefix(): void { $this->assertSame('', $this->service->sanitize('/auth/callback')); } public function testSanitizeRejectsAuthPrefixWithLocale(): void { $this->assertSame('', $this->service->sanitize('/en/auth/callback')); } public function testSanitizeRejectsApiPrefix(): void { $this->assertSame('', $this->service->sanitize('/api/v1/users')); } public function testSanitizeRejectsLoginPath(): void { $this->assertSame('', $this->service->sanitize('/login')); } public function testSanitizeRejectsLoginWithLocale(): void { $this->assertSame('', $this->service->sanitize('/de/login')); } public function testSanitizeRejectsLogoutPath(): void { $this->assertSame('', $this->service->sanitize('/logout')); } public function testSanitizeRejectsFlashPrefix(): void { $this->assertSame('', $this->service->sanitize('/flash/something')); } public function testSanitizeRejectsBrandingPrefix(): void { $this->assertSame('', $this->service->sanitize('/branding/logo.png')); } public function testSanitizeRejectsOverlongUrl(): void { $url = '/' . str_repeat('a', 2048); $this->assertSame('', $this->service->sanitize($url)); } public function testSanitizeAcceptsMaxLengthUrl(): void { $url = '/' . str_repeat('a', 2046); $this->assertSame($url, $this->service->sanitize($url)); } // ── store ───────────────────────────────────────────────── public function testStoreWritesValidUrlToSession(): void { $this->session->expects($this->once()) ->method('set') ->with('intended_url', '/admin/users/edit/5'); $this->service->store('/admin/users/edit/5', $this->session); } public function testStoreIgnoresInvalidUrl(): void { $this->session->expects($this->never())->method('set'); $this->service->store('https://evil.com', $this->session); } public function testStoreIgnoresEmptyUrl(): void { $this->session->expects($this->never())->method('set'); $this->service->store('', $this->session); } // ── retrieve ────────────────────────────────────────────── public function testRetrieveReturnsStoredUrl(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn('/admin/dashboard'); $this->assertSame('/admin/dashboard', $this->service->retrieve($this->session)); } public function testRetrieveReturnsNullWhenEmpty(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn(null); $this->assertNull($this->service->retrieve($this->session)); } public function testRetrieveReturnsNullForEmptyString(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn(''); $this->assertNull($this->service->retrieve($this->session)); } public function testRetrieveReturnsNullForNonStringValue(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn(42); $this->assertNull($this->service->retrieve($this->session)); } // ── retrieveAndForget ───────────────────────────────────── public function testRetrieveAndForgetReturnsRoutePathAndRemovesIt(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn('/admin/users'); $this->session->expects($this->once()) ->method('remove') ->with('intended_url'); $this->assertSame('admin/users', $this->service->retrieveAndForget($this->session)); } public function testRetrieveAndForgetStripsLocalePrefix(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn('/de/admin/users/edit/5'); $this->session->expects($this->once()) ->method('remove') ->with('intended_url'); $this->assertSame('admin/users/edit/5', $this->service->retrieveAndForget($this->session)); } public function testRetrieveAndForgetPreservesQueryString(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn('/de/admin/users?page=2&sort=name'); $this->session->expects($this->once()) ->method('remove') ->with('intended_url'); $this->assertSame('admin/users?page=2&sort=name', $this->service->retrieveAndForget($this->session)); } public function testRetrieveAndForgetReturnsFallbackWhenNoUrlStored(): void { $this->session->expects($this->any()) ->method('get') ->with('intended_url') ->willReturn(null); $this->session->expects($this->once()) ->method('remove') ->with('intended_url'); $this->assertSame('admin', $this->service->retrieveAndForget($this->session)); } // ── buildLoginRedirect ──────────────────────────────────── public function testBuildLoginRedirectAppendsReturnTo(): void { $result = $this->service->buildLoginRedirect('/en/login', '/en/admin/users/edit/5'); $this->assertSame('/en/login?return_to=%2Fen%2Fadmin%2Fusers%2Fedit%2F5', $result); } public function testBuildLoginRedirectAppendsWithAmpersandWhenQueryExists(): void { $result = $this->service->buildLoginRedirect('/en/login?tenant=acme', '/en/admin/users'); $this->assertSame('/en/login?tenant=acme&return_to=%2Fen%2Fadmin%2Fusers', $result); } public function testBuildLoginRedirectReturnsLoginPathWhenReturnToIsInvalid(): void { $result = $this->service->buildLoginRedirect('/en/login', 'https://evil.com'); $this->assertSame('/en/login', $result); } public function testBuildLoginRedirectReturnsLoginPathWhenReturnToIsEmpty(): void { $result = $this->service->buildLoginRedirect('/en/login', ''); $this->assertSame('/en/login', $result); } public function testBuildLoginRedirectReturnsLoginPathWhenReturnToIsBlockedPath(): void { $result = $this->service->buildLoginRedirect('/en/login', '/auth/callback'); $this->assertSame('/en/login', $result); } }