fix: harden Vultr create, Gmail identity, and provider retries

Wrap Vultr server creation in DB transactions and delete the remote
instance when local persistence fails (API and Livewire). Scope
plus/dot email normalization to gmail.com/googlemail.com only.
Use throw:false on DigitalOcean/Vultr HTTP retries, and normalize
service log line counts via normalizeLogLines.
This commit is contained in:
Andras Bacsai
2026-07-16 13:42:53 +02:00
parent 2719d66042
commit 7d699818e8
15 changed files with 429 additions and 63 deletions
+48
View File
@@ -1,11 +1,59 @@
<?php
use App\Exceptions\RateLimitException;
use App\Services\DigitalOceanService;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class);
it('maps final DigitalOcean API error responses', function (int $status, string $message) {
Http::fake([
'https://api.digitalocean.com/v2/droplets' => Http::response(['message' => $message], $status),
]);
$exception = null;
try {
(new DigitalOceanService('test-token'))->createDroplet([]);
} catch (Throwable $caught) {
$exception = $caught;
}
expect($exception)->toBeInstanceOf(Exception::class)
->and($exception)->not->toBeInstanceOf(RequestException::class)
->and($exception->getMessage())->toBe('DigitalOcean API error: '.$message)
->and($exception->getCode())->toBe($status);
Http::assertSentCount(3);
})->with([
'unauthorized' => [401, 'Unable to authenticate you'],
'unprocessable entity' => [422, 'Droplet name is invalid'],
]);
it('maps a final DigitalOcean rate-limit response with Retry-After', function () {
Http::fake([
'https://api.digitalocean.com/v2/droplets' => Http::sequence()
->push(['message' => 'Temporary provider error'], 500)
->push(['message' => 'Temporary provider error'], 500)
->push(['message' => 'Too many requests'], 429, ['Retry-After' => '45']),
]);
try {
(new DigitalOceanService('test-token'))->createDroplet([]);
} catch (RateLimitException $exception) {
expect($exception->getMessage())->toBe('Rate limit exceeded. Please try again later.')
->and($exception->retryAfter)->toBe(45);
Http::assertSentCount(3);
return;
}
test()->fail('Expected a RateLimitException to be thrown.');
});
it('fetches paginated regions from DigitalOcean', function () {
Http::fake([
'https://api.digitalocean.com/v2/regions?page=1&per_page=50' => Http::response([