From c0057c237a91cbeedbbc6bd379aef7046d2cc862 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 4 Sep 2026 12:47:47 -0300 Subject: [PATCH] refactor(integration): resolver credenciales mediante instancias Resuelve primero la instancia del cliente y luego la del tipo de sitio cuando existe contexto de tenant. Adapta SMTP y Telepagos, con tokens identificados por instancia y version de credenciales. Separa la edicion de instancias de sus asociaciones y conserva el guardado por cliente creando una instancia nueva. Impide eliminar instancias en uso y adapta las pruebas existentes, incluida la herencia SMTP. --- .../Models/IntegrationInstance.php | 6 +++ .../Services/BaseIntegrationService.php | 28 ++++++++----- .../Services/ClientIntegrationService.php | 11 ++++- .../IntegrationAssociationService.php | 27 +++++++++++++ .../Services/IntegrationInstanceService.php | 40 +++++++++++++++++++ .../Integration/Services/MailService.php | 22 +++++----- .../Services/TelepagosIntegrationService.php | 10 ++--- .../Concerns/CreatesIntegrationInstances.php | 21 ++++++++++ .../Integration/IntegrationServiceTest.php | 25 ++++++------ tests/Feature/Integration/MailServiceTest.php | 32 ++++++++++++--- .../Integration/TelepagosWebhookTest.php | 5 ++- .../MailTest/MailTestControllerTest.php | 17 ++++---- 12 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 app/Domains/Integration/Services/IntegrationAssociationService.php create mode 100644 app/Domains/Integration/Services/IntegrationInstanceService.php create mode 100644 tests/Concerns/CreatesIntegrationInstances.php diff --git a/app/Domains/Integration/Models/IntegrationInstance.php b/app/Domains/Integration/Models/IntegrationInstance.php index e2d7aed..db242eb 100644 --- a/app/Domains/Integration/Models/IntegrationInstance.php +++ b/app/Domains/Integration/Models/IntegrationInstance.php @@ -9,6 +9,12 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class IntegrationInstance extends Model { + // The ciphertext changes whenever credentials are saved, so old tokens cannot be reused. + public function tokenCacheKey(): string + { + return 'integration_token:instance:'.$this->id.':'.hash('sha256', (string) $this->getRawOriginal('integration_data')); + } + protected $fillable = ['integration_code', 'name', 'integration_data']; protected $hidden = ['integration_data']; diff --git a/app/Domains/Integration/Services/BaseIntegrationService.php b/app/Domains/Integration/Services/BaseIntegrationService.php index 865ab56..0ca9302 100644 --- a/app/Domains/Integration/Services/BaseIntegrationService.php +++ b/app/Domains/Integration/Services/BaseIntegrationService.php @@ -5,6 +5,8 @@ namespace App\Domains\Integration\Services; use App\Domains\Client\Models\Client; use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; +use App\Domains\Integration\Models\IntegrationInstance; +use App\Domains\Integration\Models\WebsiteTypeIntegration; use App\Domains\Tenant\Models\Tenant; use Exception; use Illuminate\Http\Client\PendingRequest; @@ -32,9 +34,9 @@ abstract class BaseIntegrationService protected ?Integration $integration = null; /** - * The client-owned integration configuration. + * The effective integration instance. */ - protected ?ClientIntegration $clientIntegration = null; + protected ?IntegrationInstance $integrationInstance = null; /** * Set the integration code. @@ -85,7 +87,7 @@ abstract class BaseIntegrationService } /** - * Load the integration definition and its client-owned configuration. + * Load the integration definition and its effective instance configuration. * * @throws Exception */ @@ -95,6 +97,7 @@ abstract class BaseIntegrationService throw new Exception('Integration code is not set.'); } + $this->integrationInstance = null; $this->integration = Integration::where('integration_code', $this->integrationCode)->first(); if (! $this->integration) { throw new Exception("Integration with code '{$this->integrationCode}' not found."); @@ -104,11 +107,18 @@ abstract class BaseIntegrationService throw new Exception('Client context is not set.'); } - $this->clientIntegration = ClientIntegration::where('client_id', $this->clientContext->id) + $this->integrationInstance = ClientIntegration::with('integrationInstance')->where('client_id', $this->clientContext->id) ->where('integration_code', $this->integrationCode) - ->first(); + ->first()?->integrationInstance; - if (! $this->clientIntegration && $this->integration->requires_configuration) { + if (! $this->integrationInstance && $this->tenant?->website_type_code) { + $this->integrationInstance = WebsiteTypeIntegration::with('integrationInstance') + ->where('website_type_code', $this->tenant->website_type_code) + ->where('integration_code', $this->integrationCode) + ->first()?->integrationInstance; + } + + if (! $this->integrationInstance && $this->integration->requires_configuration) { throw new Exception("Client '{$this->clientContext->code}' does not have integration '{$this->integrationCode}' configured."); } } @@ -131,15 +141,15 @@ abstract class BaseIntegrationService } /** - * Get an integration setting from the client-owned configuration. + * Get an integration setting from the effective instance configuration. */ protected function getIntegrationSetting(string $key, mixed $default = null): mixed { - if (! $this->clientIntegration || ! $this->clientIntegration->integration_data) { + if (! $this->integrationInstance || ! $this->integrationInstance->integration_data) { return $default; } - return $this->clientIntegration->integration_data[$key] ?? $default; + return $this->integrationInstance->integration_data[$key] ?? $default; } /** diff --git a/app/Domains/Integration/Services/ClientIntegrationService.php b/app/Domains/Integration/Services/ClientIntegrationService.php index 1657259..be2cd67 100644 --- a/app/Domains/Integration/Services/ClientIntegrationService.php +++ b/app/Domains/Integration/Services/ClientIntegrationService.php @@ -13,6 +13,7 @@ class ClientIntegrationService public function getClientIntegration(Client $client, string $integrationCode): ?ClientIntegration { return $client->integrations() + ->with(['integration', 'integrationInstance']) ->where('integration_code', $integrationCode) ->first(); } @@ -20,7 +21,7 @@ class ClientIntegrationService /** @return Collection */ public function getAllForClient(Client $client): Collection { - return $client->integrations()->with('integration')->get(); + return $client->integrations()->with(['integration', 'integrationInstance'])->get(); } public function updateOrCreateIntegration( @@ -29,12 +30,18 @@ class ClientIntegrationService array $data, ): ClientIntegration { return DB::transaction(function () use ($client, $integration, $data): ClientIntegration { + // Legacy configuration endpoint replaces this client's instance only. + $instance = app(IntegrationInstanceService::class)->create([ + 'integration_code' => $integration->integration_code, + 'name' => $integration->name.' / '.$client->name, + 'integration_data' => $data, + ]); $clientIntegration = ClientIntegration::query()->updateOrCreate( [ 'client_id' => $client->id, 'integration_code' => $integration->integration_code, ], - ['integration_data' => $data], + ['integration_instance_id' => $instance->id], ); $service = $this->resolveService($integration->integration_code); diff --git a/app/Domains/Integration/Services/IntegrationAssociationService.php b/app/Domains/Integration/Services/IntegrationAssociationService.php new file mode 100644 index 0000000..8d474da --- /dev/null +++ b/app/Domains/Integration/Services/IntegrationAssociationService.php @@ -0,0 +1,27 @@ +integration_code === $code, 422, 'The instance belongs to another integration.'); + + return $owner->integrations()->updateOrCreate( + ['integration_code' => $code], + ['integration_instance_id' => $instance->id], + )->load('integrationInstance'); + } + + public function detach(Client|WebsiteType $owner, string $code): void + { + $owner->integrations()->where('integration_code', $code)->delete(); + } +} diff --git a/app/Domains/Integration/Services/IntegrationInstanceService.php b/app/Domains/Integration/Services/IntegrationInstanceService.php new file mode 100644 index 0000000..7a86d37 --- /dev/null +++ b/app/Domains/Integration/Services/IntegrationInstanceService.php @@ -0,0 +1,40 @@ +lockForUpdate()->findOrFail($instance->id); + $cacheKey = $instance->tokenCacheKey(); + $instance->update($data); + if (array_key_exists('integration_data', $data)) { + DB::afterCommit(fn () => Cache::forget($cacheKey)); + } + + return $instance; + }); + } + + public function delete(IntegrationInstance $instance): void + { + DB::transaction(function () use ($instance): void { + $instance = IntegrationInstance::query()->lockForUpdate()->findOrFail($instance->id); + abort_if($instance->clientIntegrations()->exists() || $instance->websiteTypeIntegrations()->exists(), 409, 'Unlink the instance before deleting it.'); + $cacheKey = $instance->tokenCacheKey(); + $instance->delete(); + DB::afterCommit(fn () => Cache::forget($cacheKey)); + }); + } +} diff --git a/app/Domains/Integration/Services/MailService.php b/app/Domains/Integration/Services/MailService.php index c264c24..d2cd30f 100644 --- a/app/Domains/Integration/Services/MailService.php +++ b/app/Domains/Integration/Services/MailService.php @@ -29,7 +29,7 @@ class MailService extends BaseIntegrationService private ?Mailer $mailer = null; - private bool $usesClientMailer = false; + private bool $usesInstanceMailer = false; public function __construct(?MailFactory $mailFactory = null) { @@ -40,12 +40,12 @@ class MailService extends BaseIntegrationService { parent::forTenant($tenantCode); - if ($this->clientIntegration) { + if ($this->integrationInstance) { $this->mailer = $this->resolveMailer(); - $this->usesClientMailer = true; + $this->usesInstanceMailer = true; } else { $this->mailer = $this->mailFactory->mailer(); - $this->usesClientMailer = false; + $this->usesInstanceMailer = false; } return $this; @@ -56,12 +56,12 @@ class MailService extends BaseIntegrationService parent::forClient($client); $this->tenant = $this->clientContext?->tenants()->first(); - if ($this->clientIntegration) { + if ($this->integrationInstance) { $this->mailer = $this->resolveMailer(); - $this->usesClientMailer = true; + $this->usesInstanceMailer = true; } else { $this->mailer = $this->mailFactory->mailer(); - $this->usesClientMailer = false; + $this->usesInstanceMailer = false; } return $this; @@ -122,8 +122,8 @@ class MailService extends BaseIntegrationService public function mailerName(): string { - return $this->usesClientMailer - ? 'client-smtp' + return $this->usesInstanceMailer + ? 'integration-smtp' : (string) config('mail.default'); } @@ -187,7 +187,7 @@ class MailService extends BaseIntegrationService private function resolveMailer(): Mailer { - $data = $this->clientIntegration?->integration_data; + $data = $this->integrationInstance?->integration_data; if (! is_array($data)) { throw new InvalidArgumentException('La configuración SMTP del cliente no es válida.'); @@ -205,7 +205,7 @@ class MailService extends BaseIntegrationService } $mailer = $this->mailFactory->build([ - 'name' => 'client-smtp-'.$this->clientContext?->id, + 'name' => 'integration-smtp-'.$this->integrationInstance?->id, 'transport' => 'smtp', 'scheme' => $data['MAIL_SCHEME'] ?? null, 'host' => $data['MAIL_HOST'], diff --git a/app/Domains/Integration/Services/TelepagosIntegrationService.php b/app/Domains/Integration/Services/TelepagosIntegrationService.php index 528690b..ef99f7f 100644 --- a/app/Domains/Integration/Services/TelepagosIntegrationService.php +++ b/app/Domains/Integration/Services/TelepagosIntegrationService.php @@ -45,11 +45,11 @@ class TelepagosIntegrationService extends BaseIntegrationService */ public function getToken(): string { - if (! $this->clientIntegration || ! $this->clientContext) { + if (! $this->integrationInstance) { throw new Exception('Client integration is not loaded. Call forTenant() or forClient() first.'); } - $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; + $cacheKey = $this->integrationInstance->tokenCacheKey(); $token = Cache::get($cacheKey); @@ -94,7 +94,7 @@ class TelepagosIntegrationService extends BaseIntegrationService // Calculate TTL and subtract a buffer of 60 seconds $ttlSeconds = max(1, $expiresAt->diffInSeconds(now()) - 60); - $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; + $cacheKey = $this->integrationInstance->tokenCacheKey(); Cache::put($cacheKey, $token, $ttlSeconds); return $token; @@ -220,11 +220,11 @@ class TelepagosIntegrationService extends BaseIntegrationService */ public function clearToken(): void { - if (! $this->clientContext) { + if (! $this->integrationInstance) { return; } - $cacheKey = "integration_token:{$this->clientContext->id}:{$this->integrationCode}"; + $cacheKey = $this->integrationInstance->tokenCacheKey(); Cache::forget($cacheKey); } diff --git a/tests/Concerns/CreatesIntegrationInstances.php b/tests/Concerns/CreatesIntegrationInstances.php new file mode 100644 index 0000000..8b37c0d --- /dev/null +++ b/tests/Concerns/CreatesIntegrationInstances.php @@ -0,0 +1,21 @@ + $attributes['integration_code'], + 'name' => 'Test instance', + 'integration_data' => $attributes['integration_data'], + ]); + unset($attributes['integration_data']); + + return ClientIntegration::create($attributes + ['integration_instance_id' => $instance->id]); + } +} diff --git a/tests/Feature/Integration/IntegrationServiceTest.php b/tests/Feature/Integration/IntegrationServiceTest.php index b14440c..847a4e6 100644 --- a/tests/Feature/Integration/IntegrationServiceTest.php +++ b/tests/Feature/Integration/IntegrationServiceTest.php @@ -4,7 +4,6 @@ namespace Tests\Feature\Integration; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; use App\Domains\Integration\Services\TelepagosIntegrationService; use App\Domains\Tenant\Models\Tenant; @@ -15,10 +14,12 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; +use Tests\Concerns\CreatesIntegrationInstances; use Tests\TestCase; class IntegrationServiceTest extends TestCase { + use CreatesIntegrationInstances; use RefreshDatabase; private Tenant $tenant; @@ -110,7 +111,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -139,7 +140,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -198,7 +199,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -242,7 +243,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -272,7 +273,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -323,7 +324,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -375,7 +376,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -427,7 +428,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -480,7 +481,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -541,7 +542,7 @@ class IntegrationServiceTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $this->tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ @@ -566,7 +567,7 @@ class IntegrationServiceTest extends TestCase Log::shouldReceive('error') ->once() ->with('Telepagos get cash-in details failed: Cashin no encontrado', \Mockery::on(function ($context) { - return $context['cashin_id'] === 6351 + return $context['cashin_id'] === '6351' && $context['response_status'] === 404 && $context['response_body'] === ['status' => 'error', 'message' => 'Cashin no encontrado']; })); diff --git a/tests/Feature/Integration/MailServiceTest.php b/tests/Feature/Integration/MailServiceTest.php index a252681..6e7ffeb 100644 --- a/tests/Feature/Integration/MailServiceTest.php +++ b/tests/Feature/Integration/MailServiceTest.php @@ -4,28 +4,32 @@ namespace Tests\Feature\Integration; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; +use App\Domains\Integration\Models\IntegrationInstance; use App\Domains\Integration\Services\ClientIntegrationService; +use App\Domains\Integration\Services\IntegrationAssociationService; use App\Domains\Integration\Services\MailService; use App\Domains\Tenant\Models\Tenant; +use App\Domains\Tenant\Models\WebsiteType; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailer; use Illuminate\Mail\MailManager; use Illuminate\Support\Facades\Mail; use Mockery; +use Tests\Concerns\CreatesIntegrationInstances; use Tests\TestCase; class MailServiceTest extends TestCase { + use CreatesIntegrationInstances; use RefreshDatabase; public function test_it_builds_an_isolated_smtp_mailer_from_the_client_integration(): void { $tenant = $this->createTenant(); $this->createEmailIntegration(); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => $this->emailData(), @@ -40,7 +44,7 @@ class MailServiceTest extends TestCase $manager->shouldReceive('build') ->once() ->with(Mockery::on(fn (array $config): bool => $config === [ - 'name' => 'client-smtp-'.$tenant->client_id, + 'name' => 'integration-smtp-'.IntegrationInstance::firstOrFail()->id, 'transport' => 'smtp', 'scheme' => 'smtp', 'host' => 'smtp.example.com', @@ -54,7 +58,7 @@ class MailServiceTest extends TestCase $service = (new MailService($manager))->forTenant($tenant->codigo); - $this->assertSame('client-smtp', $service->mailerName()); + $this->assertSame('integration-smtp', $service->mailerName()); } public function test_it_uses_the_default_mailer_when_client_configuration_is_not_required(): void @@ -84,7 +88,7 @@ class MailServiceTest extends TestCase Mail::fake(); $tenant = $this->createTenant(); $this->createEmailIntegration(); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => $this->emailData(), @@ -118,6 +122,24 @@ class MailServiceTest extends TestCase Mail::assertSent(Mailable::class, 1); } + public function test_it_uses_the_website_type_smtp_instance_without_a_client_association(): void + { + Mail::fake(); + $tenant = $this->createTenant(); + $this->createEmailIntegration(); + $type = WebsiteType::create(['codigo' => 'mail-brand', 'nombre' => 'Mail Brand']); + $tenant->update(['website_type_code' => $type->codigo]); + $instance = IntegrationInstance::create([ + 'integration_code' => 'email', 'name' => 'Brand SMTP', 'integration_data' => $this->emailData(), + ]); + (new IntegrationAssociationService)->associate($type, 'email', $instance); + + $service = (new MailService)->forTenant($tenant->codigo); + self::assertSame('integration-smtp', $service->mailerName()); + $service->send('customer@example.com', 'Brand mail', '

Brand SMTP

'); + Mail::assertSent(Mailable::class, 1); + } + private function createTenant(): Tenant { $logo = Attachment::create([ diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php index b3d6e51..0edfed9 100644 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ b/tests/Feature/Integration/TelepagosWebhookTest.php @@ -11,7 +11,6 @@ use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Variant; -use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\TelepagosPayment; @@ -22,10 +21,12 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Str; +use Tests\Concerns\CreatesIntegrationInstances; use Tests\TestCase; class TelepagosWebhookTest extends TestCase { + use CreatesIntegrationInstances; use RefreshDatabase; protected function setUp(): void @@ -633,7 +634,7 @@ class TelepagosWebhookTest extends TestCase ], ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $tenant->client_id, 'integration_code' => 'telepagos_homo', 'integration_data' => [ diff --git a/tests/Feature/MailTest/MailTestControllerTest.php b/tests/Feature/MailTest/MailTestControllerTest.php index 05a3bab..f30d565 100644 --- a/tests/Feature/MailTest/MailTestControllerTest.php +++ b/tests/Feature/MailTest/MailTestControllerTest.php @@ -4,17 +4,18 @@ namespace Tests\Feature\MailTest; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; -use App\Domains\Integration\Models\ClientIntegration; use App\Domains\Integration\Models\Integration; use App\Domains\MailTest\Mailables\TestMail; use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Mail\Mailable; use Illuminate\Support\Facades\Mail; +use Tests\Concerns\CreatesIntegrationInstances; use Tests\TestCase; class MailTestControllerTest extends TestCase { + use CreatesIntegrationInstances; use RefreshDatabase; public function test_it_sends_a_test_email(): void @@ -22,7 +23,7 @@ class MailTestControllerTest extends TestCase Mail::fake(); $tenant = $this->createTenant(); - $response = $this->postJson('/api/acme/mail-test/send', [ + $response = $this->withHeader('Accept-Language', 'es')->postJson('/api/acme/mail-test/send', [ 'to' => 'recipient@example.com', 'subject' => 'SMTP test', 'message' => 'Test message', @@ -32,7 +33,7 @@ class MailTestControllerTest extends TestCase ->assertJsonPath('message', 'Correo de prueba enviado correctamente.') ->assertJsonPath('recipient', 'recipient@example.com') ->assertJsonPath('tenant_code', 'acme') - ->assertJsonPath('mailer', 'tenant-smtp') + ->assertJsonPath('mailer', 'integration-smtp') ->assertJsonStructure(['sent_at']); Mail::assertSent(Mailable::class, function (Mailable $mail) use ($tenant): bool { @@ -48,7 +49,7 @@ class MailTestControllerTest extends TestCase Mail::fake(); $this->createTenant(); - $this->postJson('/api/acme/mail-test/send', [ + $this->withHeader('Accept-Language', 'es')->postJson('/api/acme/mail-test/send', [ 'to' => 'recipient@example.com', ])->assertOk(); @@ -63,7 +64,7 @@ class MailTestControllerTest extends TestCase Mail::fake(); $this->createTenant(); - $this->postJson('/api/acme/mail-test/send', [ + $this->withHeader('Accept-Language', 'es')->postJson('/api/acme/mail-test/send', [ 'to' => 'invalid-email', ])->assertUnprocessable() ->assertJsonValidationErrors(['to']); @@ -117,7 +118,7 @@ class MailTestControllerTest extends TestCase { Mail::fake(); - $this->postJson('/api/unknown/mail-test/send', [ + $this->withHeader('Accept-Language', 'es')->postJson('/api/unknown/mail-test/send', [ 'to' => 'recipient@example.com', ])->assertNotFound(); @@ -129,7 +130,7 @@ class MailTestControllerTest extends TestCase Mail::fake(); $tenant = $this->createTenant(); - $this->postJson("/api/{$tenant->id}/mail-test/send", [ + $this->withHeader('Accept-Language', 'es')->postJson("/api/{$tenant->id}/mail-test/send", [ 'to' => 'recipient@example.com', ])->assertNotFound(); @@ -171,7 +172,7 @@ class MailTestControllerTest extends TestCase 'integration_code' => 'email', 'name' => 'Email', ]); - ClientIntegration::create([ + $this->createClientIntegration([ 'client_id' => $tenant->client_id, 'integration_code' => 'email', 'integration_data' => [