Files
shopit-back/tests/Feature/Integration/IntegrationInstanceTest.php

229 lines
11 KiB
PHP

<?php
namespace Tests\Feature\Integration;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Auth\Models\User;
use App\Domains\Client\Models\Client;
use App\Domains\Integration\Models\Integration;
use App\Domains\Integration\Models\IntegrationInstance;
use App\Domains\Integration\Services\BaseIntegrationService;
use App\Domains\Integration\Services\ClientIntegrationService;
use App\Domains\Integration\Services\IntegrationAssociationService;
use App\Domains\Integration\Services\IntegrationInstanceService;
use App\Domains\Integration\Services\TelepagosIntegrationService;
use App\Domains\Tenant\Models\Tenant;
use App\Domains\Tenant\Models\WebsiteType;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class IntegrationInstanceTest extends TestCase
{
use RefreshDatabase;
private Integration $integration;
protected function setUp(): void
{
parent::setUp();
config(['services.integrations.secret' => 'instance-test-secret']);
$this->integration = Integration::create([
'integration_code' => 'test', 'name' => 'Test', 'requires_configuration' => false,
'integration_data_schema' => ['api_key' => 'required|string'],
]);
}
public function test_management_requires_an_authenticated_global_admin(): void
{
Client::create(['code' => 'acme', 'name' => 'Acme']);
WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
$this->getJson('/api/clients/acme/integrations')->assertUnauthorized();
Sanctum::actingAs(User::factory()->create());
$this->getJson('/api/clients/acme/integrations')->assertForbidden();
$this->getJson('/api/website-types/demo/integrations')->assertForbidden();
}
public function test_only_owner_endpoints_can_manage_configuration(): void
{
$this->admin();
$client = Client::create(['code' => 'acme', 'name' => 'Acme']);
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
$this->getJson('/api/integrations')->assertNotFound();
$this->getJson('/api/integration-instances')->assertNotFound();
$this->putJson('/api/clients/acme/integrations/test/instance', [])->assertNotFound();
$this->putJson('/api/clients/acme/integrations/test', ['integration_data' => []])
->assertUnprocessable()->assertJsonValidationErrors('integration_data.api_key');
$this->putJson('/api/website-types/demo/integrations/test', ['integration_data' => []])
->assertUnprocessable()->assertJsonValidationErrors('integration_data.api_key');
$this->putJson('/api/clients/acme/integrations/test', [
'integration_data' => ['api_key' => 'client-secret'],
])->assertOk()->assertJsonMissingPath('integration_data');
$clientInstanceId = $client->integrations()->firstOrFail()->integration_instance_id;
self::assertSame('client-secret', IntegrationInstance::findOrFail($clientInstanceId)->integration_data['api_key']);
$this->putJson('/api/website-types/demo/integrations/test', [
'integration_data' => ['api_key' => 'type-secret'],
])->assertCreated()
->assertJsonPath('data.integration_instance.name', 'Test / Demo')
->assertJsonMissingPath('data.integration_instance.integration_data');
$typeInstanceId = $type->integrations()->firstOrFail()->integration_instance_id;
$this->getJson('/api/website-types/demo/integrations/test')
->assertOk()->assertJsonPath('data.integration_instance_id', $typeInstanceId);
$this->deleteJson('/api/clients/acme/integrations/test')->assertNoContent();
$this->deleteJson('/api/website-types/demo/integrations/test')->assertNoContent();
}
public function test_tenant_prefers_client_then_type_and_client_context_does_not_inherit(): void
{
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
$tenant = $this->tenantForType($type);
$associations = new IntegrationAssociationService;
$associations->associate($type, 'test', $this->makeInstance('type'));
$service = $this->probe();
self::assertSame('type', $service->forTenant($tenant->codigo)->setting());
self::assertNull($service->forClient($tenant->client)->setting());
$associations->associate($tenant->client, 'test', $this->makeInstance('client'));
self::assertSame('client', $service->forTenant($tenant->codigo)->setting());
$associations->detach($tenant->client, 'test');
self::assertSame('type', $service->forTenant($tenant->codigo)->setting());
$associations->detach($type, 'test');
self::assertNull($service->forTenant($tenant->codigo)->setting());
$this->integration->update(['requires_configuration' => true]);
$this->expectException(\Exception::class);
$service->forTenant($tenant->codigo);
}
public function test_required_configuration_can_come_from_the_website_type(): void
{
$this->integration->update(['requires_configuration' => true]);
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
$tenant = $this->tenantForType($type);
(new IntegrationAssociationService)->associate($type, 'test', $this->makeInstance('type'));
self::assertSame('type', $this->probe()->forTenant($tenant->codigo)->setting());
}
public function test_legacy_save_does_not_modify_a_shared_instance(): void
{
$a = Client::create(['code' => 'a', 'name' => 'A']);
$b = Client::create(['code' => 'b', 'name' => 'B']);
$shared = $this->makeInstance('shared');
$associations = new IntegrationAssociationService;
$associations->associate($a, 'test', $shared);
$associations->associate($b, 'test', $shared);
(new ClientIntegrationService)->updateOrCreateIntegration($a, $this->integration, ['api_key' => 'private']);
self::assertSame('private', $this->probe()->forClient($a)->setting());
self::assertSame('shared', $this->probe()->forClient($b)->setting());
(new IntegrationInstanceService)->update($shared, ['integration_data' => ['api_key' => 'changed']]);
self::assertSame('changed', $this->probe()->forClient($b)->setting());
}
public function test_reconfiguring_deletes_the_previous_instance_when_it_is_no_longer_used(): void
{
$client = Client::create(['code' => 'acme', 'name' => 'Acme']);
$associations = new IntegrationAssociationService;
$previous = $this->makeInstance('previous');
$associations->associate($client, 'test', $previous);
$current = $associations->configure($client, $this->integration, ['api_key' => 'current']);
$this->assertDatabaseMissing('integration_instances', ['id' => $previous->id]);
$this->assertDatabaseHas('integration_instances', ['id' => $current->integration_instance_id]);
}
public function test_an_instance_is_deleted_only_after_its_last_association_is_removed(): void
{
$client = Client::create(['code' => 'acme', 'name' => 'Acme']);
$type = WebsiteType::create(['codigo' => 'demo', 'nombre' => 'Demo']);
$shared = $this->makeInstance('shared');
$associations = new IntegrationAssociationService;
$associations->associate($client, 'test', $shared);
$associations->associate($type, 'test', $shared);
$associations->detach($client, 'test');
$this->assertDatabaseHas('integration_instances', ['id' => $shared->id]);
$associations->detach($type, 'test');
$this->assertDatabaseMissing('integration_instances', ['id' => $shared->id]);
}
public function test_telepagos_shares_tokens_by_instance_and_refreshes_after_credential_changes(): void
{
Integration::create(['integration_code' => 'telepagos_homo', 'name' => 'Telepagos', 'url' => 'https://payments.test']);
$instance = IntegrationInstance::create([
'integration_code' => 'telepagos_homo', 'name' => 'Payments',
'integration_data' => ['username' => 'first', 'password' => 'secret'],
]);
$a = Client::create(['code' => 'a', 'name' => 'A']);
$b = Client::create(['code' => 'b', 'name' => 'B']);
$associations = new IntegrationAssociationService;
$associations->associate($a, 'telepagos_homo', $instance);
$associations->associate($b, 'telepagos_homo', $instance);
Cache::flush();
Http::fake(['https://payments.test/v2/auth/token' => Http::sequence()
->push(['status' => 'ok', 'token' => 'first-token', 'expires_at' => now()->addHour()->toDateTimeString()])
->push(['status' => 'ok', 'token' => 'new-token', 'expires_at' => now()->addHour()->toDateTimeString()])]);
self::assertSame('first-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($a)->getToken());
self::assertSame('first-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($b)->getToken());
Http::assertSentCount(1);
(new IntegrationInstanceService)->update($instance, ['integration_data' => ['username' => 'second', 'password' => 'new-secret']]);
self::assertSame('new-token', (new TelepagosIntegrationService('telepagos_homo'))->forClient($b)->getToken());
Http::assertSentCount(2);
}
private function tenantForType(WebsiteType $type): Tenant
{
$logo = Attachment::create([
'path' => 'tenants/logo.png', 'filename' => 'logo.png',
'type' => AttachmentType::Image,
'mime_type' => 'image/png',
]);
return Tenant::create([
'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test',
'website_type_code' => $type->codigo,
'primary_color' => '#112233', 'secondary_color' => '#445566',
'danger_color' => '#ff0000', 'success_color' => '#00ff00',
'header_bg_color' => '#112233', 'footer_bg_color' => '#112233',
'header_logo_id' => $logo->id, 'footer_logo_id' => $logo->id,
]);
}
private function makeInstance(string $name): IntegrationInstance
{
return IntegrationInstance::create(['integration_code' => 'test', 'name' => $name, 'integration_data' => ['api_key' => $name]]);
}
private function admin(): void
{
Sanctum::actingAs(User::factory()->create(['rol_codigo' => 'admin']));
}
private function probe(): BaseIntegrationService
{
return new class extends BaseIntegrationService
{
protected string $integrationCode = 'test';
public function getHeaders(): array
{
return [];
}
public function setting(): mixed
{
return $this->getIntegrationSetting('api_key');
}
};
}
}