request = $this->createStub(IRequest::class); $this->navigationManager = $this->createStub(INavigationManager::class); $this->urlGenerator = $this->createStub(IURLGenerator::class); $this->controller = $this->createStub(Controller::class); $this->middleware = new DesktopMiddleware( $this->request, $this->navigationManager, $this->urlGenerator, ); } private function configureDesktopRequest(string $entryHref, string $requestUri): void { $this->request->method('getHeader')->willReturn('AscDesktopEditor/6.0'); $this->navigationManager->method('getDefaultEntryIdForUser')->willReturn('dashboard'); $this->navigationManager->method('get')->willReturn(['id' => 'dashboard', 'href' => $entryHref]); $this->request->method('getRequestUri')->willReturn($requestUri); } /** * Passes through without redirecting when the User-Agent header is empty. */ public function testPassesThroughWhenUserAgentIsEmpty(): void { $this->request->method('getHeader')->willReturn(''); $this->middleware->beforeController($this->controller, 'index'); $this->addToAssertionCount(1); } /** * Passes through without redirecting when the User-Agent belongs to a regular browser, not the desktop client. */ public function testPassesThroughForNonDesktopUserAgent(): void { $this->request->method('getHeader')->willReturn('Mozilla/5.0 (X11; Linux x86_64)'); $this->middleware->beforeController($this->controller, 'index'); $this->addToAssertionCount(1); } /** * Passes through without redirecting when the user's default app is already files. */ public function testPassesThroughWhenDefaultAppIsFiles(): void { $this->request->method('getHeader')->willReturn('AscDesktopEditor/6.0'); $this->navigationManager->method('getDefaultEntryIdForUser')->willReturn('files'); $this->middleware->beforeController($this->controller, 'index'); $this->addToAssertionCount(1); } /** * Passes through without redirecting when the default app has no registered navigation entry. */ public function testPassesThroughWhenNavigationEntryNotFound(): void { $this->request->method('getHeader')->willReturn('AscDesktopEditor/6.0'); $this->navigationManager->method('getDefaultEntryIdForUser')->willReturn('dashboard'); $this->navigationManager->method('get')->willReturn(null); $this->middleware->beforeController($this->controller, 'index'); $this->addToAssertionCount(1); } /** * Passes through without redirecting when the request is on a subpath of the default app, not its root. */ public function testPassesThroughWhenOnSubpathOfDefaultApp(): void { $this->configureDesktopRequest( 'https://example.com/apps/dashboard', '/apps/dashboard/settings' ); $this->middleware->beforeController($this->controller, 'index'); $this->addToAssertionCount(1); } /** * Redirects when the desktop client lands on the root of the default app with no trailing slash on either side. */ public function testRedirectsWhenDesktopClientLandsOnDefaultApp(): void { $this->expectException(DesktopRedirectException::class); $this->configureDesktopRequest( 'https://example.com/apps/dashboard', '/apps/dashboard' ); $this->middleware->beforeController($this->controller, 'index'); } /** * Redirects correctly when the entry href has a trailing slash but the request URI does not, and vice versa. */ public function testRedirectNormalisesTrailingSlashes(): void { $this->expectException(DesktopRedirectException::class); $this->configureDesktopRequest( 'https://example.com/apps/dashboard/', '/apps/dashboard' ); $this->middleware->beforeController($this->controller, 'index'); } /** * Redirects when pretty URLs are in use (no index.php in either the entry href or the request URI). */ public function testRedirectWithPrettyUrls(): void { $this->expectException(DesktopRedirectException::class); $this->configureDesktopRequest( 'https://example.com/apps/dashboard', '/apps/dashboard' ); $this->middleware->beforeController($this->controller, 'index'); } /** * Redirects when index.php is present consistently in both the entry href and the request URI (no pretty URLs). */ public function testRedirectWithIndexPhpInBothPaths(): void { $this->expectException(DesktopRedirectException::class); $this->configureDesktopRequest( 'https://example.com/index.php/apps/dashboard', '/index.php/apps/dashboard' ); $this->middleware->beforeController($this->controller, 'index'); } /** * Redirects when Nextcloud is installed in a subdirectory and both paths share the same prefix. */ public function testRedirectWithSubdirectoryInstall(): void { $this->expectException(DesktopRedirectException::class); $this->configureDesktopRequest( 'https://example.com/nextcloud/apps/dashboard', '/nextcloud/apps/dashboard' ); $this->middleware->beforeController($this->controller, 'index'); } /** * Returns a RedirectResponse pointing to the files app when given a DesktopRedirectException. */ public function testAfterExceptionReturnsRedirectToFilesForDesktopRedirectException(): void { $this->urlGenerator->method('linkToRouteAbsolute')->willReturn('https://example.com/apps/files/'); $response = $this->middleware->afterException( $this->controller, 'index', new DesktopRedirectException() ); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('https://example.com/apps/files/', $response->getRedirectURL()); } /** * Rethrows any exception that is not a DesktopRedirectException, leaving it for other middleware to handle. */ public function testAfterExceptionRethrowsUnrelatedExceptions(): void { $this->expectException(\RuntimeException::class); $this->middleware->afterException( $this->controller, 'index', new \RuntimeException('unexpected') ); } }