完善猜成语过期与答题记录逻辑

This commit is contained in:
pllx
2026-04-29 10:32:12 +08:00
parent 2f9b2eed64
commit 5962d6d2b3
11 changed files with 685 additions and 115 deletions
+299
View File
@@ -0,0 +1,299 @@
<?php
/**
* 文件功能:猜成语控制器与后台参数测试
*
* 覆盖猜成语后台过期时间配置、手动出题清理超时回合、
* 以及超时后禁止继续答题的关键行为。
*/
namespace Tests\Feature;
use App\Models\GameConfig;
use App\Models\Idiom;
use App\Models\IdiomGameRound;
use App\Models\Room;
use App\Models\User;
use App\Services\ChatStateService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
/**
* 类功能:验证猜成语后台配置与回合过期逻辑。
*/
class IdiomQuizControllerTest extends TestCase
{
/**
* 方法功能:为本组测试准备隔离表结构,不触碰本地业务数据库。
*/
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}