diff --git a/tests/src/Kernel/Controller/ViewerControllerTest.php b/tests/src/Kernel/Controller/ViewerControllerTest.php new file mode 100644 index 00000000..13b67349 --- /dev/null +++ b/tests/src/Kernel/Controller/ViewerControllerTest.php @@ -0,0 +1,355 @@ +createMock(CollaboraDiscoveryFetcherInterface::class); + $file = dirname(__DIR__, 3) . '/fixtures/discovery.mimetypes.xml'; + $xml = file_get_contents($file); + $fetcher->method('getDiscoveryXml')->willReturn($xml); + $this->container->set(CollaboraDiscoveryFetcherInterface::class, $fetcher); + + $this->logger = new TestLogger(); + \Drupal::service('logger.factory')->addLogger($this->logger); + + $collabora_settings = \Drupal::configFactory()->getEditable('collabora_online.settings'); + $cool = $collabora_settings->get('cool'); + $cool['key_id'] = 'collabora'; + $collabora_settings->set('cool', $cool)->save(); + + $this->media = $this->createMediaEntity('document'); + $this->user = $this->createUser([ + 'access content', + 'preview document in collabora', + 'edit any document in collabora', + ]); + $fid = $this->media->getSource()->getSourceFieldValue($this->media); + $this->file = File::load($fid); + + $this->setCurrentUser($this->user); + } + + /** + * Tests successful requests. + * + * @covers ::editor + */ + public function testEditor(): void { + // Requests to view the editor. + $request = $this->createRequest('view'); + $this->assertResponseOk($request); + $request = $this->createRequest('view', write: TRUE); + $this->assertResponseOk($request); + + // Requests to edit the editor. + $request = $this->createRequest('edit', write: TRUE); + $this->assertResponseOk($request); + $request = $this->createRequest('edit'); + $this->assertResponseOk($request); + } + + /** + * Tests requests with unavailable Collabora. + * + * @covers ::editor + */ + public function testEditorCollaboraUnavailable(): void { + // Collabora is not available on tests, we need to restore the service. + $fetcher = $this->createMock(CollaboraDiscoveryFetcherInterface::class); + $this->container->set(CollaboraDiscoveryFetcherInterface::class, $fetcher); + + // Requests to view the editor. + $request = $this->createRequest('view'); + $this->assertBadRequestResponse( + 'The Collabora Online editor/viewer is not available.', + $request, + "Collabora Online is not available.
\n" . Error::DEFAULT_ERROR_MESSAGE, + ); + + // Requests to edit the editor. + $request = $this->createRequest('edit'); + $this->assertBadRequestResponse( + 'The Collabora Online editor/viewer is not available.', + $request, + "Collabora Online is not available.
\n" . Error::DEFAULT_ERROR_MESSAGE, + ); + } + + /** + * Tests requests with a scheme not matching the Collabora client URL. + * + * @covers ::editor + */ + public function testEditorMismatchScheme(): void { + // Requests to view the editor. + $request = $this->createRequest('view', 'https'); + $this->assertBadRequestResponse( + 'Viewer error: Protocol mismatch.', + $request, + "The current request uses 'https' url scheme, but the Collabora client url is 'http'.", + ); + + // Requests to edit the editor. + $request = $this->createRequest('edit', 'https'); + $this->assertBadRequestResponse( + 'Viewer error: Protocol mismatch.', + $request, + "The current request uses 'https' url scheme, but the Collabora client url is 'http'.", + ); + } + + /** + * Tests requests with a no viewer avaliable after request. + * + * @covers ::editor + */ + public function testEditorNoViewer(): void { + $request = $this->createRequest('view'); + + // Mock transcoder to force fail. + $transcoder = $this->createMock(jwtTranscoderInterface::class); + $transcoder->method('encode')->willThrowException(new CollaboraNotAvailableException()); + $this->container->set(jwtTranscoderInterface::class, $transcoder); + $this->expectException(CollaboraNotAvailableException::class); + + // Requests to view the editor. + $this->assertBadRequestResponse( + 'The Collabora Online editor/viewer is not available.', + $request, + "Cannot show the viewer/editor.
\n" . Error::DEFAULT_ERROR_MESSAGE, + ); + + // Requests to edit the editor. + $request = $this->createRequest('edit'); + $this->assertBadRequestResponse( + 'The Collabora Online editor/viewer is not available.', + $request, + [ + 'message' => "Cannot show the viewer/editor.
\n" . Error::DEFAULT_ERROR_MESSAGE, + ] + ); + } + + /** + * Creates a view/edit request. + * + * @param string $action + * E.g. 'view' or 'edit'. + * @param string $scheme + * The protocol used for the request. + * @param int|null $media_id + * Media entity id, if different from the default. + * @param int|null $user_id + * User id, if different from the default. + * @param bool $write + * TRUE if write access is requested. + * @param array $token_payload + * Explicit token payload values. + * This can be used to cause a bad token. + * + * @return \Symfony\Component\HttpFoundation\Request + * The request. + */ + protected function createRequest( + string $action, + string $scheme = 'http', + ?int $media_id = NULL, + ?int $user_id = NULL, + bool $write = FALSE, + array $token_payload = [], + ): Request { + $media_id ??= (int) $this->media->id(); + $user_id ??= (int) $this->user->id(); + $uri = "$scheme://localhost/cool/$action/$media_id"; + $token = $this->createAccessToken($media_id, $user_id, $write, $token_payload); + $parameters = [ + 'media' => $media_id, + 'edit' => $write, + 'access_token' => $token, + ]; + return Request::create($uri, 'GET', $parameters); + } + + /** + * Retrieves an encoded access token. + * + * @param int|null $fid + * The file id. + * @param int|null $uid + * The user id. + * @param bool $write + * The write permission. + * @param array $payload + * Explicit payload values. + * This can be used to cause a bad token. + * + * @return string + * The enconded token. + */ + protected function createAccessToken(?int $fid = NULL, ?int $uid = NULL, bool $write = FALSE, array $payload = []): string { + /** @var \Drupal\collabora_online\Jwt\JwtTranscoderInterface $transcoder */ + $transcoder = \Drupal::service(JwtTranscoderInterface::class); + $expire_timestamp = gettimeofday(TRUE) + 1000; + $payload += [ + 'fid' => (string) ($fid ?? $this->media->id()), + 'uid' => (string) ($uid ?? $this->user->id()), + 'wri' => $write, + 'exp' => $expire_timestamp, + ]; + return $transcoder->encode($payload, $expire_timestamp); + } + + /** + * Asserts an sucessful response given a request. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request to perform. + * @param string $message + * Message to distinguish this from other assertions. + */ + protected function assertResponseOk(Request $request, string $message = ''): void { + $response = $this->handleRequest($request); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode(), $message); + $this->assertStringContainsString('iframe', $response->getContent(), $message); + $this->assertEquals('', $response->headers->get('Content-Type'), $message); + } + + /** + * Asserts an bad request response given a request. + * + * @param string $expected_content + * The expected content. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request to perform. + * @param array $expected_log + * The expected log entry. + * @param string $message + * Message to distinguish this from other assertions. + */ + protected function assertBadRequestResponse(string $expected_content, Request $request, string $expected_log = '', string $message = ''): void { + $this->assertResponse( + Response::HTTP_BAD_REQUEST, + $expected_content, + 'text/plain', + $request, + $message, + ); + + if ($expected_log) { + $this->assertTrue( + $this->logger->hasRecord($expected_log), + sprintf('The logger does not contain a record like: "%s".', $expected_log) + ); + $this->logger->reset(); + } + } + + /** + * Asserts status code and content in a response given a request. + * + * @param int $expected_code + * The expected response status code. + * @param string $expected_content + * The expected response content. + * @param string $expected_content_type + * The type of content of the response. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request to perform. + * @param string $message + * Message to distinguish this from other assertions. + */ + protected function assertResponse(int $expected_code, string $expected_content, string $expected_content_type, Request $request, string $message = ''): void { + $response = $this->handleRequest($request); + + $this->assertEquals($expected_code, $response->getStatusCode(), $message); + $this->assertEquals($expected_content, $response->getContent(), $message); + $this->assertEquals($expected_content_type, $response->headers->get('Content-Type'), $message); + } + + /** + * Handles a request and gets the response. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Incoming request. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response. + */ + protected function handleRequest(Request $request): Response { + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ + $kernel = \Drupal::service('http_kernel'); + return $kernel->handle($request); + } + +}