BannerPublicationTest.php 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Constants\HttpStatus;
  4. use App\Http\Controllers\admin\Banner as AdminBanner;
  5. use App\Http\Middleware\CheckButtonPermission;
  6. use App\Models\Banner;
  7. use App\Services\BannerVisibilityService;
  8. use Illuminate\Config\Repository;
  9. use Illuminate\Database\Capsule\Manager;
  10. use Illuminate\Database\Schema\Blueprint;
  11. use Illuminate\Events\Dispatcher;
  12. use Illuminate\Foundation\Application;
  13. use Illuminate\Http\JsonResponse;
  14. use Illuminate\Http\Request;
  15. use Illuminate\Routing\Router;
  16. use Illuminate\Support\Facades\DB;
  17. use Illuminate\Support\Facades\Facade;
  18. use Illuminate\Support\Facades\Schema;
  19. use Illuminate\Translation\ArrayLoader;
  20. use Illuminate\Translation\Translator;
  21. use Illuminate\Validation\Factory;
  22. use PHPUnit\Framework\TestCase;
  23. /** Exercises real banner controllers and queries against an isolated, prefixed SQLite database. */
  24. class BannerPublicationTest extends TestCase
  25. {
  26. private Application $app;
  27. protected function setUp(): void
  28. {
  29. Facade::clearResolvedInstances();
  30. $this->app = new Application(dirname(__DIR__, 2));
  31. $this->app->instance('config', new Repository([
  32. 'app' => ['locale' => 'zh', 'url' => 'https://banner.example.test', 'timezone' => 'Asia/Shanghai'],
  33. ]));
  34. Facade::setFacadeApplication($this->app);
  35. $db = new Manager($this->app);
  36. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  37. $db->setAsGlobal();
  38. $db->bootEloquent();
  39. $this->app->instance('db', $db->getDatabaseManager());
  40. $this->app->bind('db.schema', fn () => $db->schema());
  41. $translator = new Translator(new ArrayLoader(), 'zh');
  42. $this->app->instance('translator', $translator);
  43. $this->app->instance('validator', new Factory($translator, $this->app));
  44. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  45. Request::macro('validate', function (array $rules) {
  46. return app('validator')->make($this->all(), $rules)->validate();
  47. });
  48. $db->schema()->create('banner', function (Blueprint $t) {
  49. $t->id(); $t->string('title'); $t->string('image'); $t->string('link', 2048)->nullable();
  50. $t->string('click_type', 16)->nullable(); $t->string('link_pc', 2048)->nullable(); $t->string('link_h5', 2048)->nullable();
  51. $t->string('type'); $t->integer('sort')->default(0); $t->string('lang'); $t->timestamps();
  52. });
  53. $this->migration()->up();
  54. $db->schema()->create('activity_rewards', function (Blueprint $t) {
  55. $t->id(); $t->integer('status')->default(1);
  56. $t->integer('start_time'); $t->integer('end_time');
  57. });
  58. $this->request('index');
  59. }
  60. protected function tearDown(): void
  61. {
  62. DB::disconnect();
  63. Facade::clearResolvedInstances();
  64. parent::tearDown();
  65. }
  66. private function migration()
  67. {
  68. return require dirname(__DIR__, 2) . '/database/migrations/2026_10_24_140000_add_status_to_banner.php';
  69. }
  70. private function request(string $action, array $params = []): void
  71. {
  72. $this->app->instance('request', Request::create('/admin/banner/' . $action,
  73. $action === 'index' ? 'GET' : 'POST', $params));
  74. }
  75. private function form(array $overrides = []): array
  76. {
  77. return $overrides + [
  78. 'title' => '轮播图', 'image' => '/banner.png', 'type' => 'pc', 'lang' => 'zh',
  79. ];
  80. }
  81. private function save(array $params = []): array
  82. {
  83. $this->request('update', $this->form($params));
  84. return (new AdminBanner())->update()->getData(true);
  85. }
  86. private function setStatus(array $params): array
  87. {
  88. $this->request('status', $params);
  89. return (new AdminBanner())->status()->getData(true);
  90. }
  91. private function listing(array $params = []): array
  92. {
  93. $this->request('index', $params);
  94. $result = (new AdminBanner())->index()->getData(true);
  95. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  96. return $result['data'];
  97. }
  98. private function banner(array $params = []): int
  99. {
  100. return DB::table('banner')->insertGetId($params + $this->form() + ['link' => null, 'sort' => 0, 'status' => 1]);
  101. }
  102. private function activity(int $id, int $start, int $end, int $status = 1): void
  103. {
  104. DB::table('activity_rewards')->insert([
  105. 'id' => $id, 'status' => $status, 'start_time' => $start, 'end_time' => $end,
  106. ]);
  107. }
  108. private function decorated(int $now): array
  109. {
  110. return collect(BannerVisibilityService::decorate(Banner::query()->orderBy('id')->get(), $now))->keyBy('id')->all();
  111. }
  112. private function assertState(array $row, ?int $manual, int $effective, ?string $reason, ?string $activityId): void
  113. {
  114. $this->assertSame($manual, $row['manual_status']);
  115. $this->assertSame($effective, $row['status']);
  116. $this->assertSame($effective, $row['effective_status']);
  117. $this->assertSame($reason, $row['offline_reason']);
  118. $this->assertSame($activityId, $row['activity_id']);
  119. $this->assertSame($activityId === null, $row['can_manual_toggle']);
  120. }
  121. public function test_migration_publishes_existing_banners_by_default_and_rolls_back_only_its_column(): void
  122. {
  123. $this->migration()->down();
  124. $this->assertFalse(Schema::hasColumn('banner', 'status'));
  125. $id = DB::table('banner')->insertGetId($this->form(['title' => '历史轮播图', 'sort' => 23]) + ['link' => 'https://example.test']);
  126. $this->migration()->up();
  127. $row = DB::table('banner')->find($id);
  128. $this->assertSame(1, (int) $row->status);
  129. $this->assertSame('历史轮播图', $row->title);
  130. $this->assertSame(23, (int) $row->sort);
  131. $this->assertSame('https://example.test', $row->link);
  132. $newId = DB::table('banner')->insertGetId($this->form());
  133. $this->assertSame(1, (int) DB::table('banner')->where('id', $newId)->value('status'));
  134. $this->migration()->down();
  135. $this->assertFalse(Schema::hasColumn('banner', 'status'));
  136. $this->assertSame(2, DB::table('banner')->count());
  137. $this->assertSame('历史轮播图', DB::table('banner')->where('id', $id)->value('title'));
  138. }
  139. public function test_create_defaults_to_manual_on_and_legacy_edit_preserves_omitted_status_link_and_sort(): void
  140. {
  141. $result = $this->save(['sort' => 27, 'link' => 'https://example.test/promotion', 'type' => 'app', 'lang' => 'en']);
  142. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  143. $banner = Banner::query()->firstOrFail();
  144. $this->assertSame(1, $banner->status);
  145. $this->assertSame('app', $banner->type);
  146. $this->assertSame('en', $banner->lang);
  147. $this->assertSame(27, (int) $banner->sort);
  148. $this->assertSame(HttpStatus::OK, $this->setStatus(['id' => $banner->id, 'status' => 0])['code']);
  149. $result = $this->save(['id' => $banner->id, 'title' => '编辑标题', 'type' => 'app', 'lang' => 'en']);
  150. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  151. $banner->refresh();
  152. $this->assertSame('编辑标题', $banner->title);
  153. $this->assertSame(0, $banner->status);
  154. $this->assertSame('https://example.test/promotion', $banner->link);
  155. $this->assertSame(27, (int) $banner->sort);
  156. $this->assertSame('app', $banner->type);
  157. $this->assertSame('en', $banner->lang);
  158. $this->assertSame('/banner.png', $banner->getRawOriginal('image'));
  159. $this->assertSame('https://banner.example.test/banner.png', $banner->image);
  160. }
  161. public function test_status_endpoint_accepts_both_aliases_and_repeated_values_without_changing_banner_content(): void
  162. {
  163. $id = $this->banner(['sort' => 19, 'lang' => 'en', 'link' => 'https://example.test']);
  164. foreach (['status', 'manual_status'] as $field) {
  165. foreach ([0, 0, 1, 1, '0', '1'] as $status) {
  166. $result = $this->setStatus(['id' => $id, $field => $status]);
  167. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  168. $row = DB::table('banner')->find($id);
  169. $this->assertSame((int) $status, (int) $row->status);
  170. $this->assertSame('https://example.test', $row->link);
  171. $this->assertSame(19, (int) $row->sort);
  172. $this->assertSame('en', $row->lang);
  173. }
  174. }
  175. }
  176. public function test_status_takes_precedence_with_manual_status_as_fallback_in_forms_and_status_requests(): void
  177. {
  178. foreach ([
  179. [['manual_status' => 0], 0],
  180. [['status' => 1], 1],
  181. [['manual_status' => null], 1],
  182. [['status' => 1, 'manual_status' => null], 1],
  183. [['status' => 0, 'manual_status' => 1], 0],
  184. [['status' => 1, 'manual_status' => 0], 1],
  185. ] as [$fields, $expected]) {
  186. $result = $this->save($fields);
  187. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  188. $id = DB::table('banner')->latest('id')->value('id');
  189. $this->assertSame($expected, (int) DB::table('banner')->where('id', $id)->value('status'));
  190. }
  191. foreach ([[0, 1], [1, 0]] as [$canonical, $manual]) {
  192. $result = $this->setStatus(['id' => $id, 'status' => $canonical, 'manual_status' => $manual]);
  193. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  194. $this->assertSame($canonical, (int) DB::table('banner')->where('id', $id)->value('status'));
  195. $result = $this->save(['id' => $id, 'title' => '兼容完整编辑', 'status' => $canonical, 'manual_status' => $manual]);
  196. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  197. $this->assertSame($canonical, (int) DB::table('banner')->where('id', $id)->value('status'));
  198. }
  199. }
  200. public function test_unbound_list_row_edits_and_buttons_follow_changed_status_over_stale_manual_alias(): void
  201. {
  202. $ids = [];
  203. foreach ([null, '', '0', 'https://example.test/promotion'] as $link) {
  204. $ids[] = $this->banner(['link' => $link, 'status' => 1]);
  205. }
  206. $orderedIds = array_reverse($ids);
  207. $assertPublication = function (int $id, int $expected) use ($orderedIds): array {
  208. $rows = collect($this->listing()['data'])->keyBy('id')->all();
  209. $this->assertState($rows[$id], $expected, $expected, $expected ? null : 'manual', null);
  210. $this->assertSame($expected, (int) DB::table('banner')->where('id', $id)->value('status'));
  211. $downIds = $expected ? [] : [$id];
  212. $upIds = array_values(array_diff($orderedIds, $downIds));
  213. foreach ([['0', $downIds], ['1', $upIds]] as [$status, $expectedIds]) {
  214. $page = $this->listing(['status' => $status]);
  215. $this->assertSame(count($expectedIds), $page['total']);
  216. $this->assertSame($expectedIds, array_column($page['data'], 'id'));
  217. }
  218. return $rows[$id];
  219. };
  220. foreach ($ids as $id) {
  221. $row = $assertPublication($id, 1);
  222. $originalLink = $row['link'];
  223. $row['status'] = 0;
  224. $row['title'] = '列表编辑下架';
  225. $this->assertSame(1, $row['manual_status']);
  226. $result = $this->save($row);
  227. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  228. $row = $assertPublication($id, 0);
  229. $result = $this->setStatus(['id' => $id, 'status' => 1, 'manual_status' => $row['manual_status']]);
  230. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  231. $row = $assertPublication($id, 1);
  232. $result = $this->setStatus(['id' => $id, 'status' => 0, 'manual_status' => $row['manual_status']]);
  233. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  234. $row = $assertPublication($id, 0);
  235. $row['status'] = 1;
  236. $row['title'] = '列表编辑上架';
  237. $this->assertSame(0, $row['manual_status']);
  238. $result = $this->save($row);
  239. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  240. $row = $assertPublication($id, 1);
  241. $this->assertSame('列表编辑上架', $row['title']);
  242. $this->assertSame($originalLink, $row['link']);
  243. }
  244. }
  245. public function test_create_accepts_manual_off_and_invalid_publication_values_leave_rows_unchanged(): void
  246. {
  247. $result = $this->save(['status' => 0]);
  248. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  249. $id = DB::table('banner')->value('id');
  250. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  251. foreach (['status', 'manual_status'] as $field) {
  252. foreach ($field === 'status' ? [null, -1, 2, 'invalid'] : [-1, 2, 'invalid'] as $status) {
  253. $result = $this->save(['id' => $id, 'title' => '不应保存', $field => $status]);
  254. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  255. $row = DB::table('banner')->find($id);
  256. $this->assertSame(0, (int) $row->status);
  257. $this->assertSame('轮播图', $row->title);
  258. }
  259. }
  260. foreach ([['status' => 1, 'manual_status' => 'invalid'], ['status' => 'invalid', 'manual_status' => 1]] as $fields) {
  261. $result = $this->save($fields + ['id' => $id, 'title' => '不应保存']);
  262. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  263. $result = $this->setStatus($fields + ['id' => $id]);
  264. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  265. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  266. $this->assertSame('轮播图', DB::table('banner')->where('id', $id)->value('title'));
  267. }
  268. $result = $this->save(['id' => $id, 'manual_status' => null, 'title' => '空值保持手动设置']);
  269. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  270. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  271. $this->assertSame(1, DB::table('banner')->count());
  272. }
  273. public function test_status_endpoint_rejects_missing_invalid_status_and_missing_banner(): void
  274. {
  275. $id = $this->banner();
  276. foreach ([[], ['id' => $id], ['status' => 0], ['id' => $id, 'status' => -1],
  277. ['id' => $id, 'status' => 2], ['id' => $id, 'status' => 'invalid'],
  278. ['id' => $id, 'status' => null], ['id' => 0, 'status' => 0],
  279. ['id' => 'invalid', 'status' => 0], ['manual_status' => 0],
  280. ['id' => $id, 'manual_status' => null], ['id' => $id, 'manual_status' => -1],
  281. ['id' => $id, 'manual_status' => 2], ['id' => $id, 'manual_status' => 'invalid']] as $params) {
  282. $this->assertSame(HttpStatus::VALIDATION_FAILED, $this->setStatus($params)['code']);
  283. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  284. }
  285. $result = $this->setStatus(['id' => 999999, 'status' => 0]);
  286. $this->assertSame(HttpStatus::CUSTOM_ERROR, $result['code']);
  287. $this->assertStringContainsString('数据不存在', $result['msg']);
  288. $this->assertSame(1, DB::table('banner')->count());
  289. }
  290. public function test_bound_visibility_uses_raw_seconds_and_includes_start_but_excludes_end(): void
  291. {
  292. $now = strtotime('2026-10-24 12:30:00 UTC');
  293. $expected = [];
  294. foreach ([
  295. 1 => [$now - 1, $now + 1, 1, null],
  296. 2 => [$now + 1, $now + 120, 1, 'activity_not_started'],
  297. 3 => [$now - 120, $now - 1, 1, 'activity_ended'],
  298. 4 => [$now, $now + 1, 1, null],
  299. 5 => [$now - 1, $now, 1, 'activity_ended'],
  300. 6 => [$now - 1, $now + 1, 0, 'activity_offline'],
  301. ] as $activityId => [$start, $end, $status, $reason]) {
  302. $this->activity($activityId, $start, $end, $status);
  303. $id = $this->banner(['link' => (string) $activityId]);
  304. $expected[$id] = [$reason, (string) $activityId];
  305. }
  306. $id = $this->banner(['link' => '999']);
  307. $expected[$id] = ['activity_missing', '999'];
  308. $rows = $this->decorated($now);
  309. foreach ($expected as $id => [$reason, $activityId]) {
  310. $this->assertState($rows[$id], null, $reason === null ? 1 : 0, $reason, $activityId);
  311. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  312. }
  313. }
  314. public function test_null_empty_zero_and_url_links_follow_only_the_manual_setting(): void
  315. {
  316. $expected = [];
  317. foreach ([null, '', '0', '000', 'https://example.test/activity/7'] as $link) {
  318. foreach ([0, 1] as $manual) {
  319. $id = $this->banner(['link' => $link, 'status' => $manual]);
  320. $expected[$id] = $manual;
  321. }
  322. }
  323. DB::connection()->enableQueryLog();
  324. $rows = $this->decorated(1000);
  325. $queries = DB::connection()->getQueryLog();
  326. DB::connection()->disableQueryLog();
  327. foreach ($expected as $id => $manual) {
  328. $this->assertState($rows[$id], $manual, $manual, $manual ? null : 'manual', null);
  329. }
  330. $this->assertCount(1, $queries, 'Unbound banners should not query activities.');
  331. }
  332. public function test_bound_banners_ignore_archived_manual_status_across_activity_states(): void
  333. {
  334. $now = time();
  335. $this->activity(7, $now - 100, $now - 10);
  336. $on = $this->banner(['link' => '7', 'status' => 1]);
  337. $off = $this->banner(['link' => '7', 'status' => 0]);
  338. $rows = $this->decorated($now);
  339. $this->assertState($rows[$on], null, 0, 'activity_ended', '7');
  340. $this->assertState($rows[$off], null, 0, 'activity_ended', '7');
  341. DB::table('activity_rewards')->where('id', 7)->update(['end_time' => $now + 100]);
  342. $rows = $this->decorated($now);
  343. $this->assertState($rows[$on], null, 1, null, '7');
  344. $this->assertState($rows[$off], null, 1, null, '7');
  345. DB::table('activity_rewards')->where('id', 7)->update(['status' => 0]);
  346. $this->assertState($this->decorated($now)[$on], null, 0, 'activity_offline', '7');
  347. DB::table('activity_rewards')->where('id', 7)->update(['status' => 1]);
  348. $this->assertState($this->decorated($now)[$on], null, 1, null, '7');
  349. $this->assertSame(1, (int) DB::table('banner')->where('id', $on)->value('status'));
  350. $this->assertSame(0, (int) DB::table('banner')->where('id', $off)->value('status'));
  351. }
  352. public function test_get_expired_banner_reports_actual_off_and_full_row_edit_preserves_archive_for_reopening(): void
  353. {
  354. $now = time();
  355. $this->activity(7, $now - 120, $now - 60);
  356. $on = $this->banner(['link' => '7', 'status' => 1, 'sort' => 17, 'lang' => 'en']);
  357. $off = $this->banner(['link' => '7', 'status' => 0]);
  358. $manualOff = $this->banner(['link' => 'https://example.test', 'status' => 0]);
  359. $page = $this->listing();
  360. $rows = collect($page['data'])->keyBy('id')->all();
  361. $this->assertSame(3, $page['total']);
  362. $this->assertState($rows[$on], null, 0, 'activity_ended', '7');
  363. $this->assertState($rows[$off], null, 0, 'activity_ended', '7');
  364. $this->assertState($rows[$manualOff], 0, 0, 'manual', null);
  365. $this->assertSame(1, (int) DB::table('banner')->where('id', $on)->value('status'));
  366. // Resubmitted activity-derived status and null manual_status do not alter the archived database flag.
  367. $edited = $rows[$on];
  368. $edited['title'] = '过期活动轮播图编辑';
  369. $result = $this->save($edited);
  370. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  371. $stored = DB::table('banner')->find($on);
  372. $this->assertSame(1, (int) $stored->status);
  373. $this->assertSame('过期活动轮播图编辑', $stored->title);
  374. $this->assertSame('7', $stored->link);
  375. $this->assertSame(17, (int) $stored->sort);
  376. $this->assertSame('en', $stored->lang);
  377. $this->assertSame('/banner.png', $stored->image);
  378. $rows = collect($this->listing()['data'])->keyBy('id')->all();
  379. $this->assertState($rows[$on], null, 0, 'activity_ended', '7');
  380. DB::table('activity_rewards')->where('id', 7)->update(['end_time' => time() + 600]);
  381. $rows = collect($this->listing()['data'])->keyBy('id')->all();
  382. $this->assertState($rows[$on], null, 1, null, '7');
  383. $this->assertState($rows[$off], null, 1, null, '7');
  384. $this->assertState($rows[$manualOff], 0, 0, 'manual', null);
  385. $this->assertSame(1, (int) DB::table('banner')->where('id', $on)->value('status'));
  386. $this->assertSame(0, (int) DB::table('banner')->where('id', $off)->value('status'));
  387. }
  388. public function test_bound_banners_reject_both_manual_toggle_aliases_without_database_mutation(): void
  389. {
  390. $now = time();
  391. $this->activity(7, $now - 120, $now + 600);
  392. $this->activity(8, $now - 120, $now - 60);
  393. foreach ([['7', 0], ['8', 1]] as [$link, $archivedStatus]) {
  394. $id = $this->banner(['link' => $link, 'status' => $archivedStatus]);
  395. $before = (array) DB::table('banner')->find($id);
  396. foreach (['status', 'manual_status'] as $field) {
  397. foreach ([0, 1] as $value) {
  398. $result = $this->setStatus(['id' => $id, $field => $value]);
  399. $this->assertSame(HttpStatus::CUSTOM_ERROR, $result['code']);
  400. $this->assertStringContainsString('关联活动', $result['msg']);
  401. $this->assertStringContainsString('不能手动上下架', $result['msg']);
  402. $this->assertSame($before, (array) DB::table('banner')->find($id));
  403. }
  404. }
  405. foreach ([['status' => null], ['manual_status' => null], ['status' => 'invalid'], ['manual_status' => []]] as $fields) {
  406. $result = $this->setStatus(['id' => $id] + $fields);
  407. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  408. $this->assertSame($before, (array) DB::table('banner')->find($id));
  409. }
  410. }
  411. }
  412. public function test_bound_full_forms_ignore_status_inputs_and_new_bound_rows_keep_database_default_on(): void
  413. {
  414. $now = time();
  415. $this->activity(7, $now - 120, $now + 600);
  416. $id = $this->banner(['link' => '7', 'status' => 0]);
  417. foreach ([['status' => 1], ['manual_status' => 0], ['status' => 0, 'manual_status' => 1],
  418. ['status' => 0, 'manual_status' => null]] as $fields) {
  419. $result = $this->save($fields + ['id' => $id, 'title' => '活动控制上下架']);
  420. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  421. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  422. $this->assertState($this->decorated($now)[$id], null, 1, null, '7');
  423. }
  424. foreach ([['status' => 'invalid'], ['manual_status' => []], ['status' => null]] as $fields) {
  425. $result = $this->save($fields + ['id' => $id, 'title' => '不应保存']);
  426. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  427. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  428. $this->assertSame('活动控制上下架', DB::table('banner')->where('id', $id)->value('title'));
  429. }
  430. $result = $this->save(['link' => '7', 'status' => 0, 'manual_status' => 0]);
  431. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  432. $newId = DB::table('banner')->latest('id')->value('id');
  433. $this->assertSame(1, (int) DB::table('banner')->where('id', $newId)->value('status'));
  434. $this->assertState($this->decorated($now)[$newId], null, 1, null, '7');
  435. }
  436. public function test_clearing_activity_link_restores_manual_control_and_rebinding_ignores_manual_off_input(): void
  437. {
  438. $now = time();
  439. $this->activity(7, $now - 120, $now + 600);
  440. $id = $this->banner(['link' => '7', 'status' => 0]);
  441. $this->assertState($this->decorated($now)[$id], null, 1, null, '7');
  442. $result = $this->save(['id' => $id, 'link' => null]);
  443. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  444. $this->assertState($this->decorated($now)[$id], 0, 0, 'manual', null);
  445. $result = $this->setStatus(['id' => $id, 'manual_status' => 1]);
  446. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  447. $this->assertState($this->decorated($now)[$id], 1, 1, null, null);
  448. $result = $this->save(['id' => $id, 'link' => '7', 'status' => 0, 'manual_status' => 0]);
  449. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  450. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  451. $this->assertState($this->decorated($now)[$id], null, 1, null, '7');
  452. }
  453. public function test_integer_and_leading_zero_activity_links_are_validated_and_canonicalized(): void
  454. {
  455. $this->activity(7, 1, 4102444799);
  456. foreach ([7, '7', '0007'] as $link) {
  457. $result = $this->save(['link' => $link]);
  458. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  459. $this->assertSame('7', DB::table('banner')->latest('id')->value('link'));
  460. }
  461. foreach ([999, '000999'] as $link) {
  462. $result = $this->save(['link' => $link]);
  463. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  464. $this->assertStringContainsString('绑定的活动不存在', $result['msg']);
  465. }
  466. $this->assertSame(3, DB::table('banner')->count());
  467. $id = DB::table('banner')->value('id');
  468. $result = $this->save(['id' => $id, 'link' => '999']);
  469. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  470. $this->assertSame('7', DB::table('banner')->where('id', $id)->value('link'));
  471. $result = $this->save(['link' => ['id' => 7]]);
  472. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  473. $this->assertSame(3, DB::table('banner')->count());
  474. }
  475. public function test_stale_missing_activity_link_can_be_edited_but_cannot_be_manually_toggled(): void
  476. {
  477. $id = $this->banner(['link' => '500', 'sort' => 31]);
  478. $result = $this->save(['id' => $id, 'title' => '已删除活动的轮播图']);
  479. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  480. $this->assertSame('500', DB::table('banner')->where('id', $id)->value('link'));
  481. $this->assertSame(31, (int) DB::table('banner')->where('id', $id)->value('sort'));
  482. $result = $this->save(['id' => $id, 'link' => '000500', 'status' => 0]);
  483. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  484. $this->assertState($this->decorated(time())[$id], null, 0, 'activity_missing', '500');
  485. foreach (['status', 'manual_status'] as $field) {
  486. foreach ([0, 1] as $value) {
  487. $result = $this->setStatus(['id' => $id, $field => $value]);
  488. $this->assertSame(HttpStatus::CUSTOM_ERROR, $result['code']);
  489. $this->assertStringContainsString('关联活动', $result['msg']);
  490. $this->assertStringContainsString('不能手动上下架', $result['msg']);
  491. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  492. }
  493. }
  494. }
  495. public function test_admin_pagination_counts_hidden_rows_and_batches_activity_queries(): void
  496. {
  497. $now = time();
  498. $this->activity(7, $now - 100, $now + 100);
  499. $this->activity(8, $now - 100, $now - 10);
  500. for ($i = 0; $i < 24; $i++) {
  501. $this->banner(['title' => '轮播图 ' . $i, 'link' => $i % 2 ? '7' : '8',
  502. 'status' => $i % 3 ? 1 : 0, 'sort' => $i]);
  503. }
  504. DB::connection()->enableQueryLog();
  505. $page = $this->listing(['limit' => 20, 'page' => 1]);
  506. $queries = DB::connection()->getQueryLog();
  507. DB::connection()->disableQueryLog();
  508. $this->assertSame(24, $page['total']);
  509. $this->assertCount(20, $page['data']);
  510. $this->assertCount(3, $queries, 'Admin list should use one count, one page, and one batched activity query.');
  511. $this->assertSame(range(23, 4), array_column($page['data'], 'sort'));
  512. $this->assertNotEmpty(array_filter($page['data'], fn ($row) => $row['effective_status'] === 0));
  513. $this->assertNotEmpty(array_filter($page['data'], fn ($row) => $row['effective_status'] === 1));
  514. $last = $this->listing(['limit' => 20, 'page' => 2]);
  515. $this->assertSame(24, $last['total']);
  516. $this->assertCount(4, $last['data']);
  517. $this->assertSame(range(3, 0), array_column($last['data'], 'sort'));
  518. }
  519. public function test_admin_type_filter_and_sort_order_preserve_language_and_hidden_rows(): void
  520. {
  521. $first = $this->banner(['type' => 'app', 'lang' => 'en', 'sort' => 9, 'status' => 0]);
  522. $second = $this->banner(['type' => 'app', 'lang' => 'zh', 'sort' => 9]);
  523. $third = $this->banner(['type' => 'app', 'lang' => 'zh', 'sort' => 2]);
  524. $this->banner(['type' => 'pc', 'lang' => 'en', 'sort' => 100]);
  525. $page = $this->listing(['type' => 'app', 'limit' => 20]);
  526. $this->assertSame(3, $page['total']);
  527. $this->assertSame([$second, $first, $third], array_column($page['data'], 'id'));
  528. $this->assertSame(['zh', 'en', 'zh'], array_column($page['data'], 'lang'));
  529. $this->assertSame(['app', 'app', 'app'], array_column($page['data'], 'type'));
  530. $this->assertSame([1, 0, 1], array_column($page['data'], 'effective_status'));
  531. }
  532. public function test_platform_language_and_actual_status_filters_combine_without_mutating_data(): void
  533. {
  534. $now = time();
  535. $this->activity(7, $now - 100, $now + 600);
  536. $this->activity(8, $now - 100, $now - 60);
  537. $this->activity(9, $now + 600, $now + 1200);
  538. $this->activity(10, $now - 100, $now + 600, 0);
  539. foreach ([
  540. ['pc_zh_active', 'pc', 'zh', '7', 0, 110],
  541. ['pc_zh_ended', 'pc', 'zh', '8', 1, 100],
  542. ['pc_en_on', 'pc', 'en', null, 1, 90],
  543. ['pc_en_off', 'pc', 'en', null, 0, 80],
  544. ['app_zh_active', 'app', 'zh', '7', 0, 70],
  545. ['app_zh_ended', 'app', 'zh', '8', 1, 60],
  546. ['app_en_on', 'app', 'en', null, 1, 50],
  547. ['app_en_off', 'app', 'en', null, 0, 40],
  548. ['pc_zh_future', 'pc', 'zh', '9', 1, 30],
  549. ['pc_zh_offline', 'pc', 'zh', '10', 1, 20],
  550. ['pc_zh_missing', 'pc', 'zh', '999', 1, 10],
  551. ] as [$name, $type, $lang, $link, $manual, $sort]) {
  552. $ids[$name] = $this->banner(['type' => $type, 'lang' => $lang, 'link' => $link, 'status' => $manual, 'sort' => $sort]);
  553. }
  554. $bannerBefore = DB::table('banner')->orderBy('id')->get()->toJson();
  555. $activityBefore = DB::table('activity_rewards')->orderBy('id')->get()->toJson();
  556. $cases = [
  557. [[], array_keys($ids)],
  558. [['platform' => 'pc'], ['pc_zh_active', 'pc_zh_ended', 'pc_en_on', 'pc_en_off', 'pc_zh_future', 'pc_zh_offline', 'pc_zh_missing']],
  559. [['platform' => 'h5'], ['app_zh_active', 'app_zh_ended', 'app_en_on', 'app_en_off']],
  560. [['type' => 'app'], ['app_zh_active', 'app_zh_ended', 'app_en_on', 'app_en_off']],
  561. [['platform' => 'h5', 'type' => 'app'], ['app_zh_active', 'app_zh_ended', 'app_en_on', 'app_en_off']],
  562. [['platform' => 'pc', 'type' => 'pc', 'lang' => 'en'], ['pc_en_on', 'pc_en_off']],
  563. [['lang' => 'zh'], ['pc_zh_active', 'pc_zh_ended', 'app_zh_active', 'app_zh_ended', 'pc_zh_future', 'pc_zh_offline', 'pc_zh_missing']],
  564. [['lang' => 'en'], ['pc_en_on', 'pc_en_off', 'app_en_on', 'app_en_off']],
  565. [['status' => 1], ['pc_zh_active', 'pc_en_on', 'app_zh_active', 'app_en_on']],
  566. [['status' => '0'], ['pc_zh_ended', 'pc_en_off', 'app_zh_ended', 'app_en_off', 'pc_zh_future', 'pc_zh_offline', 'pc_zh_missing']],
  567. [['platform' => 'pc', 'lang' => 'zh', 'status' => 0], ['pc_zh_ended', 'pc_zh_future', 'pc_zh_offline', 'pc_zh_missing']],
  568. [['platform' => 'h5', 'lang' => 'en', 'status' => '1'], ['app_en_on']],
  569. [['platform' => 'h5', 'lang' => 'zh', 'status' => '0'], ['app_zh_ended']],
  570. [['platform' => 'h5', 'status' => 0], ['app_zh_ended', 'app_en_off']],
  571. [['platform' => 'pc', 'lang' => 'fr', 'status' => 1], []],
  572. ];
  573. foreach ($cases as [$filters, $names]) {
  574. $page = $this->listing($filters);
  575. $expectedIds = array_map(fn ($name) => $ids[$name], $names);
  576. $this->assertSame(count($expectedIds), $page['total']);
  577. $this->assertSame($expectedIds, array_column($page['data'], 'id'));
  578. if (isset($filters['platform'])) {
  579. foreach ($page['data'] as $row) $this->assertSame($filters['platform'] === 'h5' ? 'app' : 'pc', $row['type']);
  580. }
  581. if (array_key_exists('status', $filters)) {
  582. foreach ($page['data'] as $row) $this->assertSame((int) $filters['status'], $row['status']);
  583. }
  584. }
  585. $this->assertSame($bannerBefore, DB::table('banner')->orderBy('id')->get()->toJson());
  586. $this->assertSame($activityBefore, DB::table('activity_rewards')->orderBy('id')->get()->toJson());
  587. }
  588. public function test_actual_status_is_filtered_before_pagination_and_activity_lookup_stays_batched(): void
  589. {
  590. $now = time();
  591. $this->activity(7, $now - 100, $now + 600);
  592. $this->activity(8, $now - 100, $now - 60);
  593. $up = $down = [];
  594. for ($i = 0; $i < 12; $i++) {
  595. $id = $this->banner(['type' => 'app', 'lang' => 'zh', 'link' => $i % 2 ? '8' : '7', 'status' => $i % 2, 'sort' => $i]);
  596. if ($i % 2) $down[] = $id; else $up[] = $id;
  597. }
  598. $this->banner(['type' => 'app', 'lang' => 'en', 'link' => '7', 'sort' => 100]);
  599. $this->banner(['type' => 'pc', 'lang' => 'zh', 'link' => '7', 'sort' => 101]);
  600. $up = array_reverse($up); $down = array_reverse($down);
  601. $snapshot = DB::table('banner')->orderBy('id')->get()->toJson();
  602. $filters = ['platform' => 'h5', 'lang' => 'zh', 'status' => 1, 'limit' => 2];
  603. DB::connection()->enableQueryLog();
  604. $first = $this->listing($filters);
  605. $queries = DB::connection()->getQueryLog();
  606. DB::connection()->disableQueryLog();
  607. $this->assertCount(2, $queries, 'Status filtering should fetch matching candidates and all referenced activities in two queries.');
  608. $this->assertSame(6, $first['total']);
  609. $this->assertSame(array_slice($up, 0, 2), array_column($first['data'], 'id'));
  610. $seen = array_column($first['data'], 'id');
  611. foreach ([2, 3, 4] as $number) {
  612. $page = $this->listing($filters + ['page' => $number]);
  613. $this->assertSame(6, $page['total']);
  614. $this->assertSame(array_slice($up, ($number - 1) * 2, 2), array_column($page['data'], 'id'));
  615. $seen = array_merge($seen, array_column($page['data'], 'id'));
  616. }
  617. $this->assertSame($up, $seen);
  618. foreach ([1, 2] as $number) {
  619. $page = $this->listing(['platform' => 'h5', 'type' => 'app', 'lang' => 'zh', 'status' => '0', 'page' => $number, 'limit' => 4]);
  620. $this->assertSame(6, $page['total']);
  621. $this->assertSame(array_slice($down, ($number - 1) * 4, 4), array_column($page['data'], 'id'));
  622. }
  623. $this->assertSame($snapshot, DB::table('banner')->orderBy('id')->get()->toJson());
  624. }
  625. public function test_empty_optional_filters_keep_all_languages_and_default_pagination(): void
  626. {
  627. for ($i = 0; $i < 16; $i++) {
  628. $ids[] = $this->banner(['type' => $i % 2 ? 'pc' : 'app', 'lang' => $i % 2 ? 'zh' : 'en', 'status' => $i % 2]);
  629. }
  630. $ids = array_reverse($ids);
  631. $snapshot = DB::table('banner')->orderBy('id')->get()->toJson();
  632. foreach ([[], ['platform' => null, 'type' => null, 'lang' => null, 'status' => null],
  633. ['platform' => '', 'type' => '', 'lang' => '', 'status' => '']] as $filters) {
  634. $page = $this->listing($filters);
  635. $this->assertSame(16, $page['total']);
  636. $this->assertCount(15, $page['data']);
  637. $this->assertSame(array_slice($ids, 0, 15), array_column($page['data'], 'id'));
  638. $this->assertSame(['zh', 'en'], array_values(array_unique(array_column($page['data'], 'lang'))));
  639. $last = $this->listing($filters + ['page' => 2]);
  640. $this->assertSame(16, $last['total']);
  641. $this->assertSame(array_slice($ids, 15), array_column($last['data'], 'id'));
  642. }
  643. $largePage = $this->listing(['limit' => 200]);
  644. $this->assertSame(16, $largePage['total']);
  645. $this->assertSame($ids, array_column($largePage['data'], 'id'));
  646. $this->assertSame($snapshot, DB::table('banner')->orderBy('id')->get()->toJson());
  647. }
  648. public function test_invalid_filters_and_conflicting_platform_aliases_are_rejected_without_mutation(): void
  649. {
  650. $this->banner(['type' => 'app', 'lang' => 'en']);
  651. $snapshot = DB::table('banner')->get()->toJson();
  652. foreach ([
  653. ['platform' => 'app'], ['platform' => 'android'], ['platform' => ['pc']],
  654. ['type' => 'h5'], ['type' => ['app']],
  655. ['platform' => 'pc', 'type' => 'app'], ['platform' => 'h5', 'type' => 'pc'],
  656. ['lang' => ['zh']], ['lang' => 123], ['lang' => 'abcdefghijk'],
  657. ['status' => -1], ['status' => 2], ['status' => 'up'], ['status' => '1.5'], ['status' => ['0']],
  658. ['page' => 0], ['limit' => 0], ['limit' => 201],
  659. ] as $filters) {
  660. $this->request('index', $filters);
  661. $result = (new AdminBanner())->index()->getData(true);
  662. $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
  663. }
  664. $this->assertSame($snapshot, DB::table('banner')->get()->toJson());
  665. }
  666. public function test_status_route_requires_existing_banner_edit_permission_and_preserves_superadmin_access(): void
  667. {
  668. Schema::create('menus', function (Blueprint $t) {
  669. $t->id(); $t->string('uri'); $t->integer('type'); $t->integer('status');
  670. });
  671. Schema::create('roles', function (Blueprint $t) {
  672. $t->id(); $t->string('name');
  673. });
  674. Schema::create('role_user', function (Blueprint $t) {
  675. $t->id(); $t->integer('user_id'); $t->integer('role_id');
  676. });
  677. Schema::create('role_menu', function (Blueprint $t) {
  678. $t->id(); $t->integer('role_id'); $t->integer('menu_id');
  679. });
  680. $editMenu = DB::table('menus')->insertGetId([
  681. 'uri' => 'admin/banner/update', 'type' => 2, 'status' => 1,
  682. ]);
  683. $otherMenu = DB::table('menus')->insertGetId([
  684. 'uri' => 'admin/banner/delete', 'type' => 2, 'status' => 1,
  685. ]);
  686. $router = new Router(new Dispatcher($this->app), $this->app);
  687. $this->app->instance('router', $router);
  688. $router->group(['prefix' => 'admin'], function () {
  689. require dirname(__DIR__, 2) . '/routes/admin.php';
  690. });
  691. $route = $router->getRoutes()->match(Request::create('/admin/banner/status', 'POST'));
  692. $this->assertSame('admin/banner/update', $route->defaults['permission_uri']);
  693. $this->assertContains('admin.jwt', $route->middleware());
  694. $this->assertContains('check.button.uri', $route->middleware());
  695. $this->assertSame(AdminBanner::class . '@status', $route->getAction('uses'));
  696. $id = $this->banner();
  697. $changeStatus = function (int $userId, int $status) use ($router, $id): array {
  698. $this->request('status', ['id' => $id, 'manual_status' => $status, 'user' => (object) ['id' => $userId]]);
  699. $request = $this->app['request'];
  700. $route = $router->getRoutes()->match($request);
  701. $request->setRouteResolver(fn () => $route);
  702. return (new CheckButtonPermission())->handle($request,
  703. fn () => (new AdminBanner())->status())->getData(true);
  704. };
  705. // The status URI has no separate menu: it must inherit the existing edit button gate.
  706. $this->assertSame(0, DB::table('menus')->where('uri', 'admin/banner/status')->count());
  707. $result = $changeStatus(20, 0);
  708. $this->assertSame(-1, $result['code']);
  709. $this->assertSame('无权限访问', $result['msg']);
  710. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  711. DB::table('roles')->insert(['id' => 5, 'name' => 'editor']);
  712. DB::table('role_user')->insert(['user_id' => 20, 'role_id' => 5]);
  713. DB::table('role_menu')->insert(['role_id' => 5, 'menu_id' => $otherMenu]);
  714. $this->assertSame(-1, $changeStatus(20, 0)['code']);
  715. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  716. DB::table('role_menu')->insert(['role_id' => 5, 'menu_id' => $editMenu]);
  717. $result = $changeStatus(20, 0);
  718. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  719. $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
  720. $result = $changeStatus(1, 1);
  721. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  722. $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
  723. $this->assertSame(0, DB::table('role_user')->where('user_id', 1)->count());
  724. }
  725. public function test_click_types_save_external_and_dual_internal_links_without_activity_binding(): void
  726. {
  727. $result = $this->save(['click_type' => 'external', 'link' => 'https://example.test/promo', 'status' => 0]);
  728. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  729. $id = (int) DB::table('banner')->max('id');
  730. $row = collect($this->listing()['data'])->firstWhere('id', $id);
  731. $this->assertSame(['external', 'https://example.test/promo', null, null],
  732. [$row['click_type'], $row['link'], $row['link_pc'], $row['link_h5']]);
  733. $this->assertState($row, 0, 0, 'manual', null);
  734. $result = $this->save(['id' => $id, 'click_type' => 'internal', 'link_pc' => '/pc/promo?id=1', 'link_h5' => '/h5/promo']);
  735. $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
  736. $saved = DB::table('banner')->find($id);
  737. $this->assertSame(['internal', null, '/pc/promo?id=1', '/h5/promo'], [$saved->click_type, $saved->link, $saved->link_pc, $saved->link_h5]);
  738. // Omitting every link field keeps the saved paths; numeric-looking internal paths never bind an activity.
  739. $this->assertSame(HttpStatus::OK, $this->save(['id' => $id, 'title' => '只改标题'])['code']);
  740. $this->assertSame('/h5/promo', DB::table('banner')->where('id', $id)->value('link_h5'));
  741. $this->assertSame(HttpStatus::OK, $this->setStatus(['id' => $id, 'status' => 1])['code']);
  742. $this->assertSame(HttpStatus::OK, $this->save(['id' => $id, 'click_type' => 'none'])['code']);
  743. $saved = DB::table('banner')->find($id);
  744. $this->assertSame(['none', null, null, null], [$saved->click_type, $saved->link, $saved->link_pc, $saved->link_h5]);
  745. }
  746. public function test_activity_click_type_requires_existing_id_and_invalid_links_leave_rows_unchanged(): void
  747. {
  748. $this->activity(9, 0, PHP_INT_MAX);
  749. $this->assertSame(HttpStatus::OK, $this->save(['click_type' => 'activity', 'link' => '009'])['code']);
  750. $id = (int) DB::table('banner')->max('id');
  751. $saved = DB::table('banner')->find($id);
  752. $this->assertSame(['activity', '9'], [$saved->click_type, $saved->link]);
  753. $this->assertSame(-3, $this->setStatus(['id' => $id, 'status' => 0])['code']);
  754. foreach ([
  755. ['click_type' => 'activity', 'link' => ''],
  756. ['click_type' => 'activity', 'link' => '404'],
  757. ['click_type' => 'external', 'link' => '/relative'],
  758. ['click_type' => 'external', 'link' => 'javascript:alert(1)'],
  759. ['click_type' => 'internal', 'link_pc' => '/pc'],
  760. ['click_type' => 'internal', 'link_pc' => '//evil.test', 'link_h5' => '/h5'],
  761. ['click_type' => 'jump'],
  762. ] as $params) {
  763. $this->assertSame(HttpStatus::VALIDATION_FAILED, $this->save(['id' => $id] + $params)['code'], json_encode($params));
  764. $this->assertSame(['activity', '9'], [DB::table('banner')->where('id', $id)->value('click_type'), DB::table('banner')->where('id', $id)->value('link')]);
  765. }
  766. }
  767. public function test_legacy_link_only_requests_and_unclassified_rows_infer_the_click_type(): void
  768. {
  769. $this->activity(5, 0, PHP_INT_MAX);
  770. $this->assertSame(HttpStatus::OK, $this->save(['link' => 5])['code']);
  771. $this->assertSame(HttpStatus::OK, $this->save(['link' => '/h5/legacy'])['code']);
  772. $this->assertSame(HttpStatus::OK, $this->save(['link' => 'https://example.test/x'])['code']);
  773. $this->assertSame(HttpStatus::OK, $this->save(['link' => null])['code']);
  774. $types = DB::table('banner')->orderBy('id')->get(['click_type', 'link', 'link_pc', 'link_h5'])->map(fn ($r) => (array) $r)->all();
  775. $this->assertSame([
  776. ['click_type' => 'activity', 'link' => '5', 'link_pc' => null, 'link_h5' => null],
  777. ['click_type' => 'internal', 'link' => '/h5/legacy', 'link_pc' => '/h5/legacy', 'link_h5' => '/h5/legacy'],
  778. ['click_type' => 'external', 'link' => 'https://example.test/x', 'link_pc' => null, 'link_h5' => null],
  779. ['click_type' => 'none', 'link' => null, 'link_pc' => null, 'link_h5' => null],
  780. ], $types);
  781. $legacy = $this->banner(['link' => '5', 'click_type' => null]);
  782. $row = collect($this->listing()['data'])->firstWhere('id', $legacy);
  783. $this->assertSame(['activity', '5', '5'], [$row['click_type'], $row['link'], $row['activity_id']]);
  784. }
  785. }