153 lines
6.2 KiB
PHP
153 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Console;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
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 RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
class ProductCatalogMirrorSyncCommandTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'upstream_mirrors.refresh_enabled' => true,
|
|
'upstream_mirrors.product_catalog' => [
|
|
'dataset_key' => ProductCatalogMirrorRepository::DATASET_KEY,
|
|
'batch_size' => 1000,
|
|
'max_batches_per_run' => 5,
|
|
'full_reconcile_hours' => 168,
|
|
'reconcile_weekday' => 0,
|
|
'reconcile_start_hour' => 2,
|
|
'reconcile_window_hours' => 6,
|
|
],
|
|
]);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Carbon::setTestNow();
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_it_runs_incremental_sync_with_dedicated_limits(): void
|
|
{
|
|
Carbon::setTestNow('2026-08-17 10:00:00');
|
|
$this->bindDependencies([
|
|
'status' => 'succeeded',
|
|
'run_mode' => 'incremental',
|
|
'initial_completed_at' => '2026-08-01 02:00:00',
|
|
'last_full_succeeded_at' => '2026-08-16 04:00:00',
|
|
], false, $this->syncResult('incremental'));
|
|
|
|
$this->artisan('business-mirror:sync-product-catalog')
|
|
->expectsOutputToContain('普通商品增量同步:完成')
|
|
->assertSuccessful();
|
|
}
|
|
|
|
public function test_it_starts_a_due_full_reconcile_inside_the_weekly_window(): void
|
|
{
|
|
Carbon::setTestNow('2026-08-23 02:30:00');
|
|
$this->bindDependencies([
|
|
'status' => 'succeeded',
|
|
'run_mode' => 'incremental',
|
|
'initial_completed_at' => '2026-08-01 02:00:00',
|
|
'last_full_succeeded_at' => '2026-08-15 02:00:00',
|
|
], true, $this->syncResult('full', 'running'));
|
|
|
|
$this->artisan('business-mirror:sync-product-catalog')
|
|
->expectsOutputToContain('普通商品全量校准:进行中')
|
|
->assertSuccessful();
|
|
}
|
|
|
|
public function test_it_continues_an_in_progress_full_reconcile_outside_the_start_window(): void
|
|
{
|
|
Carbon::setTestNow('2026-08-24 10:00:00');
|
|
$this->bindDependencies([
|
|
'status' => 'running',
|
|
'run_mode' => 'full',
|
|
'initial_completed_at' => '2026-08-01 02:00:00',
|
|
'last_full_succeeded_at' => '2026-08-15 02:00:00',
|
|
'cursor' => ['autoid' => 5000],
|
|
], true, $this->syncResult('full', 'running'));
|
|
|
|
$this->artisan('business-mirror:sync-product-catalog')->assertSuccessful();
|
|
}
|
|
|
|
public function test_it_does_not_automatically_start_the_first_full_sync(): void
|
|
{
|
|
Carbon::setTestNow('2026-08-23 02:30:00');
|
|
$this->bindDependencies([
|
|
'status' => 'pending',
|
|
'initial_completed_at' => null,
|
|
], false, $this->syncResult('incremental', 'requires_full_sync'));
|
|
|
|
$this->artisan('business-mirror:sync-product-catalog')
|
|
->expectsOutputToContain('需经授权后显式使用 --full')
|
|
->assertSuccessful();
|
|
}
|
|
|
|
public function test_it_reports_failure_without_claiming_the_old_mirror_was_removed(): void
|
|
{
|
|
Carbon::setTestNow('2026-08-17 10:00:00');
|
|
$dataset = Mockery::mock(BusinessMirrorDataset::class);
|
|
$datasets = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
|
$datasets->shouldReceive('get')->once()->with(ProductCatalogMirrorRepository::DATASET_KEY)->andReturn($dataset);
|
|
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
|
$mirror->shouldReceive('state')->once()->with($dataset)->andReturn([
|
|
'status' => 'succeeded',
|
|
'initial_completed_at' => '2026-08-01 02:00:00',
|
|
'last_full_succeeded_at' => '2026-08-16 04:00:00',
|
|
]);
|
|
$synchronizer = Mockery::mock(BusinessMirrorSynchronizer::class);
|
|
$synchronizer->shouldReceive('sync')->once()->andThrow(new RuntimeException('upstream_unavailable'));
|
|
$this->app->instance(BusinessMirrorDatasetRegistry::class, $datasets);
|
|
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
|
|
$this->app->instance(BusinessMirrorSynchronizer::class, $synchronizer);
|
|
|
|
$this->artisan('business-mirror:sync-product-catalog')
|
|
->expectsOutputToContain('已保留上一版共享数据')
|
|
->assertFailed();
|
|
}
|
|
|
|
/** @param array<string,mixed> $state */
|
|
private function bindDependencies(array $state, bool $full, array $result): void
|
|
{
|
|
$dataset = Mockery::mock(BusinessMirrorDataset::class);
|
|
$datasets = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
|
$datasets->shouldReceive('get')->once()->with(ProductCatalogMirrorRepository::DATASET_KEY)->andReturn($dataset);
|
|
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
|
$mirror->shouldReceive('state')->once()->with($dataset)->andReturn($state);
|
|
$synchronizer = Mockery::mock(BusinessMirrorSynchronizer::class);
|
|
$synchronizer->shouldReceive('sync')
|
|
->once()
|
|
->with(ProductCatalogMirrorRepository::DATASET_KEY, $full, 1000, 5)
|
|
->andReturn($result);
|
|
|
|
$this->app->instance(BusinessMirrorDatasetRegistry::class, $datasets);
|
|
$this->app->instance(GenericBusinessMirrorRepository::class, $mirror);
|
|
$this->app->instance(BusinessMirrorSynchronizer::class, $synchronizer);
|
|
}
|
|
|
|
/** @return array{status:string,mode:string,batches:int,rows:int,dataset:string} */
|
|
private function syncResult(string $mode, string $status = 'succeeded'): array
|
|
{
|
|
return [
|
|
'status' => $status,
|
|
'mode' => $mode,
|
|
'batches' => 2,
|
|
'rows' => 2000,
|
|
'dataset' => ProductCatalogMirrorRepository::DATASET_KEY,
|
|
];
|
|
}
|
|
}
|