e137de1183
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
178 lines
6.0 KiB
PHP
178 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Unit\SystemStore;
|
|
|
|
use DateTimeImmutable;
|
|
use KTXC\Models\Tenant\TenantConfiguration;
|
|
use KTXC\Models\Tenant\TenantObject;
|
|
use KTXC\Resource\ProviderManager;
|
|
use KTXC\Service\TenantService;
|
|
use KTXC\SystemStore\SystemStoreManager;
|
|
use KTXF\Resource\BinaryResource;
|
|
use KTXF\Resource\Provider\ProviderInterface;
|
|
use KTXF\Resource\SystemIdentity;
|
|
use KTXF\SystemStore\BlobInfo;
|
|
use KTXF\SystemStore\InvalidKeyException;
|
|
use KTXF\SystemStore\Provider\ProviderBaseInterface;
|
|
use KTXF\SystemStore\Service\SystemStoreServiceInterface;
|
|
use KTXF\SystemStore\SystemStoreException;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class SystemStoreManagerTest extends TestCase
|
|
{
|
|
private TenantService&MockObject $tenants;
|
|
private ProviderManager&MockObject $providers;
|
|
private ProviderBaseInterface&MockObject $provider;
|
|
private SystemStoreServiceInterface&MockObject $service;
|
|
private SystemStoreManager $manager;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$configuration = (new TenantConfiguration())->jsonDeserialize([
|
|
'stores' => [
|
|
'previews' => [
|
|
'provider' => 'storage-provider',
|
|
'service' => 'preview-storage',
|
|
'namespace' => 'tenant-previews',
|
|
],
|
|
],
|
|
]);
|
|
$tenant = (new TenantObject())
|
|
->setIdentifier('tenant-a')
|
|
->setConfiguration($configuration);
|
|
|
|
$this->tenants = $this->createMock(TenantService::class);
|
|
$this->tenants->method('fetchById')->with('tenant-a')->willReturn($tenant);
|
|
|
|
$this->service = $this->createMock(SystemStoreServiceInterface::class);
|
|
$this->provider = $this->createMock(ProviderBaseInterface::class);
|
|
$this->provider->method('serviceFetch')
|
|
->with('tenant-a', SystemIdentity::USER, 'preview-storage')
|
|
->willReturn($this->service);
|
|
|
|
$this->providers = $this->createMock(ProviderManager::class);
|
|
$this->providers->method('resolve')
|
|
->with(ProviderInterface::TYPE_SYSTEM_STORE, 'storage-provider')
|
|
->willReturn($this->provider);
|
|
|
|
$this->manager = new SystemStoreManager($this->tenants, $this->providers);
|
|
}
|
|
|
|
#[Test]
|
|
public function delegatesStatWithPhysicalNamespaceAndReturnsLogicalKey(): void
|
|
{
|
|
$this->service->expects(self::once())
|
|
->method('stat')
|
|
->with('tenant-previews/documents/report.pdf')
|
|
->willReturn($this->info('tenant-previews/documents/report.pdf'));
|
|
|
|
$info = $this->manager->stat('tenant-a', 'previews', 'documents/report.pdf');
|
|
|
|
self::assertSame('documents/report.pdf', $info?->key);
|
|
}
|
|
|
|
#[Test]
|
|
public function delegatesReadDeleteAndWriteWithPhysicalNamespace(): void
|
|
{
|
|
$resource = $this->resource();
|
|
|
|
$this->service->expects(self::once())
|
|
->method('read')
|
|
->with('tenant-previews/image.webp')
|
|
->willReturn($resource);
|
|
$this->service->expects(self::once())
|
|
->method('delete')
|
|
->with('tenant-previews/image.webp', null)
|
|
->willReturn(true);
|
|
$this->service->expects(self::once())
|
|
->method('write')
|
|
->with('tenant-previews/image.webp', $resource, ['variant' => 'popover'], null)
|
|
->willReturn($this->info('tenant-previews/image.webp'));
|
|
|
|
self::assertSame($resource, $this->manager->read('tenant-a', 'previews', 'image.webp'));
|
|
self::assertTrue($this->manager->delete('tenant-a', 'previews', 'image.webp'));
|
|
self::assertSame(
|
|
'image.webp',
|
|
$this->manager->write(
|
|
'tenant-a',
|
|
'previews',
|
|
'image.webp',
|
|
$resource,
|
|
['variant' => 'popover'],
|
|
)->key,
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function lazilyListsLogicalKeysWithinTheNamespace(): void
|
|
{
|
|
$this->service->expects(self::once())
|
|
->method('list')
|
|
->with('tenant-previews/documents/')
|
|
->willReturn((function (): iterable {
|
|
yield $this->info('tenant-previews/documents/one.webp');
|
|
yield $this->info('tenant-previews/documents/two.webp');
|
|
})());
|
|
|
|
$items = iterator_to_array($this->manager->list('tenant-a', 'previews', 'documents/'));
|
|
|
|
self::assertSame(['documents/one.webp', 'documents/two.webp'], array_column($items, 'key'));
|
|
}
|
|
|
|
#[Test]
|
|
public function rejectsTraversalKeys(): void
|
|
{
|
|
$this->service->expects(self::never())->method('read');
|
|
$this->expectException(InvalidKeyException::class);
|
|
|
|
$this->manager->read('tenant-a', 'previews', '../secret');
|
|
}
|
|
|
|
#[Test]
|
|
public function rejectsProviderResultsOutsideTheConfiguredNamespace(): void
|
|
{
|
|
$this->service->expects(self::once())
|
|
->method('stat')
|
|
->willReturn($this->info('another-tenant/secret'));
|
|
$this->expectException(SystemStoreException::class);
|
|
|
|
$this->manager->stat('tenant-a', 'previews', 'secret');
|
|
}
|
|
|
|
#[Test]
|
|
public function rejectsAnUnconfiguredLogicalStore(): void
|
|
{
|
|
$this->service->expects(self::never())->method('stat');
|
|
$this->expectException(SystemStoreException::class);
|
|
$this->expectExceptionMessage("System store 'icons' is not configured");
|
|
|
|
$this->manager->stat('tenant-a', 'icons', 'logo.svg');
|
|
}
|
|
|
|
private function info(string $key): BlobInfo
|
|
{
|
|
return new BlobInfo(
|
|
key: $key,
|
|
mimeType: 'image/webp',
|
|
size: 123,
|
|
etag: 'revision-1',
|
|
modifiedAt: new DateTimeImmutable('2026-08-28T12:00:00+00:00'),
|
|
);
|
|
}
|
|
|
|
private function resource(): BinaryResource
|
|
{
|
|
return new BinaryResource(
|
|
'image.webp',
|
|
'image/webp',
|
|
(function (): \Generator {
|
|
yield 'content';
|
|
})(),
|
|
);
|
|
}
|
|
}
|