|
@@ -0,0 +1,432 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace Tests\Integration;
|
|
|
|
|
+
|
|
|
|
|
+use App\Constants\HttpStatus;
|
|
|
|
|
+use App\Http\Controllers\admin\Banner as AdminBanner;
|
|
|
|
|
+use App\Http\Middleware\CheckButtonPermission;
|
|
|
|
|
+use App\Models\Banner;
|
|
|
|
|
+use App\Services\BannerVisibilityService;
|
|
|
|
|
+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\Support\Facades\Schema;
|
|
|
|
|
+use Illuminate\Translation\ArrayLoader;
|
|
|
|
|
+use Illuminate\Translation\Translator;
|
|
|
|
|
+use Illuminate\Validation\Factory;
|
|
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
|
|
+
|
|
|
|
|
+/** Exercises real banner controllers and queries against an isolated, prefixed SQLite database. */
|
|
|
|
|
+class BannerPublicationTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ private Application $app;
|
|
|
|
|
+
|
|
|
|
|
+ protected function setUp(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
|
|
+ $this->app = new Application(dirname(__DIR__, 2));
|
|
|
|
|
+ $this->app->instance('config', new Repository([
|
|
|
|
|
+ 'app' => ['locale' => 'zh', 'url' => 'https://banner.example.test', '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();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ $db->schema()->create('banner', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('title'); $t->string('image'); $t->string('link')->nullable();
|
|
|
|
|
+ $t->string('type'); $t->integer('sort')->default(0); $t->string('lang'); $t->timestamps();
|
|
|
|
|
+ });
|
|
|
|
|
+ $this->migration()->up();
|
|
|
|
|
+ $db->schema()->create('activity_rewards', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->integer('status')->default(1);
|
|
|
|
|
+ $t->integer('start_time'); $t->integer('end_time');
|
|
|
|
|
+ });
|
|
|
|
|
+ $this->request('index');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected function tearDown(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ DB::disconnect();
|
|
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
|
|
+ parent::tearDown();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function migration()
|
|
|
|
|
+ {
|
|
|
|
|
+ return require dirname(__DIR__, 2) . '/database/migrations/2026_10_24_140000_add_status_to_banner.php';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function request(string $action, array $params = []): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->app->instance('request', Request::create('/admin/banner/' . $action,
|
|
|
|
|
+ $action === 'index' ? 'GET' : 'POST', $params));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function form(array $overrides = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return $overrides + [
|
|
|
|
|
+ 'title' => '轮播图', 'image' => '/banner.png', 'type' => 'pc', 'lang' => 'zh',
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function save(array $params = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->request('update', $this->form($params));
|
|
|
|
|
+ return (new AdminBanner())->update()->getData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function status(array $params): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->request('status', $params);
|
|
|
|
|
+ return (new AdminBanner())->status()->getData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function listing(array $params = []): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->request('index', $params);
|
|
|
|
|
+ $result = (new AdminBanner())->index()->getData(true);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ return $result['data'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function banner(array $params = []): int
|
|
|
|
|
+ {
|
|
|
|
|
+ return DB::table('banner')->insertGetId($params + $this->form() + ['link' => null, 'sort' => 0, 'status' => 1]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function activity(int $id, int $start, int $end, int $status = 1): void
|
|
|
|
|
+ {
|
|
|
|
|
+ DB::table('activity_rewards')->insert([
|
|
|
|
|
+ 'id' => $id, 'status' => $status, 'start_time' => $start, 'end_time' => $end,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function decorated(int $now): array
|
|
|
|
|
+ {
|
|
|
|
|
+ return collect(BannerVisibilityService::decorate(Banner::query()->orderBy('id')->get(), $now))->keyBy('id')->all();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function assertState(array $row, int $manual, int $effective, ?string $reason, ?string $activityId): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->assertSame($manual, $row['status']);
|
|
|
|
|
+ $this->assertSame($effective, $row['effective_status']);
|
|
|
|
|
+ $this->assertSame($reason, $row['offline_reason']);
|
|
|
|
|
+ $this->assertSame($activityId, $row['activity_id']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_migration_publishes_existing_banners_by_default_and_rolls_back_only_its_column(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->migration()->down();
|
|
|
|
|
+ $this->assertFalse(Schema::hasColumn('banner', 'status'));
|
|
|
|
|
+ $id = DB::table('banner')->insertGetId($this->form(['title' => '历史轮播图', 'sort' => 23]) + ['link' => 'https://example.test']);
|
|
|
|
|
+ $this->migration()->up();
|
|
|
|
|
+ $row = DB::table('banner')->find($id);
|
|
|
|
|
+ $this->assertSame(1, (int) $row->status);
|
|
|
|
|
+ $this->assertSame('历史轮播图', $row->title);
|
|
|
|
|
+ $this->assertSame(23, (int) $row->sort);
|
|
|
|
|
+ $this->assertSame('https://example.test', $row->link);
|
|
|
|
|
+ $newId = DB::table('banner')->insertGetId($this->form());
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $newId)->value('status'));
|
|
|
|
|
+ $this->migration()->down();
|
|
|
|
|
+ $this->assertFalse(Schema::hasColumn('banner', 'status'));
|
|
|
|
|
+ $this->assertSame(2, DB::table('banner')->count());
|
|
|
|
|
+ $this->assertSame('历史轮播图', DB::table('banner')->where('id', $id)->value('title'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_create_defaults_to_manual_on_and_legacy_edit_preserves_omitted_status_link_and_sort(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $result = $this->save(['sort' => 27, 'link' => 'https://example.test/promotion', 'type' => 'app', 'lang' => 'en']);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $banner = Banner::query()->firstOrFail();
|
|
|
|
|
+ $this->assertSame(1, $banner->status);
|
|
|
|
|
+ $this->assertSame('app', $banner->type);
|
|
|
|
|
+ $this->assertSame('en', $banner->lang);
|
|
|
|
|
+ $this->assertSame(27, (int) $banner->sort);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $this->status(['id' => $banner->id, 'status' => 0])['code']);
|
|
|
|
|
+ $result = $this->save(['id' => $banner->id, 'title' => '编辑标题', 'type' => 'app', 'lang' => 'en']);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $banner->refresh();
|
|
|
|
|
+ $this->assertSame('编辑标题', $banner->title);
|
|
|
|
|
+ $this->assertSame(0, $banner->status);
|
|
|
|
|
+ $this->assertSame('https://example.test/promotion', $banner->link);
|
|
|
|
|
+ $this->assertSame(27, (int) $banner->sort);
|
|
|
|
|
+ $this->assertSame('app', $banner->type);
|
|
|
|
|
+ $this->assertSame('en', $banner->lang);
|
|
|
|
|
+ $this->assertSame('/banner.png', $banner->getRawOriginal('image'));
|
|
|
|
|
+ $this->assertSame('https://banner.example.test/banner.png', $banner->image);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_status_endpoint_accepts_repeated_zero_and_one_without_changing_banner_content(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $id = $this->banner(['sort' => 19, 'lang' => 'en', 'link' => 'https://example.test']);
|
|
|
|
|
+ foreach ([0, 0, 1, 1, '0', '1'] as $status) {
|
|
|
|
|
+ $result = $this->status(['id' => $id, 'status' => $status]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $row = DB::table('banner')->find($id);
|
|
|
|
|
+ $this->assertSame((int) $status, (int) $row->status);
|
|
|
|
|
+ $this->assertSame('https://example.test', $row->link);
|
|
|
|
|
+ $this->assertSame(19, (int) $row->sort);
|
|
|
|
|
+ $this->assertSame('en', $row->lang);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_create_accepts_manual_off_and_invalid_publication_values_leave_rows_unchanged(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $result = $this->save(['status' => 0]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $id = DB::table('banner')->value('id');
|
|
|
|
|
+ $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+ foreach ([null, -1, 2, 'invalid'] as $status) {
|
|
|
|
|
+ $result = $this->save(['id' => $id, 'title' => '不应保存', 'status' => $status]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
|
|
|
|
|
+ $row = DB::table('banner')->find($id);
|
|
|
|
|
+ $this->assertSame(0, (int) $row->status);
|
|
|
|
|
+ $this->assertSame('轮播图', $row->title);
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->assertSame(1, DB::table('banner')->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_status_endpoint_rejects_missing_invalid_status_and_missing_banner(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $id = $this->banner();
|
|
|
|
|
+ foreach ([[], ['id' => $id], ['status' => 0], ['id' => $id, 'status' => -1],
|
|
|
|
|
+ ['id' => $id, 'status' => 2], ['id' => $id, 'status' => 'invalid'],
|
|
|
|
|
+ ['id' => $id, 'status' => null], ['id' => 0, 'status' => 0],
|
|
|
|
|
+ ['id' => 'invalid', 'status' => 0]] as $params) {
|
|
|
|
|
+ $this->assertSame(HttpStatus::VALIDATION_FAILED, $this->status($params)['code']);
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+ }
|
|
|
|
|
+ $result = $this->status(['id' => 999999, 'status' => 0]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::CUSTOM_ERROR, $result['code']);
|
|
|
|
|
+ $this->assertStringContainsString('数据不存在', $result['msg']);
|
|
|
|
|
+ $this->assertSame(1, DB::table('banner')->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_bound_visibility_uses_raw_seconds_and_includes_start_but_excludes_end(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $now = strtotime('2026-10-24 12:30:00 UTC');
|
|
|
|
|
+ $expected = [];
|
|
|
|
|
+ foreach ([
|
|
|
|
|
+ 1 => [$now - 1, $now + 1, 1, null],
|
|
|
|
|
+ 2 => [$now + 1, $now + 120, 1, 'activity_not_started'],
|
|
|
|
|
+ 3 => [$now - 120, $now - 1, 1, 'activity_ended'],
|
|
|
|
|
+ 4 => [$now, $now + 1, 1, null],
|
|
|
|
|
+ 5 => [$now - 1, $now, 1, 'activity_ended'],
|
|
|
|
|
+ 6 => [$now - 1, $now + 1, 0, 'activity_offline'],
|
|
|
|
|
+ ] as $activityId => [$start, $end, $status, $reason]) {
|
|
|
|
|
+ $this->activity($activityId, $start, $end, $status);
|
|
|
|
|
+ $id = $this->banner(['link' => (string) $activityId]);
|
|
|
|
|
+ $expected[$id] = [$reason, (string) $activityId];
|
|
|
|
|
+ }
|
|
|
|
|
+ $id = $this->banner(['link' => '999']);
|
|
|
|
|
+ $expected[$id] = ['activity_missing', '999'];
|
|
|
|
|
+ $rows = $this->decorated($now);
|
|
|
|
|
+ foreach ($expected as $id => [$reason, $activityId]) {
|
|
|
|
|
+ $this->assertState($rows[$id], 1, $reason === null ? 1 : 0, $reason, $activityId);
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_null_empty_zero_and_url_links_follow_only_the_manual_setting(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $expected = [];
|
|
|
|
|
+ foreach ([null, '', '0', '000', 'https://example.test/activity/7'] as $link) {
|
|
|
|
|
+ foreach ([0, 1] as $manual) {
|
|
|
|
|
+ $id = $this->banner(['link' => $link, 'status' => $manual]);
|
|
|
|
|
+ $expected[$id] = $manual;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ DB::connection()->enableQueryLog();
|
|
|
|
|
+ $rows = $this->decorated(1000);
|
|
|
|
|
+ $queries = DB::connection()->getQueryLog();
|
|
|
|
|
+ DB::connection()->disableQueryLog();
|
|
|
|
|
+ foreach ($expected as $id => $manual) {
|
|
|
|
|
+ $this->assertState($rows[$id], $manual, $manual, $manual ? null : 'manual', null);
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->assertCount(1, $queries, 'Unbound banners should not query activities.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_manual_on_cannot_override_ended_activity_and_reopening_respects_manual_off(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $now = time();
|
|
|
|
|
+ $this->activity(7, $now - 100, $now - 10);
|
|
|
|
|
+ $on = $this->banner(['link' => '7', 'status' => 0]);
|
|
|
|
|
+ $off = $this->banner(['link' => '7', 'status' => 0]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $this->status(['id' => $on, 'status' => 1])['code']);
|
|
|
|
|
+ $rows = $this->decorated($now);
|
|
|
|
|
+ $this->assertState($rows[$on], 1, 0, 'activity_ended', '7');
|
|
|
|
|
+ $this->assertState($rows[$off], 0, 0, 'manual', '7');
|
|
|
|
|
+ DB::table('activity_rewards')->where('id', 7)->update(['end_time' => $now + 100]);
|
|
|
|
|
+ $rows = $this->decorated($now);
|
|
|
|
|
+ $this->assertState($rows[$on], 1, 1, null, '7');
|
|
|
|
|
+ $this->assertState($rows[$off], 0, 0, 'manual', '7');
|
|
|
|
|
+ DB::table('activity_rewards')->where('id', 7)->update(['status' => 0]);
|
|
|
|
|
+ $this->assertState($this->decorated($now)[$on], 1, 0, 'activity_offline', '7');
|
|
|
|
|
+ DB::table('activity_rewards')->where('id', 7)->update(['status' => 1]);
|
|
|
|
|
+ $this->assertState($this->decorated($now)[$on], 1, 1, null, '7');
|
|
|
|
|
+ $this->assertSame(0, (int) DB::table('banner')->where('id', $off)->value('status'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_integer_and_leading_zero_activity_links_are_validated_and_canonicalized(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->activity(7, 1, 4102444799);
|
|
|
|
|
+ foreach ([7, '7', '0007'] as $link) {
|
|
|
|
|
+ $result = $this->save(['link' => $link]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $this->assertSame('7', DB::table('banner')->latest('id')->value('link'));
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ([999, '000999'] as $link) {
|
|
|
|
|
+ $result = $this->save(['link' => $link]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
|
|
|
|
|
+ $this->assertStringContainsString('绑定的活动不存在', $result['msg']);
|
|
|
|
|
+ }
|
|
|
|
|
+ $this->assertSame(3, DB::table('banner')->count());
|
|
|
|
|
+ $id = DB::table('banner')->value('id');
|
|
|
|
|
+ $result = $this->save(['id' => $id, 'link' => '999']);
|
|
|
|
|
+ $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
|
|
|
|
|
+ $this->assertSame('7', DB::table('banner')->where('id', $id)->value('link'));
|
|
|
|
|
+ $result = $this->save(['link' => ['id' => 7]]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::VALIDATION_FAILED, $result['code']);
|
|
|
|
|
+ $this->assertSame(3, DB::table('banner')->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_stale_missing_activity_link_can_be_edited_unchanged_and_taken_offline(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $id = $this->banner(['link' => '500', 'sort' => 31]);
|
|
|
|
|
+ $result = $this->save(['id' => $id, 'title' => '已删除活动的轮播图']);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $this->assertSame('500', DB::table('banner')->where('id', $id)->value('link'));
|
|
|
|
|
+ $this->assertSame(31, (int) DB::table('banner')->where('id', $id)->value('sort'));
|
|
|
|
|
+ $result = $this->save(['id' => $id, 'link' => '000500', 'status' => 0]);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $this->assertState($this->decorated(time())[$id], 0, 0, 'manual', '500');
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $this->status(['id' => $id, 'status' => 1])['code']);
|
|
|
|
|
+ $this->assertState($this->decorated(time())[$id], 1, 0, 'activity_missing', '500');
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $this->status(['id' => $id, 'status' => 0])['code']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_admin_pagination_counts_hidden_rows_and_batches_activity_queries(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $now = time();
|
|
|
|
|
+ $this->activity(7, $now - 100, $now + 100);
|
|
|
|
|
+ $this->activity(8, $now - 100, $now - 10);
|
|
|
|
|
+ for ($i = 0; $i < 24; $i++) {
|
|
|
|
|
+ $this->banner(['title' => '轮播图 ' . $i, 'link' => $i % 2 ? '7' : '8',
|
|
|
|
|
+ 'status' => $i % 3 ? 1 : 0, 'sort' => $i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ DB::connection()->enableQueryLog();
|
|
|
|
|
+ $page = $this->listing(['limit' => 20, 'page' => 1]);
|
|
|
|
|
+ $queries = DB::connection()->getQueryLog();
|
|
|
|
|
+ DB::connection()->disableQueryLog();
|
|
|
|
|
+ $this->assertSame(24, $page['total']);
|
|
|
|
|
+ $this->assertCount(20, $page['data']);
|
|
|
|
|
+ $this->assertCount(3, $queries, 'Admin list should use one count, one page, and one batched activity query.');
|
|
|
|
|
+ $this->assertSame(range(23, 4), array_column($page['data'], 'sort'));
|
|
|
|
|
+ $this->assertNotEmpty(array_filter($page['data'], fn ($row) => $row['effective_status'] === 0));
|
|
|
|
|
+ $this->assertNotEmpty(array_filter($page['data'], fn ($row) => $row['effective_status'] === 1));
|
|
|
|
|
+ $last = $this->listing(['limit' => 20, 'page' => 2]);
|
|
|
|
|
+ $this->assertSame(24, $last['total']);
|
|
|
|
|
+ $this->assertCount(4, $last['data']);
|
|
|
|
|
+ $this->assertSame(range(3, 0), array_column($last['data'], 'sort'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_admin_type_filter_and_sort_order_preserve_language_and_hidden_rows(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $first = $this->banner(['type' => 'app', 'lang' => 'en', 'sort' => 9, 'status' => 0]);
|
|
|
|
|
+ $second = $this->banner(['type' => 'app', 'lang' => 'zh', 'sort' => 9]);
|
|
|
|
|
+ $third = $this->banner(['type' => 'app', 'lang' => 'zh', 'sort' => 2]);
|
|
|
|
|
+ $this->banner(['type' => 'pc', 'lang' => 'en', 'sort' => 100]);
|
|
|
|
|
+ $page = $this->listing(['type' => 'app', 'limit' => 20]);
|
|
|
|
|
+ $this->assertSame(3, $page['total']);
|
|
|
|
|
+ $this->assertSame([$second, $first, $third], array_column($page['data'], 'id'));
|
|
|
|
|
+ $this->assertSame(['zh', 'en', 'zh'], array_column($page['data'], 'lang'));
|
|
|
|
|
+ $this->assertSame(['app', 'app', 'app'], array_column($page['data'], 'type'));
|
|
|
|
|
+ $this->assertSame([1, 0, 1], array_column($page['data'], 'effective_status'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_status_route_requires_existing_banner_edit_permission_and_preserves_superadmin_access(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ Schema::create('menus', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('uri'); $t->integer('type'); $t->integer('status');
|
|
|
|
|
+ });
|
|
|
|
|
+ Schema::create('roles', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('name');
|
|
|
|
|
+ });
|
|
|
|
|
+ Schema::create('role_user', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->integer('user_id'); $t->integer('role_id');
|
|
|
|
|
+ });
|
|
|
|
|
+ Schema::create('role_menu', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->integer('role_id'); $t->integer('menu_id');
|
|
|
|
|
+ });
|
|
|
|
|
+ $editMenu = DB::table('menus')->insertGetId([
|
|
|
|
|
+ 'uri' => 'admin/banner/update', 'type' => 2, 'status' => 1,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $otherMenu = DB::table('menus')->insertGetId([
|
|
|
|
|
+ 'uri' => 'admin/banner/delete', 'type' => 2, 'status' => 1,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ $router = new Router(new Dispatcher($this->app), $this->app);
|
|
|
|
|
+ $this->app->instance('router', $router);
|
|
|
|
|
+ $router->group(['prefix' => 'admin'], function () {
|
|
|
|
|
+ require dirname(__DIR__, 2) . '/routes/admin.php';
|
|
|
|
|
+ });
|
|
|
|
|
+ $route = $router->getRoutes()->match(Request::create('/admin/banner/status', 'POST'));
|
|
|
|
|
+ $this->assertSame('admin/banner/update', $route->defaults['permission_uri']);
|
|
|
|
|
+ $this->assertContains('admin.jwt', $route->middleware());
|
|
|
|
|
+ $this->assertContains('check.button.uri', $route->middleware());
|
|
|
|
|
+ $this->assertSame(AdminBanner::class . '@status', $route->getAction('uses'));
|
|
|
|
|
+ $id = $this->banner();
|
|
|
|
|
+ $changeStatus = function (int $userId, int $status) use ($router, $id): array {
|
|
|
|
|
+ $this->request('status', ['id' => $id, 'status' => $status, 'user' => (object) ['id' => $userId]]);
|
|
|
|
|
+ $request = $this->app['request'];
|
|
|
|
|
+ $route = $router->getRoutes()->match($request);
|
|
|
|
|
+ $request->setRouteResolver(fn () => $route);
|
|
|
|
|
+ return (new CheckButtonPermission())->handle($request,
|
|
|
|
|
+ fn () => (new AdminBanner())->status())->getData(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // The status URI has no separate menu: it must inherit the existing edit button gate.
|
|
|
|
|
+ $this->assertSame(0, DB::table('menus')->where('uri', 'admin/banner/status')->count());
|
|
|
|
|
+ $result = $changeStatus(20, 0);
|
|
|
|
|
+ $this->assertSame(-1, $result['code']);
|
|
|
|
|
+ $this->assertSame('无权限访问', $result['msg']);
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+
|
|
|
|
|
+ DB::table('roles')->insert(['id' => 5, 'name' => 'editor']);
|
|
|
|
|
+ DB::table('role_user')->insert(['user_id' => 20, 'role_id' => 5]);
|
|
|
|
|
+ DB::table('role_menu')->insert(['role_id' => 5, 'menu_id' => $otherMenu]);
|
|
|
|
|
+ $this->assertSame(-1, $changeStatus(20, 0)['code']);
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+
|
|
|
|
|
+ DB::table('role_menu')->insert(['role_id' => 5, 'menu_id' => $editMenu]);
|
|
|
|
|
+ $result = $changeStatus(20, 0);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $this->assertSame(0, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+
|
|
|
|
|
+ $result = $changeStatus(1, 1);
|
|
|
|
|
+ $this->assertSame(HttpStatus::OK, $result['code'], $result['msg']);
|
|
|
|
|
+ $this->assertSame(1, (int) DB::table('banner')->where('id', $id)->value('status'));
|
|
|
|
|
+ $this->assertSame(0, DB::table('role_user')->where('user_id', 1)->count());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|