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