Files
electionvue/vendor/tecnickcom/tc-lib-file/test/CacheTest.php
T
2026-08-30 22:02:02 +00:00

400 lines
14 KiB
PHP

<?php
/**
* CacheTest.php
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-filecache
*
* This file is part of tc-lib-pdf-filecache software library.
*/
namespace Test;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
/**
* Unit Test
*
* @since 2011-05-23
* @category Library
* @package File
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-filecache
*/
class CacheTest extends TestUtil
{
protected function getTestObject(): \Com\Tecnick\File\Cache
{
return new \Com\Tecnick\File\Cache('1_2-a+B/c');
}
public function testAutoPrefix(): void
{
$cache = new \Com\Tecnick\File\Cache();
$this->assertNotEmpty($cache->getFilePrefix());
}
public function testGetCachePath(): void
{
$cache = $this->getTestObject();
$cachePath = $cache->getCachePath();
// The normalized cache path always ends with the platform separator.
$this->assertSame(\DIRECTORY_SEPARATOR, \substr($cachePath, -1));
$cache->setCachePath();
$this->assertEquals($cachePath, $cache->getCachePath());
// Use the real temp dir and compare realpath-to-realpath so the
// assertion holds on every platform (e.g. macOS /var -> /private/var
// symlink, Windows backslash separators).
$path = \sys_get_temp_dir();
$real = \realpath($path);
if ($real === false) {
self::fail('the system temp dir must resolve');
}
$cache->setCachePath($path);
$this->assertSame($real . \DIRECTORY_SEPARATOR, $cache->getCachePath());
}
public function testGetFilePrefix(): void
{
$cache = $this->getTestObject();
$filePrefix = $cache->getFilePrefix();
$this->assertEquals('_1_2-a-B_c_', $filePrefix);
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testGetNewFileName(): void
{
$cache = $this->getTestObject();
$val = $cache->getNewFileName('tst', '0123');
$this->bcAssertMatchesRegularExpression('/_1_2-a-B_c_tst_0123_/', $val);
\unlink($val);
}
/**
* The full prefix, type and key must survive in the generated filename so
* the prefix-based glob in delete()/deleteOlderThan() matches on every
* platform. tempnam() alone truncates the prefix to 3 chars on Windows.
*
* @throws \Com\Tecnick\File\Exception
*/
public function testGetNewFileNamePreservesFullPrefix(): void
{
$cache = $this->getTestObject();
$fileA = $cache->getNewFileName('typ', 'k1');
$fileB = $cache->getNewFileName('typ', 'k2');
try {
$this->assertNotSame($fileA, $fileB, 'each call must return a distinct file');
$this->assertTrue(\file_exists($fileA));
$this->assertTrue(\file_exists($fileB));
$this->assertStringStartsWith($cache->getFilePrefix() . 'typ_k1_', \basename($fileA));
$this->assertStringStartsWith($cache->getFilePrefix() . 'typ_k2_', \basename($fileB));
} finally {
if (\is_file($fileA)) {
\unlink($fileA);
}
if (\is_file($fileB)) {
\unlink($fileB);
}
}
}
/**
* Characters that are invalid in Windows filenames (and glob metacharacters)
* are stripped from the generated name, keeping it valid and consistent with
* the patterns delete() builds.
*
* @throws \Com\Tecnick\File\Exception
*/
public function testGetNewFileNameSanitizesUnsafeTypeAndKey(): void
{
$cache = $this->getTestObject();
$file = $cache->getNewFileName('a/b*', 'c:d?');
try {
$this->assertStringStartsWith($cache->getFilePrefix() . 'ab_cd_', \basename($file));
$this->assertTrue(\file_exists($file));
} finally {
if (\is_file($file)) {
\unlink($file);
}
}
}
public function testNormalizePathInvalid(): void
{
$cache = $this->getTestObject();
// invoke protected normalizePath via reflection so we can test
// the branch where realpath() returns false
$ref = new \ReflectionMethod($cache, 'normalizePath');
$invalid = \sys_get_temp_dir() . '/nonexistent_' . \uniqid('', true);
$this->assertFalse(\file_exists($invalid), 'Sanity check: path should not exist');
$this->assertSame('', $ref->invoke($cache, $invalid));
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testDelete(): void
{
$cache = $this->getTestObject();
$idk = 0;
/** @var array<int, string> $file */
$file = [];
for ($idx = 1; $idx <= 2; ++$idx) {
for ($idy = 1; $idy <= 2; ++$idy) {
$file[$idk] = $cache->getNewFileName((string) $idx, (string) $idy);
\file_put_contents($file[$idk], '');
$this->assertTrue(\file_exists($file[$idk]));
++$idk;
}
}
$f0 = $file[0] ?? '';
$f1 = $file[1] ?? '';
$f2 = $file[2] ?? '';
$f3 = $file[3] ?? '';
// delete a specific type/key pair
$cache->delete('2', '1');
$this->assertFalse(\file_exists($f2));
// delete all entries for type "1"
$cache->delete('1');
$this->assertFalse(\file_exists($f0));
$this->assertFalse(\file_exists($f1));
$this->assertTrue(\file_exists($f3));
// delete everything
$cache->delete();
$this->assertFalse(\file_exists($f3));
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testKeyOnlyDeletesAll(): void
{
$cache = $this->getTestObject();
$file = $cache->getNewFileName('foo', 'bar');
\file_put_contents($file, '');
$this->assertTrue(\file_exists($file));
// key-only call should treat as delete all
$cache->delete(null, 'bar');
$this->assertFalse(\file_exists($file));
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testDeleteNonExistingPatterns(): void
{
$cache = $this->getTestObject();
$file = $cache->getNewFileName('foo', 'bar');
\file_put_contents($file, '');
$this->assertTrue(\file_exists($file));
// deleting a type that does not exist should leave the file in place
$cache->delete('no-such-type');
$this->assertTrue(\file_exists($file));
// deleting a non-existent key under existing type
$cache->delete('foo', 'no-such-key');
$this->assertTrue(\file_exists($file));
\unlink($file);
}
public function testEachInstanceHasOwnPrefix(): void
{
// Each instance should have its own prefix
$cache1 = new \Com\Tecnick\File\Cache('pfx1');
$cache2 = new \Com\Tecnick\File\Cache('pfx2');
$prefix1 = $cache1->getFilePrefix();
$prefix2 = $cache2->getFilePrefix();
// Prefixes should be different since each instance is independent
$this->assertNotSame($prefix1, $prefix2);
$this->assertStringContainsString('pfx1', $prefix1);
$this->assertStringContainsString('pfx2', $prefix2);
}
public function testEachInstanceHasOwnCachePath(): void
{
$cache1 = new \Com\Tecnick\File\Cache();
$path1 = $cache1->getCachePath();
$tempdir = \sys_get_temp_dir() . '/cache_test_' . \uniqid();
\mkdir($tempdir);
$cache2 = new \Com\Tecnick\File\Cache();
$cache2->setCachePath($tempdir);
$path2 = $cache2->getCachePath();
// Paths should be different for each instance
$this->assertNotSame($path1, $path2);
$this->assertStringContainsString('cache_test_', $path2);
\rmdir($tempdir);
}
// -------------------------------------------------------------------------
// Issue 4: glob-injection sanitisation in delete()
// -------------------------------------------------------------------------
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testDeleteGlobCharsInTypeSanitised(): void
{
$cache = new \Com\Tecnick\File\Cache('safepfx');
// Create a real file we do NOT want deleted.
$real = $cache->getNewFileName('safe', '1');
\file_put_contents($real, '');
$this->assertTrue(\file_exists($real));
// Call delete() with glob metacharacters in $type — must not expand.
$cache->delete('*', null);
// The real file must still exist because '*' was stripped to '' and
// the resulting pattern matched nothing (or only unrelated files).
// If glob injection were possible every file would be gone.
$this->assertTrue(\file_exists($real), 'Glob injection via $type must not delete unrelated files');
\unlink($real);
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testDeleteGlobCharsInKeySanitised(): void
{
$cache = new \Com\Tecnick\File\Cache('safepfx2');
$real = $cache->getNewFileName('mytype', 'goodkey');
\file_put_contents($real, '');
$this->assertTrue(\file_exists($real));
// Inject glob metacharacter in $key — must be stripped.
$cache->delete('mytype', '?');
$this->assertTrue(\file_exists($real), 'Glob injection via $key must not delete unrelated files');
\unlink($real);
}
// -------------------------------------------------------------------------
// Issue 6: per-instance cache properties
// -------------------------------------------------------------------------
// Testing covered by testEachInstanceHasOwnPrefix() and testEachInstanceHasOwnCachePath()
// -------------------------------------------------------------------------
// Issue 9: createNewFileName()
// -------------------------------------------------------------------------
// Testing the exception path of getNewFileName() requires tempnam() to return false,
// which is not deterministic in this environment because tempnam() can fall back
// to the system temporary directory.
// -------------------------------------------------------------------------
// Issue 10: deleteOlderThan()
// -------------------------------------------------------------------------
public function testDeleteOlderThanNoFiles(): void
{
// Call deleteOlderThan() when no files exist for this cache prefix.
// glob() returns [] so the early-return on line 171 is exercised.
$cache = new \Com\Tecnick\File\Cache('emptyprefix_' . \uniqid('', true));
$cache->deleteOlderThan(3600);
// No exception thrown is the expected outcome.
$this->expectNotToPerformAssertions();
}
/**
* @throws \Com\Tecnick\File\Exception
*/
public function testDeleteOlderThan(): void
{
$cache = new \Com\Tecnick\File\Cache('ttl');
$old = $cache->getNewFileName('aged', '1');
$fresh = $cache->getNewFileName('aged', '2');
\file_put_contents($old, '');
\file_put_contents($fresh, '');
// Back-date the "old" file to 2 hours ago.
\touch($old, \time() - 7200);
// Delete files older than 1 hour.
$cache->deleteOlderThan(3600);
$this->assertFalse(\file_exists($old), 'Expired file must be deleted');
$this->assertTrue(\file_exists($fresh), 'Fresh file must be kept');
\unlink($fresh);
}
/**
* When the host application defines K_PATH_CACHE without a trailing separator,
* the fallback path must still be normalized so generated files land inside the
* cache directory instead of escaping into its parent (e.g. ".../cache" + name
* yielding ".../cache_<name>"). Runs in a separate process because K_PATH_CACHE
* is a constant that cannot be (re)defined once the cache class has set it.
*
* @throws \Com\Tecnick\File\Exception
*/
#[RunInSeparateProcess]
#[PreserveGlobalState(false)]
public function testGetNewFileNameRespectsCachePathWithoutTrailingSeparator(): void
{
$dir = \sys_get_temp_dir() . \DIRECTORY_SEPARATOR . 'tcfilecache_' . \uniqid('', true);
$this->assertTrue(\mkdir($dir, 0o700));
// Define K_PATH_CACHE without a trailing separator, before the cache class
// has a chance to define it from sys_get_temp_dir().
\define('K_PATH_CACHE', $dir);
try {
$cache = new \Com\Tecnick\File\Cache('trailsep');
// The reported cache path must end with the platform separator.
$cachePath = $cache->getCachePath();
$this->assertSame(\DIRECTORY_SEPARATOR, \substr($cachePath, -1));
$file = $cache->getNewFileName('tmp', '0');
try {
// The generated file must live inside the cache directory.
$this->assertTrue(\str_starts_with($file, $cachePath));
$this->assertSame(\realpath($dir), \realpath(\dirname($file)));
$this->assertTrue(\is_file($file));
} finally {
if (\is_file($file)) {
\unlink($file);
}
}
} finally {
\rmdir($dir);
}
}
}