Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/Test/TenantTestTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Hakam\MultiTenancyBundle\Test;

use Hakam\MultiTenancyBundle\Doctrine\ORM\TenantEntityManager;
use Hakam\MultiTenancyBundle\Event\SwitchDbEvent;
use Hakam\MultiTenancyBundle\EventListener\DbSwitchEventListener;
use Symfony\Component\DependencyInjection\ContainerInterface;

trait TenantTestTrait
{
/**
* Switches to the given tenant, executes the callback, and resets state automatically.
*
* @return mixed The callback's return value
*/
public function runInTenant(string $tenantIdentifier, callable $callback): mixed
{
$container = $this->getTenantTestContainer();

$container->get('event_dispatcher')->dispatch(new SwitchDbEvent($tenantIdentifier));

try {
return $callback();
} finally {
$this->resetTenantState();
}
}

/**
* Switches to the given tenant without automatic cleanup.
* Call resetTenantState() manually (typically in tearDown()).
*/
public function switchToTenant(string $tenantIdentifier): void
{
$container = $this->getTenantTestContainer();

$container->get('event_dispatcher')->dispatch(new SwitchDbEvent($tenantIdentifier));
}

/**
* Resets all tenant-related state: clears EM identity map, closes connection, resets listener.
* Safe to call even when no tenant is active.
*/
public function resetTenantState(): void
{
$container = $this->getTenantTestContainer();

try {
$container->get('tenant_entity_manager')->clear();
} catch (\Throwable) {
}

try {
$container->get('doctrine')->getConnection('tenant')->close();
} catch (\Throwable) {
}

try {
$container->get(DbSwitchEventListener::class)->reset();
} catch (\Throwable) {
}
}

public function getTenantEntityManager(): TenantEntityManager
{
return $this->getTenantTestContainer()->get('tenant_entity_manager');
}

private function getTenantTestContainer(): ContainerInterface
{
if (!method_exists(static::class, 'getContainer')) {
throw new \LogicException(sprintf(
'The trait "%s" requires the test class to extend KernelTestCase or provide a getContainer() method.',
__TRAIT__,
));
}

return static::getContainer();
}
}
13 changes: 8 additions & 5 deletions tests/Integration/TenantConnectionConfigDTOFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
use Hakam\MultiTenancyBundle\Config\TenantConnectionConfigDTO;
use Hakam\MultiTenancyBundle\Enum\DatabaseStatusEnum;
use Hakam\MultiTenancyBundle\Enum\DriverTypeEnum;
use Hakam\MultiTenancyBundle\Event\SwitchDbEvent;
use Hakam\MultiTenancyBundle\Test\TenantTestTrait;

class TenantConnectionConfigDTOFlowTest extends IntegrationTestCase
{
use TenantTestTrait {
getTenantEntityManager as traitGetTenantEntityManager;
}

public function testDTOFromEntityPreservesAllFields(): void
{
$tenant = $this->insertTenantConfig(
Expand Down Expand Up @@ -114,15 +118,14 @@ public function testDTOUsedInConnectionSwitching(): void

// Full end-to-end: config provider -> event listener -> connection switch
// Verifies the entire DTO flow works without errors
$dispatcher = $this->getContainer()->get('event_dispatcher');
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

// After switch, EM identity map should be cleared
$tenantEM = $this->getTenantEntityManager();
$this->assertNotNull($tenantEM);

// Second dispatch to same tenant should be a no-op (tracked by listener)
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
// Second switch to same tenant should be a no-op (tracked by listener)
$this->switchToTenant((string) $tenant->getId());
$this->assertTrue(true, 'Full DTO -> switch flow completed without error');
}
}
32 changes: 13 additions & 19 deletions tests/Integration/TenantConnectionSwitchingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

use Hakam\MultiTenancyBundle\Enum\DatabaseStatusEnum;
use Hakam\MultiTenancyBundle\Enum\DriverTypeEnum;
use Hakam\MultiTenancyBundle\Event\SwitchDbEvent;
use Hakam\MultiTenancyBundle\Test\TenantTestTrait;

class TenantConnectionSwitchingTest extends IntegrationTestCase
{
use TenantTestTrait {
getTenantEntityManager as traitGetTenantEntityManager;
}

public function testSwitchingToTenantDatabaseViaEvent(): void
{
$tenant = $this->insertTenantConfig(
Expand All @@ -16,10 +20,8 @@ public function testSwitchingToTenantDatabaseViaEvent(): void
driver: DriverTypeEnum::SQLITE,
);

$dispatcher = $this->getContainer()->get('event_dispatcher');

// Should not throw - the switch event is handled by the listener
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

// Verify the tenant connection is still valid after switching
$tenantConnection = $this->getContainer()->get('doctrine')->getConnection('tenant');
Expand All @@ -37,11 +39,9 @@ public function testSwitchingBetweenTwoTenants(): void
status: DatabaseStatusEnum::DATABASE_MIGRATED,
);

$dispatcher = $this->getContainer()->get('event_dispatcher');

// Switch to tenant 1 then tenant 2 without errors
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant1->getId()));
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant2->getId()));
$this->switchToTenant((string) $tenant1->getId());
$this->switchToTenant((string) $tenant2->getId());

$this->assertTrue(true, 'Switching between tenants completed without error');
}
Expand All @@ -53,14 +53,12 @@ public function testSwitchToSameTenantIsNoOp(): void
status: DatabaseStatusEnum::DATABASE_MIGRATED,
);

$dispatcher = $this->getContainer()->get('event_dispatcher');

// First switch
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

// Second switch to same tenant should not throw or cause issues
// (the listener tracks the current db and skips redundant switches)
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

$this->assertTrue(true, 'Double switch to same tenant completed without error');
}
Expand All @@ -85,8 +83,7 @@ public function testSwitchClearsEntityManagerIdentityMap(): void
status: DatabaseStatusEnum::DATABASE_MIGRATED,
);

$dispatcher = $this->getContainer()->get('event_dispatcher');
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

// After switch, entity manager should be cleared
$this->assertFalse($tenantEM->contains($product));
Expand All @@ -96,8 +93,7 @@ public function testSwitchEventWithInvalidIdThrowsException(): void
{
$this->expectException(\RuntimeException::class);

$dispatcher = $this->getContainer()->get('event_dispatcher');
$dispatcher->dispatch(new SwitchDbEvent('999999'));
$this->switchToTenant('999999');
}

public function testSwitchEventTriggersListener(): void
Expand All @@ -112,11 +108,9 @@ public function testSwitchEventTriggersListener(): void
password: 'my_pass',
);

$dispatcher = $this->getContainer()->get('event_dispatcher');

// Dispatching the event should work without errors
// The listener resolves the config, clears the EM, and switches the connection
$dispatcher->dispatch(new SwitchDbEvent((string) $tenant->getId()));
$this->switchToTenant((string) $tenant->getId());

// Verify the tenant entity manager was cleared (can't contain previous entities)
$tenantEM = $this->getTenantEntityManager();
Expand Down
133 changes: 133 additions & 0 deletions tests/Integration/TenantTestTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace Hakam\MultiTenancyBundle\Tests\Integration;

use Hakam\MultiTenancyBundle\Enum\DatabaseStatusEnum;
use Hakam\MultiTenancyBundle\Enum\DriverTypeEnum;
use Hakam\MultiTenancyBundle\EventListener\DbSwitchEventListener;
use Hakam\MultiTenancyBundle\Test\TenantTestTrait;

class TenantTestTraitTest extends IntegrationTestCase
{
use TenantTestTrait {
getTenantEntityManager as traitGetTenantEntityManager;
}

public function testRunInTenantExecutesCallback(): void
{
$tenant = $this->insertTenantConfig(
dbName: 'trait_test_db',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);

$result = $this->runInTenant((string) $tenant->getId(), function () {
return 'callback_executed';
});

$this->assertSame('callback_executed', $result);
}

public function testRunInTenantResetsStateAfterExecution(): void
{
$tenant = $this->insertTenantConfig(
dbName: 'trait_reset_db',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);

$this->runInTenant((string) $tenant->getId(), function () {
// do nothing
});

// After runInTenant, the listener's internal state should be reset
$listener = $this->getContainer()->get(DbSwitchEventListener::class);
$ref = new \ReflectionProperty($listener, 'currentTenantIdentifier');
$this->assertNull($ref->getValue($listener));

$refDb = new \ReflectionProperty($listener, 'currentTenantDbName');
$this->assertNull($refDb->getValue($listener));
}

public function testRunInTenantResetsStateOnException(): void
{
$tenant = $this->insertTenantConfig(
dbName: 'trait_exception_db',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);

try {
$this->runInTenant((string) $tenant->getId(), function () {
throw new \RuntimeException('Test exception');
});
} catch (\RuntimeException) {
// expected
}

// State should still be reset despite the exception
$listener = $this->getContainer()->get(DbSwitchEventListener::class);
$ref = new \ReflectionProperty($listener, 'currentTenantIdentifier');
$this->assertNull($ref->getValue($listener));
}

public function testSwitchToTenantAndManualReset(): void
{
$tenant = $this->insertTenantConfig(
dbName: 'trait_manual_db',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);

$this->switchToTenant((string) $tenant->getId());

// Listener should have the tenant set
$listener = $this->getContainer()->get(DbSwitchEventListener::class);
$ref = new \ReflectionProperty($listener, 'currentTenantIdentifier');
$this->assertNotNull($ref->getValue($listener));

// Manual reset
$this->resetTenantState();

$this->assertNull($ref->getValue($listener));
}

public function testSequentialRunInTenantCalls(): void
{
$tenant1 = $this->insertTenantConfig(
dbName: 'trait_seq_one',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);
$tenant2 = $this->insertTenantConfig(
dbName: 'trait_seq_two',
status: DatabaseStatusEnum::DATABASE_MIGRATED,
driver: DriverTypeEnum::SQLITE,
);

$result1 = $this->runInTenant((string) $tenant1->getId(), function () {
return 'first';
});

$result2 = $this->runInTenant((string) $tenant2->getId(), function () {
return 'second';
});

$this->assertSame('first', $result1);
$this->assertSame('second', $result2);

// Listener state should be clean after both calls
$listener = $this->getContainer()->get(DbSwitchEventListener::class);
$ref = new \ReflectionProperty($listener, 'currentTenantIdentifier');
$this->assertNull($ref->getValue($listener));
}

public function testResetTenantStateIsIdempotent(): void
{
// Calling resetTenantState when no tenant is active should not throw
$this->resetTenantState();
$this->resetTenantState();

$this->assertTrue(true, 'resetTenantState is safe to call when no tenant is active');
}
}
Loading