diff --git a/app/Services/ReleaseChecker.php b/app/Services/ReleaseChecker.php index 6de2333..2bd58f5 100644 --- a/app/Services/ReleaseChecker.php +++ b/app/Services/ReleaseChecker.php @@ -23,7 +23,17 @@ class ReleaseChecker private function cacheKey(string $channel): string { - return 'clusev:latest-release:'.$channel; + return 'clusev:latest-release:'.$this->repoTag().':'.$channel; + } + + /** + * Short discriminator of the configured repository, mixed into every cache key so a cached + * tag/changelog can never be reused across a host/repo change (e.g. the promotion pipeline + * re-points CLUSEV_REPOSITORY from the private mirror to the public repo). + */ + private function repoTag(): string + { + return substr(md5((string) config('clusev.repository')), 0, 8); } public function channel(): string @@ -146,7 +156,7 @@ class ReleaseChecker } [$base, $owner, $name] = $parts; - $key = 'clusev:remote-changelog:'.$ref; + $key = 'clusev:remote-changelog:'.$this->repoTag().':'.$ref; $cached = Cache::get($key); if (is_string($cached)) { return $cached; diff --git a/tests/Feature/ReleaseCheckerTest.php b/tests/Feature/ReleaseCheckerTest.php index 5f1d79e..9707da1 100644 --- a/tests/Feature/ReleaseCheckerTest.php +++ b/tests/Feature/ReleaseCheckerTest.php @@ -30,10 +30,16 @@ class ReleaseCheckerTest extends TestCase config()->set('clusev.repository', self::REPO); } + /** The cache key ReleaseChecker uses for the configured repo (host-discriminated). */ + private function tagKey(string $channel = 'stable'): string + { + return 'clusev:latest-release:'.substr(md5(self::REPO), 0, 8).':'.$channel; + } + public function test_update_available_is_true_when_the_cache_holds_a_newer_tag(): void { config()->set('clusev.version', '0.9.10'); - Cache::put('clusev:latest-release:stable', '0.9.11', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.11', now()->addMinutes(30)); $this->assertTrue(app(ReleaseChecker::class)->updateAvailable()); } @@ -44,10 +50,10 @@ class ReleaseCheckerTest extends TestCase $this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'empty cache → no badge'); - Cache::put('clusev:latest-release:stable', '0.9.11', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.11', now()->addMinutes(30)); $this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'equal version → no badge'); - Cache::put('clusev:latest-release:stable', '0.9.9', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.9', now()->addMinutes(30)); $this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'stale older cache → no badge'); } @@ -55,7 +61,7 @@ class ReleaseCheckerTest extends TestCase { Http::fake(); config()->set('clusev.version', '0.9.10'); - Cache::put('clusev:latest-release:stable', '0.9.12', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.12', now()->addMinutes(30)); $this->assertTrue(app(ReleaseChecker::class)->updateAvailable()); Http::assertNothingSent(); @@ -68,7 +74,7 @@ class ReleaseCheckerTest extends TestCase $tag = app(ReleaseChecker::class)->refresh('stable'); $this->assertSame('0.9.13', $tag); - $this->assertSame('0.9.13', Cache::get('clusev:latest-release:stable')); + $this->assertSame('0.9.13', Cache::get($this->tagKey())); } public function test_check_update_command_warms_the_cache(): void @@ -77,7 +83,7 @@ class ReleaseCheckerTest extends TestCase $this->artisan('clusev:check-update')->assertSuccessful(); - $this->assertSame('0.9.20', Cache::get('clusev:latest-release:stable')); + $this->assertSame('0.9.20', Cache::get($this->tagKey())); } public function test_sidebar_shows_the_update_badge_only_when_an_update_is_available(): void @@ -90,7 +96,7 @@ class ReleaseCheckerTest extends TestCase $this->get('/audit')->assertOk()->assertDontSee(__('shell.update_available')); // Cached newer release → the sidebar Version item carries the badge. - Cache::put('clusev:latest-release:stable', '0.9.11', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.11', now()->addMinutes(30)); $this->get('/audit')->assertOk()->assertSee(__('shell.update_available')); } @@ -116,9 +122,10 @@ class ReleaseCheckerTest extends TestCase $tag = app(ReleaseChecker::class)->refresh('stable'); $this->assertSame('0.9.20', $tag); - // The GitHub tag API host/path must be used, with the mandatory User-Agent header. + // The GitHub tag API host/path must be used, with OUR explicit User-Agent (GitHub rejects + // requests with none; assert the value, not just presence — Guzzle always sets some UA). Http::assertSent(fn ($req) => str_contains($req->url(), 'api.github.com/repos/clusev/clusev/tags') - && $req->hasHeader('User-Agent')); + && $req->hasHeader('User-Agent', 'Clusev-Panel')); } public function test_fetch_changelog_uses_raw_githubusercontent_for_a_github_repository(): void diff --git a/tests/Feature/VersionUpdateCheckTest.php b/tests/Feature/VersionUpdateCheckTest.php index afd3146..c7645b2 100644 --- a/tests/Feature/VersionUpdateCheckTest.php +++ b/tests/Feature/VersionUpdateCheckTest.php @@ -44,6 +44,12 @@ class VersionUpdateCheckTest extends TestCase parent::tearDown(); } + /** The cache key ReleaseChecker uses for the configured repo (host-discriminated). */ + private function tagKey(string $channel = 'stable'): string + { + return 'clusev:latest-release:'.substr(md5(self::REPO), 0, 8).':'.$channel; + } + public function test_reports_update_available_from_the_remote_tag_api(): void { config()->set('clusev.version', '0.9.4'); @@ -173,7 +179,7 @@ class VersionUpdateCheckTest extends TestCase // A stale cache from before an update must not render a "latest" below the installed // version (which would look like the panel is behind a release it has already passed). config()->set('clusev.version', '0.9.10'); - Cache::put('clusev:latest-release:stable', '0.9.9', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.9', now()->addMinutes(30)); Livewire::test(Index::class)->assertViewHas('latestTag', null); } @@ -204,7 +210,7 @@ class VersionUpdateCheckTest extends TestCase { // The cached check (badge already knew) surfaces the update + button on mount, no re-click. config()->set('clusev.version', '0.9.10'); - Cache::put('clusev:latest-release:stable', '0.9.11', now()->addMinutes(30)); + Cache::put($this->tagKey(), '0.9.11', now()->addMinutes(30)); Livewire::test(Index::class) ->assertSet('updateState', 'update')