From b171afba2f28fe3b5ac4200f2c41d7228d4a5465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 13 Apr 2026 14:26:37 +0200 Subject: [PATCH] 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. --- .../Resources/public/types/index.ts | 15 ++ .../Normalizer/StoredObjectNormalizer.php | 11 ++ .../Normalizer/StoredObjectNormalizerTest.php | 169 +++++++++++++----- 3 files changed, 150 insertions(+), 45 deletions(-) diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts index 882d46716..3bad465d3 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts @@ -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 { diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php index 97067f289..66c8ab243 100644 --- a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/StoredObjectNormalizer.php @@ -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; } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php index 6bf1f0da6..03e95cf49 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/StoredObjectNormalizerTest.php @@ -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']]);