151 lines
6.1 KiB
PHP
151 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use App\Services\ReleaseChecker;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* The shared release lookup behind the Versions page, the sidebar update badge and the scheduled
|
|
* refresh. updateAvailable() is a pure cache read (no network); refresh() does the remote check.
|
|
*/
|
|
class ReleaseCheckerTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
// Neutral self-hosted host so the suite is hermetic (never depends on the ambient .env) and
|
|
// leaks no real repository URL into the published tests. GitHub-host coverage sets its own URL.
|
|
private const REPO = 'https://selfhosted.test/o/clusev';
|
|
|
|
private const TAGS_URL = 'selfhosted.test/api/v1/repos/o/clusev/tags*';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Cache::flush();
|
|
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($this->tagKey(), '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($this->tagKey(), '0.9.11', now()->addMinutes(30));
|
|
$this->assertFalse(app(ReleaseChecker::class)->updateAvailable(), 'equal version → no badge');
|
|
|
|
Cache::put($this->tagKey(), '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($this->tagKey(), '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($this->tagKey()));
|
|
}
|
|
|
|
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($this->tagKey()));
|
|
}
|
|
|
|
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]));
|
|
|
|
// The badge chip is ALWAYS in the DOM (so the navBadges poller can reveal it live without a
|
|
// page reload); no update → it carries the `hidden` attribute.
|
|
$html = $this->get('/audit')->assertOk()->getContent();
|
|
$this->assertMatchesRegularExpression('/data-nav-badge="update"[^>]*\shidden/', $html);
|
|
|
|
// Cached newer release → the same chip renders WITHOUT hidden (visible).
|
|
Cache::put($this->tagKey(), '0.9.11', now()->addMinutes(30));
|
|
$html = $this->get('/audit')->assertOk()->getContent();
|
|
$this->assertDoesNotMatchRegularExpression('/data-nav-badge="update"[^>]*\shidden/', $html);
|
|
}
|
|
|
|
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 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', 'Clusev-Panel'));
|
|
}
|
|
|
|
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'));
|
|
}
|
|
}
|