1
0

DbipDownloaderTest.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Enrichment;
  4. use App\Infrastructure\Enrichment\Downloaders\DbipDownloader;
  5. use App\Infrastructure\Enrichment\Downloaders\DownloaderException;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * SEC_REVIEW F49 — `DbipDownloader::gunzipWithCap` streams via
  9. * `gzopen`/`gzread` and aborts past a configurable cap so a
  10. * compromised DB-IP endpoint serving a tiny gzip whose decompressed
  11. * form is multi-GB cannot OOM the api or fill the disk.
  12. */
  13. final class DbipDownloaderTest extends TestCase
  14. {
  15. private string $tmpDir = '';
  16. protected function setUp(): void
  17. {
  18. $this->tmpDir = sys_get_temp_dir() . '/irdb-dbip-' . bin2hex(random_bytes(6));
  19. mkdir($this->tmpDir);
  20. }
  21. protected function tearDown(): void
  22. {
  23. if ($this->tmpDir !== '' && is_dir($this->tmpDir)) {
  24. foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
  25. @unlink($f);
  26. }
  27. @rmdir($this->tmpDir);
  28. }
  29. }
  30. public function testNormalGunzipPasses(): void
  31. {
  32. $gzPath = $this->writeGzip('hello world');
  33. $outPath = $this->tmpDir . '/out.bin';
  34. DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
  35. self::assertFileExists($outPath);
  36. self::assertSame('hello world', file_get_contents($outPath));
  37. // gz file is removed on success.
  38. self::assertFileDoesNotExist($gzPath);
  39. }
  40. public function testOutputOverCapIsRejectedAndCleanedUp(): void
  41. {
  42. // 4 KiB of plaintext, cap at 1 KiB.
  43. $gzPath = $this->writeGzip(str_repeat('A', 4096));
  44. $outPath = $this->tmpDir . '/out.bin';
  45. $threw = false;
  46. try {
  47. DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
  48. } catch (DownloaderException $e) {
  49. $threw = true;
  50. self::assertStringContainsString('exceeds cap 1024 bytes', $e->getMessage());
  51. }
  52. self::assertTrue($threw, 'expected DownloaderException for over-cap output');
  53. // Partial output must NOT be left on disk — caller treats
  54. // existence of `$outPath` as "verified ready to swap".
  55. self::assertFileDoesNotExist($outPath);
  56. // gz input was NOT cleaned up (caller decides whether to retry).
  57. self::assertFileExists($gzPath);
  58. }
  59. public function testEmptyGzipIsRejected(): void
  60. {
  61. // gzip of an empty string produces a valid gzip with 0 bytes
  62. // payload — the verifier downstream needs SOMETHING; an empty
  63. // file is suspicious enough to bail on.
  64. $gzPath = $this->writeGzip('');
  65. $outPath = $this->tmpDir . '/out.bin';
  66. $threw = false;
  67. try {
  68. DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 1024);
  69. } catch (DownloaderException $e) {
  70. $threw = true;
  71. self::assertStringContainsString('empty output', $e->getMessage());
  72. }
  73. self::assertTrue($threw);
  74. self::assertFileDoesNotExist($outPath);
  75. }
  76. public function testMissingInputIsRejected(): void
  77. {
  78. $this->expectException(DownloaderException::class);
  79. DbipDownloader::gunzipWithCap(
  80. $this->tmpDir . '/does-not-exist.gz',
  81. $this->tmpDir . '/out.bin',
  82. maxBytes: 1024,
  83. );
  84. }
  85. /**
  86. * Streams >chunk-size of plaintext through the gunzip helper to
  87. * prove the chunked read loop accumulates correctly.
  88. */
  89. public function testLargeInputStreamsCorrectly(): void
  90. {
  91. // 256 KiB of distinguishable bytes (4 × 64 KiB chunks).
  92. $payload = str_repeat('abcd', 256 * 1024 / 4);
  93. self::assertSame(256 * 1024, strlen($payload));
  94. $gzPath = $this->writeGzip($payload);
  95. $outPath = $this->tmpDir . '/big.bin';
  96. DbipDownloader::gunzipWithCap($gzPath, $outPath, maxBytes: 512 * 1024);
  97. self::assertFileExists($outPath);
  98. self::assertSame($payload, file_get_contents($outPath));
  99. }
  100. private function writeGzip(string $plain): string
  101. {
  102. $path = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.gz';
  103. $compressed = gzencode($plain, 6);
  104. self::assertNotFalse($compressed);
  105. file_put_contents($path, $compressed);
  106. return $path;
  107. }
  108. }