generated from Nodarx/template
92e85aed47
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
160 lines
8.9 KiB
PHP
160 lines
8.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Service\TenantService;
|
|
use KTXM\ServiceWopi\Controllers\WopiController;
|
|
use KTXM\ServiceWopi\Discovery\DiscoveryClient;
|
|
use KTXM\ServiceWopi\Host\Configuration;
|
|
use KTXM\ServiceWopi\Host\Documents;
|
|
use KTXM\ServiceWopi\Host\HostException;
|
|
use KTXM\ServiceWopi\Host\WopiService;
|
|
use KTXM\ServiceWopi\Host\ProofValidator;
|
|
use KTXM\ServiceWopi\Host\SessionStore;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class MemorySessions extends SessionStore
|
|
{
|
|
public array $records = [];
|
|
public array $cacheRecords = [];
|
|
public function __construct() {}
|
|
public function fileId(string $tenant, string $user, string $resource): string { return str_repeat('a', 32); }
|
|
public function save(string $token, array $session): void { $this->records[hash('sha256', $token)] = $session; }
|
|
public function find(string $tenant, string $file, string $token, int $now): ?array
|
|
{
|
|
$s = $this->records[hash('sha256', $token)] ?? null;
|
|
return $s && $s['tenant'] === $tenant && $s['file'] === $file && $s['expires'] > $now && $s['mode'] === 'view' ? $s : null;
|
|
}
|
|
public function cached(string $tenant, string $server, int $now): ?string { return $this->cacheRecords[$tenant . $server] ?? null; }
|
|
public function cache(string $tenant, string $server, string $xml, int $expires): void { $this->cacheRecords[$tenant . $server] = $xml; }
|
|
}
|
|
|
|
final class HostTest extends TestCase
|
|
{
|
|
public function testServiceConfigurationTakesPrecedenceAndDerivesDomain(): void
|
|
{
|
|
$tenants = $this->createMock(TenantService::class);
|
|
$tenants->method('fetchServiceConfiguration')->willReturn(['enabled' => true, 'office_server' => 'https://office.test']);
|
|
$tenants->expects(self::never())->method('fetchSettings');
|
|
$tenants->method('fetchById')->willReturn((new \KTXC\Models\Tenant\TenantObject())->setDomains(new \KTXC\Models\Tenant\DomainCollection(['app.test'])));
|
|
$wopi = new WopiService($tenants, new MemorySessions(), $this->createStub(Documents::class), $this->createStub(DiscoveryClient::class), $this->createStub(ProofValidator::class));
|
|
self::assertSame('https://app.test', $wopi->configuration('tenant')->origin->origin);
|
|
}
|
|
|
|
public function testServicesSurviveTenantConfigurationRoundTrip(): void
|
|
{
|
|
$services = ['wopi' => ['enabled' => true, 'office_server' => 'https://office.test'], 'other' => ['enabled' => false]];
|
|
$config = (new \KTXC\Models\Tenant\TenantConfiguration())->jsonDeserialize(['services' => $services]);
|
|
self::assertSame($services, $config->jsonSerialize()['services']);
|
|
}
|
|
|
|
public function testAmbiguousRuntimeOriginFailsClosed(): void
|
|
{
|
|
$this->expectException(HostException::class);
|
|
new Configuration(['enabled' => true, 'office_server' => 'https://office.test'], ['app.test', 'alias.test']);
|
|
}
|
|
|
|
public function testRouterSelectsSeparateAbsoluteHandlers(): void
|
|
{
|
|
$reflection = new \ReflectionClass(\KTXC\Routing\Router::class);
|
|
$router = $reflection->newInstanceWithoutConstructor();
|
|
$reflection->getProperty('logger')->setValue($router, new \Psr\Log\NullLogger());
|
|
$reflection->getMethod('extract')->invoke($router, dirname(__DIR__, 3) . '/lib/Controllers/WopiController.php', '/m/service_wopi');
|
|
$reflection->getProperty('initialized')->setValue($router, true);
|
|
$id = str_repeat('a', 32);
|
|
foreach (['' => 'retrieveMeta', '/contents' => 'retrieveContents', '/unknown' => 'unsupported'] as $suffix => $handler) {
|
|
$route = $router->match(Request::create('https://app.test/m/service_wopi/wopi/files/' . $id . $suffix));
|
|
self::assertSame($handler, $route?->classMethodName);
|
|
self::assertFalse($route->authenticated);
|
|
if ($suffix !== '/unknown') { self::assertSame($id, $route->params['id']); }
|
|
}
|
|
self::assertSame('unsupported', $router->match(Request::create('https://app.test/m/service_wopi/wopi/files/' . $id . '/contents', 'POST'))?->classMethodName);
|
|
}
|
|
|
|
private function setupWopi(bool $proofValid = true): array
|
|
{
|
|
$tenants = $this->createStub(TenantService::class);
|
|
$tenants->method('fetchServiceConfiguration')->willReturn(['enabled' => true, 'office_server' => 'https://office.test', 'origin_server' => 'https://app.test']);
|
|
$store = new MemorySessions();
|
|
$documents = $this->createStub(Documents::class);
|
|
$documents->method('read')->willReturn(['label' => 'file.docx', 'size' => 4, 'content' => "a\0bc", 'version' => 'version-1']);
|
|
$client = $this->createStub(DiscoveryClient::class);
|
|
$client->method('fetchXml')->willReturn('<wopi-discovery><net-zone name="external-https"><app name="Word"><action name="view" ext="docx" urlsrc="https://office.test/view?"/><action name="edit" ext="xlsx" urlsrc="https://office.test/edit?"/></app></net-zone><proof-key modulus="abc" exponent="AQAB"/></wopi-discovery>');
|
|
$proof = $this->createStub(ProofValidator::class);
|
|
$proof->method('valid')->willReturn($proofValid);
|
|
return [new WopiService($tenants, $store, $documents, $client, $proof), $store];
|
|
}
|
|
|
|
private function request(string $token, string $suffix = ''): Request
|
|
{
|
|
return Request::create('https://app.test/m/service_wopi/wopi/files/' . str_repeat('a', 32) . $suffix . '?access_token=' . $token);
|
|
}
|
|
|
|
public function testLaunchAndReadOnlyEndpoints(): void
|
|
{
|
|
[$wopi, $store] = $this->setupWopi();
|
|
self::assertSame([WopiService::FORMATS['docx']], $wopi->capabilities('tenant', 'user')['mimeTypes']);
|
|
$launch = $wopi->launch('tenant', 'user', 'default:personal:folder:file');
|
|
self::assertMatchesRegularExpression('/^[a-f0-9]{64}$/', $launch['accessToken']);
|
|
self::assertStringNotContainsString($launch['accessToken'], json_encode($store->records));
|
|
self::assertStringNotContainsString($launch['accessToken'], $launch['actionUrl']);
|
|
self::assertSame('view', $launch['mode']);
|
|
self::assertGreaterThan(time() * 1000, $launch['accessTokenTtl']);
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('enabled')->willReturn(true);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant');
|
|
$controller = new WopiController($tenant, $wopi);
|
|
$metadata = $controller->retrieveMeta($this->request($launch['accessToken']), str_repeat('a', 32));
|
|
self::assertSame(200, $metadata->getStatusCode());
|
|
$data = json_decode($metadata->getContent(), true);
|
|
self::assertTrue($data['ReadOnly']);
|
|
self::assertFalse($data['UserCanWrite']);
|
|
self::assertFalse($data['SupportsLocks']);
|
|
$content = $controller->retrieveContents($this->request($launch['accessToken'], '/contents'), str_repeat('a', 32));
|
|
self::assertSame("a\0bc", $content->getContent());
|
|
self::assertStringContainsString('no-store', $content->headers->get('Cache-Control'));
|
|
self::assertSame('4', $content->headers->get('Content-Length'));
|
|
$post = Request::create('https://app.test/m/service_wopi/wopi/files/' . str_repeat('a', 32) . '/contents', 'POST');
|
|
self::assertSame(405, $controller->unsupported($post)->getStatusCode());
|
|
}
|
|
|
|
public function testRejectsExpiredTokensAndCrossTenantAccess(): void
|
|
{
|
|
[$wopi, $store] = $this->setupWopi();
|
|
$launch = $wopi->launch('tenant', 'user', 'default:personal:folder:file');
|
|
foreach (['other-tenant', 'tenant'] as $tenant) {
|
|
if ($tenant === 'tenant') $store->records[hash('sha256', $launch['accessToken'])]['expires'] = time();
|
|
try {
|
|
$wopi->access($tenant, str_repeat('a', 32), $this->request($launch['accessToken']));
|
|
self::fail('Expected token rejection');
|
|
} catch (HostException $error) { self::assertSame(401, $error->status); }
|
|
}
|
|
}
|
|
|
|
public function testRejectsProofFailure(): void
|
|
{
|
|
[$wopi] = $this->setupWopi(false);
|
|
$launch = $wopi->launch('tenant', 'user', 'default:personal:folder:file');
|
|
$this->expectException(HostException::class);
|
|
$this->expectExceptionMessage('Invalid WOPI proof');
|
|
$wopi->access('tenant', str_repeat('a', 32), $this->request($launch['accessToken']));
|
|
}
|
|
|
|
public function testRejectsChangedFile(): void
|
|
{
|
|
[$wopi, $store] = $this->setupWopi();
|
|
$launch = $wopi->launch('tenant', 'user', 'default:personal:folder:file');
|
|
$store->records[hash('sha256', $launch['accessToken'])]['version'] = 'old-version';
|
|
$this->expectException(HostException::class);
|
|
$this->expectExceptionMessage('document changed');
|
|
$wopi->access('tenant', str_repeat('a', 32), $this->request($launch['accessToken']));
|
|
}
|
|
|
|
public function testConfigurationRequiresExplicitEnablementAndHttps(): void
|
|
{
|
|
$this->expectException(HostException::class);
|
|
new Configuration([]);
|
|
}
|
|
}
|