'pgsql', 'app.key' => 'test-key']); Schema::swap(Mockery::mock(Builder::class)); } protected function tearDown(): void { Sleep::fake(false); parent::tearDown(); } private function expectSchemaChecks(): void { DB::shouldReceive('selectOne')->with("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS installed") ->once()->andReturn((object) ['installed' => true]); Schema::shouldReceive('hasTable')->times(7)->andReturn(true); } public function test_recovery_is_retried_and_startup_succeeds_without_exposing_the_exception(): void { Sleep::fake(); DB::shouldReceive('selectOne')->with('SELECT 1')->once() ->andThrow(new RuntimeException('SQLSTATE[08006] FATAL: the database system is in recovery mode; secret-password')); DB::shouldReceive('purge')->once(); DB::shouldReceive('selectOne')->with('SELECT 1')->once()->andReturn((object) ['value' => 1]); $this->expectSchemaChecks(); $this->artisan('portal:dev-check', ['--wait' => 10]) ->expectsOutputToContain('[AGUARDA] PostgreSQL em arranque ou recuperação') ->doesntExpectOutputToContain('secret-password') ->expectsOutput('[OK] Ligação à base de dados') ->assertSuccessful(); Sleep::assertSleptTimes(1); } public function test_recovery_without_wait_fails_with_an_actionable_diagnosis(): void { DB::shouldReceive('selectOne')->with('SELECT 1')->once() ->andThrow(new RuntimeException('SQLSTATE[57P03] server starting')); DB::shouldReceive('purge')->never(); $this->artisan('portal:dev-check') ->expectsOutputToContain('PostgreSQL em arranque ou recuperação') ->expectsOutputToContain('./deploy/local/start.ps1') ->expectsOutput('[FALHA] Ligação à base de dados') ->assertFailed(); } public function test_persistent_unavailability_stops_at_the_deadline(): void { DB::shouldReceive('selectOne')->with('SELECT 1')->twice() ->andThrow(new RuntimeException('SQLSTATE[08006] connection refused')); DB::shouldReceive('purge')->once(); $this->artisan('portal:dev-check', ['--wait' => 1]) ->expectsOutputToContain('[AGUARDA] PostgreSQL indisponível') ->expectsOutput('[FALHA] Ligação à base de dados') ->assertFailed(); } /** @return array */ public static function permanentFailures(): array { return [ 'authentication behind generic PDO state' => ['SQLSTATE[08006] password authentication failed secret-password', 'Autenticação PostgreSQL recusada'], 'authentication SQLSTATE' => ['SQLSTATE[28P01] secret-password', 'Autenticação PostgreSQL recusada'], 'missing database behind generic PDO state' => ['SQLSTATE[08006] database "missing" does not exist secret-password', 'A base PostgreSQL configurada não existe'], 'missing database SQLSTATE' => ['SQLSTATE[3D000] secret-password', 'A base PostgreSQL configurada não existe'], 'unknown failure' => ['unexpected secret-password', 'Falha PostgreSQL não transitória'], ]; } #[DataProvider('permanentFailures')] public function test_configuration_failures_do_not_wait_or_expose_credentials(string $message, string $diagnosis): void { Sleep::fake(); $error = new QueryException('pgsql', 'SELECT 1', [], new RuntimeException($message)); DB::shouldReceive('selectOne')->with('SELECT 1')->once()->andThrow($error); DB::shouldReceive('purge')->never(); $this->artisan('portal:dev-check', ['--wait' => 900]) ->expectsOutputToContain($diagnosis) ->doesntExpectOutputToContain('secret-password') ->assertFailed(); Sleep::assertNeverSlept(); } public function test_schema_errors_are_not_reported_as_connection_failures(): void { DB::shouldReceive('selectOne')->with('SELECT 1')->once()->andReturn((object) ['value' => 1]); DB::shouldReceive('selectOne')->with("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS installed") ->once()->andThrow(new RuntimeException('permission denied secret-password')); $this->artisan('portal:dev-check') ->expectsOutput('[OK] Ligação à base de dados') ->expectsOutput('[FALHA] Esquema da base de dados verificável') ->doesntExpectOutputToContain('secret-password') ->assertFailed(); } public function test_an_invalid_wait_is_rejected_before_database_access(): void { DB::shouldReceive('selectOne')->never(); $this->artisan('portal:dev-check', ['--wait' => -1]) ->expectsOutput('--wait deve ser um inteiro entre 0 e 3600 segundos.') ->assertExitCode(2); } }