CaiWuXiTong/tests/Feature/Console/BusinessMirrorScheduleTest.php
limeng 0dc4a5f55a fix: 失效数据源不再阻断种菜报销同步 (#843)
逐项隔离无效调度配置,正常数据集继续同步;记录脱敏警告并保留失败退出码。相关 4 项测试、22 项断言通过。
2026-11-03 19:25:12 +08:00

146 lines
7.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Console;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use Mockery;
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
use Tests\TestCase;
class BusinessMirrorScheduleTest extends TestCase
{
public function test_stale_source_does_not_block_healthy_datasets_and_reports_failure(): void
{
config([
'upstream_mirrors.refresh_enabled' => true,
'upstream_pipeline.sync.max_datasets' => 2,
]);
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
$objects = [];
foreach (['healthy-a', 'stale-source', 'planting-work-orders'] as $key) {
$objects[$key] = Mockery::mock(BusinessMirrorDataset::class);
if ($key === 'stale-source') {
$registry->shouldReceive('scheduled')->once()->with($key)
->andThrow(new InvalidArgumentException('Unregistered source: private diagnostic'));
$mirror->shouldNotReceive('state')->with($objects[$key]);
continue;
}
$registry->shouldReceive('scheduled')->once()->with($key)->andReturn(true);
$registry->shouldReceive('definitionValue')->with($key, 'full_reconcile_hours', 0)->andReturn(0);
$mirror->shouldReceive('state')->with($objects[$key])->andReturn([
'initial_completed_at' => '2026-10-19 00:00:00',
'last_attempted_at' => '2026-10-25 00:00:00',
'status' => 'succeeded',
]);
$sync->shouldReceive('sync')->once()->with($key, false, null, null)
->ordered()->andReturn(['status' => 'succeeded', 'batches' => 1, 'rows' => 1]);
}
$registry->shouldReceive('all')->once()->andReturn($objects);
Log::shouldReceive('warning')->once()->with('共享业务镜像调度配置无效,已跳过该数据集。', [
'dataset' => 'stale-source',
'exception' => InvalidArgumentException::class,
]);
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
$this->artisan('business-mirror:sync-tables')
->expectsOutputToContain('stale-source:调度配置无效,已跳过')
->assertFailed();
}
public function test_unapproved_first_sync_is_skipped(): void
{
config(['upstream_mirrors.refresh_enabled' => true]);
$dataset = Mockery::mock(BusinessMirrorDataset::class);
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
$registry->shouldReceive('all')->once()->andReturn(['pending' => $dataset]);
$registry->shouldReceive('scheduled')->with('pending')->andReturn(true);
$registry->shouldReceive('automaticInitialSyncAllowed')->with('pending')->andReturn(false);
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
$mirror->shouldReceive('state')->with($dataset)->andReturn(null);
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
$sync->shouldNotReceive('sync');
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
$this->artisan('business-mirror:sync-tables')->assertSuccessful();
}
public function test_catchup_and_routine_datasets_both_receive_a_turn(): void
{
config(['upstream_mirrors.refresh_enabled' => true, 'upstream_pipeline.sync.max_datasets' => 2]);
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
$registry->shouldReceive('definitionValue')->with(Mockery::type('string'), 'full_reconcile_hours', 0)->andReturn(0);
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
$objects = [];
foreach (['routine-a', 'routine-b', 'catchup-z'] as $key) {
$objects[$key] = Mockery::mock(BusinessMirrorDataset::class);
$registry->shouldReceive('scheduled')->with($key)->andReturn(true);
$mirror->shouldReceive('state')->with($objects[$key])->andReturn([
'initial_completed_at' => '2026-08-01 00:00:00', 'last_attempted_at' => '2026-08-01 00:00:00',
'status' => $key === 'catchup-z' ? 'running' : 'succeeded',
]);
}
$registry->shouldReceive('all')->andReturn($objects);
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
$result = ['status' => 'succeeded', 'batches' => 1, 'rows' => 1];
$sync->shouldReceive('sync')->once()->with('routine-a', false, null, null)->ordered()->andReturn($result);
$sync->shouldReceive('sync')->once()->with('catchup-z', false, null, 6)->ordered()->andReturn($result);
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
$this->artisan('business-mirror:sync-tables')->assertSuccessful();
}
public function test_generic_rotation_excludes_datasets_with_a_dedicated_scheduler(): void
{
config([
'upstream_mirrors.refresh_enabled' => true,
'upstream_mirrors.table_sync.dedicated_datasets' => [
ProductCatalogMirrorRepository::DATASET_KEY,
],
]);
$catalog = Mockery::mock(BusinessMirrorDataset::class);
$other = Mockery::mock(BusinessMirrorDataset::class);
$datasets = Mockery::mock(BusinessMirrorDatasetRegistry::class);
$datasets->shouldReceive('definitionValue')->with(Mockery::type('string'), 'full_reconcile_hours', 0)->andReturn(0);
$datasets->shouldReceive('all')->once()->andReturn([
ProductCatalogMirrorRepository::DATASET_KEY => $catalog,
'test.dbo.other' => $other,
]);
$datasets->shouldReceive('scheduled')->once()->with('test.dbo.other')->andReturn(true);
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
$mirror->shouldReceive('state')->once()->with($other)->andReturn([
'initial_completed_at' => '2026-08-01 02:00:00',
'last_attempted_at' => '2026-08-15 01:00:00',
]);
$mirror->shouldNotReceive('state')->with($catalog);
$synchronizer = Mockery::mock(BusinessMirrorSynchronizer::class);
$synchronizer->shouldReceive('sync')
->once()
->with('test.dbo.other', false, null, null)
->andReturn([
'status' => 'succeeded',
'mode' => 'incremental',
'batches' => 1,
'rows' => 10,
'dataset' => 'test.dbo.other',
]);
$this->app->instance(BusinessMirrorDatasetRegistry::class, $datasets);
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
$this->app->instance(BusinessMirrorSynchronizer::class, $synchronizer);
$this->artisan('business-mirror:sync-tables')->assertSuccessful();
}
}