issue($kind); self::assertSame(1, preg_match('/^irdb_(rep|con|adm|svc)_[A-Z2-7]{32}$/', $raw), "format mismatch for {$kind->value}: {$raw}"); self::assertStringStartsWith('irdb_' . $kind->code() . '_', $raw); } } public function testIssuedTokensAreUnique(): void { $issuer = new TokenIssuer(); $set = []; for ($i = 0; $i < 50; ++$i) { $set[$issuer->issue(TokenKind::Admin)] = true; } self::assertCount(50, $set); } public function testIssuedTokenRoundTripsThroughParse(): void { $issuer = new TokenIssuer(); foreach (TokenKind::cases() as $kind) { $raw = $issuer->issue($kind); $parsed = Token::parse($raw); self::assertNotNull($parsed, "parse failed for {$raw}"); self::assertSame($kind, $parsed->kind); self::assertSame($raw, $parsed->raw); } } public function testIssuedBodyAlwaysExactlyThirtyTwoBase32Chars(): void { // SEC_REVIEW F39: 20 bytes (160 bits) divides exactly by 5 → 32 // base32 chars with zero trailing-bit ambiguity. The previous dead // `str_pad` branch in `base32Encode` (which prompted the F39 // finding) is gone, but the property it implied — every char // carries 5 useful bits — must hold across many random samples. $issuer = new TokenIssuer(); for ($i = 0; $i < 100; ++$i) { $raw = $issuer->issue(TokenKind::Admin); $body = substr($raw, strlen('irdb_adm_')); self::assertSame(32, strlen($body), "issued body must be 32 chars: {$raw}"); self::assertSame(1, preg_match('/^[A-Z2-7]{32}$/', $body), "issued body must use canonical base32: {$raw}"); } } }