Fake the external currency-rate provider in balance-evolution tests

The stray-request guard added earlier surfaced five non-hermetic tests that
reached the real currency-rate CDN (cdn.jsdelivr.net / currency-api.pages.dev)
without an Http::fake(): four balance-evolution cases in AccountControllerTest
and one loan projection case in LoanTest. They passed on main only because CI
has network access and the request actually went out; under the guard the
unfaked request now throws, returning 500.

Adds a fakeCurrencyApi() helper in tests/Pest.php that stubs the provider's
/currencies/{code}.min.json endpoint with a deterministic 1:1 rate in the exact
shape the services parse ($response->json($currency)), and calls it from both
files' beforeEach. The stub returns null for non-matching URLs, so it never
shadows another test's own fakes nor the stray-request guard.

Note: a single global default fake in Pest.php was rejected on purpose — Laravel
resolves stubs first-match-wins in registration order, so a beforeEach-level
fake would shadow the per-test currency fakes in CurrencyConversionServiceTest
and ExchangeRateServiceTest and break their assertions.
This commit is contained in:
Víctor Falcón 2026-07-04 20:52:15 +02:00
parent f803a4a9b0
commit 269b90ccb5
3 changed files with 45 additions and 0 deletions

View File

@ -14,6 +14,11 @@ use App\Models\User;
beforeEach(function () {
config(['landing.hide_auth_buttons' => false]);
// The balance-evolution endpoint converts account currency via the external
// currency-rate provider; fake it so these tests stay hermetic under the
// stray-request guard instead of hitting the CDN.
fakeCurrencyApi();
$this->user = User::factory()->onboarded()->create();
$this->actingAs($this->user);
});

View File

@ -15,6 +15,11 @@ use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\assertDatabaseMissing;
beforeEach(function () {
// The balance-evolution endpoint converts account currency via the external
// currency-rate provider; fake it so these tests stay hermetic under the
// stray-request guard instead of hitting the CDN.
fakeCurrencyApi();
$this->user = User::factory()->onboarded()->create();
$this->bank = Bank::factory()->create();
$this->service = app(LoanAmortizationService::class);

View File

@ -13,6 +13,7 @@ use App\Services\Banking\BalanceSyncService;
use App\Services\Banking\Sync\BankingConnectionSyncerFactory;
use App\Services\Banking\TransactionSyncService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Stripe\Collection as StripeCollection;
@ -303,3 +304,37 @@ function runSync(
$job->handle(app(BankingConnectionSyncerFactory::class));
}
/**
* Fake the external currency-rate provider (the jsdelivr CDN and its pages.dev
* fallback) that CurrencyConversionService and ExchangeRateService fetch from,
* returning a deterministic 1:1 rate for every currency. Tests that trigger a
* currency conversion (e.g. the balance-evolution endpoint) can call this in
* their setup to stay hermetic under the stray-request guard.
*
* The stub only matches the provider's `/currencies/{code}.min.json` path and
* returns null otherwise, so it never shadows another test's own Http::fake()
* nor the stray-request guard for unrelated hosts.
*/
function fakeCurrencyApi(): void
{
Http::fake(function (Request $request) {
if (! preg_match('#/currencies/([a-z0-9]+)\.min\.json#i', $request->url(), $matches)) {
return null;
}
$currency = strtolower($matches[1]);
return Http::response([
'date' => '2024-01-01',
$currency => [
'usd' => 1.0,
'eur' => 1.0,
'gbp' => 1.0,
'jpy' => 1.0,
'btc' => 1.0,
'eth' => 1.0,
],
]);
});
}