From 1f6de3cb1130f2f6ac0df4b376d2be60c488cd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Dec 2024 23:20:08 +0100 Subject: [PATCH] Implement TempUrlLocalStorageGenerator and its tests Added the full implementation for TempUrlLocalStorageGenerator, including methods to generate signed URLs and POST requests with expiration and signature logic. Introduced corresponding unit tests to validate functionality using mocked dependencies. --- .../TempUrlLocalStorageGenerator.php | 50 +++++++++++- .../TempUrlLocalStorageGeneratorTest.php | 78 +++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php diff --git a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php index 81f69927c..957413b3a 100644 --- a/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php +++ b/src/Bundle/ChillDocStoreBundle/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGenerator.php @@ -14,16 +14,62 @@ namespace Chill\DocStoreBundle\AsyncUpload\Driver\LocalStorage; use Chill\DocStoreBundle\AsyncUpload\SignedUrl; use Chill\DocStoreBundle\AsyncUpload\SignedUrlPost; use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface; +use Chill\DocStoreBundle\Entity\StoredObject; +use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class TempUrlLocalStorageGenerator implements TempUrlGeneratorInterface { + private const SIGNATURE_DURATION = 180; + + public function __construct( + private readonly string $secret, + private readonly ClockInterface $clock, + private readonly UrlGeneratorInterface $urlGenerator, + ) {} + public function generate(string $method, string $object_name, ?int $expire_delay = null): SignedUrl { - // TODO: Implement generate() method. + $expiration = $this->clock->now()->getTimestamp() + min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + + return new SignedUrl( + $method, + $this->urlGenerator->generate('chill_docstore_stored_object_operate', [ + 'object_name' => $object_name, + 'exp' => $expiration, + 'sig' => $this->sign($method, $object_name, $expiration), + ], UrlGeneratorInterface::ABSOLUTE_URL), + \DateTimeImmutable::createFromFormat('U', (string) $expiration), + $object_name, + ); } public function generatePost(?int $expire_delay = null, ?int $submit_delay = null, int $max_file_count = 1, ?string $object_name = null): SignedUrlPost { - // TODO: Implement generatePost() method. + $submitDelayComputed = min($submit_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + $expireDelayComputed = min($expire_delay ?? self::SIGNATURE_DURATION, self::SIGNATURE_DURATION); + $objectNameComputed = $object_name ?? StoredObject::generatePrefix(); + $expiration = $this->clock->now()->getTimestamp() + $expireDelayComputed + $submitDelayComputed; + + return new SignedUrlPost( + $this->urlGenerator->generate( + 'chill_docstore_storedobject_post', + ['prefix' => $objectNameComputed], + UrlGeneratorInterface::ABSOLUTE_URL + ), + \DateTimeImmutable::createFromFormat('U', (string) $expiration), + $objectNameComputed, + 15_000_000, + 1, + $submitDelayComputed, + '', + $objectNameComputed, + $this->sign('POST', $object_name, $expiration), + ); + } + + private function sign(string $method, string $object_name, int $expiration): string + { + return hash_hmac('sha512', $method, sprintf('%s.%s.%s.%d', $method, $this->secret, $object_name, $expiration)); } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php new file mode 100644 index 000000000..f7170abcd --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/AsyncUpload/Driver/LocalStorage/TempUrlLocalStorageGeneratorTest.php @@ -0,0 +1,78 @@ +prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('chill_docstore_stored_object_operate', [ + 'object_name' => 'testABC', + 'exp' => $exp = 1734307200 + 180, + 'sig' => '528a426aebea86ada86035e9f3299788a074b77d3f926c6c15492457f81e88cfa98b270ba489d5d7588612eadaeeb7717a57887b33932d6d449b5f441252e600', + ], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn($url = 'http://example.com/public/doc-store/stored-object/operate/testABC'); + + $generator = $this->buildGenerator($urlGenerator->reveal()); + + $signedUrl = $generator->generate('GET', 'testABC'); + + self::assertEquals($url, $signedUrl->url); + self::assertEquals('testABC', $signedUrl->object_name); + self::assertEquals($exp, $signedUrl->expires->getTimestamp()); + self::assertEquals('GET', $signedUrl->method); + } + + public function testGeneratePost(): void + { + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('chill_docstore_storedobject_post', [ + 'prefix' => 'prefixABC', + ], UrlGeneratorInterface::ABSOLUTE_URL) + ->shouldBeCalled() + ->willReturn($url = 'http://example.com/public/doc-store/stored-object/prefixABC'); + + $generator = $this->buildGenerator($urlGenerator->reveal()); + + $signedUrl = $generator->generatePost(object_name: 'prefixABC'); + + self::assertEquals($url, $signedUrl->url); + self::assertEquals('prefixABC', $signedUrl->object_name); + self::assertEquals(1734307200 + 180 + 180, $signedUrl->expires->getTimestamp()); + self::assertEquals('POST', $signedUrl->method); + self::assertEquals('47e9271917c6c9149a47248d88eeadcbc1f804138e445f3406d39f6aa51b2d976c2ee6e5b1196d2bf88852b627b330596c2fbf6f31b06837e9f41cf56103a4e4', $signedUrl->signature); + } + + private function buildGenerator(UrlGeneratorInterface $urlGenerator): TempUrlLocalStorageGenerator + { + return new TempUrlLocalStorageGenerator( + 'abc', + new MockClock('2024-12-16T00:00:00+00:00'), + $urlGenerator, + ); + } +}