| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Enrichment;
- use App\Infrastructure\Enrichment\Downloaders\DbipDownloader;
- use App\Infrastructure\Enrichment\Downloaders\DownloaderException;
- use PHPUnit\Framework\TestCase;
- /**
- * SEC_REVIEW F49 — `DbipDownloader::gunzipWithCap` streams via
- * `gzopen`/`gzread` and aborts past a configurable cap so a
- * compromised DB-IP endpoint serving a tiny gzip whose decompressed
- * form is multi-GB cannot OOM the api or fill the disk.
- */
- final class DbipDownloaderTest extends TestCase
- {
- private string $tmpDir = '';
- protected function setUp(): void
- {
- $this->tmpDir = sys_get_temp_dir() . '/irdb-dbip-' . bin2hex(random_bytes(6));
- mkdir($this->tmpDir);
- }
- protected function tearDown(): void
- {
- if ($this->tmpDir !== '' && is_dir($this->tmpDir)) {
- foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
- @unlink($f);
- }
- @rmdir($this->tmpDir);
- }
- }
- public function testNormalGunzipPasses(): void
- {
- $gzPath = $this->writeGzip('hello world');
- $outPath = $this->tmpDir . '/out.bin';
- DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
- self::assertFileExists($outPath);
- self::assertSame('hello world', file_get_contents($outPath));
- // gz file is removed on success.
- self::assertFileDoesNotExist($gzPath);
- }
- public function testOutputOverCapIsRejectedAndCleanedUp(): void
- {
- // 4 KiB of plaintext, cap at 1 KiB.
- $gzPath = $this->writeGzip(str_repeat('A', 4096));
- $outPath = $this->tmpDir . '/out.bin';
- $threw = false;
- try {
- DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
- } catch (DownloaderException $e) {
- $threw = true;
- self::assertStringContainsString('exceeds cap 1024 bytes', $e->getMessage());
- }
- self::assertTrue($threw, 'expected DownloaderException for over-cap output');
- // Partial output must NOT be left on disk — caller treats
- // existence of `$outPath` as "verified ready to swap".
- self::assertFileDoesNotExist($outPath);
- // gz input was NOT cleaned up (caller decides whether to retry).
- self::assertFileExists($gzPath);
- }
- public function testEmptyGzipIsRejected(): void
- {
- // gzip of an empty string produces a valid gzip with 0 bytes
- // payload — the verifier downstream needs SOMETHING; an empty
- // file is suspicious enough to bail on.
- $gzPath = $this->writeGzip('');
- $outPath = $this->tmpDir . '/out.bin';
- $threw = false;
- try {
- DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
- } catch (DownloaderException $e) {
- $threw = true;
- self::assertStringContainsString('empty output', $e->getMessage());
- }
- self::assertTrue($threw);
- self::assertFileDoesNotExist($outPath);
- }
- public function testMissingInputIsRejected(): void
- {
- $this->expectException(DownloaderException::class);
- DbipDownloader::gunzipWithCap(
- $this->tmpDir . '/does-not-exist.gz',
- $this->tmpDir . '/out.bin',
- maxBytes: 1024,
- );
- }
- /**
- * Streams >chunk-size of plaintext through the gunzip helper to
- * prove the chunked read loop accumulates correctly.
- */
- public function testLargeInputStreamsCorrectly(): void
- {
- // 256 KiB of distinguishable bytes (4 × 64 KiB chunks).
- $payload = str_repeat('abcd', 256 * 1024 / 4);
- self::assertSame(256 * 1024, strlen($payload));
- $gzPath = $this->writeGzip($payload);
- $outPath = $this->tmpDir . '/big.bin';
- DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 512 * 1024);
- self::assertFileExists($outPath);
- self::assertSame($payload, file_get_contents($outPath));
- }
- private function writeGzip(string $plain): string
- {
- $path = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.gz';
- $compressed = gzencode($plain, 6);
- self::assertNotFalse($compressed);
- file_put_contents($path, $compressed);
- return $path;
- }
- }
|