Files
preauth/tests/Unit/Trait/GetTotpTraitTest.php
T
lyra d2eb914637 fix: must-fix items for v1.0 release
Security:
- Add SecurityHeadersListener (X-Content-Type-Options, X-Frame-Options,
  CSP, Referrer-Policy, HSTS)
- Replace document.write() with document.documentElement.innerHTML
  in login JS to avoid CSP violations
- Add CSS escaping (|e('css')) to env color values in _style.html.twig
- Document CSRF protection model: nonce serves as CSRF token for POST
  form path (single-use, server-generated, 120s TTL)
- Reduce TOTP verification window from 10 periods (±5 min) to 1 (±30s)
- Remove hardcoded APP_SECRET from bin/franken.sh (now uses env or
  generates random)
- Remove backup code values from debug log output
- Add .env to .gitignore

Bug fixes:
- Fix ->json access on possibly-null  in LoginListener
  (uses null-safe operator ?->)
- Fix validReturn() not checking false from parse_url (could cause
  TypeError on malformed URLs)
- Add isHit() race condition check in AcceptListener and AllowListener
- Add try/finally in Kernel::terminate() so parent::terminate() always
  runs even if persist() throws
- Add input validation to GenerateBackupCodesCommand (reject count < 1)
- Use Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait
  instead of literal 500

Docker/CI:
- Explicitly install curl in Docker final image (needed for healthcheck)
- Update workflow tag pattern to v*.*.* (standardize on v-prefix)
- Extract version without v-prefix for Docker image tag
- Remove stale develop branch from CI triggers
- Fix publish.yaml git remote add to use set-url on re-runs

Code quality:
- Add declare(strict_types=1) to all interface files
- Add #[AsCommand] attribute to GenerateBackupCodesCommand
- Fix BackupCodeInterface default count to match implementation (10)
- Lowercase host before TLD lookup in DomainManager
- Expand TLD list with many missing multi-part TLDs (.com.au, .co.jp,
  .com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect
  vulnerabilities
- Disable unused Symfony sessions in framework.yaml

Tests:
- Update DomainManagerTest for corrected TLD parsing (.com.au, .co.jp,
  .com.br now correctly recognized as multi-part)
- Update GetTotpTraitTest for corrected error message
- Update GenerateBackupCodesCommandTest: zero count now throws exception
2026-08-11 16:33:00 -04:00

112 lines
3.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Trait;
use App\ConfigBag;
use App\Tests\Support\TotpTestHelper;
use App\Trait\GetTotpTrait;
use OTPHP\TOTPInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\Exception\HttpException;
final class GetTotpTraitTest extends TestCase
{
use TotpTestHelper;
private function makeObject(): object
{
return new class () {
use GetTotpTrait;
public function publicGetTotp(): TOTPInterface
{
return $this->getTotp();
}
};
}
public function testSetConfigSetsProperty(): void
{
$obj = $this->makeObject();
$config = $this->makeConfig();
$obj->setConfig($config);
$reflection = new \ReflectionProperty($obj, 'config');
self::assertSame($config, $reflection->getValue($obj));
}
public function testGetTotpReturnsTotpInterface(): void
{
$obj = $this->makeObject();
$obj->setConfig($this->makeConfig());
$totp = $obj->publicGetTotp();
self::assertInstanceOf(TOTPInterface::class, $totp);
}
public function testGetTotpReturnsValidCode(): void
{
$obj = $this->makeObject();
$obj->setConfig($this->makeConfig());
$totp = $obj->publicGetTotp();
// the code at the frozen time should match our helper
self::assertSame($this->validTotpCode(), $totp->now());
}
public function testGetTotpThrowsOnInvalidUri(): void
{
$obj = $this->makeObject();
$clock = $this->frozenClock();
$utilities = $this->createUtilities($clock);
$config = new ConfigBag(
$utilities,
$clock,
3600,
'not-a-valid-uri',
0,
false,
'Error',
'Teapot',
'Too Many'
);
$obj->setConfig($config);
// Factory::loadFromProvisioningUri throws InvalidProvisioningUriException
// which is not caught by getTotp() since the instanceof check only runs
// after a successful load — so we expect a Throwable here
$this->expectException(\Throwable::class);
$obj->publicGetTotp();
}
public function testGetTotpThrowsHttpExceptionWhenNotTotpType(): void
{
// A HOTP URI loads successfully as an OTPInterface but is NOT a TOTPInterface,
// so the instanceof check in getTotp() should throw an HttpException(500)
$obj = $this->makeObject();
$clock = $this->frozenClock();
$utilities = $this->createUtilities($clock);
$config = new ConfigBag(
$utilities,
$clock,
3600,
'otpauth://hotp/Test-HOTP?secret=JBSWY3DPEHPK3PXP&counter=0',
0,
false,
'Error',
'Teapot',
'Too Many'
);
$obj->setConfig($config);
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Internal Server Error');
$obj->publicGetTotp();
}
}