setFilename('encrypted.txt') ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) ->setIv(unpack('C*', 'abcdefghijklmnop')), hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string 'The quick brown fox jumps over the lazy dog', // clear ]; // Non-encrypted object yield [ (new StoredObject())->setFilename('non-encrypted.txt'), // The StoredObject 'The quick brown fox jumps over the lazy dog', // Encrypted 'The quick brown fox jumps over the lazy dog', // Clear ]; /* UNHAPPY SCENARIO */ // Encrypted object with issue during HTTP communication yield [ (new StoredObject()) ->setFilename('error_during_http_request.txt') ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) ->setIv(unpack('C*', 'abcdefghijklmnop')), hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string 'The quick brown fox jumps over the lazy dog', // clear StoredObjectManagerException::class, ]; // Encrypted object with issue during HTTP communication: Invalid status code yield [ (new StoredObject()) ->setFilename('invalid_statuscode.txt') ->setKeyInfos(['k' => base64_encode('S9NIHMaFHOWzLPez3jZOIHBaNfBrMQUR5zvqBz6kme8')]) ->setIv(unpack('C*', 'abcdefghijklmnop')), hex2bin('741237d255fd4f7eddaaa9058912a84caae28a41b10b34d4e3e3abe41d3b9b47cb0dd8f22c3c883d4f0e9defa75ff662'), // Binary encoded string 'The quick brown fox jumps over the lazy dog', // clear StoredObjectManagerException::class, ]; // Erroneous encrypted: Unable to decrypt exception. yield [ (new StoredObject()) ->setFilename('unable_to_decrypt.txt') ->setKeyInfos(['k' => base64_encode('WRONG_PASS_PHRASE')]) ->setIv(unpack('C*', 'abcdefghijklmnop')), 'WRONG_ENCODED_VALUE', // Binary encoded string 'The quick brown fox jumps over the lazy dog', // clear StoredObjectManagerException::class, ]; } /** * @dataProvider getDataProvider */ public function testRead(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) { if (null !== $exceptionClass) { $this->expectException($exceptionClass); } $storedObjectManager = $this->getSubject($storedObject, $encodedContent); self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); } /** * @dataProvider getDataProvider */ public function testWrite(StoredObject $storedObject, string $encodedContent, string $clearContent, ?string $exceptionClass = null) { if (null !== $exceptionClass) { $this->expectException($exceptionClass); } $storedObjectManager = $this->getSubject($storedObject, $encodedContent); $storedObjectManager->write($storedObject, $clearContent); self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); } public function testWriteWithDeleteAt() { $storedObject = new StoredObject(); $expectedRequests = [ function ($method, $url, $options): MockResponse { self::assertEquals('PUT', $method); self::assertArrayHasKey('headers', $options); self::assertIsArray($options['headers']); self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); return new MockResponse('', ['http_code' => 201]); }, function ($method, $url, $options): MockResponse { self::assertEquals('PUT', $method); self::assertArrayHasKey('headers', $options); self::assertIsArray($options['headers']); self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); self::assertContains('X-Delete-At: 1711014260', $options['headers']); return new MockResponse('', ['http_code' => 201]); }, ]; $client = new MockHttpClient($expectedRequests); $manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); $manager->write($storedObject, 'ok'); // with a deletedAt date $storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260')); $manager->write($storedObject, 'ok'); } private function getHttpClient(string $encodedContent): HttpClientInterface { $callback = static function ($method, $url, $options) use ($encodedContent) { if (Request::METHOD_GET === $method) { switch ($url) { case 'https://example.com/non-encrypted.txt': case 'https://example.com/encrypted.txt': return new MockResponse($encodedContent, ['http_code' => 200]); case 'https://example.com/error_during_http_request.txt': return new TransportException('error_during_http_request.txt'); case 'https://example.com/invalid_statuscode.txt': return new MockResponse($encodedContent, ['http_code' => 404]); } } if (Request::METHOD_PUT === $method) { switch ($url) { case 'https://example.com/non-encrypted.txt': case 'https://example.com/encrypted.txt': return new MockResponse($encodedContent, ['http_code' => 201]); case 'https://example.com/error_during_http_request.txt': throw new TransportException('error_during_http_request.txt'); case 'https://example.com/invalid_statuscode.txt': return new MockResponse($encodedContent, ['http_code' => 404]); } } return new MockResponse('Not found'); }; return new MockHttpClient($callback); } private function getSubject(StoredObject $storedObject, string $encodedContent): StoredObjectManagerInterface { return new StoredObjectManager( $this->getHttpClient($encodedContent), $this->getTempUrlGenerator($storedObject) ); } private function getTempUrlGenerator(StoredObject $storedObject): TempUrlGeneratorInterface { $response = new SignedUrl( 'PUT', 'https://example.com/'.$storedObject->getFilename(), new \DateTimeImmutable('1 hours'), $storedObject->getFilename() ); $tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class); $tempUrlGenerator ->method('generate') ->withAnyParameters() ->willReturn($response); return $tempUrlGenerator; } }