OperationExchangeListTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\Operation;
  4. use App\Services\PlayedGameService;
  5. use App\Services\Sharing\ShareDefaults;
  6. use Carbon\Carbon;
  7. use Illuminate\Config\Repository;
  8. use Illuminate\Database\Capsule\Manager;
  9. use Illuminate\Foundation\Application;
  10. use Illuminate\Http\Request;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Facade;
  13. use Illuminate\Translation\ArrayLoader;
  14. use Illuminate\Translation\Translator;
  15. use Illuminate\Validation\Factory;
  16. use PHPUnit\Framework\Attributes\DataProvider;
  17. use PHPUnit\Framework\TestCase;
  18. /** Real controller and source orders, isolated from the application database and .env. */
  19. class OperationExchangeListTest extends TestCase
  20. {
  21. private Application $app;
  22. protected function setUp(): void
  23. {
  24. parent::setUp();
  25. Facade::clearResolvedInstances();
  26. $this->app = new Application(dirname(__DIR__, 2));
  27. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
  28. Facade::setFacadeApplication($this->app);
  29. $db = new Manager($this->app);
  30. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  31. $db->setAsGlobal();
  32. $db->bootEloquent();
  33. $this->app->instance('db', $db->getDatabaseManager());
  34. $this->app->bind('db.schema', fn () => $db->schema());
  35. $translator = new Translator(new ArrayLoader(), 'zh');
  36. $this->app->instance('translator', $translator);
  37. $this->app->instance('validator', new Factory($translator, $this->app));
  38. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  39. Request::macro('validate', function (array $rules) {
  40. return app('validator')->make($this->all(), $rules)->validate();
  41. });
  42. foreach ([
  43. 'users' => 'id INTEGER PRIMARY KEY, user_id TEXT, member_id TEXT, account TEXT, first_name TEXT, "from" INTEGER DEFAULT 1, created_at TEXT',
  44. 'wallets' => 'id INTEGER PRIMARY KEY, member_id TEXT, available_balance NUMERIC DEFAULT 0',
  45. 'balance_logs' => 'id INTEGER PRIMARY KEY, member_id TEXT, change_type TEXT, amount NUMERIC, created_at TEXT',
  46. 'third_game_orders' => 'id INTEGER PRIMARY KEY, user_id INTEGER, member_id TEXT, game_type INTEGER, status INTEGER DEFAULT 1, bet_amount NUMERIC DEFAULT 10, bet_time TEXT',
  47. 'sport_order' => 'id INTEGER PRIMARY KEY, user_id TEXT, amount NUMERIC DEFAULT 10, status INTEGER DEFAULT 1, pay_status INTEGER DEFAULT 1, return_status INTEGER DEFAULT 0, is_faker INTEGER DEFAULT 0, create_time INTEGER',
  48. 'sport_game_order' => 'id INTEGER PRIMARY KEY, member_id TEXT, amount NUMERIC DEFAULT 10, status INTEGER DEFAULT 1, created_at TEXT',
  49. 'lhc_order' => 'id INTEGER PRIMARY KEY, member_id TEXT, amount NUMERIC DEFAULT 10, lottery_status INTEGER DEFAULT 1, is_faker INTEGER DEFAULT 0, created_at INTEGER',
  50. 'bets' => 'id INTEGER PRIMARY KEY, member_id TEXT, amount NUMERIC DEFAULT 10, status INTEGER DEFAULT 2, created_at TEXT',
  51. 'jisu_game_order' => 'id INTEGER PRIMARY KEY, member_id TEXT, amount NUMERIC DEFAULT 10, status INTEGER DEFAULT 1, created_at TEXT',
  52. ] as $table => $columns) {
  53. DB::statement('CREATE TABLE bot_' . $table . ' (' . $columns . ')');
  54. }
  55. foreach ([['100', 'alice', 1], ['200', 'bob', 1], ['300', 'robot', 2]] as $i => [$member, $account, $from]) {
  56. DB::table('users')->insert(['id' => $i + 1, 'user_id' => $member, 'member_id' => $member,
  57. 'account' => $account, 'first_name' => $account, 'from' => $from, 'created_at' => '2026-11-0' . ($i + 1) . ' 12:00:00']);
  58. DB::table('wallets')->insert(['member_id' => $member, 'available_balance' => 88]);
  59. }
  60. }
  61. protected function tearDown(): void
  62. {
  63. DB::disconnect();
  64. Facade::clearResolvedInstances();
  65. parent::tearDown();
  66. }
  67. private function response(array $params = []): array
  68. {
  69. $this->app->instance('request', Request::create('/admin/operation/exchangeList', 'GET', $params));
  70. return (new Operation())->exchangeList()->getData(true);
  71. }
  72. private function rows(array $params = []): array
  73. {
  74. $body = $this->response($params);
  75. $this->assertSame(0, $body['code'], $body['msg']);
  76. return array_column($body['data']['data'], null, 'member_id');
  77. }
  78. private function order(string $table, string $member = '100', array $extra = [], string $at = '2026-11-04 12:00:00'): void
  79. {
  80. $timestamp = in_array($table, ['sport_order', 'lhc_order'], true);
  81. $memberColumn = $table === 'sport_order' ? 'user_id' : 'member_id';
  82. $timeColumn = $table === 'sport_order' ? 'create_time' : ($table === 'third_game_orders' ? 'bet_time' : 'created_at');
  83. DB::table($table)->insert($extra + [$memberColumn => $member,
  84. $timeColumn => $timestamp ? Carbon::parse($at, 'Asia/Shanghai')->timestamp : $at]);
  85. }
  86. private function funds(string $type, string $amount, string $member = '100', string $at = '2026-11-04 12:00:00'): void
  87. {
  88. DB::table('balance_logs')->insert(['member_id' => $member, 'change_type' => $type, 'amount' => $amount, 'created_at' => $at]);
  89. }
  90. public function test_database_writes_track_every_report_source_and_only_relevant_updates(): void
  91. {
  92. $migration = require dirname(__DIR__, 2) . '/database/migrations/2026_11_06_130000_track_admin_exchange_list_changes.php';
  93. $migration->up();
  94. $changes = DB::table('admin_exchange_list_changes');
  95. DB::table('users')->where('member_id', '100')->update(['first_name' => 'Alice']);
  96. DB::table('wallets')->where('member_id', '100')->increment('available_balance', 2);
  97. $this->funds('人工充值', '30');
  98. $this->funds('提现', '-10');
  99. foreach (['sport_order', 'third_game_orders', 'sport_game_order', 'lhc_order', 'bets', 'jisu_game_order'] as $table) {
  100. $this->order($table, extra: $table === 'third_game_orders' ? ['game_type' => 1] : []);
  101. }
  102. $this->assertSame(10, $changes->count());
  103. $row = $this->rows()['100'];
  104. $this->assertSame('Alice', $row['first_name']);
  105. $this->assertSame(90, $row['money']);
  106. $this->assertSame('20.00', $row['withdraw_recharge_difference']);
  107. $this->assertSame('10.00', $row['order_amount']);
  108. $this->assertCount(3, $row['played_games']);
  109. $this->funds('结算', '5');
  110. DB::table('wallets')->update(['available_balance' => DB::raw('available_balance')]);
  111. DB::table('users')->where('member_id', '100')->update(['first_name' => 'Alice']);
  112. $this->assertSame(10, $changes->count());
  113. DB::beginTransaction();
  114. DB::table('sport_order')->update(['return_status' => 2]);
  115. DB::table('balance_logs')->where('change_type', '提现')->delete();
  116. $this->assertSame(12, $changes->count());
  117. DB::rollBack();
  118. $this->assertSame(10, $changes->count());
  119. DB::table('balance_logs')->where('change_type', '提现')->update(['change_type' => '结算']);
  120. DB::table('wallets')->where('member_id', '200')->delete();
  121. $this->assertSame(12, $changes->count());
  122. $this->assertSame(['100'], array_map('strval', array_keys($this->rows())));
  123. $migration->down();
  124. DB::table('wallets')->increment('available_balance');
  125. $this->assertFalse(DB::connection()->getSchemaBuilder()->hasTable('admin_exchange_list_changes'));
  126. }
  127. public function test_all_nine_commission_categories_are_deduplicated_and_old_orders_are_included(): void
  128. {
  129. foreach (range(1, 7) as $type) $this->order('third_game_orders', extra: ['game_type' => $type]);
  130. $this->order('third_game_orders', extra: ['game_type' => 1]);
  131. $this->order('sport_order');
  132. $this->order('sport_game_order');
  133. // No sharing facts, child orders or legacy ID cutoff are needed to recognize historical play.
  134. $this->order('lhc_order', extra: ['id' => 1]);
  135. $this->order('bets');
  136. $this->order('jisu_game_order');
  137. $rows = $this->rows();
  138. $this->assertSame(ShareDefaults::categories(), array_column($rows['100']['played_games'], 'label', 'category'));
  139. $this->assertSame([], $rows['200']['played_games']);
  140. $this->assertArrayNotHasKey('300', $rows);
  141. }
  142. public function test_cancelled_unpaid_fake_zero_stake_and_unknown_games_do_not_count_as_play(): void
  143. {
  144. $this->order('third_game_orders', extra: ['game_type' => 1, 'status' => 2]);
  145. $this->order('third_game_orders', extra: ['game_type' => 2, 'status' => 3]);
  146. $this->order('third_game_orders', extra: ['game_type' => 8]);
  147. $this->order('third_game_orders', extra: ['game_type' => 3, 'bet_amount' => 0]);
  148. $this->order('sport_order', extra: ['pay_status' => 0]);
  149. $this->order('sport_order', extra: ['return_status' => 2]);
  150. $this->order('sport_order', extra: ['is_faker' => 1]);
  151. $this->order('sport_game_order', extra: ['status' => 3]);
  152. $this->order('lhc_order', extra: ['lottery_status' => 3]);
  153. $this->order('lhc_order', extra: ['lottery_status' => 4]);
  154. $this->order('lhc_order', extra: ['is_faker' => 1]);
  155. $this->order('bets', extra: ['status' => 3]);
  156. $this->order('jisu_game_order', extra: ['status' => 3]);
  157. $this->assertSame([], $this->rows()['100']['played_games']);
  158. }
  159. public function test_pending_bets_count_without_waiting_for_commission_settlement(): void
  160. {
  161. $this->order('third_game_orders', extra: ['game_type' => 1, 'status' => 0]);
  162. $this->order('sport_order');
  163. $this->order('sport_game_order', extra: ['status' => 0]);
  164. $this->order('lhc_order', extra: ['lottery_status' => 0]);
  165. $this->order('bets', extra: ['status' => 1]);
  166. $this->order('jisu_game_order', extra: ['status' => 0]);
  167. $this->assertSame(['third:1', 'self:sport', 'self:lottery'], array_column($this->rows()['100']['played_games'], 'category'));
  168. }
  169. public static function sourcesWithoutFakeFlag(): array
  170. {
  171. return [
  172. 'lottery' => ['lhc_order', 'self:lottery'],
  173. 'sport' => ['sport_order', 'self:sport'],
  174. ];
  175. }
  176. #[DataProvider('sourcesWithoutFakeFlag')]
  177. public function test_missing_optional_fake_flag_does_not_break_the_list_or_date_filters(string $table, string $category): void
  178. {
  179. DB::statement('ALTER TABLE bot_' . $table . ' DROP COLUMN is_faker');
  180. $this->order($table);
  181. $this->order($table, '200', at: '2026-11-03 23:59:59');
  182. $this->order($table, '300');
  183. $this->funds('充值', '100');
  184. $this->funds('提现', '-25');
  185. $rows = $this->rows(['start_time' => '2026-11-04', 'end_time' => '2026-11-04', 'sort_field' => 'recharge']);
  186. $this->assertSame(['100', '200'], array_column($rows, 'member_id'));
  187. $this->assertSame([$category], array_column($rows['100']['played_games'], 'category'));
  188. $this->assertSame('75.00', $rows['100']['withdraw_recharge_difference']);
  189. $this->assertSame([], $rows['200']['played_games']);
  190. $this->assertArrayNotHasKey('300', $rows);
  191. $this->assertSame([$category], array_column($this->rows()['200']['played_games'], 'category'));
  192. }
  193. public static function absentGameTables(): array
  194. {
  195. $cases = [];
  196. foreach (['jisu_game_order', 'bets', 'sport_game_order', 'lhc_order', 'third_game_orders'] as $table) {
  197. $cases[$table] = [$table];
  198. }
  199. return $cases;
  200. }
  201. #[DataProvider('absentGameTables')]
  202. public function test_absent_optional_game_table_does_not_break_the_list_or_other_categories(string $table): void
  203. {
  204. $this->order('third_game_orders', extra: ['game_type' => 1]);
  205. $this->order('sport_order');
  206. $this->order('sport_game_order');
  207. $this->order('lhc_order');
  208. $this->order('bets');
  209. $this->order('jisu_game_order');
  210. DB::statement('DROP TABLE bot_' . $table);
  211. $this->funds('充值', '100');
  212. $this->funds('提现', '-25');
  213. $this->funds('充值', '200', '200');
  214. $rows = $this->rows(['start_time' => '2026-11-04', 'end_time' => '2026-11-04',
  215. 'sort_field' => 'withdraw_recharge_difference', 'sort_order' => 'desc']);
  216. $this->assertSame(['200', '100'], array_column($rows, 'member_id'));
  217. $expected = $table === 'third_game_orders' ? ['self:sport', 'self:lottery'] : ['third:1', 'self:sport', 'self:lottery'];
  218. $this->assertSame($expected, array_column($rows['100']['played_games'], 'category'));
  219. $this->assertSame('75.00', $rows['100']['withdraw_recharge_difference']);
  220. $this->assertSame([], $rows['200']['played_games']);
  221. }
  222. public function test_absent_jisu_table_and_lottery_fake_flag_are_supported_together(): void
  223. {
  224. DB::statement('DROP TABLE bot_jisu_game_order');
  225. DB::statement('ALTER TABLE bot_lhc_order DROP COLUMN is_faker');
  226. $this->order('lhc_order');
  227. $this->assertSame(['self:lottery'], array_column($this->rows()['100']['played_games'], 'category'));
  228. }
  229. public function test_no_game_tables_returns_no_played_categories(): void
  230. {
  231. foreach (['third_game_orders', 'sport_order', 'sport_game_order', 'lhc_order', 'bets', 'jisu_game_order'] as $table) {
  232. DB::statement('DROP TABLE bot_' . $table);
  233. }
  234. $this->assertSame([], PlayedGameService::forMembers(['100']));
  235. }
  236. public function test_third_party_identity_supports_user_id_only_and_does_not_mix_members(): void
  237. {
  238. $this->order('third_game_orders', extra: ['user_id' => 1, 'member_id' => null, 'game_type' => 1]);
  239. $this->order('third_game_orders', '200', ['user_id' => 2, 'game_type' => 2]);
  240. $this->order('third_game_orders', '100', ['user_id' => 2, 'game_type' => 3]); // conflicting identity
  241. $rows = $this->rows();
  242. $this->assertSame(['third:1'], array_column($rows['100']['played_games'], 'category'));
  243. $this->assertSame(['third:2'], array_column($rows['200']['played_games'], 'category'));
  244. }
  245. public static function gameSources(): array
  246. {
  247. return array_map(fn ($table) => [$table], ['third_game_orders', 'sport_order', 'sport_game_order', 'lhc_order', 'bets', 'jisu_game_order']);
  248. }
  249. #[DataProvider('gameSources')]
  250. public function test_each_game_source_respects_the_selected_dates(string $table): void
  251. {
  252. $extra = $table === 'third_game_orders' ? ['game_type' => 1] : [];
  253. $this->order($table, extra: $extra, at: '2026-11-03 23:59:59');
  254. $this->order($table, extra: $extra, at: '2026-11-05 00:00:00');
  255. $params = ['start_time' => '2026-11-04', 'end_time' => '2026-11-04'];
  256. $this->assertSame([], $this->rows($params)['100']['played_games']);
  257. $this->order($table, extra: $extra, at: '2026-11-04 00:00:00');
  258. $this->assertCount(1, $this->rows($params)['100']['played_games']);
  259. }
  260. public function test_profit_is_recharge_minus_withdraw_and_deducts_refunded_withdrawals(): void
  261. {
  262. $this->funds('充值', '1000.11');
  263. $this->funds('人工充值', '200.22');
  264. $this->funds('三方充值', '300.33');
  265. $this->funds('提现', '-2000.44');
  266. $this->funds('三方提现', '-500.55');
  267. $this->funds('三方提现', '500.55'); // failed withdrawal returned in full
  268. $this->funds('优惠活动', '9999');
  269. $this->funds('人工扣款', '-9999');
  270. $this->funds('充值', '8888', '300');
  271. $row = $this->rows()['100'];
  272. $this->assertSame('1,500.66', $row['recharge']);
  273. $this->assertSame('2,000.44', $row['withdraw']);
  274. $this->assertSame('-499.78', $row['withdraw_recharge_difference']);
  275. $this->funds('提现', '-100', '200');
  276. $this->funds('充值', '500', '200');
  277. $this->assertSame('400.00', $this->rows()['200']['withdraw_recharge_difference']);
  278. }
  279. public function test_date_filters_apply_to_money_categories_and_existing_order_amount_at_day_boundaries(): void
  280. {
  281. foreach (['2026-11-04 00:00:00', '2026-11-04 23:59:59'] as $at) {
  282. $this->order('sport_order', at: $at);
  283. $this->order('lhc_order', at: $at);
  284. $this->funds('充值', '100', at: $at);
  285. }
  286. $this->order('third_game_orders', extra: ['game_type' => 1], at: '2026-11-03 23:59:59');
  287. $this->order('third_game_orders', extra: ['game_type' => 2], at: '2026-11-05 00:00:00');
  288. $this->order('sport_order', at: '2026-11-03 23:59:59');
  289. $this->order('sport_order', at: '2026-11-05 00:00:00');
  290. $this->funds('充值', '999', at: '2026-11-03 23:59:59');
  291. $this->funds('提现', '-999', at: '2026-11-05 00:00:00');
  292. $this->funds('提现', '-80');
  293. $row = $this->rows(['start_time' => '2026-11-04', 'end_time' => '2026-11-04'])['100'];
  294. $this->assertSame('200.00', $row['recharge']);
  295. $this->assertSame('80.00', $row['withdraw']);
  296. $this->assertSame('120.00', $row['withdraw_recharge_difference']);
  297. $this->assertSame('20.00', $row['order_amount']);
  298. $this->assertSame(['self:sport', 'self:lottery'], array_column($row['played_games'], 'category'));
  299. $this->assertCount(4, $this->rows()['100']['played_games']);
  300. }
  301. public function test_empty_users_default_to_zero_and_filters_and_pagination_keep_the_response_shape(): void
  302. {
  303. $row = $this->rows(['account' => 'lic'])['100'];
  304. foreach (['recharge', 'withdraw', 'withdraw_recharge_difference', 'order_amount'] as $key) $this->assertSame('0.00', $row[$key]);
  305. $this->assertSame([], $row['played_games']);
  306. $this->assertSame([], $this->rows(['account' => 'missing']));
  307. $body = $this->response(['page' => 2, 'limit' => 1]);
  308. $this->assertSame(2, $body['data']['total']);
  309. $this->assertSame(['100'], array_column($body['data']['data'], 'member_id'));
  310. $this->assertNotSame(0, $this->response(['start_time' => '2026-11-04'])['code']);
  311. }
  312. private function sortingMembers(): void
  313. {
  314. foreach (['400' => 'carol', '500' => 'dave'] as $member => $account) {
  315. DB::table('users')->insert(['user_id' => (string) $member, 'member_id' => (string) $member,
  316. 'account' => $account, 'first_name' => $account, 'created_at' => '2026-11-04 12:00:00']);
  317. DB::table('wallets')->insert(['member_id' => (string) $member]);
  318. }
  319. $this->funds('充值', '2000.10');
  320. $this->funds('提现', '-50.10');
  321. $this->funds('三方充值', '100.00', '200');
  322. $this->funds('三方提现', '-1500.01', '200');
  323. $this->funds('三方提现', '-9000', '200');
  324. $this->funds('三方提现', '9000', '200'); // refunded, must not raise the withdrawal ranking
  325. $this->funds('人工充值', '900', '400');
  326. $this->funds('提现', '-100', '400');
  327. $this->funds('充值', '999999', '300'); // virtual user
  328. foreach (['100' => '9', '200' => '1000', '400' => '90', '500' => '0'] as $member => $balance) {
  329. DB::table('wallets')->where('member_id', (string) $member)->update(['available_balance' => $balance]);
  330. }
  331. }
  332. public static function amountSorts(): array
  333. {
  334. $cases = [];
  335. foreach ([
  336. 'recharge' => ['500', '200', '400', '100'],
  337. 'withdraw' => ['500', '100', '400', '200'],
  338. 'withdraw_recharge_difference' => ['200', '500', '400', '100'],
  339. 'money' => ['500', '100', '400', '200'],
  340. ] as $field => $ascending) {
  341. $cases[$field . '_asc'] = [$field, 'asc', $ascending];
  342. $cases[$field . '_desc'] = [$field, 'desc', array_reverse($ascending)];
  343. }
  344. return $cases;
  345. }
  346. #[DataProvider('amountSorts')]
  347. public function test_amount_sorting_is_numeric_and_precedes_pagination(string $field, string $direction, array $expected): void
  348. {
  349. $this->sortingMembers();
  350. $actual = [];
  351. foreach ([1, 2] as $page) {
  352. $body = $this->response(['sort_field' => $field, 'sort_order' => $direction, 'limit' => 2, 'page' => $page]);
  353. $this->assertSame(0, $body['code'], $body['msg']);
  354. $this->assertSame(4, $body['data']['total']);
  355. array_push($actual, ...array_column($body['data']['data'], 'member_id'));
  356. }
  357. $this->assertSame($expected, $actual);
  358. }
  359. public function test_amount_sorting_uses_selected_dates_and_member_filters(): void
  360. {
  361. $this->sortingMembers();
  362. $this->funds('充值', '100000', '200', '2026-11-03 12:00:00');
  363. $this->funds('提现', '-100000', '100', '2026-11-05 12:00:00');
  364. $dates = ['start_time' => '2026-11-04', 'end_time' => '2026-11-04'];
  365. foreach (['recharge' => '100', 'withdraw' => '200', 'withdraw_recharge_difference' => '100'] as $field => $member) {
  366. $body = $this->response($dates + ['sort_field' => $field, 'limit' => 1]);
  367. $this->assertSame(0, $body['code'], $body['msg']);
  368. $this->assertSame($member, $body['data']['data'][0]['member_id']);
  369. }
  370. $body = $this->response(['sort_field' => 'recharge', 'limit' => 1]);
  371. $this->assertSame('200', $body['data']['data'][0]['member_id']);
  372. $this->assertSame(['400'], array_column($this->response($dates + ['sort_field' => 'recharge', 'account' => 'car'])['data']['data'], 'member_id'));
  373. }
  374. public function test_equal_amounts_have_stable_order_across_pages_and_default_order_is_preserved(): void
  375. {
  376. DB::table('users')->insert(['id' => 4, 'user_id' => '400', 'member_id' => '400',
  377. 'account' => 'carol', 'first_name' => 'carol', 'created_at' => '2026-11-02 12:00:00']);
  378. DB::table('wallets')->insert(['member_id' => '400', 'available_balance' => 88]);
  379. foreach (['recharge', 'withdraw', 'withdraw_recharge_difference', 'money'] as $field) {
  380. $members = [];
  381. foreach ([1, 2, 3] as $page) {
  382. $body = $this->response(['sort_field' => $field, 'sort_order' => 'asc', 'limit' => 1, 'page' => $page]);
  383. $this->assertSame(0, $body['code'], $body['msg']);
  384. $members[] = $body['data']['data'][0]['member_id'];
  385. }
  386. $this->assertSame(['400', '200', '100'], $members);
  387. }
  388. $this->assertSame(['400', '200', '100'], array_column($this->response()['data']['data'], 'member_id'));
  389. }
  390. public static function invalidSorts(): array
  391. {
  392. return [
  393. [['sort_field' => 'created_at']],
  394. [['sort_field' => 'money desc, users.id']],
  395. [['sort_field' => ['money']]],
  396. [['sort_field' => 'money', 'sort_order' => 'descending']],
  397. [['sort_field' => 'money', 'sort_order' => ['asc']]],
  398. ];
  399. }
  400. #[DataProvider('invalidSorts')]
  401. public function test_invalid_sort_fields_and_directions_are_rejected(array $params): void
  402. {
  403. $this->assertNotSame(0, $this->response($params)['code']);
  404. }
  405. }