*/ private array $envBackup = []; /** @var string[] */ private array $envKeys = [ 'LOCAL_ADMIN_EMAIL', 'LOCAL_ADMIN_PASSWORD_HASH', 'LOCAL_ADMIN_PASSWORD', 'LOCAL_ADMIN_NAME', ]; protected function setUp(): void { parent::setUp(); foreach ($this->envKeys as $k) { $this->envBackup[$k] = getenv($k); putenv($k); // unset } } protected function tearDown(): void { foreach ($this->envKeys as $k) { $prev = $this->envBackup[$k] ?? false; if ($prev === false) { putenv($k); } else { putenv("{$k}={$prev}"); } } parent::tearDown(); } public function testDisabledWhenEmailOrHashMissing(): void { self::assertFalse(LocalAdmin::isEnabled(), 'no env at all'); putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); self::assertFalse(LocalAdmin::isEnabled(), 'email only'); putenv('LOCAL_ADMIN_EMAIL'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT)); self::assertFalse(LocalAdmin::isEnabled(), 'hash only'); } public function testEnabledWhenBothSet(): void { putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT)); self::assertTrue(LocalAdmin::isEnabled()); } public function testVerifyAcceptsCorrectPasswordAgainstStoredHash(): void { $hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT); putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . $hash); self::assertTrue(LocalAdmin::verify('admin@example.com', 'correct horse battery staple')); } public function testVerifyRejectsWrongPassword(): void { putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT)); self::assertFalse(LocalAdmin::verify('admin@example.com', 'hunter1')); } public function testVerifyRejectsWrongEmail(): void { putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT)); self::assertFalse(LocalAdmin::verify('attacker@example.com', 'hunter2')); } public function testVerifyRejectsLegacyPlaintextEnvVar(): void { // Prior versions read LOCAL_ADMIN_PASSWORD verbatim. After R01-N01 the // hash-only contract means a plaintext env var alone must NOT enable // the fallback or authenticate anyone. (Belt-and-braces: if an // operator forgot to migrate, /auth/local should 404, not silently // accept the old value.) putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD=hunter2'); self::assertFalse(LocalAdmin::isEnabled()); self::assertFalse(LocalAdmin::verify('admin@example.com', 'hunter2')); } public function testVerifyReturnsFalseWhenDisabledRegardlessOfInput(): void { self::assertFalse(LocalAdmin::verify('admin@example.com', 'anything')); } public function testEmailIsTrimmedOnComparison(): void { putenv('LOCAL_ADMIN_EMAIL=admin@example.com'); putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('pw', PASSWORD_DEFAULT)); self::assertTrue(LocalAdmin::verify(' admin@example.com ', 'pw')); } public function testDisplayNameDefaultsAndOverride(): void { self::assertSame('Local Admin', LocalAdmin::displayName()); putenv('LOCAL_ADMIN_NAME=Site Operator'); self::assertSame('Site Operator', LocalAdmin::displayName()); } }