feat(health): dynamic schema conformity inspection from Migrator definitions and Oracle DUAL ping
This commit is contained in:
parent
d2c9e0807a
commit
b4f3c0bdb9
3 changed files with 113 additions and 253 deletions
|
|
@ -32,43 +32,14 @@ use Psr\Log\LoggerInterface;
|
|||
use Shared\DDD\Event\DomainEventDispatcher;
|
||||
use Shared\DDD\Event\TransactionalDomainEventDispatcher;
|
||||
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class DomainServiceProvider extends ServiceProvider
|
||||
{
|
||||
#[\Override]
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind('arcadiaConnection', static function (Application $app): Connection {
|
||||
$storageConfig = $app->make(ArcadiaStorageConfig::class);
|
||||
|
||||
return DB::connection($storageConfig->databaseConnection);
|
||||
});
|
||||
$this->app->bind(
|
||||
EmployeeDirectory::class,
|
||||
fn (Application $app) => new ArcadiaEmployeeDirectory($app->make('arcadiaConnection'))
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoSheetDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoSheetDirectory(
|
||||
$app->make('arcadiaConnection'),
|
||||
$app->make(BrandRepository::class),
|
||||
$app->make(IncoMapper::class)
|
||||
)
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoShortSheetDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoShortSheetDirectory($app->make('arcadiaConnection'), $app->make(BrandRepository::class))
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoCompositionDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoCompositionDirectory($app->make('arcadiaConnection'))
|
||||
);
|
||||
$this->app->bind(
|
||||
ArcadiaBrandDirectory::class,
|
||||
fn (Application $app) => new ArcadiaMarqueBrandDirectory($app->make('arcadiaConnection'))
|
||||
);
|
||||
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
|
||||
$this->app->bind(ClientAuditLogDirectory::class, EloquentClientAuditLogDirectory::class);
|
||||
$this->registerArcadiaDirectory($this->app);
|
||||
|
||||
$this->app->bind(BrandCache::class, static function (Application $app): BrandCache {
|
||||
$storageConfig = $app->make(ArcadiaStorageConfig::class);
|
||||
|
|
@ -84,4 +55,47 @@ class DomainServiceProvider extends ServiceProvider
|
|||
|
||||
$this->app->singleton(UsecaseExecutor::class, LaravelExecutor::class);
|
||||
}
|
||||
|
||||
private function registerArcadiaDirectory(Application $app): void
|
||||
{
|
||||
$this->app->bind('arcadiaConnection', static function (Application $app): Connection {
|
||||
$storageConfig = $app->make(ArcadiaStorageConfig::class);
|
||||
|
||||
return DB::connection($storageConfig->databaseConnection);
|
||||
});
|
||||
$this->app->bind(
|
||||
EmployeeDirectory::class,
|
||||
fn (Application $app) => new ArcadiaEmployeeDirectory($this->getArcadiaConnection($app))
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoSheetDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoSheetDirectory(
|
||||
$this->getArcadiaConnection($app),
|
||||
$app->make(BrandRepository::class),
|
||||
$app->make(IncoMapper::class)
|
||||
)
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoShortSheetDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoShortSheetDirectory($this->getArcadiaConnection($app), $app->make(BrandRepository::class))
|
||||
);
|
||||
$this->app->bind(
|
||||
IncoCompositionDirectory::class,
|
||||
fn (Application $app) => new ArcadiaIncoCompositionDirectory($this->getArcadiaConnection($app))
|
||||
);
|
||||
$this->app->bind(
|
||||
ArcadiaBrandDirectory::class,
|
||||
fn (Application $app) => new ArcadiaMarqueBrandDirectory($this->getArcadiaConnection($app))
|
||||
);
|
||||
$this->app->bind(BrandRepository::class, EloquentBrandRepository::class);
|
||||
$this->app->bind(ClientAuditLogDirectory::class, EloquentClientAuditLogDirectory::class);
|
||||
}
|
||||
|
||||
private function getArcadiaConnection(Application $app): Connection
|
||||
{
|
||||
$connection = $app->make('arcadiaConnection');
|
||||
Assert::isInstanceOf($connection, Connection::class);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,31 +14,18 @@ use Application\Health\Dto\HealthChecks;
|
|||
use Application\Health\Dto\MailCheckResult;
|
||||
use Application\Health\Enums\ComponentStatus;
|
||||
use Application\Health\Enums\HealthStatus;
|
||||
use Domain\Models\ValueObjects\Employees\EmployeeSortColumn;
|
||||
use Domain\Models\ValueObjects\Employees\EmployeeSorter;
|
||||
use Domain\Models\ValueObjects\Inco\IncoId;
|
||||
use Domain\Models\ValueObjects\Inco\IncoSheetSortColumn;
|
||||
use Domain\Models\ValueObjects\Inco\IncoSheetSorter;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory;
|
||||
use Domain\Ports\Directories\EmployeeDirectory;
|
||||
use Domain\Ports\Directories\IncoCompositionDirectory;
|
||||
use Domain\Ports\Directories\IncoSheetDirectory;
|
||||
use Database\ArcadiaReplicate\Migrator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Infra\Config\ArcadiaStorageConfig;
|
||||
use Shared\Pagination;
|
||||
use Shared\Time\Utc;
|
||||
|
||||
final readonly class LaravelSystemHealthChecker implements HealthChecker
|
||||
{
|
||||
public function __construct(
|
||||
private ArcadiaStorageConfig $storageConfig,
|
||||
private ArcadiaBrandDirectory $brandDirectory,
|
||||
private EmployeeDirectory $employeeDirectory,
|
||||
private IncoSheetDirectory $incoSheetDirectory,
|
||||
private IncoCompositionDirectory $incoCompositionDirectory,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -69,22 +56,34 @@ final readonly class LaravelSystemHealthChecker implements HealthChecker
|
|||
#[\Override]
|
||||
public function checkArcadiaConformity(): ArcadiaConformityResult
|
||||
{
|
||||
$brandCheck = $this->probeBrands();
|
||||
$employeeCheck = $this->probeEmployees();
|
||||
$incoSheetProbe = $this->probeIncoSheets();
|
||||
$incoCompoCheck = $this->probeIncoCompositions($incoSheetProbe['firstIncoId']);
|
||||
$connection = DB::connection($this->storageConfig->databaseConnection);
|
||||
$checks = [];
|
||||
$isAllUp = true;
|
||||
|
||||
$checks = [
|
||||
$brandCheck,
|
||||
$employeeCheck,
|
||||
$incoSheetProbe['check'],
|
||||
$incoCompoCheck,
|
||||
];
|
||||
foreach (Migrator::DEFINITIONS as $definition) {
|
||||
$tableName = $definition::name();
|
||||
$schemaName = $definition::schemaName();
|
||||
$target = "{$schemaName}.{$tableName}";
|
||||
|
||||
$isAllUp = $brandCheck->isUp()
|
||||
&& $employeeCheck->isUp()
|
||||
&& $incoSheetProbe['check']->isUp()
|
||||
&& $incoCompoCheck->isUp();
|
||||
try {
|
||||
$connection->table($tableName)->limit(0)->get();
|
||||
$checks[] = new DirectoryConformityCheck(
|
||||
name: strtolower($tableName),
|
||||
status: ComponentStatus::UP,
|
||||
target: $target,
|
||||
sampleCount: 0,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$isAllUp = false;
|
||||
$checks[] = new DirectoryConformityCheck(
|
||||
name: strtolower($tableName),
|
||||
status: ComponentStatus::DOWN,
|
||||
target: $target,
|
||||
sampleCount: 0,
|
||||
errorMessage: $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new ArcadiaConformityResult(
|
||||
status: $isAllUp ? HealthStatus::HEALTHY : HealthStatus::UNHEALTHY,
|
||||
|
|
@ -93,138 +92,24 @@ final readonly class LaravelSystemHealthChecker implements HealthChecker
|
|||
);
|
||||
}
|
||||
|
||||
private function probeBrands(): DirectoryConformityCheck
|
||||
{
|
||||
try {
|
||||
$brands = $this->brandDirectory->findAll();
|
||||
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'brands',
|
||||
status: ComponentStatus::UP,
|
||||
target: 'MARQUE',
|
||||
sampleCount: count($brands),
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'brands',
|
||||
status: ComponentStatus::DOWN,
|
||||
target: 'MARQUE',
|
||||
sampleCount: 0,
|
||||
errorMessage: $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function probeEmployees(): DirectoryConformityCheck
|
||||
{
|
||||
try {
|
||||
$employees = $this->employeeDirectory->findAll(
|
||||
Pagination::ofPage(1, 5),
|
||||
new EmployeeSorter(EmployeeSortColumn::arcadiaId, \SortDirection::Ascending)
|
||||
);
|
||||
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'employees',
|
||||
status: ComponentStatus::UP,
|
||||
target: 'RU_PERSONNE_SIRH',
|
||||
sampleCount: count($employees->getItems()),
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'employees',
|
||||
status: ComponentStatus::DOWN,
|
||||
target: 'RU_PERSONNE_SIRH',
|
||||
sampleCount: 0,
|
||||
errorMessage: $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{check: DirectoryConformityCheck, firstIncoId: ?IncoId}
|
||||
*/
|
||||
private function probeIncoSheets(): array
|
||||
{
|
||||
try {
|
||||
$incoSheets = $this->incoSheetDirectory->findAll(
|
||||
Pagination::ofPage(1, 5),
|
||||
new IncoSheetSorter(IncoSheetSortColumn::incoSheetId, \SortDirection::Ascending)
|
||||
);
|
||||
$incoItems = $incoSheets->getItems();
|
||||
$firstIncoId = ! empty($incoItems) ? $incoItems[0]->id : null;
|
||||
|
||||
return [
|
||||
'check' => new DirectoryConformityCheck(
|
||||
name: 'inco_sheets',
|
||||
status: ComponentStatus::UP,
|
||||
target: 'INCO + FAM_VENTE',
|
||||
sampleCount: count($incoItems),
|
||||
),
|
||||
'firstIncoId' => $firstIncoId,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'check' => new DirectoryConformityCheck(
|
||||
name: 'inco_sheets',
|
||||
status: ComponentStatus::DOWN,
|
||||
target: 'INCO + FAM_VENTE',
|
||||
sampleCount: 0,
|
||||
errorMessage: $e->getMessage(),
|
||||
),
|
||||
'firstIncoId' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function probeIncoCompositions(?IncoId $firstIncoId): DirectoryConformityCheck
|
||||
{
|
||||
try {
|
||||
$targetIncoId = $firstIncoId ?? new IncoId('000000');
|
||||
$composition = $this->incoCompositionDirectory->findByIncoId($targetIncoId);
|
||||
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'inco_compositions',
|
||||
status: ComponentStatus::UP,
|
||||
target: 'INCO_COMPO',
|
||||
sampleCount: $composition !== null ? 1 : 0,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return new DirectoryConformityCheck(
|
||||
name: 'inco_compositions',
|
||||
status: ComponentStatus::DOWN,
|
||||
target: 'INCO_COMPO',
|
||||
sampleCount: 0,
|
||||
errorMessage: $e->getMessage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkDefaultDatabase(): DatabaseCheckResult
|
||||
{
|
||||
$connection = Config::string('database.default');
|
||||
|
||||
try {
|
||||
DB::connection($connection)->select('SELECT 1');
|
||||
|
||||
return new DatabaseCheckResult(
|
||||
status: ComponentStatus::UP,
|
||||
connection: $connection,
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return new DatabaseCheckResult(
|
||||
status: ComponentStatus::DOWN,
|
||||
connection: $connection,
|
||||
message: $e->getMessage(),
|
||||
);
|
||||
}
|
||||
return $this->checkDatabaseConnection(Config::string('database.default'));
|
||||
}
|
||||
|
||||
private function checkArcadiaDatabase(): DatabaseCheckResult
|
||||
{
|
||||
$connection = $this->storageConfig->databaseConnection;
|
||||
return $this->checkDatabaseConnection($this->storageConfig->databaseConnection);
|
||||
}
|
||||
|
||||
private function checkDatabaseConnection(string $connection): DatabaseCheckResult
|
||||
{
|
||||
try {
|
||||
DB::connection($connection)->select('SELECT 1');
|
||||
$conn = DB::connection($connection);
|
||||
$driver = $conn->getDriverName();
|
||||
$query = $driver === 'oracle' ? 'SELECT 1 FROM DUAL' : 'SELECT 1';
|
||||
|
||||
$conn->select($query);
|
||||
|
||||
return new DatabaseCheckResult(
|
||||
status: ComponentStatus::UP,
|
||||
|
|
|
|||
|
|
@ -6,15 +6,11 @@ namespace Tests\Integration\Infra\Services\Health;
|
|||
|
||||
use Application\Health\Enums\ComponentStatus;
|
||||
use Application\Health\Enums\HealthStatus;
|
||||
use Domain\Ports\Directories\ArcadiaBrandDirectory;
|
||||
use Domain\Ports\Directories\EmployeeDirectory;
|
||||
use Domain\Ports\Directories\IncoCompositionDirectory;
|
||||
use Domain\Ports\Directories\IncoSheetDirectory;
|
||||
use Database\ArcadiaReplicate\Migrator;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Infra\Config\ArcadiaStorageConfig;
|
||||
use Infra\Services\Health\LaravelSystemHealthChecker;
|
||||
use Shared\Paginated;
|
||||
use Shared\Pagination;
|
||||
use Tests\TestCase;
|
||||
|
||||
covers(LaravelSystemHealthChecker::class);
|
||||
|
|
@ -22,32 +18,10 @@ covers(LaravelSystemHealthChecker::class);
|
|||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
describe('LaravelSystemHealthChecker checkArcadiaConformity Integration Test', function (): void {
|
||||
it('returns healthy status when all directory probes succeed', function (): void {
|
||||
it('returns healthy status when all 56 replicated tables and views exist', function (): void {
|
||||
// GIVEN
|
||||
$storageConfig = resolve(ArcadiaStorageConfig::class);
|
||||
$brandDirectory = mock(ArcadiaBrandDirectory::class);
|
||||
$employeeDirectory = mock(EmployeeDirectory::class);
|
||||
$incoSheetDirectory = mock(IncoSheetDirectory::class);
|
||||
$incoCompoDirectory = mock(IncoCompositionDirectory::class);
|
||||
|
||||
$brandDirectory->shouldReceive('findAll')->withAnyArgs()->once()->andReturn([]);
|
||||
$employeeDirectory->shouldReceive('findAll')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andReturn(new Paginated(Pagination::ofPage(1, 5), [], 0));
|
||||
$incoSheetDirectory->shouldReceive('findAll')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andReturn(new Paginated(Pagination::ofPage(1, 5), [], 0));
|
||||
$incoCompoDirectory->shouldReceive('findByIncoId')->withAnyArgs()->once()->andReturn(null);
|
||||
|
||||
$checker = new LaravelSystemHealthChecker(
|
||||
$storageConfig,
|
||||
$brandDirectory,
|
||||
$employeeDirectory,
|
||||
$incoSheetDirectory,
|
||||
$incoCompoDirectory,
|
||||
);
|
||||
$storageConfig = resolve(ArcadiaStorageConfig::class);
|
||||
$checker = new LaravelSystemHealthChecker($storageConfig);
|
||||
|
||||
// WHEN
|
||||
$result = $checker->checkArcadiaConformity();
|
||||
|
|
@ -55,45 +29,22 @@ describe('LaravelSystemHealthChecker checkArcadiaConformity Integration Test', f
|
|||
// THEN
|
||||
expect($result->isConform())->toBeTrue()
|
||||
->and($result->status)->toBe(HealthStatus::HEALTHY)
|
||||
->and(count($result->checks))->toBe(4)
|
||||
->and($result->checks[0]->status)->toBe(ComponentStatus::UP)
|
||||
->and($result->checks[1]->status)->toBe(ComponentStatus::UP)
|
||||
->and($result->checks[2]->status)->toBe(ComponentStatus::UP)
|
||||
->and($result->checks[3]->status)->toBe(ComponentStatus::UP);
|
||||
->and(count($result->checks))->toBe(count(Migrator::DEFINITIONS));
|
||||
|
||||
foreach ($result->checks as $check) {
|
||||
expect($check->status)->toBe(ComponentStatus::UP)
|
||||
->and($check->errorMessage)->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns unhealthy status with captured dissonance when a directory probe fails', function (): void {
|
||||
it('returns unhealthy status with captured dissonance when tables like ADDITIF or FAM_VENTE are missing or renamed', function (): void {
|
||||
// GIVEN
|
||||
$storageConfig = resolve(ArcadiaStorageConfig::class);
|
||||
$brandDirectory = mock(ArcadiaBrandDirectory::class);
|
||||
$employeeDirectory = mock(EmployeeDirectory::class);
|
||||
$incoSheetDirectory = mock(IncoSheetDirectory::class);
|
||||
$incoCompoDirectory = mock(IncoCompositionDirectory::class);
|
||||
$storageConfig = resolve(ArcadiaStorageConfig::class);
|
||||
$checker = new LaravelSystemHealthChecker($storageConfig);
|
||||
|
||||
$brandDirectory->shouldReceive('findAll')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andThrow(new \RuntimeException('ORA-00904: "LIB_MARQ": invalid identifier'));
|
||||
$employeeDirectory->shouldReceive('findAll')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andReturn(new Paginated(Pagination::ofPage(1, 5), [], 0));
|
||||
$incoSheetDirectory->shouldReceive('findAll')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andReturn(new Paginated(Pagination::ofPage(1, 5), [], 0));
|
||||
$incoCompoDirectory->shouldReceive('findByIncoId')
|
||||
->withAnyArgs()
|
||||
->once()
|
||||
->andReturn(null);
|
||||
|
||||
$checker = new LaravelSystemHealthChecker(
|
||||
$storageConfig,
|
||||
$brandDirectory,
|
||||
$employeeDirectory,
|
||||
$incoSheetDirectory,
|
||||
$incoCompoDirectory,
|
||||
);
|
||||
// Drop ADDITIF and FAM_VENTE to simulate table rename/removal in Oracle schema
|
||||
Schema::connection($storageConfig->databaseConnection)->dropIfExists('ADDITIF');
|
||||
Schema::connection($storageConfig->databaseConnection)->dropIfExists('FAM_VENTE');
|
||||
|
||||
// WHEN
|
||||
$result = $checker->checkArcadiaConformity();
|
||||
|
|
@ -101,9 +52,19 @@ describe('LaravelSystemHealthChecker checkArcadiaConformity Integration Test', f
|
|||
// THEN
|
||||
expect($result->isConform())->toBeFalse()
|
||||
->and($result->hasDissonances())->toBeTrue()
|
||||
->and($result->status)->toBe(HealthStatus::UNHEALTHY)
|
||||
->and($result->checks[0]->status)->toBe(ComponentStatus::DOWN)
|
||||
->and($result->checks[0]->errorMessage)->toContain('ORA-00904')
|
||||
->and($result->checks[1]->status)->toBe(ComponentStatus::UP);
|
||||
->and($result->status)->toBe(HealthStatus::UNHEALTHY);
|
||||
|
||||
$additifCheck = collect($result->checks)->firstWhere('name', 'additif');
|
||||
$famVenteCheck = collect($result->checks)->firstWhere('name', 'fam_vente');
|
||||
$marqueCheck = collect($result->checks)->firstWhere('name', 'marque');
|
||||
|
||||
expect($additifCheck)->not->toBeNull()
|
||||
->and($additifCheck->status)->toBe(ComponentStatus::DOWN)
|
||||
->and($additifCheck->errorMessage)->not->toBeNull()
|
||||
->and($famVenteCheck)->not->toBeNull()
|
||||
->and($famVenteCheck->status)->toBe(ComponentStatus::DOWN)
|
||||
->and($famVenteCheck->errorMessage)->not->toBeNull()
|
||||
->and($marqueCheck)->not->toBeNull()
|
||||
->and($marqueCheck->status)->toBe(ComponentStatus::UP);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue