set('clusev.repository', self::REPO); } 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)); $this->assertTrue(app(ReleaseChecker::class)->updateAvailable()); } public function test_update_available_is_false_when_cache_is_at_or_below_installed_or_empty(): void { config()->set('clusev.version', '0.9.11'); $this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'empty cache → no badge'); Cache::put('clusev:latest-release:stable', '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)); $this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'stale older cache → no badge'); } public function test_update_available_does_no_network(): void { Http::fake(); config()->set('clusev.version', '0.9.10'); Cache::put('clusev:latest-release:stable', '0.9.12', now()->addMinutes(30)); $this->assertTrue(app(ReleaseChecker::class)->updateAvailable()); Http::assertNothingSent(); } public function test_refresh_fetches_and_caches_the_newest_tag(): void { Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.13'], ['name' => 'v0.9.12']])]); $tag = app(ReleaseChecker::class)->refresh('stable'); $this->assertSame('0.9.13', $tag); $this->assertSame('0.9.13', Cache::get('clusev:latest-release:stable')); } public function test_check_update_command_warms_the_cache(): void { Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.20']])]); $this->artisan('clusev:check-update')->assertSuccessful(); $this->assertSame('0.9.20', Cache::get('clusev:latest-release:stable')); } public function test_sidebar_shows_the_update_badge_only_when_an_update_is_available(): void { Http::fake(); // any stray check must not hit the network config()->set('clusev.version', '0.9.10'); $this->actingAs(User::factory()->create(['must_change_password' => false])); // No cached latest → no badge. $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)); $this->get('/audit')->assertOk()->assertSee(__('shell.update_available')); } public function test_no_update_check_when_no_repository_is_configured(): void { Http::fake(); // any network call would be a bug config()->set('clusev.repository', ''); config()->set('clusev.version', '0.9.10'); Cache::flush(); $this->assertFalse(app(ReleaseChecker::class)->updateAvailable()); $this->assertNull(app(ReleaseChecker::class)->refresh('stable')); Http::assertNothingSent(); } public function test_refresh_uses_the_github_api_for_a_github_repository(): void { config()->set('clusev.repository', 'https://github.com/clusev/clusev'); Http::fake([ 'api.github.com/repos/clusev/clusev/tags*' => Http::response([['name' => 'v0.9.20'], ['name' => 'v0.9.19']]), ]); $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. Http::assertSent(fn ($req) => str_contains($req->url(), 'api.github.com/repos/clusev/clusev/tags') && $req->hasHeader('User-Agent')); } public function test_fetch_changelog_uses_raw_githubusercontent_for_a_github_repository(): void { config()->set('clusev.repository', 'https://github.com/clusev/clusev'); Http::fake([ 'raw.githubusercontent.com/clusev/clusev/v0.9.20/CHANGELOG.md' => Http::response( "# Changelog\n\n## [0.9.20] - 2026-06-22\n\n### Hinzugefügt\n- Neues.\n" ), ]); $body = app(ReleaseChecker::class)->fetchChangelog('v0.9.20'); $this->assertNotNull($body); $this->assertStringContainsString('0.9.20', $body); // GitHub serves raw files from raw.githubusercontent.com with the ref IN the path (no ?ref=). Http::assertSent(fn ($req) => str_contains($req->url(), 'raw.githubusercontent.com/clusev/clusev/v0.9.20/CHANGELOG.md')); } }