clusev/tests/Feature/VersionUpdateCheckTest.php

161 lines
5.8 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Versions\Index;
use App\Models\AuditEvent;
use App\Models\Setting;
use App\Models\User;
use App\Services\DeploymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Livewire;
use Tests\TestCase;
/**
* The "check for updates" action. The production image ships no .git, so the latest release
* must come from the public Git host's tag API — not a local read (which would always be empty
* in prod and wrongly report "no release tagged"). These tests drive that API with Http::fake.
*/
class VersionUpdateCheckTest extends TestCase
{
use RefreshDatabase;
private const TAGS_URL = 'git.bave.dev/api/v1/repos/boban/clusev/tags*';
protected function setUp(): void
{
parent::setUp();
Cache::flush(); // never inherit a cached latest-release between tests
app(DeploymentService::class)->clearUpdateRequest(); // never inherit a stray sentinel
$this->actingAs(User::factory()->create(['must_change_password' => false]));
}
protected function tearDown(): void
{
app(DeploymentService::class)->clearUpdateRequest(); // leave the storage dir clean
parent::tearDown();
}
public function test_reports_update_available_from_the_remote_tag_api(): void
{
config()->set('clusev.version', '0.9.4');
Http::fake([self::TAGS_URL => Http::response([
['name' => 'v0.9.6'], ['name' => 'v0.9.5'], ['name' => 'v0.9.4'],
])]);
Livewire::test(Index::class)
->call('checkUpdates')
->assertSet('updateState', 'update')
->assertSee('0.9.6');
}
public function test_reports_current_when_installed_is_at_or_above_the_latest_tag(): void
{
config()->set('clusev.version', '0.9.9');
Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.6']])]);
Livewire::test(Index::class)
->call('checkUpdates')
->assertSet('updateState', 'current');
}
public function test_stable_channel_ignores_prereleases_but_beta_sees_them(): void
{
config()->set('clusev.version', '0.9.6');
Http::fake([self::TAGS_URL => Http::response([
['name' => 'v1.0.0-beta1'], ['name' => 'v0.9.6'],
])]);
// stable: 1.0.0-beta1 is not offered, 0.9.6 == installed → current
Setting::put('release_channel', 'stable');
Livewire::test(Index::class)->call('checkUpdates')->assertSet('updateState', 'current');
// beta: the prerelease is newer → update available
Cache::flush();
Setting::put('release_channel', 'beta');
Livewire::test(Index::class)
->call('checkUpdates')
->assertSet('updateState', 'update')
->assertSee('1.0.0-beta1');
}
public function test_degrades_gracefully_when_the_remote_api_is_unreachable(): void
{
// Installed version above any real local .git tag, so the local fallback can't claim an
// update — this asserts a failed remote lookup never crashes the action.
config()->set('clusev.version', '99.0.0');
Http::fake([self::TAGS_URL => Http::response('upstream down', 500)]);
Livewire::test(Index::class)
->call('checkUpdates')
->assertSet('updateState', 'current')
->assertSet('lastChecked', now()->format('H:i'));
}
public function test_request_update_writes_the_sentinel_and_audits_when_available(): void
{
config()->set('clusev.version', '0.9.4');
Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.6']])]);
Livewire::test(Index::class)
->call('requestUpdate')
->assertSet('updateRequested', true);
$this->assertTrue(
app(DeploymentService::class)->updateRequested(),
'the update sentinel a host watcher reacts to must exist',
);
$this->assertTrue(AuditEvent::where('action', 'deploy.update_request')->exists());
}
public function test_request_update_refuses_when_already_current(): void
{
config()->set('clusev.version', '0.9.9');
Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.6']])]);
Livewire::test(Index::class)
->call('requestUpdate')
->assertSet('updateRequested', false)
->assertSet('updateState', 'current');
$this->assertFalse(
app(DeploymentService::class)->updateRequested(),
'no sentinel when there is nothing newer to apply',
);
}
public function test_build_info_prefers_the_baked_config_over_a_git_read(): void
{
// Production bakes sha/branch into config via .env (no .git in the image).
config()->set('clusev.build.sha', 'abc1234');
config()->set('clusev.build.branch', 'feat/v1-foundation');
Livewire::test(Index::class)
->assertViewHas('build', ['sha' => 'abc1234', 'branch' => 'feat/v1-foundation'])
->assertSee('abc1234')
->assertSee('feat/v1-foundation');
}
public function test_request_update_is_throttled_per_user(): void
{
config()->set('clusev.version', '0.9.4');
Http::fake([self::TAGS_URL => Http::response([['name' => 'v0.9.6']])]);
$key = 'update-request:'.auth()->id();
for ($i = 0; $i < 3; $i++) {
RateLimiter::hit($key, 600);
}
Livewire::test(Index::class)
->call('requestUpdate')
->assertSet('updateRequested', false);
$this->assertFalse(
app(DeploymentService::class)->updateRequested(),
'the per-user throttle must block the sentinel write',
);
}
}