From cc255487ed03bc717bb67d39f74bb71fb0136c25 Mon Sep 17 00:00:00 2001 From: acoulton Date: Wed, 15 Jan 2020 23:37:08 +0000 Subject: [PATCH 1/3] Add StaticAssetUrlProvider to provide URLs for assets in dev/prod Provides simple cache-busted local URLs in development environments, and remote (e.g. cloud storage / s3) urls in production. --- CHANGELOG.md | 3 + src/Assets/StaticAssetUrlProvider.php | 128 +++++++++++++++++ .../Assets/StaticAssetUrlProviderTest.php | 132 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 src/Assets/StaticAssetUrlProvider.php create mode 100644 test/unit/Assets/StaticAssetUrlProviderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed6080..3291857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### Unreleased +* Add StaticAssetUrlProvider to provide simple cache-busted local URLs for CSS etc in local + dev or remote (e.g. cloud storage / s3) urls in production. + ### v1.2.1 (2019-11-15) * Allow DeploymentConfig->map() to return values in standalone environment diff --git a/src/Assets/StaticAssetUrlProvider.php b/src/Assets/StaticAssetUrlProvider.php new file mode 100644 index 0000000..c393d06 --- /dev/null +++ b/src/Assets/StaticAssetUrlProvider.php @@ -0,0 +1,128 @@ +mode = $mode; + $this->local_asset_path = rtrim($local_asset_path, '/'); + + if ($this->mode === static::MODE_REMOTE) { + $this->remote_asset_url = $this->loadAssetBaseUrl($asset_base_url_file); + } + } + + protected function loadAssetBaseUrl(string $asset_base_url_file): string + { + if ( ! file_exists($asset_base_url_file)) { + throw new InvalidArgumentException('No asset base url file at '.$asset_base_url_file); + } + + $url = require $asset_base_url_file; + + if (empty($url) OR ! is_string($url)) { + throw new RuntimeException('Invalid content in asset base url file '.$asset_base_url_file); + } + + return $url; + } + + /** + * Generates a URL for a static asset, given its relative path. + * + * @param string $rel_path the path of the asset within the docroot / uploaded asset files + * + * @return string the full URL to render to the client + */ + public function getUrl(string $rel_path): string + { + if ($rel_path[0] !== '/') { + $rel_path = '/'.$rel_path; + } + + if ($this->mode === static::MODE_LOCAL) { + return $this->getLocalTimestampedUrl($rel_path); + } else { + return $this->remote_asset_url.$rel_path; + } + } + + /** + * @param string $rel_path + * + * @return string + */ + protected function getLocalTimestampedUrl(string $rel_path): string + { + $local_path = $this->local_asset_path.$rel_path; + if ( ! file_exists($local_path)) { + throw new RuntimeException('Undefined asset file '.$local_path); + } + + return $rel_path.'?v='.filemtime($local_path); + } +} diff --git a/test/unit/Assets/StaticAssetUrlProviderTest.php b/test/unit/Assets/StaticAssetUrlProviderTest.php new file mode 100644 index 0000000..39d2e79 --- /dev/null +++ b/test/unit/Assets/StaticAssetUrlProviderTest.php @@ -0,0 +1,132 @@ + StaticAssetUrlProvider::MODE_LOCAL, + 'local_asset_path' => __DIR__, + 'asset_base_url_file' => __FILE__, + ]; + + /** + * @var vfsStreamDirectory + */ + protected $vfs; + + public function test_it_is_initialisable() + { + $this->assertInstanceOf(StaticAssetUrlProvider::class, $this->newSubject()); + } + + public function test_it_throws_in_invalid_mode() + { + $this->options['mode'] = 'some-junk'; + $this->expectException(\InvalidArgumentException::class); + $this->newSubject(); + } + + public function test_in_local_mode_get_url_throws_if_file_does_not_exist() + { + $subject = $this->newSubject(); + $this->expectException(\RuntimeException::class); + $subject->getUrl('assets/some-file.css'); + } + + /** + * @testWith ["assets/my-file.css"] + * ["/assets/my-file.css"] + */ + public function test_in_local_mode_get_url_returns_absolute_url_with_mtime_suffix($rel_path) + { + vfsStream::create( + [ + 'assets' => [ + 'my-file.css' => 'some-content', + ], + ], + $this->vfs->getChild('docroot') + ); + $this->vfs->getChild('docroot/assets/my-file.css')->lastModified(123456); + $this->assertSame( + '/assets/my-file.css?v=123456', + $this->newSubject()->getUrl($rel_path) + ); + } + + public function test_in_remote_mode_get_url_throws_if_asset_base_url_file_does_not_exist() + { + $this->options['asset_base_url_file'] = $this->vfs->url().'/no-such-file.php'; + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('no-such-file.php'); + $this->newSubject(); + } + + /** + * @testWith [""] + * ["some content that is not php"] + * [" $file_content], + $this->vfs + ); + + $this->options['asset_base_url_file'] = $this->vfs->getChild('asset-base-url.php')->url(); + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid content in asset base url'); + $this->newSubject(); + } + + /** + * @testWith ["assets/my-file.css"] + * ["/assets/my-file.css"] + */ + public function test_it_remote_mode_get_url_returns_url_prefixed_with_base_url($rel_path) + { + vfsStream::create( + ['asset-base-url.php' => 'vfs + ); + + $this->options['asset_base_url_file'] = $this->vfs->getChild('asset-base-url.php')->url(); + $this->options['mode'] = StaticAssetUrlProvider::MODE_REMOTE; + + $this->assertSame( + 'https://i.am.the.walrus/branch/sha/assets/my-file.css', + $this->newSubject()->getUrl($rel_path) + ); + } + + protected function setUp() + { + parent::setUp(); + $this->vfs = vfsStream::setup('vfs', NULL, ['docroot' => []]); + $this->options['local_asset_path'] = $this->vfs->getChild('docroot')->url(); + $this->options['asset_base_url_file'] = $this->vfs->url().'/asset-base-url.php'; + } + + protected function newSubject() + { + return new StaticAssetUrlProvider( + $this->options['mode'], + $this->options['local_asset_path'], + $this->options['asset_base_url_file'] + ); + } + +} From d012894e67bfc9aff7560c5ef1c057cbc459f6ff Mon Sep 17 00:00:00 2001 From: acoulton Date: Thu, 16 Jan 2020 00:04:01 +0000 Subject: [PATCH 2/3] Bump changelog for 1.3.0 release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3291857..a33e32f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### Unreleased +### v1.3.0 (2020-01-16) + * Add StaticAssetUrlProvider to provide simple cache-busted local URLs for CSS etc in local dev or remote (e.g. cloud storage / s3) urls in production. From fb36a2e81a07c49656a7542366ab0044b37269ff Mon Sep 17 00:00:00 2001 From: acoulton Date: Thu, 16 Jan 2020 00:20:42 +0000 Subject: [PATCH 3/3] Randomise keypair name in tests to attempt to fix flickering test The test doesn't always fail on travis, possibly some crossover between testcases somehow? Though that's not really supposed to be happening. Use a randomised key name so it defo can't conflict. --- test/unit/DeploymentConfig/ConfigValueDecrypterTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php b/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php index 0a46352..034e85a 100644 --- a/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php +++ b/test/unit/DeploymentConfig/ConfigValueDecrypterTest.php @@ -145,10 +145,11 @@ public function test_it_throws_if_required_decryption_key_is_not_present() $valid_encrypted_val = sodium_crypto_box_seal('whoops', sodium_crypto_box_publickey($kp)); // The actual encrypted value is fine, but the problem is that the keypair it specifies isn't present with the // correct name. - $subject = $this->newSubject(); + $other_keypair_name = uniqid('very'); + $subject = $this->newSubject(); $this->expectException(InvalidConfigException::class); $this->expectExceptionMessage('Unknown config decryption key'); - $subject->decrypt('#SECRET-very#'.$valid_encrypted_val); + $subject->decrypt("#SECRET-$other_keypair_name#$valid_encrypted_val"); } public function provider_failed_decrypt()