diff --git a/docs/_data/partials/hyde-pages-api/hyde-kernel-filesystem-methods.md b/docs/_data/partials/hyde-pages-api/hyde-kernel-filesystem-methods.md index 55dfcdafe4b..fa76f9881e7 100644 --- a/docs/_data/partials/hyde-pages-api/hyde-kernel-filesystem-methods.md +++ b/docs/_data/partials/hyde-pages-api/hyde-kernel-filesystem-methods.md @@ -1,7 +1,7 @@
- + #### `filesystem()` @@ -56,7 +56,7 @@ Hyde::pathToRelative(string $path): string No description provided. ```php -Hyde::assets(): Illuminate\Support\Collection +Hyde::assets(): \Illuminate\Support\Collection ``` diff --git a/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php b/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php index 888f3abf4d0..b294170474f 100644 --- a/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php +++ b/packages/framework/src/Foundation/Concerns/ForwardsFilesystem.php @@ -44,6 +44,7 @@ public function pathToRelative(string $path): string return $this->filesystem->pathToRelative($path); } + /** @return \Illuminate\Support\Collection */ public function assets(): Collection { return $this->filesystem->assets(); diff --git a/packages/framework/src/Support/Filesystem/MediaFile.php b/packages/framework/src/Support/Filesystem/MediaFile.php index 5457e18ba03..2c2835880b8 100644 --- a/packages/framework/src/Support/Filesystem/MediaFile.php +++ b/packages/framework/src/Support/Filesystem/MediaFile.php @@ -6,18 +6,16 @@ use Hyde\Hyde; use Hyde\Facades\Config; +use Hyde\Facades\Filesystem; use Illuminate\Support\Collection; use Hyde\Framework\Exceptions\FileNotFoundException; use Illuminate\Support\Str; use function Hyde\unslash; use function Hyde\path_join; +use function Hyde\trim_slashes; use function extension_loaded; -use function file_exists; use function array_merge; -use function filesize; -use function pathinfo; -use function is_file; /** * File abstraction for a project media file. @@ -27,18 +25,39 @@ class MediaFile extends ProjectFile /** @var array The default extensions for media types */ final public const EXTENSIONS = ['png', 'svg', 'jpg', 'jpeg', 'gif', 'ico', 'css', 'js']; - /** @return \Illuminate\Support\Collection The array keys are the filenames relative to the _media/ directory */ - public static function all(): Collection + public readonly int $length; + public readonly string $mimeType; + public readonly string $hash; + + public function __construct(string $path) { - return Hyde::assets(); + parent::__construct($this->getNormalizedPath($path)); + + $this->length = $this->findContentLength(); + $this->mimeType = $this->findMimeType(); + $this->hash = $this->findHash(); } - /** @return array Array of filenames relative to the _media/ directory */ + /** + * Get an array of media asset filenames relative to the `_media/` directory. + * + * @return array {@example `['app.css', 'images/logo.svg']`} + */ public static function files(): array { return static::all()->keys()->all(); } + /** + * Get a collection of all media files, parsed into `MediaFile` instances, keyed by the filenames relative to the `_media/` directory. + * + * @return \Illuminate\Support\Collection + */ + public static function all(): Collection + { + return Hyde::assets(); + } + /** * Get the absolute path to the media source directory, or a file within it. */ @@ -60,11 +79,12 @@ public static function outputPath(string $path = ''): string return Hyde::sitePath(Hyde::getMediaOutputDirectory()); } - $path = unslash($path); - - return Hyde::sitePath(Hyde::getMediaOutputDirectory()."/$path"); + return Hyde::sitePath(path_join(Hyde::getMediaOutputDirectory(), unslash($path))); } + /** + * Get the path to the media file relative to the media directory. + */ public function getIdentifier(): string { return Str::after($this->getPath(), Hyde::getMediaDirectory().'/'); @@ -75,21 +95,60 @@ public function toArray(): array return array_merge(parent::toArray(), [ 'length' => $this->getContentLength(), 'mimeType' => $this->getMimeType(), + 'hash' => $this->getHash(), ]); } public function getContentLength(): int { - if (! is_file($this->getAbsolutePath())) { - throw new FileNotFoundException($this->path); + return $this->length; + } + + public function getMimeType(): string + { + return $this->mimeType; + } + + public function getHash(): string + { + return $this->hash; + } + + /** @internal */ + public static function getCacheBustKey(string $file): string + { + return Config::getBool('hyde.enable_cache_busting', true) && Filesystem::exists(static::sourcePath("$file")) + ? '?v='.static::make($file)->getHash() + : ''; + } + + protected function getNormalizedPath(string $path): string + { + $path = Hyde::pathToRelative($path); + + // Normalize paths using output directory to have source directory prefix + if (str_starts_with($path, Hyde::getMediaOutputDirectory()) && str_starts_with(Hyde::getMediaDirectory(), '_')) { + $path = '_'.$path; } - return filesize($this->getAbsolutePath()); + // Normalize the path to include the media directory + $path = static::sourcePath(trim_slashes(Str::after($path, Hyde::getMediaDirectory()))); + + if (Filesystem::missing($path)) { + throw new FileNotFoundException($path); + } + + return $path; } - public function getMimeType(): string + protected function findContentLength(): int + { + return Filesystem::size($this->getPath()); + } + + protected function findMimeType(): string { - $extension = pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION); + $extension = $this->getExtension(); // See if we can find a mime type for the extension instead of // having to rely on a PHP extension and filesystem lookups. @@ -112,18 +171,15 @@ public function getMimeType(): string return $lookup[$extension]; } - if (extension_loaded('fileinfo') && file_exists($this->getAbsolutePath())) { - return mime_content_type($this->getAbsolutePath()); + if (extension_loaded('fileinfo') && Filesystem::exists($this->getPath())) { + return Filesystem::mimeType($this->getPath()); } return 'text/plain'; } - /** @internal */ - public static function getCacheBustKey(string $file): string + protected function findHash(): string { - return Config::getBool('hyde.enable_cache_busting', true) && file_exists(MediaFile::sourcePath("$file")) - ? '?v='.md5_file(MediaFile::sourcePath("$file")) - : ''; + return Filesystem::hash($this->getPath(), 'crc32'); } } diff --git a/packages/framework/src/Support/Filesystem/ProjectFile.php b/packages/framework/src/Support/Filesystem/ProjectFile.php index eb29ba893e9..36376fbc14c 100644 --- a/packages/framework/src/Support/Filesystem/ProjectFile.php +++ b/packages/framework/src/Support/Filesystem/ProjectFile.php @@ -11,8 +11,6 @@ use Hyde\Support\Concerns\Serializable; use Hyde\Support\Contracts\SerializableContract; -use function pathinfo; - /** * Filesystem abstraction for a file stored in the project. */ @@ -71,6 +69,6 @@ public function getContents(): string public function getExtension(): string { - return pathinfo($this->getAbsolutePath(), PATHINFO_EXTENSION); + return Filesystem::extension($this->getPath()); } } diff --git a/packages/framework/tests/Feature/Foundation/FilesystemTest.php b/packages/framework/tests/Feature/Foundation/FilesystemTest.php index 01f7e83ac11..a65c92d8d67 100644 --- a/packages/framework/tests/Feature/Foundation/FilesystemTest.php +++ b/packages/framework/tests/Feature/Foundation/FilesystemTest.php @@ -377,6 +377,7 @@ public function testAssetsMethodGetsAllSiteAssetsAsArray() 'path' => '_media/app.css', 'length' => 123, 'mimeType' => 'text/css', + 'hash' => hash_file('crc32', Hyde::path('_media/app.css')), ], ], $assets); } diff --git a/packages/framework/tests/Feature/Foundation/HyperlinksTest.php b/packages/framework/tests/Feature/Foundation/HyperlinksTest.php index 9a7893a0d6a..88fa946bf24 100644 --- a/packages/framework/tests/Feature/Foundation/HyperlinksTest.php +++ b/packages/framework/tests/Feature/Foundation/HyperlinksTest.php @@ -107,8 +107,8 @@ public function testMediaLinkHelperUsesConfiguredMediaDirectory() public function testMediaLinkHelperWithValidationAndExistingFile() { - $this->file('_media/foo'); - $this->assertSame('media/foo?v=d41d8cd98f00b204e9800998ecf8427e', $this->class->mediaLink('foo', true)); + $this->file('_media/foo', 'test'); + $this->assertSame('media/foo?v=accf8b33', $this->class->mediaLink('foo', true)); } public function testMediaLinkHelperWithValidationAndNonExistingFile() diff --git a/packages/framework/tests/Feature/Support/MediaFileTest.php b/packages/framework/tests/Feature/Support/MediaFileTest.php new file mode 100644 index 00000000000..df8756e6551 --- /dev/null +++ b/packages/framework/tests/Feature/Support/MediaFileTest.php @@ -0,0 +1,121 @@ +file('_media/test.txt', 'Hello, World!'); + + $mediaFile = MediaFile::make('test.txt'); + + $this->assertInstanceOf(MediaFile::class, $mediaFile); + $this->assertSame('test.txt', $mediaFile->getName()); + $this->assertSame('_media/test.txt', $mediaFile->getPath()); + $this->assertSame(Hyde::path('_media/test.txt'), $mediaFile->getAbsolutePath()); + $this->assertSame('Hello, World!', $mediaFile->getContents()); + $this->assertSame('txt', $mediaFile->getExtension()); + + $this->assertSame([ + 'name' => 'test.txt', + 'path' => '_media/test.txt', + 'length' => 13, + 'mimeType' => 'text/plain', + 'hash' => 'dffed8e6', + ], $mediaFile->toArray()); + } + + public function testMediaFileDiscovery() + { + // App.css is a default file + $this->file('_media/image.png', 'PNG content'); + $this->file('_media/style.css', 'CSS content'); + $this->file('_media/script.js', 'JS content'); + + $allFiles = MediaFile::all(); + + $this->assertCount(4, $allFiles); + $this->assertArrayHasKey('image.png', $allFiles); + $this->assertArrayHasKey('style.css', $allFiles); + $this->assertArrayHasKey('script.js', $allFiles); + + $fileNames = MediaFile::files(); + $this->assertSame(['image.png', 'app.css', 'style.css', 'script.js'], $fileNames); + } + + public function testMediaFileProperties() + { + $content = str_repeat('a', 1024); // 1KB content + $this->file('_media/large_file.txt', $content); + + $mediaFile = MediaFile::make('large_file.txt'); + + $this->assertSame(1024, $mediaFile->getContentLength()); + $this->assertSame('text/plain', $mediaFile->getMimeType()); + $this->assertSame(hash('crc32', $content), $mediaFile->getHash()); + } + + public function testMediaFilePathHandling() + { + $this->file('_media/subfolder/nested_file.txt', 'Nested content'); + + $mediaFile = MediaFile::make('subfolder/nested_file.txt'); + + $this->assertSame('subfolder/nested_file.txt', $mediaFile->getIdentifier()); + $this->assertSame('_media/subfolder/nested_file.txt', $mediaFile->getPath()); + } + + public function testMediaFileExceptionHandling() + { + $this->expectException(FileNotFoundException::class); + MediaFile::make('non_existent_file.txt'); + } + + public function testMediaDirectoryCustomization() + { + Hyde::setMediaDirectory('custom_media'); + + $this->file('custom_media/custom_file.txt', 'Custom content'); + + $mediaFile = MediaFile::make('custom_file.txt'); + + $this->assertSame('custom_media/custom_file.txt', $mediaFile->getPath()); + $this->assertSame(Hyde::path('custom_media/custom_file.txt'), $mediaFile->getAbsolutePath()); + + Hyde::setMediaDirectory('_media'); + } + + public function testMediaFileOutputPaths() + { + $this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath()); + $this->assertSame(Hyde::path('_site/media/test.css'), MediaFile::outputPath('test.css')); + + Hyde::setOutputDirectory('custom_output'); + $this->assertSame(Hyde::path('custom_output/media'), MediaFile::outputPath()); + + Hyde::setOutputDirectory('_site'); + } + + public function testMediaFileCacheBusting() + { + $this->file('_media/cachebust_test.js', 'console.log("Hello");'); + + $cacheBustKey = MediaFile::getCacheBustKey('cachebust_test.js'); + + $this->assertStringStartsWith('?v=', $cacheBustKey); + $this->assertSame('?v=cd5de5e7', $cacheBustKey); // Expect CRC32 hash + } +} diff --git a/packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php b/packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php index 5c43956cbcf..f219a8d9318 100644 --- a/packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php +++ b/packages/framework/tests/Unit/Facades/AssetFacadeUnitTest.php @@ -49,7 +49,7 @@ public function testHasMediaFileHelperReturnsTrueForExistingFile() public function testMediaLinkReturnsMediaPathWithCacheKey() { $this->assertIsString($path = Asset::mediaLink('app.css')); - $this->assertSame('media/app.css?v='.md5_file(Hyde::path('_media/app.css')), $path); + $this->assertSame('media/app.css?v='.hash_file('crc32', Hyde::path('_media/app.css')), $path); } public function testMediaLinkReturnsMediaPathWithoutCacheKeyIfCacheBustingIsDisabled() @@ -72,6 +72,6 @@ public function testMediaLinkSupportsCustomMediaDirectories() $path = Asset::mediaLink('app.css'); $this->assertIsString($path); - $this->assertSame('assets/app.css?v='.md5_file(Hyde::path('_assets/app.css')), $path); + $this->assertSame('assets/app.css?v='.hash_file('crc32', Hyde::path('_assets/app.css')), $path); } } diff --git a/packages/framework/tests/Unit/Foundation/FilesystemHasMediaFilesTest.php b/packages/framework/tests/Unit/Foundation/FilesystemHasMediaFilesTest.php index 594e6966bcf..d16f987b05a 100644 --- a/packages/framework/tests/Unit/Foundation/FilesystemHasMediaFilesTest.php +++ b/packages/framework/tests/Unit/Foundation/FilesystemHasMediaFilesTest.php @@ -4,11 +4,13 @@ namespace Hyde\Framework\Testing\Unit\Foundation; +use Mockery; use Hyde\Foundation\Kernel\Filesystem; use Hyde\Hyde; use Hyde\Support\Filesystem\MediaFile; use Hyde\Testing\UnitTestCase; use Illuminate\Support\Collection; +use Illuminate\Filesystem\Filesystem as BaseFilesystem; /** * @covers \Hyde\Foundation\Kernel\Filesystem @@ -24,6 +26,12 @@ protected function setUp(): void { parent::setUp(); $this->filesystem = new TestableFilesystem(Hyde::getInstance()); + + $mock = Mockery::mock(BaseFilesystem::class)->makePartial(); + $mock->shouldReceive('missing')->andReturn(false)->byDefault(); + $mock->shouldReceive('size')->andReturn(100)->byDefault(); + $mock->shouldReceive('hash')->andReturn('hash')->byDefault(); + app()->instance(BaseFilesystem::class, $mock); } public function testAssetsMethodReturnsSameInstanceOnSubsequentCalls() diff --git a/packages/framework/tests/Unit/IncludesFacadeUnitTest.php b/packages/framework/tests/Unit/IncludesFacadeUnitTest.php index 6dac17db2f5..bcf6d47cebe 100644 --- a/packages/framework/tests/Unit/IncludesFacadeUnitTest.php +++ b/packages/framework/tests/Unit/IncludesFacadeUnitTest.php @@ -8,6 +8,7 @@ use Closure; use Hyde\Hyde; use Hyde\Support\Includes; +use Illuminate\Support\Str; use Hyde\Testing\UnitTestCase; use Hyde\Support\Facades\Render; use Illuminate\Support\HtmlString; @@ -15,6 +16,7 @@ use Illuminate\Support\Facades\Blade; use Illuminate\Filesystem\Filesystem; use Hyde\Testing\MocksKernelFeatures; +use Hyde\Framework\Services\MarkdownService; /** * @covers \Hyde\Support\Includes @@ -37,6 +39,8 @@ protected function setUp(): void $this->setupTestKernel(); $this->kernel->setRoutes(collect()); Render::swap(new RenderData()); + + app()->bind(MarkdownService::class, SimpleMarkdownServiceTestClass::class); } protected function tearDown(): void @@ -255,3 +259,15 @@ protected function assertHtmlStringIsSame(string|HtmlString $expected, mixed $ac $this->assertSame((string) $expected, $actual->toHtml()); } } + +class SimpleMarkdownServiceTestClass +{ + public function __construct(protected string $markdown = '') + { + } + + public function parse(): string + { + return Str::markdown($this->markdown); + } +} diff --git a/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php b/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php index f2521fc54f6..741646b4c7e 100644 --- a/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php +++ b/packages/framework/tests/Unit/MediaDirectoryCanBeChangedTest.php @@ -67,12 +67,12 @@ public function testCompiledPagesHaveLinksToTheRightMediaFileLocation() $this->assertFileExists(Hyde::path('_site/foo.html')); $contents = file_get_contents(Hyde::path('_site/foo.html')); $this->assertStringContainsString( - '', + '', $contents ); $this->assertStringContainsString( - '', + '', $contents ); diff --git a/packages/framework/tests/Unit/Support/MediaFileTest.php b/packages/framework/tests/Unit/Support/MediaFileTest.php deleted file mode 100644 index 16f9c1477f3..00000000000 --- a/packages/framework/tests/Unit/Support/MediaFileTest.php +++ /dev/null @@ -1,300 +0,0 @@ -cleanUpFilesystem(); - } - - /** @deprecated */ - public function testDiscoveryBenchmark() - { - $this->markTestSkipped('Uncomment this line to run the benchmark.'); - - $urls = [ - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/austria.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/boat.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/croatia.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/fireworks.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/hallstatt.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/lemonade.jpg', - 'https://raw.githubusercontent.com/caendesilva/RandomDatasets/master/Media/photographer.jpg', - ]; - - foreach ($urls as $url) { - $this->file('_media/'.basename($url), file_get_contents($url)); - } - - $this->file('_media/foo.css', 'body { color: red; }'); - $this->file('_media/foo.js', 'console.log("Hello, World!");'); - $this->file('_media/nested/foo.css', 'foo'); - $this->file('_media/empty.css', ''); - $this->file('_media/ignored', 'ignored'); - $this->directory('_media/empty'); - $this->file('_media/large.css', str_repeat('a', 1024 * 1024 * 10)); // 10 MB - - // Warm up the classloader / cache - echo 'Warmup: '.\Illuminate\Support\Benchmark::measure(function () { - MediaFile::all(); - })."ms\n"; - - echo 'Benchmark: '.\Illuminate\Support\Benchmark::measure(function () { - MediaFile::all(); - }, 1000)."ms avg/1000/its\n"; - } - - public function testCanConstruct() - { - $file = new MediaFile('foo'); - - $this->assertInstanceOf(MediaFile::class, $file); - $this->assertSame('foo', $file->path); - } - - public function testCanMake() - { - $this->assertEquals(new MediaFile('foo'), MediaFile::make('foo')); - } - - public function testCanConstructWithNestedPaths() - { - $this->assertSame('path/to/file.txt', MediaFile::make('path/to/file.txt')->path); - } - - public function testAbsolutePathIsNormalizedToRelative() - { - $this->assertSame('foo', MediaFile::make(Hyde::path('foo'))->path); - } - - public function testGetNameReturnsNameOfFile() - { - $this->assertSame('foo.txt', MediaFile::make('foo.txt')->getName()); - $this->assertSame('bar.txt', MediaFile::make('foo/bar.txt')->getName()); - } - - public function testGetPathReturnsPathOfFile() - { - $this->assertSame('foo.txt', MediaFile::make('foo.txt')->getPath()); - $this->assertSame('foo/bar.txt', MediaFile::make('foo/bar.txt')->getPath()); - } - - public function testGetAbsolutePathReturnsAbsolutePathOfFile() - { - $this->assertSame(Hyde::path('foo.txt'), MediaFile::make('foo.txt')->getAbsolutePath()); - $this->assertSame(Hyde::path('foo/bar.txt'), MediaFile::make('foo/bar.txt')->getAbsolutePath()); - } - - public function testGetContentsReturnsContentsOfFile() - { - $this->file('foo.txt', 'foo bar'); - $this->assertSame('foo bar', MediaFile::make('foo.txt')->getContents()); - } - - public function testGetExtensionReturnsExtensionOfFile() - { - $this->file('foo.txt', 'foo'); - $this->assertSame('txt', MediaFile::make('foo.txt')->getExtension()); - - $this->file('foo.png', 'foo'); - $this->assertSame('png', MediaFile::make('foo.png')->getExtension()); - } - - public function testToArrayReturnsArrayOfFileProperties() - { - $this->file('foo.txt', 'foo bar'); - - $this->assertSame([ - 'name' => 'foo.txt', - 'path' => 'foo.txt', - 'length' => 7, - 'mimeType' => 'text/plain', - ], MediaFile::make('foo.txt')->toArray()); - } - - public function testToArrayWithEmptyFileWithNoExtension() - { - $this->file('foo', 'foo bar'); - - $this->assertSame([ - 'name' => 'foo', - 'path' => 'foo', - 'length' => 7, - 'mimeType' => 'text/plain', - ], MediaFile::make('foo')->toArray()); - } - - public function testToArrayWithFileInSubdirectory() - { - mkdir(Hyde::path('foo')); - touch(Hyde::path('foo/bar.txt')); - - $this->assertSame([ - 'name' => 'bar.txt', - 'path' => 'foo/bar.txt', - 'length' => 0, - 'mimeType' => 'text/plain', - ], MediaFile::make('foo/bar.txt')->toArray()); - - Filesystem::unlink('foo/bar.txt'); - rmdir(Hyde::path('foo')); - } - - public function testGetContentLength() - { - $this->file('foo', 'Hello World!'); - $this->assertSame(12, MediaFile::make('foo')->getContentLength()); - } - - public function testGetContentLengthWithEmptyFile() - { - $this->file('foo', ''); - $this->assertSame(0, MediaFile::make('foo')->getContentLength()); - } - - public function testGetContentLengthWithDirectory() - { - $this->directory('foo'); - - $this->expectException(FileNotFoundException::class); - $this->expectExceptionMessage('File [foo] not found.'); - - MediaFile::make('foo')->getContentLength(); - } - - public function testGetContentLengthWithNonExistentFile() - { - $this->expectException(FileNotFoundException::class); - $this->expectExceptionMessage('File [foo] not found.'); - - MediaFile::make('foo')->getContentLength(); - } - - public function testGetMimeType() - { - $this->file('foo.txt', 'Hello World!'); - $this->assertSame('text/plain', MediaFile::make('foo.txt')->getMimeType()); - } - - public function testGetMimeTypeWithoutExtension() - { - $this->file('foo', 'Hello World!'); - $this->assertSame('text/plain', MediaFile::make('foo')->getMimeType()); - } - - public function testGetMimeTypeWithEmptyFile() - { - $this->file('foo', ''); - $this->assertSame('application/x-empty', MediaFile::make('foo')->getMimeType()); - } - - public function testGetMimeTypeWithDirectory() - { - $this->directory('foo'); - $this->assertSame('directory', MediaFile::make('foo')->getMimeType()); - } - - public function testGetMimeTypeWithNonExistentFile() - { - $this->assertSame('text/plain', MediaFile::make('foo')->getMimeType()); - } - - public function testAllHelperReturnsAllMediaFiles() - { - $this->assertEquals([ - 'app.css' => new MediaFile('_media/app.css'), - ], MediaFile::all()->all()); - } - - public function testAllHelperDoesNotIncludeNonMediaFiles() - { - $this->file('_media/foo.blade.php'); - - $this->assertEquals([ - 'app.css' => new MediaFile('_media/app.css'), - ], MediaFile::all()->all()); - } - - public function testFilesHelperReturnsAllMediaFiles() - { - $this->assertSame(['app.css'], MediaFile::files()); - } - - public function testGetIdentifierReturnsIdentifier() - { - $this->assertSame('foo', MediaFile::make('foo')->getIdentifier()); - } - - public function testGetIdentifierWithSubdirectory() - { - $this->assertSame('foo/bar', MediaFile::make('foo/bar')->getIdentifier()); - } - - public function testHelperForMediaPath() - { - $this->assertSame(Hyde::path('_media'), MediaFile::sourcePath()); - } - - public function testHelperForMediaPathReturnsPathToFileWithinTheDirectory() - { - $this->assertSame(Hyde::path('_media/foo.css'), MediaFile::sourcePath('foo.css')); - } - - public function testGetMediaPathReturnsAbsolutePath() - { - $this->assertSame(Hyde::path('_media'), MediaFile::sourcePath()); - } - - public function testHelperForMediaOutputPath() - { - $this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath()); - } - - public function testHelperForMediaOutputPathReturnsPathToFileWithinTheDirectory() - { - $this->assertSame(Hyde::path('_site/media/foo.css'), MediaFile::outputPath('foo.css')); - } - - public function testGetMediaOutputPathReturnsAbsolutePath() - { - $this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath()); - } - - public function testCanGetSiteMediaOutputDirectory() - { - $this->assertSame(Hyde::path('_site/media'), MediaFile::outputPath()); - } - - public function testGetSiteMediaOutputDirectoryUsesTrimmedVersionOfMediaSourceDirectory() - { - Hyde::setMediaDirectory('_foo'); - $this->assertSame(Hyde::path('_site/foo'), MediaFile::outputPath()); - } - - public function testGetSiteMediaOutputDirectoryUsesConfiguredSiteOutputDirectory() - { - Hyde::setOutputDirectory(Hyde::path('foo')); - Hyde::setMediaDirectory('bar'); - - $this->assertSame(Hyde::path('foo/bar'), MediaFile::outputPath()); - } -} diff --git a/packages/framework/tests/Unit/Support/MediaFileUnitTest.php b/packages/framework/tests/Unit/Support/MediaFileUnitTest.php new file mode 100644 index 00000000000..f07bf42cc61 --- /dev/null +++ b/packages/framework/tests/Unit/Support/MediaFileUnitTest.php @@ -0,0 +1,392 @@ +mockFilesystem = Mockery::mock(BaseFilesystem::class); + app()->instance(BaseFilesystem::class, $this->mockFilesystem); + + // Set up default expectations for commonly called methods + $this->mockFilesystem->shouldReceive('isFile')->andReturn(true)->byDefault(); + $this->mockFilesystem->shouldReceive('missing')->andReturn(false)->byDefault(); + $this->mockFilesystem->shouldReceive('extension')->andReturn('txt')->byDefault(); + $this->mockFilesystem->shouldReceive('size')->andReturn(12)->byDefault(); + $this->mockFilesystem->shouldReceive('mimeType')->andReturn('text/plain')->byDefault(); + $this->mockFilesystem->shouldReceive('hash')->andReturn(hash('crc32', 'Hello World!'))->byDefault(); + $this->mockFilesystem->shouldReceive('get')->andReturn('Hello World!')->byDefault(); + + // Mock Hyde facade + $hyde = Mockery::mock(Hyde::kernel())->makePartial(); + $hyde->shouldReceive('assets')->andReturn(collect(['app.css' => new MediaFile('_media/app.css')]))->byDefault(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + Mockery::close(); + } + + public function testCanConstruct() + { + $file = new MediaFile('foo'); + + $this->assertInstanceOf(MediaFile::class, $file); + $this->assertSame('_media/foo', $file->path); + } + + public function testCanMake() + { + $this->assertEquals(new MediaFile('foo'), MediaFile::make('foo')); + } + + public function testCanConstructWithNestedPaths() + { + $this->assertSame('_media/path/to/file.txt', MediaFile::make('path/to/file.txt')->path); + } + + public function testPathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make('foo')->path); + } + + public function testAbsolutePathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make(Hyde::path('foo'))->path); + } + + public function testMediaPathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make('_media/foo')->path); + } + + public function testAbsoluteMediaPathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make(Hyde::path('_media/foo'))->path); + } + + public function testOutputMediaPathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make('media/foo')->path); + } + + public function testAbsoluteOutputMediaPathIsNormalizedToRelativeMediaPath() + { + $this->assertSame('_media/foo', MediaFile::make(Hyde::path('media/foo'))->path); + } + + public function testCustomMediaPathsAreNormalizedToRelativeCustomizedMediaPath() + { + Hyde::setMediaDirectory('bar'); + + $this->assertSame('bar/foo', MediaFile::make('foo')->path); + $this->assertSame('bar/foo', MediaFile::make('bar/foo')->path); + $this->assertSame('bar/foo', MediaFile::make(Hyde::path('foo'))->path); + + Hyde::setMediaDirectory('_bar'); + + $this->assertSame('_bar/foo', MediaFile::make('foo')->path); + $this->assertSame('_bar/foo', MediaFile::make('_bar/foo')->path); + $this->assertSame('_bar/foo', MediaFile::make(Hyde::path('_bar/foo'))->path); + $this->assertSame('_bar/foo', MediaFile::make('bar/foo')->path); + $this->assertSame('_bar/foo', MediaFile::make(Hyde::path('foo'))->path); + + Hyde::setMediaDirectory('_media'); + } + + public function testConstructorWithVariousInputFormats() + { + $this->assertSame('_media/foo.txt', MediaFile::make('foo.txt')->path); + $this->assertSame('_media/foo.txt', MediaFile::make('_media/foo.txt')->path); + $this->assertSame('_media/foo.txt', MediaFile::make(Hyde::path('_media/foo.txt'))->path); + $this->assertSame('_media/foo.txt', MediaFile::make('media/foo.txt')->path); + } + + public function testConstructorSetsProperties() + { + $file = new MediaFile('foo.txt'); + $this->assertNotNull($file->length); + $this->assertNotNull($file->mimeType); + $this->assertNotNull($file->hash); + } + + public function testNormalizePathWithAbsolutePath() + { + $this->assertSame('_media/foo.txt', MediaFile::make(Hyde::path('_media/foo.txt'))->path); + } + + public function testNormalizePathWithRelativePath() + { + $this->assertSame('_media/foo.txt', MediaFile::make('foo.txt')->path); + } + + public function testNormalizePathWithOutputDirectoryPath() + { + Hyde::setMediaDirectory('_custom_media'); + $this->assertSame('_custom_media/foo.txt', MediaFile::make('custom_media/foo.txt')->path); + Hyde::setMediaDirectory('_media'); // Reset to default + } + + public function testNormalizePathWithAlreadyCorrectFormat() + { + $this->assertSame('_media/foo.txt', MediaFile::make('_media/foo.txt')->path); + } + + public function testNormalizePathWithParentDirectoryReferences() + { + $this->assertSame('_media/foo.txt', MediaFile::make('../_media/foo.txt')->path); + $this->assertSame('_media/baz/../bar/foo.txt', MediaFile::make('_media/baz/../bar/foo.txt')->path); // We don't do anything about this + } + + public function testGetNameReturnsNameOfFile() + { + $this->assertSame('foo.txt', MediaFile::make('foo.txt')->getName()); + $this->assertSame('bar.txt', MediaFile::make('foo/bar.txt')->getName()); + } + + public function testGetPathReturnsPathOfFile() + { + $this->assertSame('_media/foo.txt', MediaFile::make('foo.txt')->getPath()); + $this->assertSame('_media/foo/bar.txt', MediaFile::make('foo/bar.txt')->getPath()); + } + + public function testGetAbsolutePathReturnsAbsolutePathOfFile() + { + $this->assertSame(Hyde::path('_media/foo.txt'), MediaFile::make('foo.txt')->getAbsolutePath()); + $this->assertSame(Hyde::path('_media/foo/bar.txt'), MediaFile::make('foo/bar.txt')->getAbsolutePath()); + } + + public function testGetContentsReturnsContentsOfFile() + { + $this->mockFilesystem->shouldReceive('get') + ->andReturn('foo bar') + ->once(); + + $this->assertSame('foo bar', MediaFile::make('foo.txt')->getContents()); + } + + public function testGetExtensionReturnsExtensionOfFile() + { + $this->mockFilesystem->shouldReceive('extension') + ->with(Hyde::path('_media/foo.txt')) + ->andReturn('txt'); + + $this->mockFilesystem->shouldReceive('extension') + ->with(Hyde::path('_media/foo.png')) + ->andReturn('png'); + + $this->assertSame('txt', MediaFile::make('foo.txt')->getExtension()); + $this->assertSame('png', MediaFile::make('foo.png')->getExtension()); + } + + public function testToArrayReturnsArrayOfFileProperties() + { + $this->mockFilesystem->shouldReceive('size') + ->with(Hyde::path('_media/foo.txt')) + ->andReturn(7); + + $this->mockFilesystem->shouldReceive('mimeType') + ->with(Hyde::path('_media/foo.txt')) + ->andReturn('text/plain'); + + $this->mockFilesystem->shouldReceive('hash') + ->with(Hyde::path('_media/foo.txt'), 'crc32') + ->andReturn(hash('crc32', 'foo bar')); + + $this->assertSame([ + 'name' => 'foo.txt', + 'path' => '_media/foo.txt', + 'length' => 7, + 'mimeType' => 'text/plain', + 'hash' => hash('crc32', 'foo bar'), + ], MediaFile::make('foo.txt')->toArray()); + } + + public function testGetContentLength() + { + $this->mockFilesystem->shouldReceive('size') + ->with(Hyde::path('_media/foo')) + ->andReturn(12); + + $this->assertSame(12, MediaFile::make('foo')->getContentLength()); + } + + public function testGetMimeType() + { + $this->mockFilesystem->shouldReceive('mimeType') + ->with(Hyde::path('_media/foo.txt')) + ->andReturn('text/plain'); + + $this->assertSame('text/plain', MediaFile::make('foo.txt')->getMimeType()); + } + + public function testGetHashReturnsHash() + { + $this->mockFilesystem->shouldReceive('hash') + ->with(Hyde::path('_media/foo.txt'), 'crc32') + ->andReturn(hash('crc32', 'Hello World!')); + + $this->assertSame(hash('crc32', 'Hello World!'), MediaFile::make('foo.txt')->getHash()); + } + + public function testExceptionIsThrownWhenConstructingFileThatDoesNotExist() + { + $this->mockFilesystem->shouldReceive('missing') + ->with(Hyde::path('_media/foo')) + ->andReturn(true); + + $this->expectException(FileNotFoundException::class); + $this->expectExceptionMessage('File [_media/foo] not found.'); + + MediaFile::make('foo'); + } + + public function testExceptionIsNotThrownWhenConstructingFileThatDoesExist() + { + $this->mockFilesystem->shouldReceive('missing') + ->with(Hyde::path('_media/foo')) + ->andReturn(false); + + $this->assertInstanceOf(MediaFile::class, MediaFile::make('foo')); + } + + public function testGetIdentifierWithSubdirectory() + { + $this->assertSame('foo/bar', MediaFile::make('foo/bar')->getIdentifier()); + } + + public function testGetIdentifierReturnsIdentifierWithFileExtension() + { + $this->assertSame('foo.png', MediaFile::make('foo.png')->getIdentifier()); + } + + public function testGetIdentifierWithSubdirectoryWithFileExtension() + { + $this->assertSame('foo/bar.png', MediaFile::make('foo/bar.png')->getIdentifier()); + } + + public function testHelperForMediaPath() + { + $this->assertSame('/base/path/_media', MediaFile::sourcePath()); + } + + public function testHelperForMediaPathReturnsPathToFileWithinTheDirectory() + { + $this->assertSame('/base/path/_media/foo.css', MediaFile::sourcePath('foo.css')); + } + + public function testGetMediaPathReturnsAbsolutePath() + { + $this->assertSame('/base/path/_media', MediaFile::sourcePath()); + } + + public function testHelperForMediaOutputPath() + { + $this->assertSame('/base/path/_site/media', MediaFile::outputPath()); + } + + public function testHelperForMediaOutputPathReturnsPathToFileWithinTheDirectory() + { + $this->assertSame('/base/path/_site/media/foo.css', MediaFile::outputPath('foo.css')); + } + + public function testGetMediaOutputPathReturnsAbsolutePath() + { + $this->assertSame('/base/path/_site/media', MediaFile::outputPath()); + } + + public function testCanGetSiteMediaOutputDirectory() + { + $this->assertSame('/base/path/_site/media', MediaFile::outputPath()); + } + + public function testGetSiteMediaOutputDirectoryUsesTrimmedVersionOfMediaSourceDirectory() + { + Hyde::setMediaDirectory('_foo'); + $this->assertSame('/base/path/_site/foo', MediaFile::outputPath()); + Hyde::setMediaDirectory('_media'); // Reset to default + } + + public function testGetSiteMediaOutputDirectoryUsesConfiguredSiteOutputDirectory() + { + Hyde::setOutputDirectory('/base/path/foo'); + Hyde::setMediaDirectory('bar'); + + $this->assertSame('/base/path/foo/bar', MediaFile::outputPath()); + + Hyde::setOutputDirectory('/base/path/_site'); // Reset to default + Hyde::setMediaDirectory('_media'); // Reset to default + } + + public function testSourcePathWithEmptyString() + { + $this->assertSame(Hyde::path('_media'), MediaFile::sourcePath('')); + } + + public function testSourcePathWithSubdirectories() + { + $this->assertSame(Hyde::path('_media/foo/bar'), MediaFile::sourcePath('foo/bar')); + } + + public function testSourcePathWithLeadingSlash() + { + $this->assertSame(Hyde::path('_media/foo'), MediaFile::sourcePath('/foo')); + } + + public function testOutputPathWithEmptyString() + { + $this->assertSame(Hyde::sitePath('media'), MediaFile::outputPath('')); + } + + public function testOutputPathWithSubdirectories() + { + $this->assertSame(Hyde::sitePath('media/foo/bar'), MediaFile::outputPath('foo/bar')); + } + + public function testOutputPathWithLeadingSlash() + { + $this->assertSame(Hyde::sitePath('media/foo'), MediaFile::outputPath('/foo')); + } +}