|
|
@@ -0,0 +1,110 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Tests\Unit\App;
|
|
|
+
|
|
|
+use App\App\Config;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+
|
|
|
+/**
|
|
|
+ * SEC_REVIEW F35 — `INTERNAL_JOB_TOKEN` startup validation.
|
|
|
+ */
|
|
|
+final class ConfigTest extends TestCase
|
|
|
+{
|
|
|
+ public function testEmptyTokenIsRejected(): void
|
|
|
+ {
|
|
|
+ $errors = Config::collectErrors(['internal_job_token' => '']);
|
|
|
+ self::assertNotEmpty($errors);
|
|
|
+ self::assertStringContainsString('INTERNAL_JOB_TOKEN', $errors[0]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testMissingKeyIsRejected(): void
|
|
|
+ {
|
|
|
+ $errors = Config::collectErrors([]);
|
|
|
+ self::assertNotEmpty($errors);
|
|
|
+ self::assertStringContainsString('INTERNAL_JOB_TOKEN', $errors[0]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testShortHexIsRejected(): void
|
|
|
+ {
|
|
|
+ $errors = Config::collectErrors(['internal_job_token' => str_repeat('a', 31)]);
|
|
|
+ self::assertNotEmpty($errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testNonHexIsRejected(): void
|
|
|
+ {
|
|
|
+ // 32+ chars but contains non-hex — caught by the charset rule.
|
|
|
+ $errors = Config::collectErrors([
|
|
|
+ 'internal_job_token' => 'this-token-is-32-plus-chars-but-not-hex-at-all',
|
|
|
+ ]);
|
|
|
+ self::assertNotEmpty($errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testWeakLiteralFooIsRejected(): void
|
|
|
+ {
|
|
|
+ // The exact case the SEC_REVIEW called out.
|
|
|
+ $errors = Config::collectErrors(['internal_job_token' => 'foo']);
|
|
|
+ self::assertNotEmpty($errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testThirtyTwoHexCharsAccepted(): void
|
|
|
+ {
|
|
|
+ $errors = Config::collectErrors([
|
|
|
+ 'internal_job_token' => str_repeat('a', 32),
|
|
|
+ ]);
|
|
|
+ self::assertSame([], $errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testSixtyFourHexCharsAcceptedDocumentedForm(): void
|
|
|
+ {
|
|
|
+ // openssl rand -hex 32 → 64 hex chars; the documented form.
|
|
|
+ $errors = Config::collectErrors([
|
|
|
+ 'internal_job_token' => bin2hex(random_bytes(32)),
|
|
|
+ ]);
|
|
|
+ self::assertSame([], $errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testUppercaseHexAccepted(): void
|
|
|
+ {
|
|
|
+ $errors = Config::collectErrors([
|
|
|
+ 'internal_job_token' => str_repeat('A', 32),
|
|
|
+ ]);
|
|
|
+ self::assertSame([], $errors);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testValidateOrExitWritesToStderrAndExits(): void
|
|
|
+ {
|
|
|
+ // Run the failing branch in a subprocess so we can observe both
|
|
|
+ // the non-zero exit and the STDERR text without taking down PHPUnit.
|
|
|
+ $script = <<<'PHP'
|
|
|
+<?php
|
|
|
+declare(strict_types=1);
|
|
|
+require %s;
|
|
|
+App\App\Config::validateOrExit(['internal_job_token' => 'too-short']);
|
|
|
+PHP;
|
|
|
+ $autoload = var_export(__DIR__ . '/../../../vendor/autoload.php', true);
|
|
|
+ $script = sprintf($script, $autoload);
|
|
|
+
|
|
|
+ $tmp = tempnam(sys_get_temp_dir(), 'cfg');
|
|
|
+ self::assertIsString($tmp);
|
|
|
+ file_put_contents($tmp, $script);
|
|
|
+
|
|
|
+ $stderr = '';
|
|
|
+ $process = proc_open(
|
|
|
+ [PHP_BINARY, $tmp],
|
|
|
+ [1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
|
|
+ $pipes,
|
|
|
+ );
|
|
|
+ self::assertIsResource($process);
|
|
|
+ fclose($pipes[1]);
|
|
|
+ $stderr = (string) stream_get_contents($pipes[2]);
|
|
|
+ fclose($pipes[2]);
|
|
|
+ $exitCode = proc_close($process);
|
|
|
+
|
|
|
+ @unlink($tmp);
|
|
|
+
|
|
|
+ self::assertSame(1, $exitCode);
|
|
|
+ self::assertStringContainsString('INTERNAL_JOB_TOKEN', $stderr);
|
|
|
+ }
|
|
|
+}
|