Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
188 / 188
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('integration');
11
12use Phexium\Plugin\Cache\Adapter\FileCache;
13use Phexium\Plugin\Cache\Exception\CacheDirectoryNotFoundException;
14use Phexium\Plugin\Clock\Adapter\FrozenClock;
15use Phexium\Plugin\Clock\Adapter\OffsetClock;
16
17beforeEach(function (): void {
18    $this->tempDir = sys_get_temp_dir().'/cache_test_'.uniqid();
19    mkdir($this->tempDir, 0o777, true);
20    $this->clock = new OffsetClock(new FrozenClock('2025-01-01T12:00:00+00:00'));
21    $this->cache = new FileCache($this->tempDir, $this->clock);
22});
23
24afterEach(function (): void {
25    $files = glob($this->tempDir.'/*');
26
27    if ($files !== false) {
28        foreach ($files as $file) {
29            unlink($file);
30        }
31    }
32
33    if (is_dir($this->tempDir)) {
34        rmdir($this->tempDir);
35    }
36});
37
38describe('Configuration', function (): void {
39    it('throws exception if cache directory does not exist', function (): void {
40        $clock = new FrozenClock('2025-01-01T12:00:00+00:00');
41
42        expect(fn (): FileCache => new FileCache('/non-existent-directory', $clock))
43            ->toThrow(CacheDirectoryNotFoundException::class, 'Cache directory "/non-existent-directory" does not exist')
44        ;
45    });
46});
47
48describe('Basic operations', function (): void {
49    it('returns null for non-existent key', function (): void {
50        expect($this->cache->get('non-existent'))->toBeNull();
51    });
52
53    it('returns default for non-existent key', function (): void {
54        expect($this->cache->get('non-existent', 'default'))->toBe('default');
55    });
56
57    it('stores and retrieves value', function (): void {
58        $this->cache->set('key', 'value');
59
60        expect($this->cache->get('key'))->toBe('value');
61    });
62
63    it('returns true on successful set()', function (): void {
64        expect($this->cache->set('key', 'value'))->toBeTrue();
65    });
66
67    it('overwrites existing value', function (): void {
68        $this->cache->set('key', 'first');
69        $this->cache->set('key', 'second');
70
71        expect($this->cache->get('key'))->toBe('second');
72    });
73
74    it('returns false for non-existent key on has()', function (): void {
75        expect($this->cache->has('non-existent'))->toBeFalse();
76    });
77
78    it('returns true for existing key on has()', function (): void {
79        $this->cache->set('key', 'value');
80
81        expect($this->cache->has('key'))->toBeTrue();
82    });
83
84    it('returns null for cache file with invalid structure', function (): void {
85        $this->cache->set('key', 'value');
86
87        $filePath = $this->tempDir.'/'.hash('xxh128', 'key').'.cache';
88        file_put_contents($filePath, '{"invalid": true}');
89
90        expect($this->cache->has('key'))->toBeFalse();
91        expect($this->cache->get('key'))->toBeNull();
92    });
93
94    it('returns null for corrupted cache file', function (): void {
95        $this->cache->set('key', 'value');
96
97        $filePath = $this->tempDir.'/'.hash('xxh128', 'key').'.cache';
98        file_put_contents($filePath, 'invalid json');
99
100        expect($this->cache->has('key'))->toBeFalse();
101        expect($this->cache->get('key'))->toBeNull();
102    });
103
104    it('removes existing key on delete()', function (): void {
105        $this->cache->set('key', 'value');
106
107        expect($this->cache->delete('key'))->toBeTrue();
108        expect($this->cache->has('key'))->toBeFalse();
109    });
110
111    it('returns false when deleting non-existent key', function (): void {
112        expect($this->cache->delete('non-existent'))->toBeFalse();
113    });
114
115    it('removes all keys on clear()', function (): void {
116        $this->cache->set('key1', 'value1');
117        $this->cache->set('key2', 'value2');
118
119        $this->cache->clear();
120
121        expect($this->cache->has('key1'))->toBeFalse();
122        expect($this->cache->has('key2'))->toBeFalse();
123    });
124
125    it('returns true on clear()', function (): void {
126        expect($this->cache->clear())->toBeTrue();
127    });
128});
129
130describe('TTL and expiration', function (): void {
131    it('does not store value with TTL zero', function (): void {
132        expect($this->cache->set('key', 'value', 0))->toBeTrue();
133        expect(file_exists($this->tempDir.'/'.hash('xxh128', 'key').'.cache'))->toBeFalse();
134    });
135
136    it('stores value with TTL 1', function (): void {
137        expect($this->cache->set('key', 'value', 1))->toBeTrue();
138        expect(file_exists($this->tempDir.'/'.hash('xxh128', 'key').'.cache'))->toBeTrue();
139    });
140
141    it('deletes existing entry with negative TTL', function (): void {
142        $this->cache->set('key', 'value');
143        expect($this->cache->has('key'))->toBeTrue();
144
145        $this->cache->set('key', 'new-value', -1);
146
147        expect($this->cache->has('key'))->toBeFalse();
148    });
149
150    it('supports DateInterval TTL', function (): void {
151        $this->cache->set('key', 'value', new DateInterval('PT60S'));
152
153        expect($this->cache->has('key'))->toBeTrue();
154        expect($this->cache->get('key'))->toBe('value');
155
156        $this->clock->advanceSeconds(59);
157        expect($this->cache->has('key'))->toBeTrue();
158
159        $this->clock->advanceSeconds(1);
160        expect($this->cache->has('key'))->toBeFalse();
161    });
162
163    it('expires value after TTL seconds', function (): void {
164        $this->cache->set('key', 'value', 60);
165
166        expect($this->cache->has('key'))->toBeTrue();
167        expect($this->cache->get('key'))->toBe('value');
168
169        $this->clock->advanceSeconds(59);
170        expect($this->cache->has('key'))->toBeTrue();
171        expect($this->cache->get('key'))->toBe('value');
172
173        $this->clock->advanceSeconds(1);
174        expect($this->cache->has('key'))->toBeFalse();
175        expect($this->cache->get('key'))->toBeNull();
176    });
177
178    it('never expires value without TTL', function (): void {
179        $this->cache->set('key', 'value');
180
181        expect($this->cache->has('key'))->toBeTrue();
182        expect($this->cache->get('key'))->toBe('value');
183
184        $this->clock->advanceSeconds(10 * 365 * 24 * 60 * 60);
185
186        expect($this->cache->has('key'))->toBeTrue();
187        expect($this->cache->get('key'))->toBe('value');
188    });
189
190    it('lazily removes expired items on access', function (): void {
191        $this->cache->set('key', 'value', 60);
192
193        $this->clock->advanceSeconds(61);
194
195        expect($this->cache->has('key'))->toBeFalse();
196        expect($this->cache->delete('key'))->toBeFalse();
197    });
198});
199
200describe('Special values', function (): void {
201    it('stores array value', function (): void {
202        $data = ['foo' => 'bar', 'nested' => ['a' => 1]];
203        $this->cache->set('array', $data);
204
205        expect($this->cache->get('array'))->toBe($data);
206    });
207
208    it('stores null as valid value', function (): void {
209        $this->cache->set('null-key', null);
210
211        expect($this->cache->has('null-key'))->toBeTrue();
212        expect($this->cache->get('null-key'))->toBeNull();
213    });
214
215    it('stores false as valid value', function (): void {
216        $this->cache->set('false-key', false);
217
218        expect($this->cache->has('false-key'))->toBeTrue();
219        expect($this->cache->get('false-key'))->toBeFalse();
220    });
221
222    it('stores empty string as valid value', function (): void {
223        $this->cache->set('empty-key', '');
224
225        expect($this->cache->has('empty-key'))->toBeTrue();
226        expect($this->cache->get('empty-key'))->toBe('');
227    });
228
229    it('stores zero as valid value', function (): void {
230        $this->cache->set('zero-key', 0);
231
232        expect($this->cache->has('zero-key'))->toBeTrue();
233        expect($this->cache->get('zero-key'))->toBe(0);
234    });
235});
236
237describe('Batch operations', function (): void {
238    it('returns values with defaults on getMultiple()', function (): void {
239        $this->cache->set('key1', 'value1');
240
241        $result = $this->cache->getMultiple(['key1', 'key2'], 'default');
242
243        expect($result)->toBe(['key1' => 'value1', 'key2' => 'default']);
244    });
245
246    it('stores all values on setMultiple()', function (): void {
247        $result = $this->cache->setMultiple(['key1' => 'value1', 'key2' => 'value2']);
248
249        expect($result)->toBeTrue();
250        expect($this->cache->get('key1'))->toBe('value1');
251        expect($this->cache->get('key2'))->toBe('value2');
252    });
253
254    it('applies TTL on setMultiple()', function (): void {
255        $this->cache->setMultiple(['key1' => 'value1', 'key2' => 'value2'], 60);
256
257        $this->clock->advanceSeconds(61);
258
259        expect($this->cache->has('key1'))->toBeFalse();
260        expect($this->cache->has('key2'))->toBeFalse();
261    });
262
263    it('removes specified keys on deleteMultiple()', function (): void {
264        $this->cache->set('key1', 'value1');
265        $this->cache->set('key2', 'value2');
266        $this->cache->set('key3', 'value3');
267
268        $result = $this->cache->deleteMultiple(['key1', 'key2']);
269
270        expect($result)->toBeTrue();
271        expect($this->cache->has('key1'))->toBeFalse();
272        expect($this->cache->has('key2'))->toBeFalse();
273        expect($this->cache->has('key3'))->toBeTrue();
274    });
275});