|
@@ -0,0 +1,144 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace Tests\Integration;
|
|
|
|
|
+
|
|
|
|
|
+use App\Http\Controllers\admin\User as UserController;
|
|
|
|
|
+use Illuminate\Cache\ArrayStore;
|
|
|
|
|
+use Illuminate\Cache\Repository as CacheRepository;
|
|
|
|
|
+use Illuminate\Config\Repository;
|
|
|
|
|
+use Illuminate\Database\Capsule\Manager;
|
|
|
|
|
+use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
+use Illuminate\Database\Schema\Blueprint;
|
|
|
|
|
+use Illuminate\Foundation\Application;
|
|
|
|
|
+use Illuminate\Http\Request;
|
|
|
|
|
+use Illuminate\Support\Facades\Cache;
|
|
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
|
|
+use Illuminate\Support\Facades\Facade;
|
|
|
|
|
+use Illuminate\Translation\ArrayLoader;
|
|
|
|
|
+use Illuminate\Translation\Translator;
|
|
|
|
|
+use Illuminate\Validation\Factory;
|
|
|
|
|
+use PHPUnit\Framework\Attributes\DataProvider;
|
|
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
|
|
+
|
|
|
|
|
+class UserPasswordValidationTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ private Application $app;
|
|
|
|
|
+
|
|
|
|
|
+ protected function setUp(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
|
|
+ Model::clearBootedModels();
|
|
|
|
|
+ require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
|
|
|
|
|
+ $this->app = new Application(dirname(__DIR__, 2));
|
|
|
|
|
+ $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
|
|
|
|
|
+ $this->app->instance('cache', new CacheRepository(new ArrayStore()));
|
|
|
|
|
+ $translator = new Translator(new ArrayLoader(), 'zh');
|
|
|
|
|
+ $this->app->instance('translator', $translator);
|
|
|
|
|
+ $this->app->instance('validator', new Factory($translator, $this->app));
|
|
|
|
|
+ $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
|
|
|
|
|
+ Request::macro('validate', function (array $rules) {
|
|
|
|
|
+ return app('validator')->make($this->all(), $rules)->validate();
|
|
|
|
|
+ });
|
|
|
|
|
+ Facade::setFacadeApplication($this->app);
|
|
|
|
|
+ $db = new Manager($this->app);
|
|
|
|
|
+ $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
|
|
|
|
|
+ $db->setAsGlobal();
|
|
|
|
|
+ $db->bootEloquent();
|
|
|
|
|
+ $this->app->instance('db', $db->getDatabaseManager());
|
|
|
|
|
+ $db->schema()->create('users', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('member_id'); $t->string('password'); $t->string('payment_password');
|
|
|
|
|
+ $t->string('language')->default('zh'); $t->integer('recharge_channel_group_id')->default(1); $t->timestamps();
|
|
|
|
|
+ });
|
|
|
|
|
+ $db->schema()->create('user_session', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('user_id'); $t->string('token'); $t->integer('expire_time');
|
|
|
|
|
+ });
|
|
|
|
|
+ DB::table('users')->insert([
|
|
|
|
|
+ 'member_id' => '100001', 'password' => create_password('old-login'),
|
|
|
|
|
+ 'payment_password' => password_hash('old-funds', PASSWORD_DEFAULT),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ foreach (['100001' => 'target-token', '100002' => 'other-token'] as $member => $token) {
|
|
|
|
|
+ DB::table('user_session')->insert(['user_id' => (string) $member, 'token' => $token, 'expire_time' => time() + 3600]);
|
|
|
|
|
+ Cache::put('token_user_' . $token, ['member_id' => (string) $member]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function tearDown(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ DB::disconnect();
|
|
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
|
|
+ Model::clearBootedModels();
|
|
|
|
|
+ parent::tearDown();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function reset(array $params): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->app->instance('request', Request::create('/admin/user/setPassword', 'POST', $params + ['member_id' => '100001']));
|
|
|
|
|
+ return (new UserController())->setPassword()->getData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function credentials(): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return (array) DB::table('users')->where('member_id', '100001')->first();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_invalid_passwords_are_rejected_before_either_credential_or_session_is_changed(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $before = $this->credentials();
|
|
|
|
|
+ foreach (['password', 'payment_password'] as $field) {
|
|
|
|
|
+ foreach (['12345', str_repeat('a', 17), str_repeat('密', 17), 123456, array_fill(0, 6, 'a'), false] as $invalid) {
|
|
|
|
|
+ $params = ['password' => 'valid-login', 'payment_password' => 'valid-funds'];
|
|
|
|
|
+ $params[$field] = $invalid;
|
|
|
|
|
+ $body = $this->reset($params);
|
|
|
|
|
+ $this->assertSame(-3, $body['code']);
|
|
|
|
|
+ $this->assertSame($before, $this->credentials());
|
|
|
|
|
+ $this->assertSame(2, DB::table('user_session')->count());
|
|
|
|
|
+ $this->assertTrue(Cache::has('token_user_target-token'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function validPasswords(): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return [
|
|
|
|
|
+ 'minimum' => ['abc123'],
|
|
|
|
|
+ 'maximum' => [str_repeat('a', 16)],
|
|
|
|
|
+ 'leading zeroes' => ['000000'],
|
|
|
|
|
+ 'symbols' => ['abc!@#123'],
|
|
|
|
|
+ 'unicode character limit' => [str_repeat('密', 16)],
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[DataProvider('validPasswords')]
|
|
|
|
|
+ public function test_valid_boundaries_preserve_hashing_and_login_session_invalidation(string $password): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->assertSame(0, $this->reset(['password' => $password, 'payment_password' => $password])['code']);
|
|
|
|
|
+ $row = $this->credentials();
|
|
|
|
|
+ $this->assertSame(create_password($password), $row['password']);
|
|
|
|
|
+ $this->assertTrue(password_verify($password, $row['payment_password']));
|
|
|
|
|
+ $this->assertSame(0, DB::table('user_session')->where('user_id', '100001')->count());
|
|
|
|
|
+ $this->assertFalse(Cache::has('token_user_target-token'));
|
|
|
|
|
+ $this->assertSame(1, DB::table('user_session')->where('user_id', '100002')->count());
|
|
|
|
|
+ $this->assertTrue(Cache::has('token_user_other-token'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_resetting_only_funds_password_preserves_login_password_and_session(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->assertSame(0, $this->reset(['payment_password' => 'new-funds'])['code']);
|
|
|
|
|
+ $row = $this->credentials();
|
|
|
|
|
+ $this->assertSame(create_password('old-login'), $row['password']);
|
|
|
|
|
+ $this->assertTrue(password_verify('new-funds', $row['payment_password']));
|
|
|
|
|
+ $this->assertSame(2, DB::table('user_session')->count());
|
|
|
|
|
+ $this->assertTrue(Cache::has('token_user_target-token'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_omitted_or_empty_passwords_remain_unchanged(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $before = $this->credentials();
|
|
|
|
|
+ foreach ([[], ['password' => null, 'payment_password' => null], ['password' => '', 'payment_password' => '']] as $params) {
|
|
|
|
|
+ $this->assertSame(0, $this->reset($params)['code']);
|
|
|
|
|
+ $this->assertSame($before, $this->credentials());
|
|
|
|
|
+ $this->assertSame(2, DB::table('user_session')->count());
|
|
|
|
|
+ $this->assertTrue(Cache::has('token_user_target-token'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|