app = new Application(dirname(__DIR__, 2)); $this->app->instance('config', new Repository(['app' => [ 'locale' => 'zh', 'jwt_secret' => str_repeat('k', 32), 'jwt_exp' => 86400, 'admin_jwt_idle' => 3600, 'admin_jwt_max_lifetime' => 86400 * 30, ]])); Facade::setFacadeApplication($this->app); $this->app->instance('cache', new CacheRepository(new ArrayStore())); $this->app->instance('translator', new Translator(new ArrayLoader(), 'zh')); $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory()); $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager()); $this->app->bind('db.schema', fn () => $db->schema()); Schema::create('admin', function (Blueprint $t) { $t->id(); $t->string('username'); $t->string('password')->default(''); $t->timestamps(); }); DB::table('admin')->insert(['id' => 1, 'username' => 'root']); $this->session = new AdminJwtSession(new JwtService()); } protected function tearDown(): void { Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); } private function request(string $token): array { $request = Request::create('/admin/x', 'GET'); $request->headers->set('Authorization', 'Bearer ' . $token); $passed = false; $response = (new JwtAdminMiddleware($this->session))->handle($request, function () use (&$passed) { $passed = true; return 'ok'; }); return [$passed, $response]; } private function travel(int $seconds): void { Carbon::setTestNow(Carbon::now()->addSeconds($seconds)); } public function test_activity_keeps_the_session_alive_beyond_the_idle_window(): void { $token = $this->session->issue(Admin::find(1)); $payload = $this->session->validate($token); $this->assertSame(86400 * 30, $payload->exp - $payload->iat, 'JWT exp 只作为绝对上限'); // 连续操作 5 小时(空闲窗口只有 1 小时),始终有效 for ($i = 0; $i < 10; $i++) { $this->travel(1800); $this->assertTrue($this->request($token)[0], "第 {$i} 次请求应仍在登录态"); } } public function test_session_expires_after_idle_window_without_requests(): void { $token = $this->session->issue(Admin::find(1)); $this->travel(3599); $this->assertTrue($this->request($token)[0]); $this->travel(3601); [$passed, $response] = $this->request($token); $this->assertFalse($passed, '空闲超过 1 小时应失效'); $this->assertSame(\App\Constants\HttpStatus::AUTHORIZATION_HEADER_NOT_FOUND, $response->getData(true)['code']); } public function test_revoked_and_forged_tokens_are_rejected(): void { $token = $this->session->issue(Admin::find(1)); $this->session->revoke($this->session->validate($token)); $this->assertFalse($this->request($token)[0]); $this->assertFalse($this->request($token . 'x')[0]); } public function test_legacy_tokens_without_sid_follow_their_own_expiry(): void { $legacy = JWT::encode(['iat' => time(), 'exp' => time() + 600, 'sub' => 1, 'user_id' => 1], str_repeat('k', 32), 'HS256'); $this->assertTrue($this->request($legacy)[0], '上线前签发的 token 不受影响'); $this->assertSame(time() + 600, $this->session->expiresAt($this->session->validate($legacy))); } public function test_realtime_expiry_is_stable_within_a_connection_and_renews_after_it(): void { $token = $this->session->issue(Admin::find(1)); $first = $this->session->realtimeExpiresAt($this->session->validate($token)); $this->assertSame(Carbon::now()->getTimestamp() + 3600, $first); // 期间有操作,活跃时间顺延,但同一连接内推送到期时间保持不变(各频道 channel_data 一致) $this->travel(1800); $this->request($token); $this->assertSame($first, $this->session->realtimeExpiresAt($this->session->validate($token))); $this->assertSame(Carbon::now()->getTimestamp() + 3600, $this->session->expiresAt($this->session->validate($token))); // 到点后重连拿到新的到期时间 $this->travel(1801); $renewed = $this->session->realtimeExpiresAt($this->session->validate($token)); $this->assertGreaterThan($first, $renewed); $this->assertLessThanOrEqual($this->session->expiresAt($this->session->validate($token)), $renewed); } }