true]); $cache = new class extends UpstreamQueryCache { public array $calls = []; public function __construct() { } public function remember(string $scope, array $parameters, Closure $resolver): mixed { $this->calls[] = compact('scope', 'parameters'); return $resolver(); } }; $caches = new class($cache) extends UpstreamQueryCacheFactory { public function __construct(private readonly UpstreamQueryCache $cache) { } public function forSource(string $source): UpstreamQueryCache { return $this->cache; } }; $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with($scope)->andReturnFalse(); $information = new class($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class)) extends UpstreamBusinessInformation { public array $snapshots = []; public function snapshot(string $scope, array $parameters): array { $this->snapshots[] = compact('scope', 'parameters'); return ['data' => [], 'total' => 0]; } }; $this->app['request']->merge(['page' => 3]); $paginator = $information->paginate($section, $page, 25); $expected = ['scope' => $scope, 'parameters' => ['page' => 3, 'perPage' => 25]]; $this->assertSame(3, $paginator->currentPage()); $this->assertSame($expected, $cache->calls[0]); $this->assertSame($expected, $information->snapshots[0]); } /** * @dataProvider businessInformationPages */ public function test_ready_business_mirror_is_used_without_registering_a_page_snapshot( string $section, string $page, string $scope, ): void { config(['operations_replica.enabled' => false, 'upstreams.stored_configuration.enabled' => false]); $cache = new class extends UpstreamQueryCache { public function __construct() {} public function remember(string $scope, array $parameters, Closure $resolver): mixed { throw new \RuntimeException('镜像就绪后不应再读取分页快照。'); } }; $caches = new class($cache) extends UpstreamQueryCacheFactory { public function __construct(private readonly UpstreamQueryCache $cache) {} public function forSource(string $source): UpstreamQueryCache { return $this->cache; } }; $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with($scope)->andReturnTrue(); $mirror->shouldReceive('paginate')->once()->with($scope, 2, 25, [])->andReturn([ 'data' => [['id' => 10]], 'total' => 51, ]); $this->app['request']->merge(['page' => 2]); $paginator = (new UpstreamBusinessInformation($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class))) ->paginate($section, $page, 25); $this->assertSame(2, $paginator->currentPage()); $this->assertSame(51, $paginator->total()); $this->assertSame(10, $paginator->items()[0]->id); } public function test_bootstrap_without_mirror_explains_shared_connection_instead_of_requesting_upstream_credentials(): void { config(['system_database.mode' => 'bootstrap', 'upstreams.local_configuration.enabled' => false]); $caches = Mockery::mock(UpstreamQueryCacheFactory::class); $caches->shouldNotReceive('forSource'); $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnFalse(); $this->expectException(\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class); $this->expectExceptionMessage('无需另配上游 SQL Server'); (new UpstreamBusinessInformation($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class))) ->paginate('store-management', 'basic-information'); } public function test_subject_operating_report_hides_income_months_without_data_by_default(): void { $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue(); $mirror->shouldReceive('allStores')->andReturn([['erp_store_name' => '测试店铺']]); $mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue(); $mirror->shouldReceive('companiesByNames')->andReturn([]); $storeReports = Mockery::mock(StoreReportRepository::class); $storeReports->shouldReceive('rows')->andReturn([[ 'entity' => '测试主体', 'company_type' => '店铺-普通公司', 'rolling_12m_income' => 188.5, 'income_01' => null, 'income_02' => 88.5, 'income_03' => 0.0, 'income_04' => null, 'income_05' => null, 'income_06' => null, 'income_07' => null, 'income_08' => null, 'income_09' => null, 'income_10' => null, 'income_11' => null, 'income_12' => null, ]]); $storeAccounts = Mockery::mock(StoreAccountMirrorRepository::class); $storeAccounts->shouldReceive('ownershipByStoreName')->andReturn([]); $information = new UpstreamBusinessInformation( Mockery::mock(UpstreamQueryCacheFactory::class), $mirror, Mockery::mock(InstallationDetailMirrorRepository::class), $storeReports, $storeAccounts, ); $paginator = $information->paginate('company-management', 'subject-operating-report', 50); // income_02 有金额、income_03 为 0 都算有数据,其余空月份默认隐藏。 $this->assertSame( ['income_01', 'income_04', 'income_05', 'income_06', 'income_07', 'income_08', 'income_09', 'income_10', 'income_11', 'income_12'], $this->app['request']->attributes->get('subject-operating-report.default_hidden_columns'), ); $this->assertSame(1, $paginator->total()); $this->assertSame('测试主体', $paginator->items()[0]->entity); } public static function businessInformationPages(): array { return [ 'company' => ['company-management', 'basic-information', 'company-information.read'], 'store' => ['store-management', 'basic-information', 'store-information.read'], 'trademark' => ['trademark-management', 'information', 'trademark-information.read'], ]; } }