AdminJwtSessionTest.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Middleware\JwtAdminMiddleware;
  4. use App\Models\Admin;
  5. use App\Services\AdminJwtSession;
  6. use App\Services\JwtService;
  7. use Carbon\Carbon;
  8. use Firebase\JWT\JWT;
  9. use Illuminate\Cache\ArrayStore;
  10. use Illuminate\Cache\Repository as CacheRepository;
  11. use Illuminate\Config\Repository;
  12. use Illuminate\Database\Capsule\Manager;
  13. use Illuminate\Database\Schema\Blueprint;
  14. use Illuminate\Foundation\Application;
  15. use Illuminate\Http\Request;
  16. use Illuminate\Support\Facades\DB;
  17. use Illuminate\Support\Facades\Facade;
  18. use Illuminate\Support\Facades\Schema;
  19. use Illuminate\Translation\ArrayLoader;
  20. use Illuminate\Translation\Translator;
  21. use PHPUnit\Framework\TestCase;
  22. class AdminJwtSessionTest extends TestCase
  23. {
  24. private Application $app;
  25. private AdminJwtSession $session;
  26. protected function setUp(): void
  27. {
  28. Carbon::setTestNow(Carbon::createFromTimestamp(time()));
  29. Facade::clearResolvedInstances();
  30. $this->app = new Application(dirname(__DIR__, 2));
  31. $this->app->instance('config', new Repository(['app' => [
  32. 'locale' => 'zh', 'jwt_secret' => str_repeat('k', 32), 'jwt_exp' => 86400,
  33. 'admin_jwt_idle' => 3600, 'admin_jwt_max_lifetime' => 86400 * 30,
  34. ]]));
  35. Facade::setFacadeApplication($this->app);
  36. $this->app->instance('cache', new CacheRepository(new ArrayStore()));
  37. $this->app->instance('translator', new Translator(new ArrayLoader(), 'zh'));
  38. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  39. $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  40. $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager());
  41. $this->app->bind('db.schema', fn () => $db->schema());
  42. Schema::create('admin', function (Blueprint $t) { $t->id(); $t->string('username'); $t->string('password')->default(''); $t->timestamps(); });
  43. DB::table('admin')->insert(['id' => 1, 'username' => 'root']);
  44. $this->session = new AdminJwtSession(new JwtService());
  45. }
  46. protected function tearDown(): void { Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); }
  47. private function request(string $token): array
  48. {
  49. $request = Request::create('/admin/x', 'GET');
  50. $request->headers->set('Authorization', 'Bearer ' . $token);
  51. $passed = false;
  52. $response = (new JwtAdminMiddleware($this->session))->handle($request, function () use (&$passed) { $passed = true; return 'ok'; });
  53. return [$passed, $response];
  54. }
  55. private function travel(int $seconds): void
  56. {
  57. Carbon::setTestNow(Carbon::now()->addSeconds($seconds));
  58. }
  59. public function test_activity_keeps_the_session_alive_beyond_the_idle_window(): void
  60. {
  61. $token = $this->session->issue(Admin::find(1));
  62. $payload = $this->session->validate($token);
  63. $this->assertSame(86400 * 30, $payload->exp - $payload->iat, 'JWT exp 只作为绝对上限');
  64. // 连续操作 5 小时(空闲窗口只有 1 小时),始终有效
  65. for ($i = 0; $i < 10; $i++) {
  66. $this->travel(1800);
  67. $this->assertTrue($this->request($token)[0], "第 {$i} 次请求应仍在登录态");
  68. }
  69. }
  70. public function test_session_expires_after_idle_window_without_requests(): void
  71. {
  72. $token = $this->session->issue(Admin::find(1));
  73. $this->travel(3599);
  74. $this->assertTrue($this->request($token)[0]);
  75. $this->travel(3601);
  76. [$passed, $response] = $this->request($token);
  77. $this->assertFalse($passed, '空闲超过 1 小时应失效');
  78. $this->assertSame(\App\Constants\HttpStatus::AUTHORIZATION_HEADER_NOT_FOUND, $response->getData(true)['code']);
  79. }
  80. public function test_revoked_and_forged_tokens_are_rejected(): void
  81. {
  82. $token = $this->session->issue(Admin::find(1));
  83. $this->session->revoke($this->session->validate($token));
  84. $this->assertFalse($this->request($token)[0]);
  85. $this->assertFalse($this->request($token . 'x')[0]);
  86. }
  87. public function test_legacy_tokens_without_sid_follow_their_own_expiry(): void
  88. {
  89. $legacy = JWT::encode(['iat' => time(), 'exp' => time() + 600, 'sub' => 1, 'user_id' => 1], str_repeat('k', 32), 'HS256');
  90. $this->assertTrue($this->request($legacy)[0], '上线前签发的 token 不受影响');
  91. $this->assertSame(time() + 600, $this->session->expiresAt($this->session->validate($legacy)));
  92. }
  93. public function test_realtime_expiry_is_stable_within_a_connection_and_renews_after_it(): void
  94. {
  95. $token = $this->session->issue(Admin::find(1));
  96. $first = $this->session->realtimeExpiresAt($this->session->validate($token));
  97. $this->assertSame(Carbon::now()->getTimestamp() + 3600, $first);
  98. // 期间有操作,活跃时间顺延,但同一连接内推送到期时间保持不变(各频道 channel_data 一致)
  99. $this->travel(1800);
  100. $this->request($token);
  101. $this->assertSame($first, $this->session->realtimeExpiresAt($this->session->validate($token)));
  102. $this->assertSame(Carbon::now()->getTimestamp() + 3600, $this->session->expiresAt($this->session->validate($token)));
  103. // 到点后重连拿到新的到期时间
  104. $this->travel(1801);
  105. $renewed = $this->session->realtimeExpiresAt($this->session->validate($token));
  106. $this->assertGreaterThan($first, $renewed);
  107. $this->assertLessThanOrEqual($this->session->expiresAt($this->session->validate($token)), $renewed);
  108. }
  109. }