set('clusev.version', '0.9.61-beta1'); config()->set('clusev.github_token', 'tok_secret'); config()->set('clusev.staging_slug', self::SLUG); } /** @return array the step with the given key */ private function step(array $pipeline, string $key): array { foreach ($pipeline['steps'] as $s) { if ($s['key'] === $key) { return $s; } } $this->fail("no step {$key}"); } /** * Reset between pipeline phases inside one test: drop the cached step results AND the HTTP * fake stubs. Laravel 13's Http::fake() appends stubs (first match wins), so re-faking the * same URL without a fresh factory would keep returning the earlier phase's response. */ private function resetForNextPhase(): void { Cache::flush(); Http::swap(new HttpFactory(app('events'))); } public function test_non_beta_version_has_no_pipeline(): void { config()->set('clusev.version', '0.9.60'); $this->assertNull(app(PipelineStatus::class)->forTrackedBeta()); } public function test_tag_step_is_done_for_the_running_beta(): void { Http::fake(['api.github.com/*' => Http::response([], 404)]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('v0.9.61-beta1', $p['tag']); $this->assertSame('done', $this->step($p, 'tag')['state']); } public function test_mirror_done_when_the_tag_exists_on_github(): void { Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/v0.9.61-beta1' => Http::response(['ref' => 'refs/tags/v0.9.61-beta1']), 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('done', $this->step($p, 'mirror')['state']); } public function test_mirror_pending_on_404(): void { Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response([], 404), 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('pending', $this->step($p, 'mirror')['state']); } public function test_ci_running_then_done_then_failed(): void { $run = fn (string $status, ?string $conclusion) => Http::response(['workflow_runs' => [[ 'head_branch' => 'v0.9.61-beta1', 'status' => $status, 'conclusion' => $conclusion, 'html_url' => 'https://github.com/acme/staging/actions/runs/1', ]]]); Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), 'api.github.com/repos/acme/staging/actions/runs*' => $run('in_progress', null), ]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('running', $this->step($p, 'ci')['state']); $this->assertTrue($p['ciRunning']); $this->assertStringContainsString('runs/1', $this->step($p, 'ci')['url']); $this->resetForNextPhase(); Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), 'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'success'), ]); $this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); $this->resetForNextPhase(); Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), 'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'failure'), ]); $this->assertSame('failed', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); } public function test_ci_pending_when_no_run_matches_the_tag(): void { Http::fake([ 'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']), 'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => [['head_branch' => 'v9.9.9', 'status' => 'completed', 'conclusion' => 'success']]]), ]); $this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']); } public function test_no_token_makes_mirror_and_ci_unknown_without_calling_github(): void { config()->set('clusev.github_token', ''); Http::fake(); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('unknown', $this->step($p, 'mirror')['state']); $this->assertSame('unknown', $this->step($p, 'ci')['state']); Http::assertNothingSent(); } public function test_test_server_step_states(): void { Http::fake([ 'api.github.com/*' => Http::response([], 404), 'staging.local/version.json' => Http::response(['version' => '0.9.61-beta1']), ]); Setting::put('pipeline_test_server', 'http://staging.local'); $this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']); $this->resetForNextPhase(); Http::fake([ 'api.github.com/*' => Http::response([], 404), 'staging.local/version.json' => Http::response(['version' => '0.9.60']), ]); $this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']); } public function test_test_server_unset_when_no_url(): void { Http::fake(['api.github.com/*' => Http::response([], 404)]); $this->assertSame('unset', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']); } public function test_token_is_sent_in_the_header_never_in_the_url(): void { Http::fake(['api.github.com/*' => Http::response(['ref' => 'x'])]); app(PipelineStatus::class)->forTrackedBeta(); Http::assertSent(fn ($req) => $req->hasHeader('Authorization') && ! str_contains($req->url(), 'tok_secret')); } public function test_github_5xx_degrades_to_unknown(): void { Http::fake(['api.github.com/*' => Http::response('boom', 500)]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('unknown', $this->step($p, 'mirror')['state']); $this->assertSame('unknown', $this->step($p, 'ci')['state']); } public function test_github_throwing_degrades_to_unknown_never_throws(): void { Http::fake(['api.github.com/*' => fn () => throw new \RuntimeException('net down')]); $p = app(PipelineStatus::class)->forTrackedBeta(); $this->assertSame('unknown', $this->step($p, 'mirror')['state']); $this->assertSame('unknown', $this->step($p, 'ci')['state']); } public function test_test_server_unreachable_is_unknown(): void { Http::fake([ 'api.github.com/*' => Http::response([], 404), '*/version.json' => Http::response('nope', 503), ]); Setting::put('pipeline_test_server', 'http://staging.local'); $this->assertSame('unknown', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'test')['state']); } }