Files
shopit-back/tests/Unit/IntegrationInstanceSchemaTest.php

192 lines
9.7 KiB
PHP

<?php
namespace Tests\Unit;
use App\Shared\Integration\Models\ClientIntegration;
use App\Shared\Integration\Models\Integration;
use App\Shared\Integration\Models\IntegrationInstance;
use App\Shared\Integration\Models\AdminWebsiteTypeIntegration;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
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',
]);
$migrationDir = __DIR__.'/../../database/migrations/';
$this->migrations = array_map(fn (string $file) => require $migrationDir.$file, [
'2026_09_04_000000_create_integration_instances_table.php',
'2026_09_04_000001_move_client_configuration_to_integration_instances.php',
'2026_09_04_000002_create_website_type_integrations_table.php',
'2026_09_04_000003_rename_integration_configuration_requirement.php',
]);
foreach ($this->migrations as $migration) {
$migration->up();
}
$schema->rename('website_type', 'admin_website_types');
$schema->rename('website_type_integrations', 'admin_website_type_integrations');
$schema->table('admin_website_type_integrations', function (Blueprint $table): void {
$table->renameColumn('website_type_code', 'admin_website_type_code');
});
}
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'));
$schema = $db->getSchemaBuilder();
$schema->table('admin_website_type_integrations', function (Blueprint $table): void {
$table->renameColumn('admin_website_type_code', 'website_type_code');
});
$schema->rename('admin_website_type_integrations', 'website_type_integrations');
$schema->rename('admin_website_types', 'website_type');
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());
AdminWebsiteTypeIntegration::create([
'admin_website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id,
]);
self::assertTrue(ClientIntegration::firstOrFail()->integrationInstance->is($instance));
self::assertTrue(AdminWebsiteType::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);
AdminWebsiteTypeIntegration::create([
'admin_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
{
AdminWebsiteTypeIntegration::create([
'admin_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);
AdminWebsiteTypeIntegration::create([
'admin_website_type_code' => 'onticket', 'integration_code' => 'email', 'integration_instance_id' => $instance->id,
]);
}
}