Add _editor and lock to StoredObjecNormalizer. Refactor StoredObjectNormalizerTest to use prophecy and add tests for lock and editor states

- Replaced PHPUnit mocks with prophecy for improved flexibility in `StoredObjectNormalizerTest`.
- Added test cases to cover lock handling and editor state evaluation for WebDAV and WOPI methods.
- Updated `StoredObjectNormalizer` to include lock normalization and editor state computation logic.
- Extended TypeScript types to reflect lock and editor states in the front-end model.
This commit is contained in:
2026-04-13 14:26:37 +02:00
parent bb9da5dada
commit b171afba2f
3 changed files with 150 additions and 45 deletions

View File

@@ -3,6 +3,16 @@ import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpe
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
export type StoredObjectLockMethodEnum = 'webdav' | 'wopi';
export interface StoredObjectLock {
uuid: string;
method: StoredObjectLockMethodEnum;
createdAt: DateTime;
expiresAt: DateTime;
users: User[];
}
export interface StoredObject {
id: number;
title: string | null;
@@ -31,6 +41,11 @@ export interface StoredObject {
};
downloadLink?: SignedUrlGet;
};
lock: StoredObjectLock|null;
_editor?: {
webdav: boolean;
wopi: boolean;
}
}
export interface StoredObjectVersion {

View File

@@ -13,8 +13,11 @@ namespace Chill\DocStoreBundle\Serializer\Normalizer;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Entity\StoredObjectLockMethodEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Chill\DocStoreBundle\Service\Lock\StoredObjectEditorDecisionManagerInterface;
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -42,6 +45,8 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
private readonly UrlGeneratorInterface $urlGenerator,
private readonly Security $security,
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly StoredObjectEditorDecisionManagerInterface $storedObjectEditorDecisionManager,
private readonly StoredObjectLockManager $storedObjectLockManager,
) {}
public function normalize($object, ?string $format = null, array $context = [])
@@ -58,6 +63,7 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
'currentVersion' => $this->normalizer->normalize($object->getCurrentVersion(), $format, [...$context, [AbstractNormalizer::GROUPS => 'read']]),
'totalVersions' => $object->getVersions()->count(),
'lock' => $this->storedObjectLockManager->hasLock($object) ? $this->normalizer->normalize($this->storedObjectLockManager->getLock($object), $format, $context) : null,
];
// deprecated property
@@ -110,6 +116,11 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
];
}
$datas['_editor'] = [
'webdav' => $canEdit && $this->storedObjectEditorDecisionManager->canEdit($object, StoredObjectLockMethodEnum::WEBDAV),
'wopi' => $canEdit && $this->storedObjectEditorDecisionManager->canEdit($object, StoredObjectLockMethodEnum::WOPI),
];
return $datas;
}

View File

@@ -14,10 +14,15 @@ namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Entity\StoredObjectLock;
use Chill\DocStoreBundle\Entity\StoredObjectLockMethodEnum;
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
use Chill\DocStoreBundle\Service\Lock\StoredObjectEditorDecisionManagerInterface;
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -29,6 +34,8 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
class StoredObjectNormalizerTest extends TestCase
{
use ProphecyTrait;
public function testNormalize(): void
{
$storedObject = new StoredObject();
@@ -37,45 +44,51 @@ class StoredObjectNormalizerTest extends TestCase
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($storedObject, 1);
$jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class);
$jwtProvider->expects($this->once())->method('createToken')->withAnyParameters()->willReturn('token');
$jwtProvider->expects($this->once())->method('getTokenExpiration')->with('token')->willReturn($d = new \DateTimeImmutable());
$jwtProvider = $this->prophesize(JWTDavTokenProviderInterface::class);
$jwtProvider->createToken(Argument::cetera())->willReturn('token');
$jwtProvider->getTokenExpiration('token')->willReturn($d = new \DateTimeImmutable());
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->once())->method('generate')
->with(
'chill_docstore_dav_document_get',
[
'uuid' => $storedObject->getUuid(),
'access_token' => 'token',
],
UrlGeneratorInterface::ABSOLUTE_URL,
)
->willReturn($davLink = 'http://localhost/dav/token');
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
$urlGenerator->generate(
'chill_docstore_dav_document_get',
[
'uuid' => $storedObject->getUuid(),
'access_token' => 'token',
],
UrlGeneratorInterface::ABSOLUTE_URL,
)->willReturn($davLink = 'http://localhost/dav/token');
$security = $this->createMock(Security::class);
$security->expects($this->exactly(2))->method('isGranted')
->with(
$this->logicalOr(StoredObjectRoleEnum::EDIT->value, StoredObjectRoleEnum::SEE->value),
$storedObject
)
->willReturn(true);
$security = $this->prophesize(Security::class);
$security->isGranted(Argument::type('string'), $storedObject)->willReturn(true);
$globalNormalizer = $this->createMock(NormalizerInterface::class);
$globalNormalizer->expects($this->exactly(3))->method('normalize')
->withAnyParameters()
->willReturnCallback(function (?object $object, string $format, array $context) {
if (null === $object) {
$globalNormalizer = $this->prophesize(NormalizerInterface::class);
$globalNormalizer->normalize(Argument::cetera())
->will(function ($args) {
if (null === $args[0]) {
return null;
}
return ['sub' => 'sub'];
});
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
$tempUrlGenerator = $this->prophesize(TempUrlGeneratorInterface::class);
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
$normalizer->setNormalizer($globalNormalizer);
$storedObjectEditorDecisionManager = $this->prophesize(StoredObjectEditorDecisionManagerInterface::class);
$storedObjectEditorDecisionManager->canEdit($storedObject, StoredObjectLockMethodEnum::WEBDAV)->willReturn(true);
$storedObjectEditorDecisionManager->canEdit($storedObject, StoredObjectLockMethodEnum::WOPI)->willReturn(true);
$storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class);
$storedObjectLockManager->hasLock($storedObject)->willReturn(false);
$normalizer = new StoredObjectNormalizer(
$jwtProvider->reveal(),
$urlGenerator->reveal(),
$security->reveal(),
$tempUrlGenerator->reveal(),
$storedObjectEditorDecisionManager->reveal(),
$storedObjectLockManager->reveal()
);
$normalizer->setNormalizer($globalNormalizer->reveal());
$actual = $normalizer->normalize($storedObject, 'json');
@@ -93,11 +106,67 @@ class StoredObjectNormalizerTest extends TestCase
self::assertArrayHasKey('datas', $actual);
self::assertArrayHasKey('createdAt', $actual);
self::assertArrayHasKey('createdBy', $actual);
self::assertArrayHasKey('lock', $actual);
self::assertNull($actual['lock']);
self::assertArrayHasKey('_permissions', $actual);
self::assertEqualsCanonicalizing(['canEdit' => true, 'canSee' => true], $actual['_permissions']);
self::assertArrayHaskey('_links', $actual);
self::assertArrayHasKey('dav_link', $actual['_links']);
self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']);
self::assertArrayHasKey('_editor', $actual);
self::assertArrayHasKey('webdav', $actual['_editor']);
self::assertArrayHasKey('wopi', $actual['_editor']);
self::assertTrue($actual['_editor']['webdav']);
self::assertTrue($actual['_editor']['wopi']);
}
public function testNormalizeWithLock(): void
{
$storedObject = new StoredObject();
$storedObject->setTitle('test');
$reflection = new \ReflectionClass(StoredObject::class);
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($storedObject, 1);
$jwtProvider = $this->prophesize(JWTDavTokenProviderInterface::class);
$jwtProvider->createToken(Argument::cetera())->willReturn('token');
$jwtProvider->getTokenExpiration('token')->willReturn(new \DateTimeImmutable());
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
$urlGenerator->generate(Argument::cetera())->willReturn('http://localhost');
$security = $this->prophesize(Security::class);
$security->isGranted(Argument::cetera())->willReturn(true);
$lock = $this->prophesize(StoredObjectLock::class);
$globalNormalizer = $this->prophesize(NormalizerInterface::class);
$globalNormalizer->normalize(Argument::cetera())->willReturn(['sub' => 'sub']);
$tempUrlGenerator = $this->prophesize(TempUrlGeneratorInterface::class);
$storedObjectEditorDecisionManager = $this->prophesize(StoredObjectEditorDecisionManagerInterface::class);
$storedObjectEditorDecisionManager->canEdit(Argument::cetera())->willReturn(false);
$storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class);
$storedObjectLockManager->hasLock($storedObject)->willReturn(true);
$storedObjectLockManager->getLock($storedObject)->willReturn($lock->reveal());
$normalizer = new StoredObjectNormalizer(
$jwtProvider->reveal(),
$urlGenerator->reveal(),
$security->reveal(),
$tempUrlGenerator->reveal(),
$storedObjectEditorDecisionManager->reveal(),
$storedObjectLockManager->reveal()
);
$normalizer->setNormalizer($globalNormalizer->reveal());
$actual = $normalizer->normalize($storedObject, 'json');
self::assertArrayHasKey('lock', $actual);
self::assertEquals(['sub' => 'sub'], $actual['lock']);
self::assertArrayHasKey('_editor', $actual);
self::assertFalse($actual['_editor']['webdav']);
self::assertFalse($actual['_editor']['wopi']);
}
public function testWithDownloadLinkOnly(): void
@@ -109,32 +178,42 @@ class StoredObjectNormalizerTest extends TestCase
$idProperty = $reflection->getProperty('id');
$idProperty->setValue($storedObject, 1);
$jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class);
$jwtProvider->expects($this->never())->method('createToken')->withAnyParameters();
$jwtProvider = $this->prophesize(JWTDavTokenProviderInterface::class);
$jwtProvider->createToken(Argument::cetera())->shouldNotBeCalled();
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->never())->method('generate');
$urlGenerator = $this->prophesize(UrlGeneratorInterface::class);
$urlGenerator->generate(Argument::cetera())->shouldNotBeCalled();
$security = $this->createMock(Security::class);
$security->expects($this->never())->method('isGranted');
$security = $this->prophesize(Security::class);
$security->isGranted(Argument::cetera())->shouldNotBeCalled();
$globalNormalizer = $this->createMock(NormalizerInterface::class);
$globalNormalizer->expects($this->exactly(4))->method('normalize')
->withAnyParameters()
->willReturnCallback(function (?object $object, string $format, array $context) {
if (null === $object) {
$globalNormalizer = $this->prophesize(NormalizerInterface::class);
$globalNormalizer->normalize(Argument::cetera())
->will(function ($args) {
if (null === $args[0]) {
return null;
}
return ['sub' => 'sub'];
});
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
$tempUrlGenerator->expects($this->once())->method('generate')->with('GET', $storedObject->getCurrentVersion()->getFilename(), $this->isType('int'))
$tempUrlGenerator = $this->prophesize(TempUrlGeneratorInterface::class);
$tempUrlGenerator->generate('GET', $storedObject->getCurrentVersion()->getFilename(), Argument::type('int'))
->willReturn(new SignedUrl('GET', 'https://some-link/test', new \DateTimeImmutable('300 seconds'), $storedObject->getCurrentVersion()->getFilename()));
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
$normalizer->setNormalizer($globalNormalizer);
$storedObjectEditorDecisionManager = $this->prophesize(StoredObjectEditorDecisionManagerInterface::class);
$storedObjectLockManager = $this->prophesize(StoredObjectLockManager::class);
$storedObjectLockManager->hasLock($storedObject)->willReturn(false);
$normalizer = new StoredObjectNormalizer(
$jwtProvider->reveal(),
$urlGenerator->reveal(),
$security->reveal(),
$tempUrlGenerator->reveal(),
$storedObjectEditorDecisionManager->reveal(),
$storedObjectLockManager->reveal()
);
$normalizer->setNormalizer($globalNormalizer->reveal());
$actual = $normalizer->normalize($storedObject, 'json', ['groups' => ['read', 'read:download-link-only']]);