CaiWuXiTong/tests/Unit/EcommerceFinance/JianSunQueryCacheTest.php

166 lines
5.7 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Modules\EcommerceFinance\Services\JianSunQueryCache;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Tests\TestCase;
class JianSunQueryCacheTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config()->set('upstreams.snapshots.allow_inline_refresh', true);
Cache::setDefaultDriver('array');
Cache::flush();
Carbon::setTestNow('2026-07-22 10:00:00');
config()->set('upstreams.snapshots.cache_store', 'array');
config()->set('jiansun.snapshots.ttl_seconds', 60);
config()->set('jiansun.snapshots.summary_ttl_seconds', 60);
config()->set('jiansun.snapshots.options_ttl_seconds', 3600);
config()->set('jiansun.snapshots.stale_ttl_seconds', 3600);
config()->set('jiansun.snapshots.active_ttl_seconds', 3600);
config()->set('jiansun.snapshots.registry_limit', 300);
config()->set('jiansun.snapshots.refresh_limit', 2);
config()->set('jiansun.snapshots.miss_budget_per_window', 1000);
config()->set('jiansun.snapshots.budget_window_seconds', 60);
config()->set('jiansun.snapshots.max_concurrency', 2);
config()->set('jiansun.snapshots.slot_wait_ms', 0);
config()->set('jiansun.snapshots.lock_wait_seconds', 1);
config()->set('jiansun.snapshots.slow_query_ms', 5000);
config()->set('jiansun.enabled', true);
config()->set('jiansun.allowed_scopes', ['*']);
}
protected function tearDown(): void
{
Carbon::setTestNow();
Cache::flush();
parent::tearDown();
}
public function test_identical_queries_are_shared_without_user_count_limit(): void
{
$cache = app(JianSunQueryCache::class);
$calls = 0;
for ($i = 0; $i < 100; $i++) {
$value = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
$calls++;
return ['data' => [['id' => 1]], 'total' => 1];
});
$this->assertSame(1, $value['total']);
}
$this->assertSame(1, $calls);
$this->assertNotEmpty(Cache::get('jiansun:snapshots:active:v2'));
}
public function test_expired_snapshot_returns_immediately_and_is_refreshed_later(): void
{
$cache = app(JianSunQueryCache::class);
$calls = 0;
$first = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
$calls++;
return ['data' => [['version' => 1]], 'total' => 1];
});
Carbon::setTestNow(now()->addSeconds(61));
$stale = $cache->remember('goods.list', ['page' => 1], function () use (&$calls): array {
$calls++;
return ['data' => [['version' => 999]], 'total' => 1];
});
$this->assertSame($first, $stale);
$this->assertSame(1, $calls, '页面读取过期快照时不应同步访问上游。');
$this->assertSame('stale', $cache->responseHeaders()['X-Caiwu-Data-Status']);
$this->assertTrue((bool) ($cache->activeEntries(1)[0]['urgent'] ?? false));
$status = $cache->refresh('goods.list', ['page' => 1], function () use (&$calls): array {
$calls++;
return ['data' => [['version' => 2]], 'total' => 1];
});
$fresh = $cache->remember('goods.list', ['page' => 1], fn (): array => []);
$this->assertSame('fresh', $status);
$this->assertSame(2, $fresh['data'][0]['version']);
$this->assertSame('fresh', $cache->responseHeaders()['X-Caiwu-Data-Status']);
$this->assertSame(2, $calls);
}
public function test_unrelated_cold_queries_do_not_share_one_global_query_lock(): void
{
$cache = app(JianSunQueryCache::class);
$parametersA = [
'page' => 1,
'__caiwu_snapshot_context' => [
'company_id' => 0,
'access_hash' => 'system',
],
];
ksort($parametersA);
$queryA = hash('sha256', 'goods.list|' . json_encode($parametersA, JSON_UNESCAPED_UNICODE));
$lockA = Cache::lock('jiansun:query-lock:' . $queryA, 30);
$this->assertTrue($lockA->get());
try {
$value = $cache->remember('goods.summary', [], fn (): array => ['total' => 7]);
$this->assertSame(7, $value['total']);
} finally {
$lockA->release();
}
}
public function test_upstream_slots_bound_parallel_cold_queries(): void
{
$cache = app(JianSunQueryCache::class);
$slot0 = Cache::lock('jiansun:upstream-slot:0', 30);
$slot1 = Cache::lock('jiansun:upstream-slot:1', 30);
$this->assertTrue($slot0->get());
$this->assertTrue($slot1->get());
try {
$this->expectException(ServiceUnavailableHttpException::class);
$cache->remember('goods.list', ['page' => 2], fn (): array => ['data' => [], 'total' => 0]);
} finally {
$slot0->release();
$slot1->release();
}
}
public function test_disallowed_cold_scope_never_calls_upstream(): void
{
config()->set('jiansun.allowed_scopes', ['invoice-deductions.*']);
$cache = app(JianSunQueryCache::class);
$calls = 0;
try {
$cache->remember('goods.list', [], function () use (&$calls): array {
$calls++;
return [];
});
$this->fail('未开放的数据范围不应进入冷查询。');
} catch (ServiceUnavailableHttpException) {
$this->assertSame(0, $calls);
}
}
}