Files
shopit-back/tests/Unit/IntegrationInstanceSchemaTest.php
ncoronel e4146288f1 refactor(integration): generalizar el requisito de configuracion
Renombra requires_client_configuration a requires_configuration conservando los valores existentes y su rollback. Actualiza el modelo, la validacion, los seeders y los consumidores del campo para permitir configuraciones de cliente o tipo de sitio.
2026-09-04 12:47:29 -03:00

174 lines
8.6 KiB
PHP

<?php
namespace Tests\Unit;
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\WebsiteType;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Facade;
use PHPUnit\Framework\TestCase;
class IntegrationInstanceSchemaTest extends TestCase
{
private Manager $database;
private array $migrations;
private mixed $previousFacadeApplication;
private Container $previousContainer;
protected function setUp(): void
{
parent::setUp();
$this->previousFacadeApplication = Facade::getFacadeApplication();
$this->previousContainer = Container::getInstance();
$container = new Container;
Container::setInstance($container);
$container->instance('config', new Repository([
'services' => ['integrations' => ['secret' => 'schema-test-secret']],
'app' => ['cipher' => 'AES-256-CBC'],
]));
$this->database = new Manager($container);
$this->database->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'foreign_key_constraints' => true]);
$this->database->bootEloquent();
$container->instance('db', $this->database->getDatabaseManager());
$container->bind('db.schema', fn () => $this->database->getConnection()->getSchemaBuilder());
Facade::clearResolvedInstances();
Facade::setFacadeApplication($container);
$schema = $this->database->getConnection()->getSchemaBuilder();
$schema->create('clients', function (Blueprint $table): void {
$table->id();
});
$schema->create('website_type', function (Blueprint $table): void {
$table->id();
$table->string('codigo')->unique();
});
(require __DIR__.'/../../database/migrations/2026_07_03_000001_create_integrations_table.php')->up();
$schema->table('integrations', function (Blueprint $table): void {
$table->boolean('requires_client_configuration')->default(true);
});
$schema->create('client_integrations', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id')->constrained('clients')->cascadeOnDelete();
$table->string('integration_code');
$table->longText('integration_data')->nullable();
$table->timestamps();
$table->foreign('integration_code')->references('integration_code')->on('integrations')->cascadeOnDelete();
$table->unique(['client_id', 'integration_code']);
});
$db = $this->database->getConnection();
$db->table('clients')->insert(['id' => 1]);
$db->table('website_type')->insert(['codigo' => 'onticket']);
$db->table('integrations')->insert([
['integration_code' => 'email', 'name' => 'Email'],
['integration_code' => 'telepagos', 'name' => 'Telepagos'],
]);
$db->table('client_integrations')->insert([
'client_id' => 1, 'integration_code' => 'email', 'integration_data' => 'existing-ciphertext',
]);
$this->migrations = array_map(fn (string $file) => require $file, glob(__DIR__.'/../../database/migrations/2026_09_04_*.php'));
foreach ($this->migrations as $migration) {
$migration->up();
}
}
protected function tearDown(): void
{
$this->database->getConnection()->disconnect();
Facade::clearResolvedInstances();
Facade::setFacadeApplication($this->previousFacadeApplication);
Container::setInstance($this->previousContainer);
parent::tearDown();
}
public function test_migration_preserves_ciphertext_and_rollback_restores_it(): void
{
$db = $this->database->getConnection();
$association = $db->table('client_integrations')->first();
self::assertSame('existing-ciphertext', $db->table('integration_instances')->where('id', $association->integration_instance_id)->value('integration_data'));
self::assertFalse($db->getSchemaBuilder()->hasColumn('client_integrations', 'integration_data'));
foreach (array_reverse($this->migrations) as $migration) {
$migration->down();
}
self::assertSame('existing-ciphertext', $db->table('client_integrations')->value('integration_data'));
self::assertFalse($db->getSchemaBuilder()->hasTable('integration_instances'));
}
public function test_instances_encrypt_data_and_can_be_shared_through_relations(): void
{
$instance = IntegrationInstance::firstOrFail();
$instance->update(['integration_data' => ['password' => 'private-value']]);
self::assertNotSame('private-value', $instance->getRawOriginal('integration_data'));
self::assertSame(['password' => 'private-value'], $instance->fresh()->integration_data);
self::assertArrayNotHasKey('integration_data', $instance->toArray());
WebsiteTypeIntegration::create([
'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id,
]);
self::assertTrue(ClientIntegration::firstOrFail()->integrationInstance->is($instance));
self::assertTrue(WebsiteType::firstOrFail()->integrations->first()->integrationInstance->is($instance));
self::assertSame(1, $instance->clientIntegrations()->count());
self::assertSame(1, $instance->websiteTypeIntegrations()->count());
self::assertTrue(Integration::where('integration_code', 'email')->firstOrFail()->instances->first()->is($instance));
}
public function test_retry_after_foreign_key_failure_preserves_existing_instances(): void
{
$db = $this->database->getConnection();
$instanceId = $db->table('client_integrations')->value('integration_instance_id');
// Reproduce the state left by MySQL when ADD CONSTRAINT fails after the backfill.
$db->getSchemaBuilder()->table('client_integrations', function (Blueprint $table): void {
$table->dropForeign('client_integrations_instance_code_fk')
->columns(['integration_instance_id', 'integration_code']);
$table->longText('integration_data')->nullable();
});
$db->table('client_integrations')->update(['integration_data' => 'existing-ciphertext']);
$this->migrations[1]->up();
$this->migrations[1]->up();
self::assertSame(1, $db->table('integration_instances')->count());
self::assertSame($instanceId, $db->table('client_integrations')->value('integration_instance_id'));
self::assertSame('existing-ciphertext', $db->table('integration_instances')->value('integration_data'));
self::assertFalse($db->getSchemaBuilder()->hasColumn('client_integrations', 'integration_data'));
$keys = $db->getSchemaBuilder()->getForeignKeys('client_integrations');
self::assertCount(1, array_filter($keys, fn (array $key): bool => $key['columns'] === ['integration_instance_id', 'integration_code']));
}
public function test_association_rejects_an_instance_from_another_integration(): void
{
$this->expectException(QueryException::class);
WebsiteTypeIntegration::create([
'website_type_code' => 'onticket', 'integration_code' => 'telepagos', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id,
]);
}
public function test_client_cannot_have_two_instances_of_the_same_integration(): void
{
$instance = IntegrationInstance::create(['integration_code' => 'email', 'name' => 'Second']);
$this->expectException(QueryException::class);
ClientIntegration::create(['client_id' => 1, 'integration_code' => 'email', 'integration_instance_id' => $instance->id]);
}
public function test_website_type_cannot_have_two_instances_of_the_same_integration(): void
{
WebsiteTypeIntegration::create([
'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => IntegrationInstance::firstOrFail()->id,
]);
$instance = IntegrationInstance::create(['integration_code' => 'email', 'name' => 'Second']);
$this->expectException(QueryException::class);
WebsiteTypeIntegration::create([
'website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id,
]);
}
}