95 lines
3.7 KiB
PHP
95 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\System\Index as SystemIndex;
|
|
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 Livewire\Livewire;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* The dashboard "request certificate" flow: a DNS pre-check that must NOT trigger ACME on a
|
|
* mismatch (Let's-Encrypt rate-limit protection), and a per-user throttle on the action.
|
|
*/
|
|
class TlsCertificateRequestTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Cache::flush();
|
|
}
|
|
|
|
public function test_dns_mismatch_does_not_trigger_a_handshake(): void
|
|
{
|
|
$svc = Mockery::mock(DeploymentService::class)->makePartial();
|
|
$svc->shouldReceive('domainResolvesHere')->once()->with('example.com')
|
|
->andReturn(['ok' => false, 'resolved' => ['203.0.113.9'], 'serverIp' => '198.51.100.1']);
|
|
$svc->shouldReceive('probeCertificate')->never(); // never trigger ACME against a wrong host
|
|
|
|
$result = $svc->requestCertificate('example.com');
|
|
|
|
$this->assertSame('dns_mismatch', $result['status']);
|
|
$this->assertSame('dns_mismatch', $svc->certStatus()['status']);
|
|
}
|
|
|
|
public function test_dns_ok_and_handshake_success_is_issued(): void
|
|
{
|
|
$svc = Mockery::mock(DeploymentService::class)->makePartial();
|
|
$svc->shouldReceive('domainResolvesHere')->once()
|
|
->andReturn(['ok' => true, 'resolved' => ['198.51.100.1'], 'serverIp' => '198.51.100.1']);
|
|
$svc->shouldReceive('probeCertificate')->once()->with('example.com')->andReturnTrue();
|
|
|
|
$this->assertSame('issued', $svc->requestCertificate('example.com')['status']);
|
|
}
|
|
|
|
public function test_dns_ok_but_handshake_failure_is_failed(): void
|
|
{
|
|
$svc = Mockery::mock(DeploymentService::class)->makePartial();
|
|
$svc->shouldReceive('domainResolvesHere')->once()->andReturn(['ok' => true, 'resolved' => [], 'serverIp' => null]);
|
|
$svc->shouldReceive('probeCertificate')->once()->andReturnFalse();
|
|
|
|
$this->assertSame('failed', $svc->requestCertificate('example.com')['status']);
|
|
}
|
|
|
|
public function test_external_tls_mode_is_not_applicable_and_never_probes(): void
|
|
{
|
|
Setting::put('tls_mode', 'external');
|
|
|
|
$svc = Mockery::mock(DeploymentService::class)->makePartial();
|
|
$svc->shouldReceive('domainResolvesHere')->never();
|
|
$svc->shouldReceive('probeCertificate')->never();
|
|
|
|
$this->assertSame('not_applicable', $svc->requestCertificate('example.com')['status']);
|
|
}
|
|
|
|
public function test_request_action_is_throttled_per_user(): void
|
|
{
|
|
$this->mock(DeploymentService::class, function ($m) {
|
|
$m->makePartial();
|
|
// Stub the serving state directly (domain() otherwise reads a shared snapshot file).
|
|
$m->shouldReceive('domain')->andReturn('example.com');
|
|
$m->shouldReceive('externalTls')->andReturn(false);
|
|
// The network-touching request is stubbed; the throttle must cap it at 5 calls.
|
|
$m->shouldReceive('requestCertificate')->times(5)
|
|
->andReturn(['status' => 'issued', 'checkedAt' => now()->toIso8601String()]);
|
|
});
|
|
|
|
$this->actingAs(User::factory()->create(['must_change_password' => false]));
|
|
|
|
for ($i = 0; $i < 6; $i++) {
|
|
Livewire::test(SystemIndex::class)->call('requestCertificate');
|
|
}
|
|
|
|
// 6th call was throttled before reaching the service; 5 audited requests.
|
|
$this->assertSame(5, AuditEvent::where('action', 'tls.cert_request')->count());
|
|
}
|
|
}
|