MemberTokenIdleTimeoutTest.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Middleware\CheckToken;
  4. use App\Models\User;
  5. use App\Models\UserSession;
  6. use Carbon\Carbon;
  7. use Illuminate\Config\Repository;
  8. use Illuminate\Database\Capsule\Manager;
  9. use Illuminate\Database\Schema\Blueprint;
  10. use Illuminate\Foundation\Application;
  11. use Illuminate\Http\JsonResponse;
  12. use Illuminate\Http\Request;
  13. use Illuminate\Support\Facades\DB;
  14. use Illuminate\Support\Facades\Facade;
  15. use Illuminate\Translation\ArrayLoader;
  16. use Illuminate\Translation\Translator;
  17. use PHPUnit\Framework\TestCase;
  18. /** Actual member-token middleware and sessions, isolated SQLite and a fixed server clock. */
  19. class MemberTokenIdleTimeoutTest extends TestCase
  20. {
  21. private int $start;
  22. protected function setUp(): void
  23. {
  24. Facade::clearResolvedInstances();
  25. Carbon::setTestNow(Carbon::parse('2026-10-25 15:18:55', 'Asia/Shanghai'));
  26. $this->start = now()->getTimestamp();
  27. $app = new Application(dirname(__DIR__, 2));
  28. $app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
  29. Facade::setFacadeApplication($app);
  30. $db = new Manager($app);
  31. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  32. $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db', $db->getDatabaseManager());
  33. $app->instance('translator', new Translator(new ArrayLoader(), 'zh'));
  34. $app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  35. if (!function_exists('create_token') || !function_exists('lang')) require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
  36. $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(); $t->integer('last_active_time')->nullable(); });
  37. $db->schema()->create('user_session', function (Blueprint $t) {
  38. $t->id(); $t->string('user_id'); $t->string('token'); $t->bigInteger('expire_time');
  39. $t->bigInteger('create_time'); $t->bigInteger('update_time')->nullable();
  40. });
  41. DB::table('users')->insert(['user_id' => '100001', 'member_id' => '100001']);
  42. }
  43. protected function tearDown(): void
  44. {
  45. Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
  46. }
  47. private function request(?string $token): JsonResponse
  48. {
  49. $request = Request::create('/api/wallet/test', 'GET');
  50. if ($token !== null) $request->headers->set('Authorization', $token);
  51. return (new CheckToken())->handle($request, static fn($r) => new JsonResponse(['code' => 0, 'user_id' => $r->user->user_id]));
  52. }
  53. private function clock(int $time): void { Carbon::setTestNow(Carbon::createFromTimestamp($time, 'Asia/Shanghai')); }
  54. public function test_authenticated_requests_record_and_display_member_activity(): void
  55. {
  56. $user = User::query()->firstOrFail();
  57. $this->assertSame('', $user->toArray()['last_active_time']);
  58. $this->assertSame('', $user->getLastActiveTimeAttribute(0));
  59. $this->assertSame('2026-10-25 15:18', $user->getLastActiveTimeAttribute((string) $this->start));
  60. $token = UserSession::setToken('100001');
  61. $this->assertSame(401, $this->request('invalid')->getStatusCode());
  62. $this->assertNull(DB::table('users')->value('last_active_time'));
  63. $this->assertSame(0, $this->request($token)->getData(true)['code']);
  64. $this->assertSame('2026-10-25 15:18', $user->fresh()->toArray()['last_active_time']);
  65. $this->clock($this->start + 59);
  66. $this->request($token);
  67. $this->assertSame($this->start, DB::table('users')->value('last_active_time'));
  68. $this->clock($this->start + 60);
  69. $this->request($token);
  70. $this->assertSame($this->start + 60, DB::table('users')->value('last_active_time'));
  71. $this->assertSame('2026-10-25 15:19', $user->fresh()->toArray()['last_active_time']);
  72. }
  73. public function test_login_and_authenticated_activity_use_a_full_sliding_day(): void
  74. {
  75. $token = UserSession::setToken('100001');
  76. $session = DB::table('user_session')->where('token', $token)->first();
  77. $this->assertSame($this->start + 86400, (int)$session->expire_time);
  78. $this->assertSame($this->start, (int)$session->update_time);
  79. $this->clock($this->start + 3600);
  80. $this->assertSame(0, $this->request($token)->getData(true)['code']);
  81. $this->assertSame($this->start + 3600 + 86400, (int)DB::table('user_session')->value('expire_time'));
  82. $lastActivity = $this->start + 25 * 3600 - 1;
  83. $this->clock($lastActivity);
  84. $this->assertSame(0, $this->request($token)->getData(true)['code']);
  85. $this->assertSame($lastActivity + 86400, (int)DB::table('user_session')->value('expire_time'));
  86. $this->clock($lastActivity + 86400);
  87. $before = (array)DB::table('user_session')->first();
  88. $this->assertSame(401, $this->request($token)->getStatusCode());
  89. $this->assertSame($before, (array)DB::table('user_session')->first());
  90. $this->assertNull(UserSession::renewActiveToken($token, now()->getTimestamp()));
  91. $this->assertSame($before, (array)DB::table('user_session')->first());
  92. }
  93. public function test_legacy_long_expiry_cannot_bypass_idle_limit_or_be_renewed(): void
  94. {
  95. DB::table('user_session')->insert(['user_id' => '100001', 'token' => 'legacy',
  96. 'expire_time' => $this->start + 7 * 86400, 'create_time' => $this->start, 'update_time' => $this->start]);
  97. $this->clock($this->start + 86400);
  98. $before = (array)DB::table('user_session')->first();
  99. $this->assertSame(401, $this->request('legacy')->getData(true)['code']);
  100. $this->assertNull(UserSession::renewActiveToken('legacy', now()->getTimestamp()));
  101. $this->assertSame($before, (array)DB::table('user_session')->first());
  102. $this->assertSame(401, $this->request(null)->getStatusCode());
  103. $this->assertNull(UserSession::renewActiveToken('', now()->getTimestamp()));
  104. }
  105. public function test_replaced_revoked_missing_or_orphaned_sessions_cannot_be_reactivated(): void
  106. {
  107. $token = UserSession::setToken('100001');
  108. DB::table('user_session')->where('token', $token)->update(['token' => 'replacement']);
  109. $this->assertSame(401, $this->request($token)->getStatusCode());
  110. $this->assertNull(UserSession::renewActiveToken($token, $this->start));
  111. $this->assertNull(UserSession::renewActiveToken('missing', $this->start));
  112. DB::table('user_session')->update(['expire_time' => $this->start]);
  113. $before = (array)DB::table('user_session')->first();
  114. $this->assertSame(401, $this->request('replacement')->getStatusCode());
  115. $this->assertSame($before, (array)DB::table('user_session')->first());
  116. DB::table('user_session')->update(['expire_time' => $this->start + 86400]);
  117. DB::table('users')->update(['is_banned' => 1]);
  118. $before = (array)DB::table('user_session')->first();
  119. $this->assertSame(401, $this->request('replacement')->getStatusCode());
  120. $this->assertSame($before, (array)DB::table('user_session')->first());
  121. DB::table('users')->update(['is_banned' => 0, 'delete_time' => $this->start]);
  122. $this->assertSame(401, $this->request('replacement')->getStatusCode());
  123. $this->assertSame($before, (array)DB::table('user_session')->first());
  124. DB::table('users')->delete();
  125. $before = (array)DB::table('user_session')->first();
  126. $this->assertSame(401, $this->request('replacement')->getStatusCode());
  127. $this->assertSame($before, (array)DB::table('user_session')->first());
  128. }
  129. public function test_missing_activity_falls_back_to_creation_and_concurrent_renewals_are_monotonic(): void
  130. {
  131. foreach ([null, 0] as $index => $activity) {
  132. $token = 'fallback-' . $index;
  133. DB::table('user_session')->insert(['user_id' => '100001', 'token' => $token,
  134. 'expire_time' => $this->start + 7 * 86400, 'create_time' => $this->start - 3600, 'update_time' => $activity]);
  135. $session = UserSession::where('token', $token)->first();
  136. $this->assertSame($this->start + 82800, UserSession::validUntil($session->getAttributes()));
  137. $this->assertSame(0, $this->request($token)->getData(true)['code']);
  138. $this->assertSame($this->start + 86400, (int)DB::table('user_session')->where('token', $token)->value('expire_time'));
  139. }
  140. DB::table('user_session')->where('token', 'fallback-0')->update(['update_time' => $this->start + 10, 'expire_time' => $this->start + 10 + 86400]);
  141. $before = (array)DB::table('user_session')->where('token', 'fallback-0')->first();
  142. $this->assertNotNull(UserSession::renewActiveToken('fallback-0', $this->start));
  143. $this->assertSame($before, (array)DB::table('user_session')->where('token', 'fallback-0')->first());
  144. }
  145. public function test_renewal_stays_bound_to_the_original_session_identity(): void
  146. {
  147. $token = UserSession::setToken('100001');
  148. $id = (int)DB::table('user_session')->value('id');
  149. DB::table('user_session')->where('id', $id)->update(['token' => 'replacement']);
  150. DB::table('user_session')->insert(['user_id' => 'other-user', 'token' => $token,
  151. 'expire_time' => $this->start + 86400, 'create_time' => $this->start, 'update_time' => $this->start]);
  152. $before = DB::table('user_session')->orderBy('id')->get()->toJson();
  153. $this->assertNull(UserSession::renewActiveToken($token, $this->start + 1, $id));
  154. $this->assertSame($before, DB::table('user_session')->orderBy('id')->get()->toJson());
  155. }
  156. }