_<32 base32 chars>`. * * The "1000 distinct tokens" check is a probabilistic floor — at 160 bits, * the collision probability for 1000 samples is negligible (~1e-43), so * any failure here means something is very wrong upstream of `random_bytes`. */ final class TokenEntropyTest extends TestCase { private const FORMAT = '/^irdb_(rep|con|adm|svc)_[A-Z2-7]{32}$/'; public function testThousandTokensAllDistinct(): void { $issuer = new TokenIssuer(); $set = []; for ($i = 0; $i < 1000; ++$i) { $set[$issuer->issue(TokenKind::Admin)] = true; } self::assertCount(1000, $set, 'expected 1000 distinct tokens; collision detected'); } public function testEveryTokenMatchesPublishedFormat(): void { $issuer = new TokenIssuer(); foreach (TokenKind::cases() as $kind) { for ($i = 0; $i < 50; ++$i) { $raw = $issuer->issue($kind); self::assertSame( 1, preg_match(self::FORMAT, $raw), "format mismatch for {$kind->value}: {$raw}" ); self::assertStringStartsWith('irdb_' . $kind->code() . '_', $raw); } } } public function testIssuerSourcesEntropyFromRandomBytes(): void { // Static inspection: confirm `random_bytes` is the single source. // If a refactor swaps in `mt_rand`, `rand`, `microtime`, or anything // else not listed in PHP's CSPRNG API, this test fails loudly. $reflection = new ReflectionClass(TokenIssuer::class); $file = (string) $reflection->getFileName(); self::assertNotEmpty($file); $source = (string) file_get_contents($file); self::assertStringContainsString('random_bytes(', $source, 'TokenIssuer must source entropy from random_bytes (CSPRNG)'); $forbidden = ['mt_rand(', 'rand(', 'uniqid(', 'microtime(', 'srand(']; foreach ($forbidden as $needle) { self::assertStringNotContainsString($needle, $source, "TokenIssuer must not use non-CSPRNG source: {$needle}"); } } public function testRandomByteCountIsTwentyForOneSixtyBits(): void { // Re-derive 160 bits / 8 = 20 bytes and confirm the issuer asks // for that many. Catches future "let's use 16 bytes" regressions. $reflection = new ReflectionClass(TokenIssuer::class); $source = (string) file_get_contents((string) $reflection->getFileName()); self::assertSame( 1, preg_match('/random_bytes\(\s*20\s*\)/', $source), 'TokenIssuer must request 20 bytes (160 bits) of entropy' ); } }