Add StoredObjectLockApiController and tests for lock removal functionality

- Implemented `StoredObjectLockApiController` with endpoint to remove locks from stored objects.
- Added access control checks and validation for lock existence before removal.
- Wrote `StoredObjectLockApiControllerTest` to cover access denial, lock absence, and successful lock removal scenarios.
- Utilized `MockClock` in test cases to accurately simulate time-based behaviors.
This commit is contained in:
2026-04-14 15:35:22 +02:00
parent a27173ee4a
commit 7b00006943
3 changed files with 170 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
final readonly class StoredObjectLockApiController
{
public function __construct(
private Security $security,
private StoredObjectLockManager $storedObjectLockManager,
private ClockInterface $clock,
) {}
#[Route('/api/1.0/doc-store/stored-object/{uuid}/lock', name: 'chill_docstore_storedobjectlock_removelock', methods: ['DELETE'])]
public function removeLock(StoredObject $storedObject): Response
{
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
throw new AccessDeniedHttpException();
}
if (!$this->storedObjectLockManager->hasLock($storedObject)) {
throw new PreconditionFailedHttpException('No lock found for this stored object');
}
$this->storedObjectLockManager->deleteLock($storedObject, $this->clock->now());
return new Response(null, Response::HTTP_NO_CONTENT);
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\DocStoreBundle\Tests\Controller;
use Chill\DocStoreBundle\Controller\StoredObjectLockApiController;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Service\Lock\StoredObjectLockManager;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\Security\Core\Security;
/**
* @internal
*
* @coversNothing
*/
class StoredObjectLockApiControllerTest extends TestCase
{
use ProphecyTrait;
public function testRemoveLockAccessDenied(): void
{
$security = $this->prophesize(Security::class);
$lockManager = $this->prophesize(StoredObjectLockManager::class);
$clock = new MockClock();
$storedObject = new StoredObject();
$security->isGranted(StoredObjectRoleEnum::EDIT, $storedObject)->willReturn(false);
$controller = new StoredObjectLockApiController(
$security->reveal(),
$lockManager->reveal(),
$clock
);
$this->expectException(AccessDeniedHttpException::class);
$controller->removeLock($storedObject);
}
public function testRemoveLockNoLockFound(): void
{
$security = $this->prophesize(Security::class);
$lockManager = $this->prophesize(StoredObjectLockManager::class);
$clock = new MockClock();
$storedObject = new StoredObject();
$security->isGranted(StoredObjectRoleEnum::EDIT, $storedObject)->willReturn(true);
$lockManager->hasLock($storedObject)->willReturn(false);
$controller = new StoredObjectLockApiController(
$security->reveal(),
$lockManager->reveal(),
$clock
);
$this->expectException(PreconditionFailedHttpException::class);
$this->expectExceptionMessage('No lock found for this stored object');
$controller->removeLock($storedObject);
}
public function testRemoveLockSuccess(): void
{
$security = $this->prophesize(Security::class);
$lockManager = $this->prophesize(StoredObjectLockManager::class);
$clock = new MockClock('2024-01-01 12:00:00');
$storedObject = new StoredObject();
$security->isGranted(StoredObjectRoleEnum::EDIT, $storedObject)->willReturn(true);
$lockManager->hasLock($storedObject)->willReturn(true);
$lockManager->deleteLock($storedObject, $clock->now())->shouldBeCalled();
$controller = new StoredObjectLockApiController(
$security->reveal(),
$lockManager->reveal(),
$clock
);
$response = $controller->removeLock($storedObject);
self::assertSame(Response::HTTP_ACCEPTED, $response->getStatusCode());
self::assertNull($response->getContent() ?: null);
}
}

View File

@@ -196,3 +196,26 @@ paths:
$ref: '#/components/schemas/GenericDoc'
type: object
/1.0/doc-store/stored-object/{uuid}/lock:
delete:
tags:
- storedobject
summary: Force removing a lock on a stored object
parameters:
- in: path
name: uuid
required: true
allowEmptyValue: false
description: The UUID of the storedObjeect
schema:
type: string
format: uuid
responses:
204:
description: "No content"
403:
description: "Forbidden"
404:
description: "Not found"
412:
description: "No lock found for this stored object"