|
|
@@ -0,0 +1,126 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Tests\Auth;
|
|
|
+
|
|
|
+use App\Auth\LocalAdmin;
|
|
|
+use App\Tests\TestCase;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Lock-in tests for the hash-only LocalAdmin path (R01-N01). The fallback
|
|
|
+ * accepts ONLY a password_hash()-produced string in LOCAL_ADMIN_PASSWORD_HASH;
|
|
|
+ * the legacy plaintext LOCAL_ADMIN_PASSWORD env var is no longer consulted.
|
|
|
+ */
|
|
|
+final class LocalAdminTest extends TestCase
|
|
|
+{
|
|
|
+ /** @var array<string, string|false> */
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+}
|