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.
174 lines
8.6 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|