| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- <?php
- namespace Tests\Integration;
- use App\Http\Controllers\api\Rebate;
- use App\Http\Middleware\CheckToken;
- use Carbon\Carbon;
- use Illuminate\Config\Repository;
- use Illuminate\Database\Capsule\Manager;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Events\Dispatcher;
- use Illuminate\Foundation\Application;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Http\Request;
- use Illuminate\Routing\Router;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Translation\ArrayLoader;
- use Illuminate\Translation\Translator;
- use Illuminate\Validation\Factory;
- use PHPUnit\Framework\TestCase;
- /** Real API routes, member-token middleware and queries, using SQLite without loading .env. */
- class MemberRebateApiTest extends TestCase
- {
- private Application $app;
- private Router $router;
- protected function setUp(): void
- {
- parent::setUp();
- Carbon::setTestNow(Carbon::parse('2026-10-25 00:30:00', 'Asia/Shanghai'));
- Facade::clearResolvedInstances();
- $this->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]]],
- ];
- }
- }
|