Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
144 / 144
n/a
0 / 0
CRAP
n/a
0 / 0
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10pest()->group('unit');
11
12use Phexium\Plugin\QueryBus\Adapter\CachedQueryBus;
13use Phexium\Plugin\QueryBus\Internal\QueryCacheKeyGenerator;
14use Tests\Phexium\Fake\Application\Query\CacheableQuery;
15use Tests\Phexium\Fake\Application\Query\Query;
16use Tests\Phexium\Fake\Plugin\Cache\Cache;
17use Tests\Phexium\Fake\Plugin\Logger\Logger;
18use Tests\Phexium\Fake\Plugin\QueryBus\QueryBus;
19use Tests\Phexium\Fake\Presentation\QueryResponse;
20
21describe('Cache bypass', function (): void {
22    it('bypasses cache for non-cacheable query', function (): void {
23        $innerBus = new QueryBus();
24        $cache = new Cache();
25        $logger = new Logger();
26        $expectedResponse = new QueryResponse(['data' => 'test']);
27        $query = new Query('123');
28
29        $innerBus->stubResponse(Query::class, $expectedResponse);
30
31        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger);
32        $response = $cachedBus->dispatch($query);
33
34        expect($response)->toBe($expectedResponse);
35        expect($innerBus->hasDispatched(Query::class))->toBeTrue();
36        expect($cache->wasMethodCalled('get'))->toBeFalse();
37        expect($cache->wasMethodCalled('set'))->toBeFalse();
38        expect($logger->getLogs())->toBe([]);
39    });
40});
41
42describe('Cache hit', function (): void {
43    it('returns cached response without dispatching to inner bus', function (): void {
44        $innerBus = new QueryBus();
45        $cache = new Cache();
46        $logger = new Logger();
47        $cachedResponse = new QueryResponse(['data' => 'cached']);
48        $query = new CacheableQuery('123');
49
50        $cacheKey = QueryCacheKeyGenerator::generate($query);
51        $cache->set($cacheKey, $cachedResponse);
52
53        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger);
54        $response = $cachedBus->dispatch($query);
55
56        $cacheKey = QueryCacheKeyGenerator::generate($query);
57
58        expect($response)->toBe($cachedResponse);
59        expect($innerBus->count())->toBe(0);
60        expect($logger->hasLog('debug', 'Cache hit for query', [
61            'query' => CacheableQuery::class,
62            'cacheKey' => $cacheKey,
63        ]))->toBeTrue();
64    });
65
66    it('returns cached response on second dispatch for same query', function (): void {
67        $innerBus = new QueryBus();
68        $cache = new Cache();
69        $logger = new Logger();
70        $expectedResponse = new QueryResponse(['data' => 'test']);
71        $query = new CacheableQuery('xyz');
72
73        $innerBus->stubResponse(CacheableQuery::class, $expectedResponse);
74
75        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger);
76
77        $response1 = $cachedBus->dispatch($query);
78        $response2 = $cachedBus->dispatch($query);
79
80        $cacheKey = QueryCacheKeyGenerator::generate($query);
81
82        expect($response1)->toBe($expectedResponse);
83        expect($response2)->toBe($expectedResponse);
84        expect($innerBus->count())->toBe(1);
85        expect($logger->hasLog('debug', 'Cache miss for query', [
86            'query' => CacheableQuery::class,
87            'cacheKey' => $cacheKey,
88            'ttl' => 300,
89        ]))->toBeTrue();
90        expect($logger->hasLog('debug', 'Cache hit for query', [
91            'query' => CacheableQuery::class,
92            'cacheKey' => $cacheKey,
93        ]))->toBeTrue();
94    });
95});
96
97describe('Cache miss', function (): void {
98    it('dispatches to inner bus and stores response', function (): void {
99        $innerBus = new QueryBus();
100        $cache = new Cache();
101        $logger = new Logger();
102        $expectedResponse = new QueryResponse(['data' => 'fresh']);
103        $query = new CacheableQuery('456');
104
105        $innerBus->stubResponse(CacheableQuery::class, $expectedResponse);
106
107        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger);
108        $response = $cachedBus->dispatch($query);
109
110        $cacheKey = QueryCacheKeyGenerator::generate($query);
111
112        expect($response)->toBe($expectedResponse);
113        expect($innerBus->hasDispatched(CacheableQuery::class))->toBeTrue();
114        expect($cache->wasMethodCalled('set'))->toBeTrue();
115        expect($logger->hasLog('debug', 'Cache miss for query', [
116            'query' => CacheableQuery::class,
117            'cacheKey' => $cacheKey,
118            'ttl' => 300,
119        ]))->toBeTrue();
120    });
121
122    it('uses query TTL when provided', function (): void {
123        $innerBus = new QueryBus();
124        $cache = new Cache();
125        $logger = new Logger();
126        $expectedResponse = new QueryResponse(['data' => 'test']);
127        $query = new CacheableQuery('789', 600);
128
129        $innerBus->stubResponse(CacheableQuery::class, $expectedResponse);
130
131        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger, 300);
132        $cachedBus->dispatch($query);
133
134        $callHistory = $cache->getCallHistory();
135        $setCall = array_filter($callHistory, fn (array $call): bool => $call['method'] === 'set');
136        $setCall = array_values($setCall)[0];
137
138        expect($setCall['args'][2])->toBe(600);
139
140        $cacheKey = QueryCacheKeyGenerator::generate($query);
141        expect($logger->hasLog('debug', 'Cache miss for query', [
142            'query' => CacheableQuery::class,
143            'cacheKey' => $cacheKey,
144            'ttl' => 600,
145        ]))->toBeTrue();
146    });
147
148    it('uses default TTL when query returns null', function (): void {
149        $innerBus = new QueryBus();
150        $cache = new Cache();
151        $logger = new Logger();
152        $expectedResponse = new QueryResponse(['data' => 'test']);
153        $query = new CacheableQuery('abc');
154
155        $innerBus->stubResponse(CacheableQuery::class, $expectedResponse);
156
157        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger, 300);
158        $cachedBus->dispatch($query);
159
160        $callHistory = $cache->getCallHistory();
161        $setCall = array_filter($callHistory, fn (array $call): bool => $call['method'] === 'set');
162        $setCall = array_values($setCall)[0];
163
164        expect($setCall['args'][2])->toBe(300);
165
166        $cacheKey = QueryCacheKeyGenerator::generate($query);
167        expect($logger->hasLog('debug', 'Cache miss for query', [
168            'query' => CacheableQuery::class,
169            'cacheKey' => $cacheKey,
170            'ttl' => 300,
171        ]))->toBeTrue();
172    });
173
174    it('supports DateInterval TTL', function (): void {
175        $innerBus = new QueryBus();
176        $cache = new Cache();
177        $logger = new Logger();
178        $expectedResponse = new QueryResponse(['data' => 'test']);
179        $ttl = new DateInterval('PT1H');
180        $query = new CacheableQuery('def', $ttl);
181
182        $innerBus->stubResponse(CacheableQuery::class, $expectedResponse);
183
184        $cachedBus = new CachedQueryBus($innerBus, $cache, $logger);
185        $cachedBus->dispatch($query);
186
187        $callHistory = $cache->getCallHistory();
188        $setCall = array_filter($callHistory, fn (array $call): bool => $call['method'] === 'set');
189        $setCall = array_values($setCall)[0];
190
191        expect($setCall['args'][2])->toBe($ttl);
192
193        $cacheKey = QueryCacheKeyGenerator::generate($query);
194        expect($logger->hasLog('debug', 'Cache miss for query', [
195            'query' => CacheableQuery::class,
196            'cacheKey' => $cacheKey,
197            'ttl' => $ttl,
198        ]))->toBeTrue();
199    });
200});