start = now()->getTimestamp(); $app = new Application(dirname(__DIR__, 2)); $app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']])); Facade::setFacadeApplication($app); $db = new Manager($app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db', $db->getDatabaseManager()); $app->instance('translator', new Translator(new ArrayLoader(), 'zh')); $app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class { public function json($data, $status = 200, $headers = [], $options = 0) { return new JsonResponse($data, $status, $headers, $options); } }); if (!function_exists('create_token') || !function_exists('lang')) require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php'; $db->schema()->create('users', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('member_id'); $t->integer('is_banned')->default(0); $t->bigInteger('delete_time')->nullable(); }); $db->schema()->create('user_session', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('token'); $t->bigInteger('expire_time'); $t->bigInteger('create_time'); $t->bigInteger('update_time')->nullable(); }); DB::table('users')->insert(['user_id' => '100001', 'member_id' => '100001']); } protected function tearDown(): void { Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); } private function request(?string $token): JsonResponse { $request = Request::create('/api/wallet/test', 'GET'); if ($token !== null) $request->headers->set('Authorization', $token); return (new CheckToken())->handle($request, static fn($r) => new JsonResponse(['code' => 0, 'user_id' => $r->user->user_id])); } private function clock(int $time): void { Carbon::setTestNow(Carbon::createFromTimestamp($time, 'Asia/Shanghai')); } public function test_login_and_authenticated_activity_use_a_full_sliding_day(): void { $token = UserSession::setToken('100001'); $session = DB::table('user_session')->where('token', $token)->first(); $this->assertSame($this->start + 86400, (int)$session->expire_time); $this->assertSame($this->start, (int)$session->update_time); $this->clock($this->start + 3600); $this->assertSame(0, $this->request($token)->getData(true)['code']); $this->assertSame($this->start + 3600 + 86400, (int)DB::table('user_session')->value('expire_time')); $lastActivity = $this->start + 25 * 3600 - 1; $this->clock($lastActivity); $this->assertSame(0, $this->request($token)->getData(true)['code']); $this->assertSame($lastActivity + 86400, (int)DB::table('user_session')->value('expire_time')); $this->clock($lastActivity + 86400); $before = (array)DB::table('user_session')->first(); $this->assertSame(401, $this->request($token)->getStatusCode()); $this->assertSame($before, (array)DB::table('user_session')->first()); $this->assertNull(UserSession::renewActiveToken($token, now()->getTimestamp())); $this->assertSame($before, (array)DB::table('user_session')->first()); } public function test_legacy_long_expiry_cannot_bypass_idle_limit_or_be_renewed(): void { DB::table('user_session')->insert(['user_id' => '100001', 'token' => 'legacy', 'expire_time' => $this->start + 7 * 86400, 'create_time' => $this->start, 'update_time' => $this->start]); $this->clock($this->start + 86400); $before = (array)DB::table('user_session')->first(); $this->assertSame(401, $this->request('legacy')->getData(true)['code']); $this->assertNull(UserSession::renewActiveToken('legacy', now()->getTimestamp())); $this->assertSame($before, (array)DB::table('user_session')->first()); $this->assertSame(401, $this->request(null)->getStatusCode()); $this->assertNull(UserSession::renewActiveToken('', now()->getTimestamp())); } public function test_replaced_revoked_missing_or_orphaned_sessions_cannot_be_reactivated(): void { $token = UserSession::setToken('100001'); DB::table('user_session')->where('token', $token)->update(['token' => 'replacement']); $this->assertSame(401, $this->request($token)->getStatusCode()); $this->assertNull(UserSession::renewActiveToken($token, $this->start)); $this->assertNull(UserSession::renewActiveToken('missing', $this->start)); DB::table('user_session')->update(['expire_time' => $this->start]); $before = (array)DB::table('user_session')->first(); $this->assertSame(401, $this->request('replacement')->getStatusCode()); $this->assertSame($before, (array)DB::table('user_session')->first()); DB::table('user_session')->update(['expire_time' => $this->start + 86400]); DB::table('users')->update(['is_banned' => 1]); $before = (array)DB::table('user_session')->first(); $this->assertSame(401, $this->request('replacement')->getStatusCode()); $this->assertSame($before, (array)DB::table('user_session')->first()); DB::table('users')->update(['is_banned' => 0, 'delete_time' => $this->start]); $this->assertSame(401, $this->request('replacement')->getStatusCode()); $this->assertSame($before, (array)DB::table('user_session')->first()); DB::table('users')->delete(); $before = (array)DB::table('user_session')->first(); $this->assertSame(401, $this->request('replacement')->getStatusCode()); $this->assertSame($before, (array)DB::table('user_session')->first()); } public function test_missing_activity_falls_back_to_creation_and_concurrent_renewals_are_monotonic(): void { foreach ([null, 0] as $index => $activity) { $token = 'fallback-' . $index; DB::table('user_session')->insert(['user_id' => '100001', 'token' => $token, 'expire_time' => $this->start + 7 * 86400, 'create_time' => $this->start - 3600, 'update_time' => $activity]); $session = UserSession::where('token', $token)->first(); $this->assertSame($this->start + 82800, UserSession::validUntil($session->getAttributes())); $this->assertSame(0, $this->request($token)->getData(true)['code']); $this->assertSame($this->start + 86400, (int)DB::table('user_session')->where('token', $token)->value('expire_time')); } DB::table('user_session')->where('token', 'fallback-0')->update(['update_time' => $this->start + 10, 'expire_time' => $this->start + 10 + 86400]); $before = (array)DB::table('user_session')->where('token', 'fallback-0')->first(); $this->assertNotNull(UserSession::renewActiveToken('fallback-0', $this->start)); $this->assertSame($before, (array)DB::table('user_session')->where('token', 'fallback-0')->first()); } public function test_renewal_stays_bound_to_the_original_session_identity(): void { $token = UserSession::setToken('100001'); $id = (int)DB::table('user_session')->value('id'); DB::table('user_session')->where('id', $id)->update(['token' => 'replacement']); DB::table('user_session')->insert(['user_id' => 'other-user', 'token' => $token, 'expire_time' => $this->start + 86400, 'create_time' => $this->start, 'update_time' => $this->start]); $before = DB::table('user_session')->orderBy('id')->get()->toJson(); $this->assertNull(UserSession::renewActiveToken($token, $this->start + 1, $id)); $this->assertSame($before, DB::table('user_session')->orderBy('id')->get()->toJson()); } }