toBeInstanceOf(FileStream::class) ->and($vo->stream())->toBe($resource); }); it('throws exception if argument is not a resource', function (mixed $invalidResource): void { // GIVEN $resource = $invalidResource; // WHEN $action = fn () => new FileStream($resource); // THEN expect($action)->toThrow( UnexpectedStorageException::class, 'Invalid file resource.' ); })->with([ 'string' => 'not-a-resource', 'integer' => 12345, 'null' => null, 'array' => fn () => ['resource'], 'object' => fn () => new \stdClass(), ]); it('throws exception if resource is not a stream', function (): void { // GIVEN $nonStreamResource = stream_context_create(); // WHEN $action = fn () => new FileStream($nonStreamResource); // THEN expect($action)->toThrow( UnexpectedStorageException::class, 'Resource is not a valid stream.' ); }); }); describe('FileStream::fromPath', function (): void { it('can instantiate from a valid file path', function (): void { // GIVEN $tempFile = tempnam(sys_get_temp_dir(), 'test_stream_'); file_put_contents($tempFile, 'Sample file content'); try { // WHEN $vo = FileStream::fromPath($tempFile); // THEN expect($vo)->toBeInstanceOf(FileStream::class) ->and($vo->getContents())->toBe('Sample file content'); } finally { if (file_exists($tempFile)) { unlink($tempFile); } } }); it('throws exception if file does not exist', function (): void { // GIVEN $nonExistentPath = '/tmp/non_existent_file_' . uniqid() . '.txt'; // WHEN $action = fn () => FileStream::fromPath($nonExistentPath); // THEN expect($action)->toThrow( UnexpectedStorageException::class, "File does not exist or is not readable: {$nonExistentPath}" ); }); it('throws exception if file is not readable', function (): void { // GIVEN $tempFile = tempnam(sys_get_temp_dir(), 'test_unreadable_'); file_put_contents($tempFile, 'private content'); chmod($tempFile, 0000); try { // WHEN $action = fn () => FileStream::fromPath($tempFile); // THEN expect($action)->toThrow( UnexpectedStorageException::class, "File does not exist or is not readable: {$tempFile}" ); } finally { chmod($tempFile, 0666); if (file_exists($tempFile)) { unlink($tempFile); } } }); it('throws exception if fopen fails with invalid mode', function (): void { // GIVEN $tempFile = tempnam(sys_get_temp_dir(), 'test_invalid_mode_'); file_put_contents($tempFile, 'mode content'); try { // WHEN $action = fn () => FileStream::fromPath($tempFile, 'invalid-mode'); // THEN expect($action)->toThrow( UnexpectedStorageException::class, "Unable to open file stream for path: {$tempFile}" ); } finally { if (file_exists($tempFile)) { unlink($tempFile); } } }); }); describe('FileStream::fromString', function (): void { it('can instantiate from string content', function (): void { // GIVEN $content = 'Hello world from memory stream!'; // WHEN $vo = FileStream::fromString($content); // THEN expect($vo)->toBeInstanceOf(FileStream::class) ->and($vo->getContents())->toBe($content); }); it('can instantiate from an empty string', function (): void { // GIVEN $content = ''; // WHEN $vo = FileStream::fromString($content); // THEN expect($vo)->toBeInstanceOf(FileStream::class) ->and($vo->getContents())->toBe('') ->and($vo->getSize())->toBe(0); }); it('can instantiate from binary content', function (): void { // GIVEN $binaryContent = random_bytes(128); // WHEN $vo = FileStream::fromString($binaryContent); // THEN expect($vo)->toBeInstanceOf(FileStream::class) ->and($vo->getContents())->toBe($binaryContent) ->and($vo->getSize())->toBe(128); }); }); describe('FileStream methods', function (): void { it('returns the underlying resource via stream()', function (): void { // GIVEN $vo = FileStream::fromString('resource content'); // WHEN $resource = $vo->stream(); // THEN expect(is_resource($resource))->toBeTrue() ->and(get_resource_type($resource))->toBe('stream'); }); it('reads contents multiple times by rewinding seekable streams', function (): void { // GIVEN $vo = FileStream::fromString('Reusable stream content'); // WHEN $firstRead = $vo->getContents(); $secondRead = $vo->getContents(); // THEN expect($firstRead)->toBe('Reusable stream content') ->and($secondRead)->toBe('Reusable stream content'); }); it('calculates stream size correctly via getSize()', function (): void { // GIVEN $content = 'Size of this content'; $vo = FileStream::fromString($content); // WHEN $size = $vo->getSize(); // THEN expect($size)->toBe(strlen($content)); }); it('closes the stream and prevents further reading', function (): void { // GIVEN $vo = FileStream::fromString('Closing content'); $resource = $vo->stream(); // WHEN $vo->close(); // THEN expect(is_resource($resource))->toBeFalse() ->and($vo->getContents())->toBe('') ->and($vo->getSize())->toBeNull(); }); it('allows closing multiple times without error', function (): void { // GIVEN $vo = FileStream::fromString('Closing content'); // WHEN $vo->close(); $secondClose = fn () => $vo->close(); // THEN expect($secondClose)->not->toThrow(\Throwable::class); }); }); describe('FileStream destruction', function (): void { it('automatically closes stream on object destruction', function (): void { // GIVEN $resource = fopen('php://temp', 'r+b'); fwrite($resource, 'Temp data'); rewind($resource); $vo = new FileStream($resource); // WHEN unset($vo); // THEN expect(is_resource($resource))->toBeFalse(); }); });