app = new Application(dirname(__DIR__, 2)); $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']])); 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()); $this->app->bind('db.schema', fn () => $db->schema()); $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 class { public function json($data, $status = 200, $headers = [], $options = 0) { return new JsonResponse($data, $status, $headers, $options); } }); Request::macro('validate', function (array $rules) { return app('validator')->make($this->all(), $rules)->validate(); }); if (!function_exists('lang')) require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php'; $db->schema()->create('users', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('member_id')->nullable(); }); $db->schema()->create('user_session', function (Blueprint $t) { $t->id(); $t->string('user_id'); $t->string('token'); $t->bigInteger('expire_time'); $t->bigInteger('create_time'); $t->bigInteger('update_time'); }); foreach ([ '2025_10_31_145445_create_rebates.php', '2025_10_31_155143_update_rebates.php', '2025_11_03_144118_update_rebates.php', '2025_11_20_145643_update_rebates.php', '2025_11_20_151650_update_rebates.php', '2026_01_13_153629_update_rebates.php', '2026_01_28_093600_update_rebate.php', ] as $migration) { (require dirname(__DIR__, 2) . '/database/migrations/' . $migration)->up(); } foreach (['one' => '100001', 'two' => '200002', 'unlinked' => null] as $suffix => $memberId) { DB::table('users')->insert(['user_id' => 'login-' . $suffix, 'member_id' => $memberId]); DB::table('user_session')->insert([ 'user_id' => 'login-' . $suffix, 'token' => 'token-' . $suffix, 'expire_time' => now()->timestamp + 86400, 'create_time' => now()->timestamp, 'update_time' => now()->timestamp, ]); } $this->router = new Router(new Dispatcher($this->app), $this->app); $this->app->instance('router', $this->router); $this->router->aliasMiddleware('check.token', CheckToken::class); $this->router->group(['prefix' => 'api'], function () { require dirname(__DIR__, 2) . '/routes/api.php'; }); } protected function tearDown(): void { Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); } private function request(array $params = [], ?string $token = 'token-one'): JsonResponse { $request = Request::create('/api/rebate', 'GET', $params); if ($token !== null) $request->headers->set('Authorization', $token); $this->app->instance('request', $request); return $this->router->dispatch($request); } private function rebate(array $overrides = []): int { return DB::table('rebates')->insertGetId($overrides + [ 'member_id' => '100001', 'date' => '2026-10-23', 'betting_amount' => '1000.00', 'effective_betting_amount' => '800.00', 'profit' => '-100.00', 'rebate_ratio' => '0.0050', 'amount' => '0.00', 'status' => 0, 'audited_by' => 'internal-admin', 'username' => 'historical-account', 'first_name' => '历史昵称', ]); } private function listing(array $params = [], string $token = 'token-one'): array { $response = $this->request($params, $token); $this->assertSame(200, $response->getStatusCode()); $body = $response->getData(true); $this->assertSame(0, $body['code'], $body['msg']); return $body['data']; } public function test_route_requires_member_login_and_rejects_missing_invalid_expired_and_unlinked_sessions(): void { $this->rebate(); $route = $this->router->getRoutes()->match(Request::create('/api/rebate', 'GET')); $this->assertSame(Rebate::class . '@index', $route->getAction('uses')); $this->assertContains('check.token', $route->middleware()); $this->assertSame(['GET', 'HEAD'], $route->methods()); foreach ([null, 'invalid', 'Bearer token-one', 'token-unlinked'] as $token) { $response = $this->request([], $token); $this->assertSame(401, $response->getStatusCode()); $this->assertSame(401, $response->getData(true)['code']); } DB::table('user_session')->where('token', 'token-one')->update(['expire_time' => now()->timestamp]); $this->assertSame(401, $this->request()->getStatusCode()); } public function test_user_cannot_select_another_members_rows_or_total_by_overriding_identity_parameters(): void { $own = $this->rebate(); $other = $this->rebate(['member_id' => '200002', 'betting_amount' => 9000]); $before = DB::table('rebates')->orderBy('id')->get()->toJson(); $list = $this->listing([ 'member_id' => '200002', 'user_id' => 'login-two', 'id' => $other, 'username' => 'another-account', 'first_name' => '其他会员', 'user' => ['member_id' => '200002'], ]); $this->assertSame(1, $list['total']); $this->assertSame([$own], array_column($list['data'], 'id')); $this->assertSame(['100001'], array_column($list['data'], 'member_id')); $otherList = $this->listing([], 'token-two'); $this->assertSame(1, $otherList['total']); $this->assertSame([$other], array_column($otherList['data'], 'id')); $this->assertSame($before, DB::table('rebates')->orderBy('id')->get()->toJson()); } public function test_date_cutoff_uses_new_york_midnight_and_never_includes_today_or_future_rows(): void { $yesterday = $this->rebate(); $today = $this->rebate(['date' => '2026-10-24']); $this->rebate(['date' => '2026-10-25']); $list = $this->listing(); $this->assertSame(1, $list['total']); $this->assertSame([$yesterday], array_column($list['data'], 'id')); $this->assertSame(['total' => 0, 'data' => []], $this->listing(['date' => '2026-10-24'])); Carbon::setTestNow(Carbon::parse('2026-10-25 00:00:00', 'America/New_York')); $list = $this->listing(); $this->assertSame(2, $list['total']); $this->assertSame([$today, $yesterday], array_column($list['data'], 'id')); } public function test_filters_sorting_pagination_and_empty_results_use_the_same_member_scope(): void { $paid = $this->rebate(['status' => 1, 'betting_amount' => 9000]); $small = $this->rebate(['betting_amount' => 100]); $large = $this->rebate(['betting_amount' => 200]); $tied = $this->rebate(['betting_amount' => 200]); $older = $this->rebate(['date' => '2026-10-22', 'betting_amount' => 10000]); $this->rebate(['member_id' => '200002', 'status' => 1]); $all = $this->listing(); $this->assertSame(5, $all['total']); $this->assertSame([$tied, $large, $small, $paid, $older], array_column($all['data'], 'id')); $unpaid = $this->listing(['status' => '0', 'date' => '2026-10-23']); $this->assertSame(3, $unpaid['total']); $this->assertSame([$tied, $large, $small], array_column($unpaid['data'], 'id')); $page = $this->listing(['status' => 0, 'limit' => 2, 'page' => 2]); $this->assertSame(4, $page['total']); $this->assertSame([$small, $older], array_column($page['data'], 'id')); $paidList = $this->listing(['status' => '1']); $this->assertSame(1, $paidList['total']); $this->assertSame([$paid], array_column($paidList['data'], 'id')); $this->assertSame(['total' => 5, 'data' => []], $this->listing(['page' => 99])); $this->assertSame(['total' => 0, 'data' => []], $this->listing(['date' => '2020-01-01'])); $this->assertSame(5, $this->listing(['date' => null, 'status' => null, 'limit' => null, 'page' => null])['total']); $this->assertSame(5, $this->listing(['date' => '', 'status' => ''])['total']); } public function test_list_returns_display_fields_and_saved_money_without_serializing_admin_or_dynamic_backflow_fields(): void { $this->rebate(['rebate_ratio' => null]); $grantTime = Carbon::parse('2026-10-24 10:00:00', 'Asia/Shanghai')->timestamp; $paid = $this->rebate(['status' => 1, 'amount' => '4.00', 'grant_time' => $grantTime]); $before = DB::table('rebates')->get()->toJson(); $rows = collect($this->listing()['data'])->keyBy('id'); $row = $rows[$paid]; $this->assertSame([ 'id', 'date', 'member_id', 'betting_amount', 'effective_betting_amount', 'profit', 'rebate_ratio', 'amount', 'status', 'grant_time', ], array_keys($row)); $this->assertEquals(800, $row['effective_betting_amount']); $this->assertEquals(-100, $row['profit']); $this->assertEquals(0.005, $row['rebate_ratio']); $this->assertEquals(4, $row['amount']); $this->assertSame(date('Y-m-d H:i', $grantTime), $row['grant_time']); $this->assertSame('', $rows->first()['grant_time']); $this->assertNull($rows->first()['rebate_ratio']); $this->assertSame($before, DB::table('rebates')->get()->toJson()); } /** @dataProvider invalidFilters */ public function test_invalid_filters_return_a_frontend_validation_error(array $params): void { $response = $this->request($params); $this->assertSame(200, $response->getStatusCode()); $body = $response->getData(true); $this->assertSame(-1, $body['code']); $this->assertSame([], $body['data']); $this->assertNotEmpty($body['msg']); } public static function invalidFilters(): array { return [ 'page zero' => [['page' => 0]], 'invalid page' => [['page' => 'abc']], 'limit zero' => [['limit' => 0]], 'oversized limit' => [['limit' => 101]], 'invalid date' => [['date' => '2026-02-30']], 'invalid status' => [['status' => 2]], 'array status' => [['status' => [0, 1]]], ]; } }