createMock(ProviderManager::class); $registry->expects(self::once())->method('register')->with(ProviderInterface::TYPE_PREVIEW, 'wopi', Provider::class); (new Module($registry))->boot(); } public function testSupportedFormatsAndOutputs(): void { $provider = $this->provider($this->createStub(ConversionClient::class)); foreach (WopiService::FORMATS as $mime) { self::assertTrue($provider->supports($mime, $this->request())); } self::assertFalse($provider->supports('application/pdf', $this->request())); self::assertFalse($provider->supports('invalid', $this->request())); self::assertFalse($provider->supports(WopiService::FORMATS['docx'], new PreviewRequest(100, 100, 'application/pdf', 80))); } public function testDisabledWopiDoesNotOfferPreviews(): void { $tenant = $this->createStub(TenantContextInterface::class); $tenant->method('requireIdentifier')->willReturn('tenant'); $wopi = $this->createStub(WopiService::class); $wopi->method('configuration')->willThrowException(new \KTXM\ServiceWopi\Host\HostException('Disabled', 503)); $provider = new Provider($tenant, $wopi, $this->createStub(ConversionClient::class), new ImageProvider()); self::assertFalse($provider->supports(WopiService::FORMATS['docx'], $this->request())); } public function testRejectsStreamExceedingTenantLimitBeforeUpload(): void { $client = $this->createMock(ConversionClient::class); $client->expects(self::never())->method('pdf'); $this->expectException(PreviewGenerationException::class); $this->provider($client, 3)->generate($this->source('oversized'), $this->request()); } public function testConversionFailurePropagatesForManagerFallback(): void { $client = $this->createMock(ConversionClient::class); $client->expects(self::once())->method('pdf')->willThrowException(new PreviewGenerationException('Unavailable')); $this->expectException(PreviewGenerationException::class); $this->provider($client)->generate($this->source('document'), $this->request()); } public function testRendersOnlyFirstPageAndResizesToRequestedOutput(): void { // Two differently shaped, colored pages make selecting the wrong page observable. $pdf = new Imagick(); foreach ([[400, 200, 'red'], [200, 400, 'blue']] as [$width, $height, $color]) { $page = new Imagick(); $page->newImage($width, $height, $color, 'pdf'); $pdf->addImage($page); $page->clear(); } $bytes = $pdf->getImagesBlob(); $pdf->clear(); $client = $this->createMock(ConversionClient::class); $client->expects(self::once())->method('pdf')->with( self::callback(fn ($server) => $server->url === 'https://office.test'), 'document', 'docx', WopiService::FORMATS['docx'], )->willReturn($bytes); $result = $this->provider($client)->generate($this->source('document'), $this->request()); self::assertSame(100, $result->width); self::assertSame(50, $result->height); self::assertSame('image/webp', $result->resource->mimeType()); $output = implode('', iterator_to_array($result->resource->stream())); $image = new Imagick(); $image->readImageBlob($output); $color = $image->getImagePixelColor(20, 20)->getColor(); self::assertGreaterThan(200, $color['r']); self::assertLessThan(30, $color['b']); $image->clear(); } private function provider(ConversionClient $client, int $limit = 1048576): Provider { $tenant = $this->createStub(TenantContextInterface::class); $tenant->method('requireIdentifier')->willReturn('tenant'); $wopi = $this->createStub(WopiService::class); $wopi->method('configuration')->willReturn(new Configuration([ 'enabled' => true, 'office_server' => 'https://office.test', 'origin_server' => 'https://app.test', 'max_bytes' => $limit, ])); return new Provider($tenant, $wopi, $client, new ImageProvider()); } private function request(): PreviewRequest { return new PreviewRequest(100, 100, 'image/webp', 80); } private function source(string $content): PreviewSource { return new PreviewSource('document', 'file', 'version', WopiService::FORMATS['docx'], null, static fn () => new BinaryResource('file.docx', WopiService::FORMATS['docx'], (static function () use ($content): Generator { yield $content; })())); } }