rebuildTestingTables(); } /** * 方法功能:验证后台可以保存题目过期时间配置。 */ public function test_admin_can_save_idiom_expire_minutes_setting(): void { $this->withoutMiddleware(); $admin = $this->createSiteOwner(); Room::create(['room_name' => '测试房间']); $response = $this->actingAs($admin)->post(route('admin.idioms.settings.save'), [ 'reward_gold' => 88, 'reward_exp' => 66, 'auto_start_interval' => 9, 'expire_minutes' => 7, ]); $response->assertRedirect(route('admin.idioms.index')); $response->assertSessionHas('success'); $config = GameConfig::query()->where('game_key', 'idiom')->firstOrFail(); $this->assertSame(88, $config->params['reward_gold']); $this->assertSame(66, $config->params['reward_exp']); $this->assertSame(9, $config->params['auto_start_interval']); $this->assertSame(7, $config->params['expire_minutes']); } /** * 方法功能:验证后台会拦截负数的题目过期时间。 */ public function test_admin_cannot_save_negative_idiom_expire_minutes_setting(): void { $this->withoutMiddleware(); $admin = $this->createSiteOwner(); Room::create(['room_name' => '测试房间']); $response = $this->from(route('admin.idioms.index')) ->actingAs($admin) ->post(route('admin.idioms.settings.save'), [ 'reward_gold' => 88, 'reward_exp' => 66, 'auto_start_interval' => 9, 'expire_minutes' => -1, ]); $response->assertRedirect(route('admin.idioms.index')); $response->assertSessionHasErrors('expire_minutes'); } /** * 方法功能:验证手动出题前会清理已超时的旧回合,避免阻塞新题。 */ public function test_start_clears_expired_round_before_creating_new_round(): void { $this->mockChatStateService(); $admin = $this->createSiteOwner(); $room = Room::create(['room_name' => '猜成语房间']); $idiom = $this->createActiveIdiom(); GameConfig::create([ 'game_key' => 'idiom', 'name' => '猜成语', 'icon' => '🧩', 'enabled' => true, 'params' => [ 'reward_gold' => 50, 'reward_exp' => 30, 'auto_start_interval' => 0, 'expire_minutes' => 5, ], ]); $expiredRound = IdiomGameRound::create([ 'room_id' => $room->id, 'idiom_id' => $idiom->id, 'status' => 'active', 'reward_gold' => 50, 'reward_exp' => 30, 'started_at' => now()->subMinutes(10), ]); $response = $this->actingAs($admin)->postJson(route('idiom-quiz.start'), [ 'room_id' => $room->id, ]); $response->assertOk() ->assertJsonPath('status', 'success'); $expiredRound->refresh(); $this->assertSame('ended', $expiredRound->status); $this->assertNotNull($expiredRound->ended_at); $this->assertDatabaseCount('idiom_game_rounds', 2); $this->assertDatabaseHas('idiom_game_rounds', [ 'room_id' => $room->id, 'status' => 'active', ]); } /** * 方法功能:验证回合超时后不能继续答题,也不会发放奖励。 */ public function test_answer_rejects_expired_round_and_marks_it_ended(): void { $this->mockChatStateService(); $player = User::factory()->create([ 'username' => '答题用户', 'exp_num' => 10, ]); $room = Room::create(['room_name' => '答题房间']); $idiom = $this->createActiveIdiom(); GameConfig::create([ 'game_key' => 'idiom', 'name' => '猜成语', 'icon' => '🧩', 'enabled' => true, 'params' => [ 'reward_gold' => 20, 'reward_exp' => 15, 'auto_start_interval' => 0, 'expire_minutes' => 5, ], ]); $round = IdiomGameRound::create([ 'room_id' => $room->id, 'idiom_id' => $idiom->id, 'status' => 'active', 'reward_gold' => 20, 'reward_exp' => 15, 'started_at' => now()->subMinutes(6), ]); $response = $this->actingAs($player)->postJson(route('idiom-quiz.answer'), [ 'round_id' => $round->id, 'room_id' => $room->id, 'answer' => '画蛇添足', ]); $response->assertStatus(400) ->assertJsonPath('status', 'error') ->assertJsonPath('message', '该回合已超时结束'); $round->refresh(); $player->refresh(); $this->assertSame('ended', $round->status); $this->assertNotNull($round->ended_at); $this->assertNull($round->winner_id); $this->assertSame(10, $player->exp_num); } /** * 方法功能:创建站长账号,满足后台权限与手动出题权限要求。 */ private function createSiteOwner(): User { return User::factory()->create([ 'id' => 1, 'user_level' => 100, 'username' => '站长', ]); } /** * 方法功能:创建一条可用于猜成语测试的启用题目。 */ private function createActiveIdiom(): Idiom { return Idiom::create([ 'answer' => '画蛇添足', 'hint' => '🧩 四人比赛画蛇,最慢的那个反而多此一举。猜一成语', 'sort' => 1, 'is_active' => true, ]); } /** * 方法功能:重建本组测试所需的最小表结构。 */ private function rebuildTestingTables(): void { Schema::disableForeignKeyConstraints(); Schema::dropIfExists('idiom_game_rounds'); Schema::dropIfExists('idioms'); Schema::dropIfExists('game_configs'); Schema::dropIfExists('rooms'); Schema::dropIfExists('users'); Schema::enableForeignKeyConstraints(); Schema::create('users', function (Blueprint $table): void { $table->id(); $table->string('username')->nullable(); $table->string('email')->nullable(); $table->string('password')->nullable(); $table->string('remember_token', 100)->nullable(); $table->unsignedTinyInteger('sex')->default(1); $table->text('custom_join_message')->nullable(); $table->text('custom_leave_message')->nullable(); $table->integer('user_level')->default(1); $table->integer('exp_num')->default(0); $table->timestamps(); }); Schema::create('rooms', function (Blueprint $table): void { $table->id(); $table->string('room_name')->nullable(); $table->string('room_owner')->nullable(); $table->timestamps(); }); Schema::create('game_configs', function (Blueprint $table): void { $table->id(); $table->string('game_key')->unique(); $table->string('name')->nullable(); $table->string('icon')->nullable(); $table->text('description')->nullable(); $table->boolean('enabled')->default(false); $table->json('params')->nullable(); $table->timestamps(); }); Schema::create('idioms', function (Blueprint $table): void { $table->id(); $table->string('answer', 50); $table->string('hint', 255); $table->boolean('is_active')->default(true); $table->integer('sort')->default(0); $table->timestamps(); }); Schema::create('idiom_game_rounds', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('room_id'); $table->unsignedBigInteger('idiom_id'); $table->string('status', 20)->default('pending'); $table->integer('reward_gold')->default(0); $table->integer('reward_exp')->default(0); $table->unsignedBigInteger('winner_id')->nullable(); $table->string('winner_username', 50)->nullable(); $table->timestamp('started_at')->nullable(); $table->timestamp('ended_at')->nullable(); $table->timestamps(); }); } /** * 方法功能:替换聊天室状态服务,避免测试依赖真实 Redis。 */ private function mockChatStateService(): void { $chatStateService = \Mockery::mock(ChatStateService::class); $chatStateService->shouldReceive('nextMessageId')->andReturn(1); $chatStateService->shouldReceive('pushMessage')->andReturnNull(); $this->app->instance(ChatStateService::class, $chatStateService); } }