UserPasswordValidationTest.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\User as UserController;
  4. use Illuminate\Cache\ArrayStore;
  5. use Illuminate\Cache\Repository as CacheRepository;
  6. use Illuminate\Config\Repository;
  7. use Illuminate\Database\Capsule\Manager;
  8. use Illuminate\Database\Eloquent\Model;
  9. use Illuminate\Database\Schema\Blueprint;
  10. use Illuminate\Foundation\Application;
  11. use Illuminate\Http\Request;
  12. use Illuminate\Support\Facades\Cache;
  13. use Illuminate\Support\Facades\DB;
  14. use Illuminate\Support\Facades\Facade;
  15. use Illuminate\Translation\ArrayLoader;
  16. use Illuminate\Translation\Translator;
  17. use Illuminate\Validation\Factory;
  18. use PHPUnit\Framework\Attributes\DataProvider;
  19. use PHPUnit\Framework\TestCase;
  20. class UserPasswordValidationTest extends TestCase
  21. {
  22. private Application $app;
  23. protected function setUp(): void
  24. {
  25. Facade::clearResolvedInstances();
  26. Model::clearBootedModels();
  27. require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
  28. $this->app = new Application(dirname(__DIR__, 2));
  29. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
  30. $this->app->instance('cache', new CacheRepository(new ArrayStore()));
  31. $translator = new Translator(new ArrayLoader(), 'zh');
  32. $this->app->instance('translator', $translator);
  33. $this->app->instance('validator', new Factory($translator, $this->app));
  34. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  35. Request::macro('validate', function (array $rules) {
  36. return app('validator')->make($this->all(), $rules)->validate();
  37. });
  38. Facade::setFacadeApplication($this->app);
  39. $db = new Manager($this->app);
  40. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  41. $db->setAsGlobal();
  42. $db->bootEloquent();
  43. $this->app->instance('db', $db->getDatabaseManager());
  44. $db->schema()->create('users', function (Blueprint $t) {
  45. $t->id(); $t->string('member_id'); $t->string('password'); $t->string('payment_password');
  46. $t->string('language')->default('zh'); $t->integer('recharge_channel_group_id')->default(1); $t->timestamps();
  47. });
  48. $db->schema()->create('user_session', function (Blueprint $t) {
  49. $t->id(); $t->string('user_id'); $t->string('token'); $t->integer('expire_time');
  50. });
  51. DB::table('users')->insert([
  52. 'member_id' => '100001', 'password' => create_password('old-login'),
  53. 'payment_password' => password_hash('old-funds', PASSWORD_DEFAULT),
  54. ]);
  55. foreach (['100001' => 'target-token', '100002' => 'other-token'] as $member => $token) {
  56. DB::table('user_session')->insert(['user_id' => (string) $member, 'token' => $token, 'expire_time' => time() + 3600]);
  57. Cache::put('token_user_' . $token, ['member_id' => (string) $member]);
  58. }
  59. }
  60. protected function tearDown(): void
  61. {
  62. DB::disconnect();
  63. Facade::clearResolvedInstances();
  64. Model::clearBootedModels();
  65. parent::tearDown();
  66. }
  67. private function reset(array $params): array
  68. {
  69. $this->app->instance('request', Request::create('/admin/user/setPassword', 'POST', $params + ['member_id' => '100001']));
  70. return (new UserController())->setPassword()->getData(true);
  71. }
  72. private function credentials(): array
  73. {
  74. return (array) DB::table('users')->where('member_id', '100001')->first();
  75. }
  76. public function test_invalid_passwords_are_rejected_before_either_credential_or_session_is_changed(): void
  77. {
  78. $before = $this->credentials();
  79. foreach (['password', 'payment_password'] as $field) {
  80. foreach (['12345', str_repeat('a', 17), str_repeat('密', 17), 123456, array_fill(0, 6, 'a'), false] as $invalid) {
  81. $params = ['password' => 'valid-login', 'payment_password' => 'valid-funds'];
  82. $params[$field] = $invalid;
  83. $body = $this->reset($params);
  84. $this->assertSame(-3, $body['code']);
  85. $this->assertSame($before, $this->credentials());
  86. $this->assertSame(2, DB::table('user_session')->count());
  87. $this->assertTrue(Cache::has('token_user_target-token'));
  88. }
  89. }
  90. }
  91. public static function validPasswords(): array
  92. {
  93. return [
  94. 'minimum' => ['abc123'],
  95. 'maximum' => [str_repeat('a', 16)],
  96. 'leading zeroes' => ['000000'],
  97. 'symbols' => ['abc!@#123'],
  98. 'unicode character limit' => [str_repeat('密', 16)],
  99. ];
  100. }
  101. #[DataProvider('validPasswords')]
  102. public function test_valid_boundaries_preserve_hashing_and_login_session_invalidation(string $password): void
  103. {
  104. $this->assertSame(0, $this->reset(['password' => $password, 'payment_password' => $password])['code']);
  105. $row = $this->credentials();
  106. $this->assertSame(create_password($password), $row['password']);
  107. $this->assertTrue(password_verify($password, $row['payment_password']));
  108. $this->assertSame(0, DB::table('user_session')->where('user_id', '100001')->count());
  109. $this->assertFalse(Cache::has('token_user_target-token'));
  110. $this->assertSame(1, DB::table('user_session')->where('user_id', '100002')->count());
  111. $this->assertTrue(Cache::has('token_user_other-token'));
  112. }
  113. public function test_resetting_only_funds_password_preserves_login_password_and_session(): void
  114. {
  115. $this->assertSame(0, $this->reset(['payment_password' => 'new-funds'])['code']);
  116. $row = $this->credentials();
  117. $this->assertSame(create_password('old-login'), $row['password']);
  118. $this->assertTrue(password_verify('new-funds', $row['payment_password']));
  119. $this->assertSame(2, DB::table('user_session')->count());
  120. $this->assertTrue(Cache::has('token_user_target-token'));
  121. }
  122. public function test_omitted_or_empty_passwords_remain_unchanged(): void
  123. {
  124. $before = $this->credentials();
  125. foreach ([[], ['password' => null, 'payment_password' => null], ['password' => '', 'payment_password' => '']] as $params) {
  126. $this->assertSame(0, $this->reset($params)['code']);
  127. $this->assertSame($before, $this->credentials());
  128. $this->assertSame(2, DB::table('user_session')->count());
  129. $this->assertTrue(Cache::has('token_user_target-token'));
  130. }
  131. }
  132. }