app = new Application(dirname(__DIR__, 2)); $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']])); $translator = new Translator(new ArrayLoader(), 'zh'); $this->app->instance('translator', $translator); $this->app->instance('validator', new Factory($translator, $this->app)); $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory()); Request::macro('validate', function (array $rules) { return app('validator')->make($this->all(), $rules)->validate(); }); Facade::setFacadeApplication($this->app); $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager()); $db->schema()->create('users', function (Blueprint $t) { $t->id(); $t->string('user_id')->nullable(); $t->string('member_id'); $t->string('account')->nullable(); $t->string('from')->nullable(); $t->string('first_name')->default(''); $t->string('username')->default(''); }); $db->schema()->create('user_login', function (Blueprint $t) { $t->id(); $t->string('user_id')->nullable(); foreach (['login_account', 'login_ip', 'login_domain', 'country', 'browser', 'os', 'platform', 'user_agent'] as $field) { $t->string($field)->default(''); } $t->integer('status'); $t->dateTime('logout_time')->nullable(); $t->integer('online_duration')->nullable(); $t->timestamps(); }); foreach ([ ['1001', 'alice', '1'], ['1002', 'tg1002', '-1'], ['1003', 'guest1003', '2'], ['1004', 'kefu_member', '1'], ['1005', 'legacy', null], ['9001', 'kefu001', '0'], ['9002', 'kefu002', '0'], ['9003', 'support-alpha', '0'], ['', 'desk-blank-id', '0'], [null, 'desk-null-id', '0'], ] as [$userId, $account, $from]) { DB::table('users')->insert([ 'user_id' => $userId, 'member_id' => $account, 'account' => $account, 'from' => $from, 'first_name' => $account, ]); } } protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); Model::clearBootedModels(); parent::tearDown(); } public function test_excludes_customer_service_successes_and_failures_from_list_and_total(): void { $expected = []; foreach (['1001', '1002', '1003', '1004', '1005'] as $userId) { $expected[] = $this->log($userId, '', 1); } foreach (['9001', '9002', '9003'] as $userId) { $this->log($userId, '', 1); $this->log($userId, '', 0); } $data = $this->logs(); $this->assertSame(5, $data['total']); $this->assertSame(array_reverse($expected), array_column($data['data'], 'id')); $this->assertSame(11, DB::table('user_login')->count()); } public function test_identifies_customer_service_failures_by_account_when_user_id_is_missing(): void { foreach (['', null, 'deleted-id'] as $userId) { foreach (['kefu001', 'kefu002', 'support-alpha', 'desk-blank-id', 'desk-null-id'] as $account) { $this->log($userId, $account, 0); } } $expected = [ $this->log('', 'unknown-account', 0), $this->log(null, 'another-unknown', 0), $this->log('deleted-member', 'former-member', 0), $this->log('', 'kefu_not_staff', 0), $this->log('', '', 0), ]; $data = $this->logs(['status' => 0]); $this->assertSame(5, $data['total']); $this->assertSame(array_reverse($expected), array_column($data['data'], 'id')); foreach ($data['data'] as $row) { $this->assertNull($row['account']); } } public function test_account_and_user_filters_cannot_restore_customer_service_records(): void { $this->log('9001', 'kefu001', 1); $this->log('', 'kefu001', 0); $memberId = $this->log('1004', 'kefu_member', 1); $data = $this->logs(['account' => 'kefu']); $this->assertSame(1, $data['total']); $this->assertSame([$memberId], array_column($data['data'], 'id')); foreach ([['account' => 'kefu001'], ['user_id' => '9001'], ['member_id' => '9001'], ['first_name' => 'kefu001']] as $params) { $this->assertSame(['total' => 0, 'data' => []], $this->logs($params)); } } public function test_member_filters_and_pagination_keep_normal_failed_logins(): void { $this->log('1001', 'alice', 1); $older = $this->log('1001', 'alice', 0); $newer = $this->log('1001', 'alice', 0, '2026-11-07 12:00:01'); $this->log('9001', 'kefu001', 0, '2026-11-07 12:00:02'); $this->log('1001', 'alice', 0, '2026-11-06 23:59:59'); $params = ['account' => 'ali', 'user_id' => '1001', 'status' => 0, 'start_time' => '2026-11-07', 'end_time' => '2026-11-07', 'limit' => 1]; $data = $this->logs($params); $this->assertSame(2, $data['total']); $this->assertSame([$newer], array_column($data['data'], 'id')); $this->assertSame('alice', $data['data'][0]['account']); $this->assertFalse($data['data'][0]['is_online']); $data = $this->logs($params + ['page' => 2]); $this->assertSame(2, $data['total']); $this->assertSame([$older], array_column($data['data'], 'id')); } private function log(?string $userId, string $account, int $status, string $at = '2026-11-07 12:00:00'): int { return DB::table('user_login')->insertGetId([ 'user_id' => $userId, 'login_account' => $account, 'status' => $status, 'created_at' => $at, 'updated_at' => $at, ]); } private function logs(array $params = []): array { $this->app->instance('request', Request::create('/admin/user/loginLog', 'GET', $params)); $body = (new UserController())->loginLog()->getData(true); $this->assertSame(0, $body['code'], $body['msg']); return $body['data']; } }