clusev/tests/Feature/ReleasePageTest.php

189 lines
7.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Release\Index;
use App\Models\AuditEvent;
use App\Models\User;
use App\Support\Confirm\ConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Livewire;
use Tests\TestCase;
class ReleasePageTest extends TestCase
{
use RefreshDatabase;
private string $dir;
protected function setUp(): void
{
parent::setUp();
config()->set('clusev.release_controls', true);
config()->set('clusev.version', '0.9.58');
// Degrade public-tag reads by default so render() makes no live GitHub call; the
// promotion tests that need a public repo set clusev.public_slug themselves.
config()->set('clusev.public_slug', '');
$this->actingAs(User::factory()->create(['must_change_password' => false]));
$this->dir = storage_path('app/restart-signal');
@mkdir($this->dir, 0775, true);
@unlink($this->dir.'/release-request.json');
}
protected function tearDown(): void
{
@unlink($this->dir.'/release-request.json');
foreach (glob($this->dir.'/release-result-*.json') ?: [] as $f) {
@unlink($f);
}
parent::tearDown();
}
public function test_deploy_staging_for_an_allowed_target_writes_a_request_and_audits(): void
{
Livewire::test(Index::class)->call('deployStaging', '0.10.0');
$payload = json_decode((string) file_get_contents($this->dir.'/release-request.json'), true);
$this->assertSame('stage', $payload['action']);
$this->assertSame('0.10.0', $payload['target']);
$this->assertTrue(AuditEvent::where('action', 'deploy.staging_release')->exists());
}
public function test_deploy_staging_rejects_a_target_not_in_the_allowed_set(): void
{
Livewire::test(Index::class)->call('deployStaging', '5.5.5');
$this->assertFileDoesNotExist($this->dir.'/release-request.json');
}
public function test_deploy_staging_is_throttled_per_user(): void
{
$key = 'release-stage:'.auth()->id();
for ($i = 0; $i < 3; $i++) {
RateLimiter::hit($key, 600);
}
Livewire::test(Index::class)->call('deployStaging', '0.10.0');
$this->assertFileDoesNotExist($this->dir.'/release-request.json');
}
public function test_poll_result_surfaces_a_success(): void
{
$c = Livewire::test(Index::class)->call('deployStaging', '0.10.0');
$id = $c->get('pendingId');
file_put_contents(
$this->dir."/release-result-{$id}.json",
json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-beta1', 'message' => 'ok'])
);
$c->call('pollResult')->assertSet('pendingId', null)->assertSee('v0.10.0-beta1');
}
public function test_poll_result_audits_a_host_failure_as_an_error(): void
{
$c = Livewire::test(Index::class)->call('deployStaging', '0.10.0');
$id = $c->get('pendingId');
file_put_contents(
$this->dir."/release-result-{$id}.json",
json_encode(['id' => $id, 'ok' => false, 'tag' => null, 'message' => 'dirty-tree'])
);
$c->call('pollResult')->assertSet('pendingId', null);
$this->assertTrue(AuditEvent::where('action', 'deploy.staging_release_failed')->exists());
}
public function test_mount_aborts_when_the_flag_is_off(): void
{
config()->set('clusev.release_controls', false);
Livewire::test(Index::class)->assertStatus(404);
}
public function test_render_includes_the_live_pipeline_for_a_beta(): void
{
config()->set('clusev.version', '0.9.61-beta1');
config()->set('clusev.staging_slug', ''); // degrade: no GitHub calls
config()->set('clusev.github_token', '');
Livewire::test(Index::class)
->assertViewHas('pipeline', fn ($p) => is_array($p) && $p['tag'] === 'v0.9.61-beta1');
}
public function test_render_pipeline_is_null_for_a_stable_version(): void
{
config()->set('clusev.version', '0.9.60');
Livewire::test(Index::class)->assertViewHas('pipeline', null);
}
public function test_deploy_public_dispatches_and_audits_for_a_beta(): void
{
config()->set('clusev.version', '0.10.0-beta1');
config()->set('clusev.github_token', 'tok');
config()->set('clusev.staging_slug', 'acme/staging');
config()->set('clusev.release_branch', 'feat/v1-foundation');
Http::fake(['api.github.com/*' => Http::response('', 204)]);
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-beta1']);
ConfirmToken::confirm($token); // the modal confirmation step
Livewire::test(Index::class)->call('applyDeployPublic', $token);
$this->assertTrue(AuditEvent::where('action', 'deploy.public')->exists());
}
public function test_promote_stable_uses_the_beta_base_as_the_stable_tag(): void
{
config()->set('clusev.version', '0.10.0-beta3');
config()->set('clusev.github_token', 'tok');
config()->set('clusev.staging_slug', 'acme/staging');
config()->set('clusev.release_branch', 'feat/v1-foundation');
Http::fake(['api.github.com/*' => Http::response('', 204)]);
$token = ConfirmToken::issue('releaseStable', ['betaTag' => 'v0.10.0-beta3']);
ConfirmToken::confirm($token); // the modal confirmation step
Livewire::test(Index::class)->call('applyPromoteStable', $token);
Http::assertSent(fn ($req) => str_contains($req->url(), 'promote-stable.yml/dispatches')
&& $req['inputs']['stableTag'] === 'v0.10.0');
$this->assertTrue(AuditEvent::where('action', 'deploy.stable')->exists());
}
public function test_yank_only_dispatches_for_a_tag_that_exists_on_public(): void
{
config()->set('clusev.github_token', 'tok');
config()->set('clusev.staging_slug', 'acme/staging');
config()->set('clusev.public_slug', 'acme/public');
config()->set('clusev.release_branch', 'feat/v1-foundation');
Http::fake([
'api.github.com/repos/acme/public/tags*' => Http::response([['name' => 'v0.9.9']]),
'api.github.com/*/dispatches' => Http::response('', 204),
]);
// a tag NOT in publicTags is refused (no dispatch, no audit)
$bad = ConfirmToken::issue('releaseYank', ['tag' => 'v9.9.9']);
ConfirmToken::confirm($bad); // the modal confirmation step
Livewire::test(Index::class)->call('applyYank', $bad);
$this->assertFalse(AuditEvent::where('action', 'deploy.yank')->exists());
// the real public tag is dispatched + audited
$good = ConfirmToken::issue('releaseYank', ['tag' => 'v0.9.9']);
ConfirmToken::confirm($good); // the modal confirmation step
Livewire::test(Index::class)->call('applyYank', $good);
$this->assertTrue(AuditEvent::where('action', 'deploy.yank')->exists());
}
public function test_publish_actions_are_a_no_op_for_a_stable_version(): void
{
config()->set('clusev.version', '0.10.0'); // not a beta
config()->set('clusev.public_slug', ''); // degrade: no public-tags read on render
Http::fake();
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-beta1']);
ConfirmToken::confirm($token); // the modal confirmation step
Livewire::test(Index::class)->call('applyDeployPublic', $token);
Http::assertNothingSent();
}
}