LocalAdminTest.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <?php
  2. /*
  3. * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
  4. * SPDX-License-Identifier: Apache-2.0
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * See the LICENSE file in the project root for the full license text.
  9. */
  10. declare(strict_types=1);
  11. namespace App\Tests\Auth;
  12. use App\Auth\LocalAdmin;
  13. use App\Tests\TestCase;
  14. /**
  15. * Lock-in tests for the hash-only LocalAdmin path (R01-N01). The fallback
  16. * accepts ONLY a password_hash()-produced string in LOCAL_ADMIN_PASSWORD_HASH;
  17. * the legacy plaintext LOCAL_ADMIN_PASSWORD env var is no longer consulted.
  18. */
  19. final class LocalAdminTest extends TestCase
  20. {
  21. /** @var array<string, string|false> */
  22. private array $envBackup = [];
  23. /** @var string[] */
  24. private array $envKeys = [
  25. 'LOCAL_ADMIN_EMAIL',
  26. 'LOCAL_ADMIN_PASSWORD_HASH',
  27. 'LOCAL_ADMIN_PASSWORD',
  28. 'LOCAL_ADMIN_NAME',
  29. ];
  30. protected function setUp(): void
  31. {
  32. parent::setUp();
  33. foreach ($this->envKeys as $k) {
  34. $this->envBackup[$k] = getenv($k);
  35. putenv($k); // unset
  36. }
  37. }
  38. protected function tearDown(): void
  39. {
  40. foreach ($this->envKeys as $k) {
  41. $prev = $this->envBackup[$k] ?? false;
  42. if ($prev === false) {
  43. putenv($k);
  44. } else {
  45. putenv("{$k}={$prev}");
  46. }
  47. }
  48. parent::tearDown();
  49. }
  50. public function testDisabledWhenEmailOrHashMissing(): void
  51. {
  52. self::assertFalse(LocalAdmin::isEnabled(), 'no env at all');
  53. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  54. self::assertFalse(LocalAdmin::isEnabled(), 'email only');
  55. putenv('LOCAL_ADMIN_EMAIL');
  56. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT));
  57. self::assertFalse(LocalAdmin::isEnabled(), 'hash only');
  58. }
  59. public function testEnabledWhenBothSet(): void
  60. {
  61. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  62. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT));
  63. self::assertTrue(LocalAdmin::isEnabled());
  64. }
  65. public function testVerifyAcceptsCorrectPasswordAgainstStoredHash(): void
  66. {
  67. $hash = password_hash('correct horse battery staple', PASSWORD_DEFAULT);
  68. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  69. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . $hash);
  70. self::assertTrue(LocalAdmin::verify('admin@example.com', 'correct horse battery staple'));
  71. }
  72. public function testVerifyRejectsWrongPassword(): void
  73. {
  74. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  75. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT));
  76. self::assertFalse(LocalAdmin::verify('admin@example.com', 'hunter1'));
  77. }
  78. public function testVerifyRejectsWrongEmail(): void
  79. {
  80. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  81. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('hunter2', PASSWORD_DEFAULT));
  82. self::assertFalse(LocalAdmin::verify('attacker@example.com', 'hunter2'));
  83. }
  84. public function testVerifyRejectsLegacyPlaintextEnvVar(): void
  85. {
  86. // Prior versions read LOCAL_ADMIN_PASSWORD verbatim. After R01-N01 the
  87. // hash-only contract means a plaintext env var alone must NOT enable
  88. // the fallback or authenticate anyone. (Belt-and-braces: if an
  89. // operator forgot to migrate, /auth/local should 404, not silently
  90. // accept the old value.)
  91. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  92. putenv('LOCAL_ADMIN_PASSWORD=hunter2');
  93. self::assertFalse(LocalAdmin::isEnabled());
  94. self::assertFalse(LocalAdmin::verify('admin@example.com', 'hunter2'));
  95. }
  96. public function testVerifyReturnsFalseWhenDisabledRegardlessOfInput(): void
  97. {
  98. self::assertFalse(LocalAdmin::verify('admin@example.com', 'anything'));
  99. }
  100. public function testEmailIsTrimmedOnComparison(): void
  101. {
  102. putenv('LOCAL_ADMIN_EMAIL=admin@example.com');
  103. putenv('LOCAL_ADMIN_PASSWORD_HASH=' . password_hash('pw', PASSWORD_DEFAULT));
  104. self::assertTrue(LocalAdmin::verify(' admin@example.com ', 'pw'));
  105. }
  106. public function testDisplayNameDefaultsAndOverride(): void
  107. {
  108. self::assertSame('Local Admin', LocalAdmin::displayName());
  109. putenv('LOCAL_ADMIN_NAME=Site Operator');
  110. self::assertSame('Site Operator', LocalAdmin::displayName());
  111. }
  112. }