|
|
@@ -0,0 +1,124 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tests\Integration;
|
|
|
+
|
|
|
+use App\Http\Middleware\JwtAdminMiddleware;
|
|
|
+use App\Models\Admin;
|
|
|
+use App\Services\AdminJwtSession;
|
|
|
+use App\Services\JwtService;
|
|
|
+use Carbon\Carbon;
|
|
|
+use Firebase\JWT\JWT;
|
|
|
+use Illuminate\Cache\ArrayStore;
|
|
|
+use Illuminate\Cache\Repository as CacheRepository;
|
|
|
+use Illuminate\Config\Repository;
|
|
|
+use Illuminate\Database\Capsule\Manager;
|
|
|
+use Illuminate\Database\Schema\Blueprint;
|
|
|
+use Illuminate\Foundation\Application;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Facade;
|
|
|
+use Illuminate\Support\Facades\Schema;
|
|
|
+use Illuminate\Translation\ArrayLoader;
|
|
|
+use Illuminate\Translation\Translator;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+
|
|
|
+class AdminJwtSessionTest extends TestCase
|
|
|
+{
|
|
|
+ private Application $app;
|
|
|
+ private AdminJwtSession $session;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ Carbon::setTestNow(Carbon::createFromTimestamp(time()));
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
+ $this->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);
|
|
|
+ }
|
|
|
+}
|