ldc-file-storage-service/tests/Unit/Lib/ValueObjects/FileStreamTest.php

259 lines
7.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Uwil\Storage\Tests\Unit\Lib\ValueObjects;
use Uwil\Storage\Exceptions\UnexpectedStorageException;
use Uwil\Storage\Lib\ValueObjects\FileStream;
covers(FileStream::class);
describe('FileStream constructor', function (): void {
it('can instantiate with a valid stream resource', function (): void {
// GIVEN
$resource = fopen('php://temp', 'r+b');
// WHEN
$vo = new FileStream($resource);
// THEN
expect($vo)->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();
});
});