| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Auth;
- use App\Domain\Auth\TokenIssuer;
- use App\Domain\Auth\TokenKind;
- use PHPUnit\Framework\TestCase;
- use ReflectionClass;
- /**
- * SPEC §M14.3: prove tokens carry the entropy the SPEC promises (160 bits
- * from a CSPRNG), and that the wire format is exactly
- * `irdb_<kind3>_<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'
- );
- }
- }
|