259 lines
7.3 KiB
PHP
259 lines
7.3 KiB
PHP
|
|
<?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();
|
||
|
|
});
|
||
|
|
});
|