ajuste no header e organizaco de elementos, e ajuste no footer
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-20 11:39:25 +02:00
parent a128c5a1b8
commit 97a08df2a0
33 changed files with 2272 additions and 166 deletions
@@ -0,0 +1,128 @@
<?php
namespace Tests\Unit;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Builder;
use Illuminate\Foundation\Testing\TestCase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Sleep;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;
// All database calls are mocked. No migrations, cleanup or real database access.
class CheckDevelopmentEnvironmentTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.default' => '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<string, array{string, string}> */
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);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace Tests\Unit;
use App\Mail\ContactMessageReceivedMail;
use Illuminate\Foundation\Testing\TestCase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use RuntimeException;
// No database or external email: all transport calls are intercepted.
class PortalContactTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['contact_messages.recipient' => 'tempo@example.invalid']);
DB::shouldReceive('connection')->never();
}
/** @return array<string, string> */
private function message(): array
{
return ['name' => 'Teste QA', 'email' => 'sender@example.invalid', 'subject' => 'Pedido de contacto',
'message' => 'Mensagem de teste do adaptador local.'];
}
public function test_the_original_fields_are_delivered_through_the_local_mail_transport(): void
{
Mail::fake();
$this->postJson('/contacto/mensagem', [...$this->message(), 'platform' => 'Other'])
->assertCreated()->assertJson(['success' => true]);
Mail::assertSent(ContactMessageReceivedMail::class, fn (ContactMessageReceivedMail $mail) => $mail->hasTo('tempo@example.invalid') && $mail->contactMessage['platform'] === 'InvesteMoz'
&& $mail->contactMessage['phone'] === null
&& $mail->envelope()->replyTo[0]->address === 'sender@example.invalid'
&& $mail->envelope()->subject === 'Pedido de contacto');
Mail::assertSentCount(1);
}
public function test_invalid_fields_are_rejected_before_sending(): void
{
Mail::fake();
$this->postJson('/contacto/mensagem', ['name' => 'A', 'email' => 'invalid', 'subject' => 'AB', 'message' => 'short'])
->assertUnprocessable()->assertJsonValidationErrors(['name', 'email', 'subject', 'message']);
Mail::assertNothingSent();
}
public function test_transport_failure_is_not_reported_as_success(): void
{
Mail::shouldReceive('to')->once()->with('tempo@example.invalid')->andReturnSelf();
Mail::shouldReceive('send')->once()->andThrow(new RuntimeException('transport unavailable'));
$this->postJson('/contacto/mensagem', $this->message())
->assertStatus(500)->assertJson(['success' => false]);
}
public function test_the_copied_email_template_escapes_user_content(): void
{
$mail = new ContactMessageReceivedMail([...$this->message(), 'phone' => null, 'platform' => 'InvesteMoz',
'message' => '<script>alert(1)</script>']);
$html = $mail->render();
$this->assertStringContainsString('&lt;script&gt;', $html);
$this->assertStringNotContainsString('<script>', $html);
}
}