Skip to content

Commit dcada97

Browse files
committed
feature/add-test
1 parent dc508c5 commit dcada97

11 files changed

Lines changed: 1423 additions & 2 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Functional Tests (Real DB)
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master, dev]
8+
9+
jobs:
10+
functional-tests:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
include:
16+
- db: mysql
17+
db_image: mysql:8.0
18+
db_port: 3306
19+
db_user: root
20+
db_password: root
21+
db_driver: pdo_mysql
22+
server_version: '8.0'
23+
- db: postgres
24+
db_image: postgres:15
25+
db_port: 5432
26+
db_user: postgres
27+
db_password: postgres
28+
db_driver: pdo_pgsql
29+
server_version: '15'
30+
31+
name: Functional (${{ matrix.db }})
32+
33+
services:
34+
database:
35+
image: ${{ matrix.db_image }}
36+
env:
37+
MYSQL_ROOT_PASSWORD: root
38+
POSTGRES_PASSWORD: postgres
39+
ports:
40+
- ${{ matrix.db_port }}:${{ matrix.db_port }}
41+
options: >-
42+
--health-cmd "${{ matrix.db == 'mysql' && 'mysqladmin ping -h 127.0.0.1' || 'pg_isready' }}"
43+
--health-interval 10s
44+
--health-timeout 5s
45+
--health-retries 5
46+
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- uses: shivammathur/setup-php@v2
51+
with:
52+
php-version: '8.4'
53+
extensions: pdo_mysql, pdo_pgsql
54+
coverage: none
55+
56+
- name: Install dependencies
57+
run: composer install --no-ansi --no-interaction --no-progress --prefer-dist
58+
59+
- name: Run functional tests
60+
env:
61+
TENANT_DB_DRIVER: ${{ matrix.db_driver }}
62+
TENANT_DB_HOST: 127.0.0.1
63+
TENANT_DB_PORT: ${{ matrix.db_port }}
64+
TENANT_DB_USER: ${{ matrix.db_user }}
65+
TENANT_DB_PASSWORD: ${{ matrix.db_password }}
66+
TENANT_DB_SERVER_VERSION: ${{ matrix.server_version }}
67+
run: vendor/bin/phpunit --testsuite functional --no-coverage

phpunit.xml.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0"/>
2020
<env name="APP_DEBUG" value="1"/>
2121
<env name="DATABASE_URL" value="mysql://root:@localhost:3306/main_db"/>
22+
<env name="TENANT_DB_DRIVER" value=""/>
23+
<env name="TENANT_DB_HOST" value=""/>
24+
<env name="TENANT_DB_PORT" value=""/>
25+
<env name="TENANT_DB_USER" value=""/>
26+
<env name="TENANT_DB_PASSWORD" value=""/>
27+
<env name="TENANT_DB_SERVER_VERSION" value=""/>
2228
</php>
2329
<testsuites>
2430
<testsuite name="unit">
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hakam\MultiTenancyBundle\Tests\Functional\DatabaseLifecycle;
6+
7+
use Hakam\MultiTenancyBundle\Config\TenantConnectionConfigDTO;
8+
use Hakam\MultiTenancyBundle\Enum\DatabaseStatusEnum;
9+
use Hakam\MultiTenancyBundle\Event\SwitchDbEvent;
10+
use Hakam\MultiTenancyBundle\Port\TenantConfigProviderInterface;
11+
use Hakam\MultiTenancyBundle\Port\TenantDatabaseManagerInterface;
12+
use Hakam\MultiTenancyBundle\Tests\Integration\Fixtures\Entity\TenantProduct;
13+
use Hakam\MultiTenancyBundle\Tests\Integration\Fixtures\Service\InMemoryTenantConfigProvider;
14+
use Hakam\MultiTenancyBundle\Tests\Integration\Kernel\IntegrationTestKernel;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
17+
use Symfony\Component\Filesystem\Filesystem;
18+
19+
class ConfigManagerOverrideTest extends RealDatabaseTestCase
20+
{
21+
private bool $useCustomProvider = false;
22+
23+
protected function bootKernel(): void
24+
{
25+
$tenantBootDbName = $this->driver === 'pdo_pgsql' ? 'postgres' : '';
26+
$pass = $this->password !== '' ? ':' . $this->password : '';
27+
$scheme = $this->driver === 'pdo_pgsql' ? 'pgsql' : 'mysql';
28+
$dsn = sprintf(
29+
'%s://%s%s@%s:%d/%s',
30+
$scheme,
31+
$this->user,
32+
$pass,
33+
$this->host,
34+
$this->port,
35+
$tenantBootDbName
36+
);
37+
38+
$kernelConfig = [
39+
'tenant_connection' => [
40+
'url' => $dsn,
41+
'driver' => $this->driver,
42+
'host' => $this->host,
43+
'port' => (string) $this->port,
44+
'charset' => 'utf8',
45+
'server_version' => $this->serverVersion,
46+
],
47+
];
48+
49+
$serviceRegistrar = null;
50+
if ($this->useCustomProvider) {
51+
$kernelConfig['tenant_config_provider'] = 'test.in_memory_config_provider';
52+
$serviceRegistrar = function (ContainerBuilder $container): void {
53+
$container->register('test.in_memory_config_provider', InMemoryTenantConfigProvider::class)
54+
->setPublic(true);
55+
};
56+
}
57+
58+
// Clear stale container cache
59+
$kernel = new IntegrationTestKernel($kernelConfig, $serviceRegistrar);
60+
$cacheDir = $kernel->getCacheDir();
61+
if (is_dir($cacheDir)) {
62+
(new Filesystem())->remove($cacheDir);
63+
}
64+
65+
static::$kernel = new IntegrationTestKernel($kernelConfig, $serviceRegistrar);
66+
static::$kernel->boot();
67+
static::$container = static::$kernel->getContainer()->has('test.service_container')
68+
? static::$kernel->getContainer()->get('test.service_container')
69+
: static::$kernel->getContainer();
70+
}
71+
72+
private function rebootWithCustomProvider(): void
73+
{
74+
if (static::$kernel !== null) {
75+
static::$kernel->shutdown();
76+
static::$kernel = null;
77+
static::$container = null;
78+
}
79+
80+
$this->useCustomProvider = true;
81+
$this->bootKernel();
82+
$this->createMainSchema();
83+
}
84+
85+
private function createRealTenantDatabase(string $dbName): void
86+
{
87+
$dto = TenantConnectionConfigDTO::fromArgs(
88+
identifier: null,
89+
driver: $this->driverType,
90+
dbStatus: DatabaseStatusEnum::DATABASE_NOT_CREATED,
91+
host: $this->host,
92+
port: $this->port,
93+
dbname: $dbName,
94+
user: $this->user,
95+
password: $this->password,
96+
);
97+
98+
$manager = $this->getContainer()->get(TenantDatabaseManagerInterface::class);
99+
$manager->createTenantDatabase($dto);
100+
$this->trackDatabase($dbName);
101+
}
102+
103+
public function testCustomProviderUsedForRealDbSwitch(): void
104+
{
105+
$this->rebootWithCustomProvider();
106+
107+
$dbName = $this->generateUniqueDatabaseName();
108+
$this->createRealTenantDatabase($dbName);
109+
110+
// Populate the InMemoryTenantConfigProvider with real DB config
111+
/** @var InMemoryTenantConfigProvider $provider */
112+
$provider = $this->getContainer()->get(TenantConfigProviderInterface::class);
113+
$this->assertInstanceOf(InMemoryTenantConfigProvider::class, $provider);
114+
115+
$provider->addTenant('custom-tenant-1', TenantConnectionConfigDTO::fromArgs(
116+
identifier: 'custom-tenant-1',
117+
driver: $this->driverType,
118+
dbStatus: DatabaseStatusEnum::DATABASE_MIGRATED,
119+
host: $this->host,
120+
port: $this->port,
121+
dbname: $dbName,
122+
user: $this->user,
123+
password: $this->password,
124+
));
125+
126+
// Dispatch SwitchDbEvent — should use custom provider to resolve config
127+
/** @var EventDispatcherInterface $dispatcher */
128+
$dispatcher = $this->getContainer()->get('event_dispatcher');
129+
$dispatcher->dispatch(new SwitchDbEvent('custom-tenant-1'));
130+
131+
// Verify real connection works
132+
$tenantEm = $this->getTenantEntityManager();
133+
$result = $tenantEm->getConnection()->executeQuery('SELECT 1')->fetchOne();
134+
$this->assertEquals(1, $result);
135+
}
136+
137+
public function testAddNewTenantDbConfigAndSwitchToIt(): void
138+
{
139+
$dbName = $this->generateUniqueDatabaseName();
140+
141+
/** @var TenantDatabaseManagerInterface $manager */
142+
$manager = $this->getContainer()->get(TenantDatabaseManagerInterface::class);
143+
144+
// Add a new tenant config via the manager (runtime addition)
145+
$newTenantDto = $manager->addNewTenantDbConfig(TenantConnectionConfigDTO::fromArgs(
146+
identifier: null,
147+
driver: $this->driverType,
148+
dbStatus: DatabaseStatusEnum::DATABASE_NOT_CREATED,
149+
host: $this->host,
150+
port: $this->port,
151+
dbname: $dbName,
152+
user: $this->user,
153+
password: $this->password,
154+
));
155+
156+
$this->assertNotNull($newTenantDto->identifier, 'addNewTenantDbConfig should return a DTO with an identifier');
157+
158+
// Create the actual database
159+
$createDto = TenantConnectionConfigDTO::fromArgs(
160+
identifier: $newTenantDto->identifier,
161+
driver: $this->driverType,
162+
dbStatus: DatabaseStatusEnum::DATABASE_NOT_CREATED,
163+
host: $this->host,
164+
port: $this->port,
165+
dbname: $dbName,
166+
user: $this->user,
167+
password: $this->password,
168+
);
169+
$manager->createTenantDatabase($createDto);
170+
$this->trackDatabase($dbName);
171+
172+
// Update status to DATABASE_CREATED
173+
$manager->updateTenantDatabaseStatus($newTenantDto->identifier, DatabaseStatusEnum::DATABASE_CREATED);
174+
175+
// Switch to the new tenant DB
176+
/** @var EventDispatcherInterface $dispatcher */
177+
$dispatcher = $this->getContainer()->get('event_dispatcher');
178+
$dispatcher->dispatch(new SwitchDbEvent((string) $newTenantDto->identifier));
179+
180+
// Create schema and do CRUD
181+
$this->createTenantSchema();
182+
183+
$tenantEm = $this->getTenantEntityManager();
184+
$product = new TenantProduct();
185+
$product->setName('Runtime Tenant Product');
186+
$product->setPrice('50.00');
187+
$tenantEm->persist($product);
188+
$tenantEm->flush();
189+
190+
$tenantEm->clear();
191+
$found = $tenantEm->getRepository(TenantProduct::class)->findAll();
192+
$this->assertCount(1, $found);
193+
$this->assertSame('Runtime Tenant Product', $found[0]->getName());
194+
}
195+
196+
public function testManagerListsAndFiltersByStatus(): void
197+
{
198+
// Insert multiple tenant configs with different statuses
199+
$dbNameCreated = $this->generateUniqueDatabaseName();
200+
$dbNameMigrated1 = $this->generateUniqueDatabaseName();
201+
$dbNameMigrated2 = $this->generateUniqueDatabaseName();
202+
203+
$this->insertTenantConfig($dbNameCreated, DatabaseStatusEnum::DATABASE_CREATED);
204+
$this->insertTenantConfig($dbNameMigrated1, DatabaseStatusEnum::DATABASE_MIGRATED);
205+
$this->insertTenantConfig($dbNameMigrated2, DatabaseStatusEnum::DATABASE_MIGRATED);
206+
207+
/** @var TenantDatabaseManagerInterface $manager */
208+
$manager = $this->getContainer()->get(TenantDatabaseManagerInterface::class);
209+
210+
// listDatabases() returns only DATABASE_MIGRATED
211+
$migratedList = $manager->listDatabases();
212+
$migratedDbNames = array_map(fn($dto) => $dto->dbname, $migratedList);
213+
$this->assertContains($dbNameMigrated1, $migratedDbNames);
214+
$this->assertContains($dbNameMigrated2, $migratedDbNames);
215+
$this->assertNotContains($dbNameCreated, $migratedDbNames);
216+
217+
// getTenantDbListByDatabaseStatus(DATABASE_CREATED) returns correct subset
218+
$createdList = $manager->getTenantDbListByDatabaseStatus(DatabaseStatusEnum::DATABASE_CREATED);
219+
$createdDbNames = array_map(fn($dto) => $dto->dbname, $createdList);
220+
$this->assertContains($dbNameCreated, $createdDbNames);
221+
$this->assertNotContains($dbNameMigrated1, $createdDbNames);
222+
$this->assertNotContains($dbNameMigrated2, $createdDbNames);
223+
}
224+
}

0 commit comments

Comments
 (0)